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:
@@ -325,3 +325,57 @@ build already did some of that. Closing it properly means no two runs holding a
|
||||
`github-pages` artifact across a deployment, which is a real change to the job
|
||||
graph with real costs and is not attempted here. A retry is **not** a fix: the
|
||||
artifact is gone rather than transiently unlistable, so a retry cannot find it.
|
||||
|
||||
### 4. The release preflight, and why a loud failure was not enough
|
||||
|
||||
`scripts/release-preflight.sh` answers, before a tag exists, the question the
|
||||
Release workflow answers after.
|
||||
|
||||
**What happened.** `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's
|
||||
version, refused with *"This workspace doesn't have anything for dist to
|
||||
Release!"*, and the four jobs behind `plan` skipped. **Nothing was wrong with
|
||||
the workflow**, which is worth stating plainly because the instinct on a failed
|
||||
release run is to edit the YAML. The trigger is correct, the pattern matches,
|
||||
the workflow is enabled; `dist plan --tag=v0.1.0-rc2` succeeds today and
|
||||
`--tag=v0.1.0-rc3` reproduces the CI error exactly, which is the whole
|
||||
diagnosis and both halves of it.
|
||||
|
||||
**Why a loud, precise failure still cost something.** dist named the right
|
||||
answer in its own error text — *"`--tag=v0.1.0-rc2` will Announce: improvise"*.
|
||||
The problem is not the message, it is **when** it arrives: a tag is public and
|
||||
immutable by the time CI reads it, so the repair is a new version rather than an
|
||||
edit, and the tag that failed stays on three remotes forever. A check that runs
|
||||
after the irreversible step is a check in the wrong place, however good its
|
||||
message. So the preflight runs *before* tagging and prints the tag command only
|
||||
once it has passed — the honest path made the easy one, the same shape as
|
||||
`Basis` having no public constructor (gotcha 43).
|
||||
|
||||
**The primary check is textual and the `dist plan` run is a second opinion.**
|
||||
An absent `dist` must not read as a pass, so the script says which of the two
|
||||
ran. It also tolerates a locally-installed dist whose version differs from the
|
||||
one pinned in `dist-workspace.toml`: that stops dist before it plans, and
|
||||
reporting it as a refusal would train the reader to ignore refusals.
|
||||
|
||||
**Only the root package's version can disagree with a tag.**
|
||||
`dist-workspace.toml` says `members = ["cargo:."]`, so `gp-tree` sitting at
|
||||
`0.1.0` while everything else is on `0.1.0-rc2` is not a finding. The script
|
||||
lists such crates and says so rather than failing, because a deliberate split
|
||||
should stay deliberate and a silent one should be visible.
|
||||
|
||||
**The test's first version was vacuous, and the fault injection is what showed
|
||||
it.** `scripts/release-preflight-test.sh` originally asserted only *did it
|
||||
refuse*. Every negative case 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-exporting `.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*. The test 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 is a failure
|
||||
rather than a pass. 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 `context/design-principles.md` §7's "a check that looks like the answer,
|
||||
answering a neighbouring question", found in a checker written to prevent
|
||||
exactly that class, within an hour of writing it.
|
||||
|
||||
Reference in New Issue
Block a user