ci: a release preflight, because the tag is already public when CI refuses

THE WORKFLOW WAS NEVER BROKEN. v0.1.0-rc3 was pushed to all three remotes
while every crate still said 0.1.0-rc2; dist matches a tag against the root
package version, refused with "This workspace doesn't have anything for dist
to Release!", and the four jobs behind `plan` skipped. Proved in both
directions locally: `dist plan --tag=v0.1.0-rc2` gets past the check and
`--tag=v0.1.0-rc3` reproduces the CI error exactly.

So this changes no trigger and no job graph. What it adds is a check that runs
BEFORE the irreversible step. dist named the right answer in its own error
text; the problem is when that answer arrives -- a tag is public and immutable
by the time CI reads it, so the repair is a new version rather than an edit.

scripts/release-preflight.sh refuses a dirty tree, compares the intended tag
against the root package version, runs `dist plan` as a second opinion when
dist is on PATH (and says so when it is not, since an absent dist must not
read as a pass), and prints the tag-and-push commands only once it passes.

THE TEST'S FIRST VERSION WAS VACUOUS AND THE INJECTION IS WHAT SHOWED IT.
release-preflight-test.sh originally asserted only "did it refuse". Every
negative case still passed with the tag comparison replaced by `if false;`,
because the script refuses a DIRTY WORKING TREE first and this repo's tree is
almost always dirty -- bd auto-exports .beads/issues.jsonl on nearly every
command. Four of five checks were answering "is the tree dirty" while claiming
to answer "does it catch a bad tag".

It now runs the negative cases against a clean throwaway checkout AND asserts
the refusal names the version mismatch, so a refusal for the wrong reason
fails. Two rival faults were injected against the repaired version -- the
comparison disabled, and `expected` derived from the tag itself -- and each
produces four failures and exit 1.

That is design-principles section 7's "a check that looks like the answer,
answering a neighbouring question", found inside a checker written to prevent
that class, within an hour of writing it. Recorded in context/code/tooling.md.

No version is bumped and no tag is cut here, per the user's instruction to
make the workflow work without releasing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-21 22:20:34 -07:00
parent f34334cfca
commit aa4e901bd2
4 changed files with 274 additions and 0 deletions
+4
View File
@@ -264,6 +264,10 @@ jobs:
# No trealla needed: pr-watch is driven entirely from fixtures.
- name: The PR watcher refuses what it claims to
run: ./scripts/pr-watch-test.sh
# The release preflight is the one guard whose absence is invisible until
# a tag is already public and immutable, so its refusals are checked here.
- name: The release preflight refuses what it claims to
run: ./scripts/release-preflight-test.sh
# ---------------------------------------------------------------------------
# Lints the one crate the `test` job cannot reach.