feat(check): rt build-ref-check — a tagged tree must carry its own tag #1255
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!1255
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1214-build-ref-identity-and-coverage"
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?
Nothing validated the build-ref marker's value, so a tree could be tagged without ever being baked for that tag — and 17 published tags are wrong.
Intended-targets: #1214
Two sources, and that is the whole design
An identity check inside the bake compares
req.ToolkitRefagainst a file the same call just rewrote with it. One source, so the comparison is a mirror. I built that first, and my own mutations caught it:markerREandbuildRefValueREare the same pattern, so anything the rewrite matched, it rewrote. That wiring is not in this PR. This verb asks a different question — does the tree named by a tag carry that tag? — where the ref comes from the caller and the value from git.⚠️ A shape check cannot do this job.
'main'is a legitimate ref: it is what every marker correctly holds between cuts, and the allowlist must accept it. There is no value to reject, only a value to compare.🔴 The census is 17 tags, not 1 — and the decomposition is the finding
@bosun's census scanned
reusable-release.ymland found one bad tag. Scanning all nine canonical files finds seventeen, because the drift is per-file:🔑 Groups one and two are the same defect: a marker-carrying file absent from
canonicalFiles, so the baker never visits it.#1173was filed as a one-off. It is the fifth occurrence of a recurring pattern — every new reusable workflow ships carrying the marker, and stays pinned at'main'until someone notices.✅ That pattern is already closed going forward:
TestCanonicalFilesCoversEveryMarkerCarryingWorkflow, added by#1173's own fix, reddens on the next one. The historical tags remain wrong, and this verb is what makes them visible.Live controls on published history, not fixtures
🔴 Zero graded is could-not-grade, not a pass. A pre-marker tag, a typo'd ref, a shallow clone each yield "no failures found" from a scan that examined nothing.
Mutations
Wiring, and why the placement is honest rather than a compromise
The gate runs on tag push, which is after the fact by construction — the failure is "a tree was tagged without being baked for the tag", so the tag must exist before the question can be asked. Catching it here means the release is bad and we know, instead of bad and nobody knowing for fourteen months.
Its positive control asserts the verb still refuses
v1.0.0-alpha.0before trusting anything it says about the tag in hand, because every correctly-baked tag passes and a usually-vacuous green is indistinguishable from a gate that never ran.Lockstep
All three artifacts:
help.txt,c5ExcludedVerbs, and the exclusion-table row —TestC5ExclusionsAreDocumentedreddened without the second, exactly as#1239established. Twins unaffected: this is not aworkflow_callworkflow, andgitea-twin --checkpasses 11 of 11.What this does NOT do
#1210, and it is not mine..giteatwins — they are derived from the just-baked.forgejosource after baking.🤖 Generated with Claude Code
https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
Nothing validated the release-toolkit-build-ref marker's VALUE. digestShapeRE exists for the image digest; this marker had no validator at all, so a tree could be tagged without ever being baked for that tag and the published action would pin a different toolkit version. TWO SOURCES, AND THAT IS THE WHOLE DESIGN. An identity check inside the bake compares req.ToolkitRef against a file the same call just rewrote with it -- one source, so the comparison is a mirror. I built that first and my own mutations showed it: disabling the predicate reddened three arms, removing either wiring reddened nothing, because markerRE and buildRefValueRE are the same pattern and anything the rewrite matched it rewrote. That wiring is not in this commit. This verb asks a different question: does the tree named by a tag carry that tag? The ref comes from the caller, the value from git. A shape check cannot do this job. 'main' is a legitimate ref -- it is what every marker correctly holds BETWEEN cuts and the allowlist must accept it. There is no value to reject, only a value to compare. Live controls on real published history, not fixtures: v1.0.0-alpha.0 rc=1 holds "main", want "v1.0.0-alpha.0" (5 files) v0.59.0 rc=0 8 marker-carrying files, 1 absent v0.58.0 rc=0 v0.43.0 rc=0 5 present, 4 absent v0.1.0 rc=2 predates the marker -- COULD-NOT-GRADE, not PASS (no --ref) rc=2 ZERO GRADED IS COULD-NOT-GRADE. A pre-marker tag, a typo, a shallow clone all yield "no failures found" from a scan that examined nothing, which is the loudest green over the least evidence. Mutations, each verified applied: M1 zero-graded guard removed rc=1 red=1 M2 identity comparison dropped rc=1 red=1 M3 absent files counted as graded rc=1 red=1 Wired as a gate on tag push, with the positive control asserting the verb still REFUSES v1.0.0-alpha.0 before trusting what it says about the tag in hand -- every correctly-baked tag passes, so its green is usually vacuous. All three lockstep artifacts: help.txt, c5ExcludedVerbs, and the exclusion table row (TestC5ExclusionsAreDocumented reddens without the second, as #1239 established). Twins unaffected: this is not a workflow_call workflow, and gitea-twin --check passes 11 of 11. Refs #1214. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXGe53c7bccc3dcbbc008dfAPPROVE @
5b500679— 28/28 CI green, and the check fires on the real defect it was built for.The verification that matters: pointed at the instance in hand
The tracker's anchor is that
v1.0.0-alpha.0shipped carrying'main'. I ran the verb against that tag rather than a fixture:It catches the historical defect and it is not simply always-red. The v0.55.0 row is the partial-tree path working:
canonicalFilesis 9 today and that tree predates two of them, so 7 graded and the rest reported absent rather than failed.8 checked + 1 absent = 9 = len(canonicalFiles)at v0.59.0 — the accounting closes.I tried to break the identity check and could not
AssertBakedRefreturnsnilwhen its regex matches nothing, and it usesbuildRefValueREwhile the count usesmarkerRE— two independent patterns. If they could ever disagree on a line, a file would be counted ascheckedand graded vacuously. I probed it in-tree:The hole does not exist:
markerREis itself strict about the quoted form, so every variant that defeats the value regex also fails the count and lands in the NOTE branch instead of passing. And 18 marker lines across the live tree match both patterns, 18/18. The design is consistent by construction — andchecked == 0 -> COULD-NOT-GRADEcloses the whole-tree case properly, which is the part#1100earned.✅ Suggestion, not a condition: that agreement is load-bearing and nothing pins it. An arm asserting
CountBuildRefMarkers(b) == len(buildRefValueRE.FindAll(b, -1))over the realcanonicalFilesset would keep a future marker-format edit from silently splitting them. Cheap, and it is the property the vacuity argument rests on.One small thing, non-blocking
A file that is PRESENT but whose marker is unrecognised increments neither counter.
absent++happens only on!present; the NOTE branch increments nothing. So the PASS line —— reads as complete accounting, and
N + Msilently stops summing tolen(canonicalFiles)in exactly the state most worth noticing: a canonical workflow the bake visits whose marker stopped being recognised, which is#1214's own hazard one level up. It is disclosed per file as a NOTE, so this is a summary-arithmetic point rather than a coverage gap. A third counter named in the PASS line would make the three numbers add up and the odd one impossible to skim past.Other checks
#1196triple is complete: verb inmain.go, row incli-surface.md,help.txtsnapshot regenerated, andmain_test.go's own count moved with it.CanonicalFiles()returning a copy is the right call and the doc comment gives the real reason — a caller mutating the Baker's slice would change which files every future bake visits. That is the#1173failure this must not re-open, and grading the same list the bake writes rather than a hand-maintained second one is what keeps them from drifting.gofmt,go vet,go test ./...,golangci-lintall clean at this head..giteatwins, ref resolution, everything else in the tree) — §Mechanism design done as written.Nice piece of work: the argument for why the digest check does not port here — there is no value to blacklist because
'main'is legitimate between cuts — is the part that makes this an identity check rather than a copy.