bug(release-prep): the composer bumps README adopter pins to a version with no consumable release #1378
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#1378
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The release-prep composer bumps README's adopter-facing pins to the version being prepared, before that version has a release anyone can fetch — so the rolling release PR is red by construction and a stranger copying the README from it gets a green checkout and a failed fetch.
Measured on #1371, the current rolling PR
Why this is a regression against a decided policy
#1068decided the README names the newest tag the mirror can actually serve, precisely because a tag that resolves without a release produces a green checkout and a late, misattributed failure. The composer bumps every version token at PREPARE time, which advances those two lines past what the mirror holds and undoes that decision on every cycle.⚠️ It is not merely cosmetic on a throwaway branch: the rolling PR merges into main at cut time, so between the merge and the mirror publishing the release object, main's README is a broken instruction.
Preferred path
Advance the adopter-facing pins in the POST-CUT bookkeeping, not in prepare — the same step that already records the manifest once the release exists and is mirrored.
VERSION, the CHANGELOG heading and the internaluses:pins can move at prepare time as they do now; only the two lines that tell a stranger what to pin need the release to exist first.The alternative — exempting the rolling branch from
readme-pin-check— is worse: it silences the gate on the one branch where the pin is guaranteed to be wrong, and it leaves the post-merge window open.AC
#1379merged at81686e71.UpdateDocVersionRefskeeps the DESCRIPTIVE**Latest: vX.Y.Z**line and drops the PRESCRIPTIVE@vX.Y.Zrewrite. Mutation run independently: reverting the guard reddensTestUpdateDocVersionRefs_LeavesReadmeAdopterPinsAloneagainst a green control.readme-pin-checkshare ONE computation of newest servable release —readmepin.RetargetusespinRE, the grader's own pattern, and--fixcallsreadmepin.NewestServable/Grade. "What is a pin, and what should it say" has one implementation, so the gate and the setter cannot drift.--fixlands through a bot PR, not a direct push frommirror-release.ymltomain—set-adopter-pinpushes a branch and opens a PR (mirror-release.yml:154,161), and emits::warning::naming the branch if the PR call fails rather than losing the change silently. A mirror workflow does not get write access to the default branch.readme-pin-checkpasses on the rolling PR without being exempted from it — THE SETTLING READ, and it is the recompose rather than the merge.release-prep/rollingregenerated ata2382cf2and now carries@v0.61.1on both lines where it carried@v0.62.0. Run with the workflow's own arguments:TestUpdateDocVersionRefs_LeavesReadmeAdopterPinsAlone,TestRetarget_RewritesOnlyPrescriptivePins,TestCheckReadmePins_FixRewritesToTheNewestServable. @surveyor's independent run:control fail=0 · M1 fail=4 · M2 fail=3 · M3 fail=4 · restored fail=0.TestUpdateDocVersionRefs_StillAdvancesTheDescriptiveStatusLine— "stop touching README" would pass the other arms and fail nothing. AndTestRetarget_AlreadyCorrectIsANoOp/TestCheckReadmePins_FixOnACorrectDocumentWritesNothing, without which the mirror setter opens an empty PR on every publish.Related
#1068(the policy this undoes),#1348(mirror release objects),#1371(the PR it is red on).Anchor
Surfaced by the operator noticing CI red on both open PRs, 2026-09-06; reproduced by Bosun running
readme-pin-checkagainst the rolling branch with the workflow's own arguments.🔴 Correcting the "Preferred path" section above — it is wrong, and @shipwright measured why before building it.
What I wrote: advance the adopter-facing pins in the POST-CUT bookkeeping — the same step that already records the manifest once the release exists and is mirrored.
The release exists at that point. It is NOT mirrored. Verified independently:
⚠️ Mirroring is a manual dispatch, deliberately. So a post-cut bump names a version the mirror cannot serve — this tracker's own defect, moved a few minutes later and made harder to see.
#1348exists because six cuts went unmirrored before anyone noticed, which is the size of that window.The corrected path
The value the README should carry is already computed by
readme-pin-check: the newest stable release carrying assets on the mirror. The gate that grades the pin and the thing that sets it must share one implementation, or they drift by construction.⚠️ A
--fixthat writes files needs somewhere to land: a bot PR, the way the release bot already lands things — NOT direct push frommirror-release.ymlto the default branch. A mirror workflow with write access tomainis a bigger change than the defect it repairs.Scope: README.md only, for a reason rather than for size
UpdateDocVersionRefsrewrites@vX.Y.ZinREADME.md,docs/integration.mdandexamples/**, and a stranger copying an example hits the identical failure — butreadme-pin-checkgrades onlyREADME.md. Widening the setter past the gate leaves the other two set-but-unchecked, which is the setter/checker drift this design exists to prevent, reintroduced somewhere new.✅ So:
README.mdhere; widen the GATE in its own tracker; the setter follows the gate.Measurement and the corrected shape are @shipwright's; he asked before building rather than shipping a fix that relocated the defect.
✅ All six ACs ticked and the settling read is in. Closing.
🔑 The verification is the RECOMPOSE, not the merge — named in advance this time rather than discovered afterwards. The unit arms prove
prepno longer bumps the pin;#1371was red because a previous compose already had, andrelease-prep/rollingis regenerated from main on every compose. So:Run with the workflow's own arguments, on the regenerated branch, no exemption anywhere.
What the fix turned out to be
📌 I filed this as a scoping question — "adopter-facing pins" — and @shipwright found the actual axis:
That is why the status line should still advance at prepare while the pin must not, and it is a distinction rather than a rule. The
@is the whole discriminator, andRetargetreuses the grader's ownpinREso the setter and the checker cannot disagree about what a pin is.✅ And the argument that closes off the obvious counter-proposal is his too: prepare must work without a network, so it CANNOT know what the mirror holds. Teaching it to check is not a design preference to weigh — it is unavailable.
The control worth naming
StillAdvancesTheDescriptiveStatusLineis the arm nobody would have written from this tracker's text, and "stop touching README" would have passed every other arm and failed nothing.FixOnACorrectDocumentWritesNothingis the second — without it the mirror setter opens an empty PR on every publish.Not covered, and it has its own home
readme-pin-checkgrades onlyREADME.md, so the@vX.Y.Zpins that prepare still advances indocs/integration.mdandexamples/**are set by one rule and checked by none. Widen the GATE first, then point the setter at what it covers — the other order reintroduces set-but-unchecked somewhere new, which is the defect this tracker existed to remove. Filed separately.Implementation and both design insights are @shipwright's;
#1379reviewed and mutation-verified by @surveyor; the recompose read is mine.