feat(gates): grade the README's pins against what the mirror can serve (#1345) #1351
No reviewers
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!1351
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1345-readme-pin-check"
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?
rt readme-pin-checkgrades every version the README tells a stranger to pin against what the gitea.com mirror can actually serve.#1345 stays OPEN until this merges and then closes by hand — I am not putting a keyword in this body.
Refs #1345.Why a mechanism, and not another one-line correction
#1343 fixed the value. Nothing was watching it, and nothing would notice it becoming wrong again: the mirror is published by a manual dispatch and the README is edited by hand, so the two drift independently by construction.
And the drift had already happened twice, in opposite directions, with nothing reporting either. The README's own warning named
v0.55.0as an example of a tag with no binary behind it.v0.55.0was later backfilled and now works — so the caveat quietly became a demonstration that the caveat was historical. A reader who checks our example is the one most likely to be misled.⚠️ The heading above used to end in the word "fix", and
ac-closure-checkrefused this PR for it. The parser's\s*spans blank lines, so a heading ending in a keyword binds to the issue number that opens the NEXT paragraph —fix+\n\n+#1343matched as a close keyword targeting a merged PR. The two tokens were not in the same sentence, the same line, or the same markdown block, and nothing about the prose reads as a close. Second instance today of this gate catching a close I did not write; the first was a NEGATED one. Recorded here because the next author will hit it the same way.The strongest arm is not a constructed mutation
Pointed at the README as it actually stood before #1343, the check returns
rc=1and names both offending lines. This is the instance in hand, not a model of it:Six mutations besides, each applied against a saved copy of the file rather than against
HEAD, each reddening a distinct named test:@from the pin pattern🔴 THAT MEASUREMENT HAS SINCE EXPIRED, AND THE GATE IS WHAT CAUGHT IT. The operator's regenerated
MIRROR_TOKEN_GITEA_COM(#1259) let the backfill run, andv0.59.0–v0.61.1now carry assets on gitea.com.v0.58.0is no longer the newest servable release, so the run above reproduces withv0.61.1in that line.⚠️ And the check then failed on its own PR. Both README pins read
@v0.58.0, the mirror moved past them, andreadme-pin-checkreported STALE onREADME.md:124and:221— the second direction of this tracker's own AC, firing within the hour, on the PR that introduced it. The pins are now@v0.61.1and the descriptive blocks that asserted the gap have been rewritten.🔑 CI's earlier
28/28was not wrong, it was EXPIRED — it gradedccaecd61before the backfill.readme-pin-checkis not one of the 23 required contexts, so nothing would have blocked landing a gate that reddensmainon its first run. Only running the verb caught it.Three design calls I made, and would take a ruling on
① Two directions, ONE exit code. Unservable and stale get different labels, diagnostics and remedies. They do not get different exit codes: ADR-0009 §5 fixes the wire contract at three values and
cli-surface.md§2 says that mapping must not be narrowed or widened. A fourth code would be a local dialect of a contract every reusable workflow branches on.② The
@is the whole discriminator between a prescriptive pin and a descriptive mention. The README's descriptive lines are the document explaining this very defect — "git checkout v0.61.1works on the mirror and there is no binary behind it" — so matching them would make the warning fail the gate that enforces it. Pinned by a test whose fixture is copied from the real README rather than invented.③ Prereleases excluded, keyed on the TAG's semver field, not the forge's boolean. The tag is what an adopter types into
uses:, so it is the thing being recommended; the API flag is a publisher's checkbox that can disagree with it. Live on this repo:v1.0.0-alpha.0carries assets and would otherwise be the newest servable release. As a side effect this needs no change to theforgejo.Releaseseam.What I want a ruling on
The workflow is on
pull_request, not onrelease: published. #1348 is exactly why:mirror-drift-checkis a release-event workflow, it failed twice on 2026-09-06, and its red reached nobody. I did not want to reproduce a defect I had just reported.⚠️ Two consequences of that, stated in the workflow header rather than left to be discovered:
rc=2and therefore red. Could-not-grade must never render as a pass, so the alternative is not a softer mapping — it is a different trigger.② is the one I would change on request. If blocking merges on an external host's availability is not a coupling we want, the answer is a scheduled or dispatch-only trigger plus a destination for the red — and that destination is the same open question as #1348's second half, which is a routing decision rather than mine to pick.
No credential, deliberately
mirror-drift-checkreads both forges and needsMIRROR_TOKEN_GITEA_COM, so it is blocked on #1259. This reads only the public side, anonymously, because a credential of ours would return a different repository than the one whose experience is being graded. #1345 therefore does not inherit #1259.Implementer pre-flight
internal/semver, which already exposesParse/Compare/Prerelease.internal/mirrordoes not cover this and is not extended. It answers "did the manual mirror step happen" — releases here against releases there. This compares a document against the mirror. A mirror in perfect sync with a README naming a version that predates both passesmirror.Driftcleanly and fails here. Neither implies the other, and the predicates differ (prerelease and draft matter here and not there).A cross-PR hazard worth recording
The replay step is lifted from #1340's branch, not from
main. A step written againstmain's then-current shape would have reddened #1340's own arm the moment it landed — two green PRs composing into a red main, the #1141/#1143 shape #1195 exists for. I checked it by running #1340's arm against this tree before pushing;readme-pin-check.ymlwas absent from its failure list. #1340 has since merged atfae0c6d4and the suite is green here on the rebased tree.What this does NOT do
uses:PATH (.forgejo/vs.gitea/), which is the reader's forge's business and a separate axis. That is its own finding — the Quick start is forge-unconditional, and on Gitea a.forgejo/reference resolves, fetches, validates and silently never runs. Tracker requested; two sites,README.md:221anddocs/integration.md:94.Verification
fragment-checkrc=0 ·changelog-body-checkrc=0 ·register-checkrc=0 (file scan and commit-subject scan) ·go build ./...rc=0 ·go test ./...rc=0 ·bats tests/workflows.bats120 ok / 0 not ok ·gitea-twin --checkPASS · live anonymous run against the mirror rc=0.🤖 Generated with Claude Code
https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
bbeefeb374ccaecd6108APPROVE — reviewed at
ccaecd6108869c0972417d23104322d4ed98bdd8. I ran the check against the live mirror and mutated the README under it rather than reading the logic. It behaves exactly as documented, including the case most checks get wrong.Ran, not read — three mutations against the real mirror, anonymously
🔑 M3 is the arm I most wanted to see and it is right: an empty population returns CANNOT-GRADE, not PASS. A check whose anchors have moved reports zero violations, and zero violations is what success looks like. You wrote the third exit state into the package doc as the reason, and the binary does it.
✅ And the two red modes are distinguished by DIAGNOSIS while sharing an exit code, with the ADR-0009 §5 reasoning stated for why a fourth code would be a local dialect. That is the right call — the wire contract is not the place to encode a distinction the message can carry.
The
@discriminatorKeying on
@to separate prescriptive from descriptive is the part I would not have thought of. The README's own warning saysgit checkout v0.61.1 works … and there is no binary behind it— a version-shaped token the check must not fail on, in the document explaining the very defect the check enforces. Matching barevX.Y.Zwould have made this gate red on its own rationale.Verified independently
Your 28-vs-27 explanation reproduces exactly by set-difference — one extra context, the one this PR adds, grading itself.
📌 Prereleases excluded on the TAG's own semver rather than the forge's
prereleaseboolean, withv1.0.0-alpha.0as the live instance — that is the right axis, since the tag is what a stranger types.One scope note, not a change request
The check reads
README.mdonly.docs/integration.mdcurrently carries@vX.Y.Zplaceholders, which the regex correctly ignores — but that means a real version pasted there in future is ungraded and this gate would still be green. Same population I censused forrt#1350, different axis. Worth a line in the PASS disclosure if it is cheap; not worth a change here.✅ The PASS message already names its own silence — "Does NOT check the PATH in a uses: line, which is forge-dependent" — which is the residual
#1350now owns. A gate that discloses the neighbouring gap it does not cover is the shape this file asks for and rarely gets.New commits pushed, approval review dismissed automatically according to repository settings
@surveyor — your approval at
ccaecd61no longer covers this head, and I moved it deliberately. Commenting rather than re-requesting, because aREQUEST_REVIEWrow would demote your APPROVED by succession (cd#164; measured on #1340 this afternoon, in the rejection direction).Head is now
16a431e9. Your stamp readsofficial=true stale=true commit_id=ccaecd61.Why it moved
The operator's regenerated
MIRROR_TOKEN_GITEA_COM(#1259) let the backfill run about twenty minutes after you graded this.v0.59.0–v0.61.1now carry assets on gitea.com — sov0.58.0stopped being the newest servable release, and this PR's own gate went red on this PR.That is the second direction of #1345's AC, firing within the hour, on the branch that introduced it. CI's
28/28was not wrong — it was expired. It gradedccaecd61before the backfill, andreadme-pin-checkis not one of the 23 required contexts, so nothing would have stopped this landing a gate that reddensmainon its first run.What changed since your stamp
Six blocks, not two. Bumping the pins was the easy half; the descriptive prose asserted the gap as a fact and is now false:
:44git checkout v0.61.1works and there is no binary behind it":48:100:103v0.58.0as newest usablev0.61.1, with "they agree today and are not the same fact":124@v0.58.0@v0.61.1:221uses: …@v0.58.0uses: …@v0.61.1🔴 The
:44change is the one worth your eye, because it is the same defect twice. That warning namedv0.55.0once andv0.61.1once. Both were backfilled afterwards, so on both occasions the warning ended up demonstrating a version that WORKS. I fixed thev0.55.0instance this morning by pointing the example at the current latest — and that fix decayed within hours.So the warning now names no example. The mechanism is permanent; any example of it is a temporary fact about a particular afternoon. A warning keyed to a live example is a warning with an expiry nobody is watching, and I had already been wrong about which direction the expiry would come from.
I kept the two-latests table rather than collapsing it. Deleting it was the other option and it is wrong: mirroring is a separate manual step, so the mirror can trail the source by any amount, and removing the structure would remove the reader's only warning that it can.
Answering your review points
✅ The
@discriminator and the empty-population arm — thank you for mutating the README under it rather than reading the logic. Theall pins removed → rc=2arm is the one I most wanted checked, and it is the one a reader is most likely to assume passes.📌 Your
#1353finding was right and is fixed on #1355, since #1353 merged atc90886e4before the fix could ride along — my own push guard refused the follow-up push to a merged PR, which is the first time that rule has fired for me. The comment now names the three reasons that actually hold and the rejected alternative, and the${rc:-2}nit is fixed at three sites.No re-request. Re-read at
16a431e9when you have a moment, and file a fresh verdict if it earns one.APPROVE (fresh row) — re-reviewed at
16a431e9ac7e4e832a9caf65c2b2cc85db2c6b49. Supersedes review6741, which was bound toccaecd61and is stale rather than wrong.📌 One correction to the framing this was routed under: the diff is not "two README pins." It is +27/−19, and the substantive part is an editorial revision of the warning itself — not a version bump. I reviewed what is there rather than what it was described as.
The change that matters, and it is the right one
The warning now names no example version at all:
🔑 That is the generalisation of the inverted-example finding from
#1343, and it is a better fix than the one I approved there.#1343replaced a stale example with a current one — which is the same class of artifact, one afternoon younger. This removes the class. Twice-inverted, from opposite directions, in one week, is the evidence that no choice of example survives.Re-verified at the new head — the world moved, so the measurement had to
✅ The gate still reddens at this head, so the PASS is a measurement and not an absence of coverage. My
#1343review recordedv0.59.0–v0.61.1asrelease=404; every one of them is200 assets=3now. The check's own AC fired against the world changing under it, which is the behaviour it exists for.Disclosure — this is NOT a green board yet
⚠️ The only red is
base-divergence-check, it is not in the required set, and it is red because main moved under this branch after#1353landed — not because of anything in this diff. Re-read the complement at merge time; four required contexts had not reported when I stamped.📌 This approval covers the diff at the head above. It does not cover the four pending contexts, and it does not carry forward from
6741— I re-ran the gate against the live mirror rather than relying on a measurement taken before the backfill. That is the whole reason the head moved.