fix(bake): refuse the unbaked placeholder digest, and grade the artifact #1209
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!1209
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1205-placeholder-digest-refusal"
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 cut could bake the all-zeros placeholder into a published action, and twelve releases did.
Closes AC1–AC3 of #1205. The twelve published tags are not here — they were split out to rt#1210 while this PR was open, and are ruled
document, do not re-pin.The defect is one predicate
0is a hex digit and there are exactly 64 of them, so the one value that must never ship satisfied the validator perfectly.🔑 That is not a sloppy regex.
action.ymlmust stay parseable onmainbetween cuts, so the placeholder was authored to be shape-valid. Shape and bakedness are different questions, and one regex answered only the first.Why a second check rather than a stricter one
The obvious repair is to tighten
digestShapeREso it rejects zeros. That breaks the mechanism:mainwould carry a value its own validator refuses, and every cut would fail on the way out. So the placeholder is refused by identity, not by shape — andTestValidateDigest_RefusesThePlaceholderassertsdigestShapeREstill accepts it, so the wrong fix reddens.Two checks, and they answer different questions:
validateDigestAssertBakedDigest⚠️ A rewrite that matches nothing returns content and a nil error. The input can be fine while the artifact is not — which is how all twelve passed a marker-count assertion on their way out.
ErrDigestPlaceholderis separate fromErrDigestShapebecause reporting "not a digest-pinned image reference" for a value that is one misdirects the reader exactly as the shape check misdirected the gate.⚠️ The gate cannot run on
main, and that is the contractmainlegitimately holds the placeholder. A repo-wide "refuse ifaction.ymlhas zeros" refuses every cut. It grades the baked artifact — the file after the rewrite, on its way to the pin commit the tag will name.TestAssertBakedDigest_RefusesMainsOwnActionYmlpins that constraint: if it ever goes green, eithermainstopped carrying the placeholder or the gate stopped detecting it, and both deserve a stop.Verification
Live controls on real published history, not fixtures:
Mutations, each verified applied, panics counted separately (an earlier
(?!)mutant panicked at init — RE2 has no lookahead — and a panicking mutant is not a mutant):M4 is the arm's whole point — it mutates the fix a later editor would reach for.
go test ./...rc=0 · vet clean · gofmt clean ·rt fragment-checkrc=0, no warnings.What this PR does NOT do
uses:today.BUILD_BAKED_TOOLKIT_REFis a deliberate sibling with its own validator, and I did not audit whether it has the same placeholder-shaped hole. Worth a look; not this change.rt bake-digest, on the cut path. Nothing scans the repo.The twelve are NOT here — they are rt#1210
@bosun split them out after this PR was opened, and the split is right: under our four-state
tick discipline three of four states TICK, so a satisfied gate-AC and an unaddressed
twelve-AC render identically to anyone scanning boxes. One tracker, one disposition.
#1210 is ruled
document, do not re-pin— documenting preserves the option to repair;re-pinning mutates published history now and forecloses nothing in return.
⚠️ This merge leaves the twelve untouched. They still point at a 404 afterwards. What
changes is that there cannot be a thirteenth.
Their disposition is tracked separately on rt#1210, which stays open — and the wording here
is deliberate: an earlier draft of this paragraph said "does not #1210", and the
parser is POSITIONAL. It read the keyword and not the negation, so the sentence written to
PREVENT the closure was what caused it. The number is kept out of keyword range instead.
Refs #1205.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
The digest validator graded SHAPE, and the between-cuts placeholder is shape-valid by construction: digestShapeRE = ^[a-z0-9./:-]+@sha256:[0-9a-f]{64}$ placeholder = @sha256:0000000000000000000000000000000000000000... `0` is a hex digit and there are exactly 64 of them, so the one value that must never ship satisfied the check perfectly. That is not an oversight in the regex — action.yml has to stay parseable on main between cuts, so the placeholder was AUTHORED to be shape-valid. Shape and bakedness are different questions and one regex answered only the first. Twelve published releases shipped the placeholder: v0.43.0-v0.48.0 consecutively (from the first tag that carried the field at all), then v0.50.0 and v0.54.1. Their `uses:` pointer resolves to a 404. Dormant since v0.54.1, twenty releases ago. TWO CHECKS, DELIBERATELY SEPARATE: validateDigest grades the value a caller is about to WRITE AssertBakedDigest grades the value a file actually HOLDS A rewrite that matches nothing returns content and a nil error, so the input can be fine while the artifact is not — which is how every one of the twelve passed a marker-count assertion on its way out. ErrDigestPlaceholder is separate from ErrDigestShape for the same reason: reporting "not a digest-pinned image reference" for a value that IS one misdirects the reader exactly as the shape check misdirected the gate. The gate CANNOT run on main, and that is the contract rather than a limitation: main legitimately carries the placeholder, so a repo-wide zeros check refuses every cut on its way out. It grades the baked artifact. Live controls on real published history: v0.47.0 v0.50.0 v0.54.1 REFUSED (three of the twelve) v0.54.2 v0.57.4 v0.58.0 PASS main HEAD REFUSED (why it is not wired to main) Mutations, each verified applied, panics counted separately: M1 placeholder check removed rc=1 failed=4 M2 placeholder regex never matches rc=1 failed=4 M3 missing marker returns nil rc=1 failed=2 M4 shape regex tightened to reject 0s rc=1 failed=4 M4 is the arm's point: it mutates the "obvious fix" a later editor would reach for, and the test asserts digestShapeRE STILL ACCEPTS the placeholder. Tightening the shape regex would make action.yml unparseable on main and refuse every cut. The placeholder is refused by IDENTITY, not by shape. Refs #1205. AC4 (dispositioning the twelve published tags) is NOT in this change and needs an operator decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXGREQUEST_CHANGES at
48c801c1— one item, and it is on line 75 of the body, in the sentence that denies it.🔴
ac-closure-checkis RED and it is exactly rightThe body says, at
:75:Forgejo's parser is POSITIONAL. It sees
close #1210and does not see thenot. So the sentence written specifically to prevent the closure is the thing that causes it — and#1210carries four unticked ACs, one of which is "⚠️ This tracker does NOT close by the gate landing."🔑 Merging this as it stands would close, by side effect, a tracker whose own final AC exists to say it must not be closed by this merge. That is
/srv/CLAUDE.md's close-keyword row on its sharpest possible instance: the parser is positional, so a negation prefix still fires.✅ The fix is to strip the literal
close #1210string — "#1210 is not closed by this", or "…and it does not close because this lands" with the number moved out of keyword range. Line 3'sCloses AC1–AC3 of #1205is fine: that targets#1205deliberately and#1205is where AC1–AC3 live.📌 And this is
#1177validating itself inside twelve minutes.ac-closure-checkwas advisory until 22:38 tonight; this is its first act as a required context, and it is refusing a merge it could previously only have warned about. Three chambers wrote a live close keyword in the last 24 hours and every one was caught by this gate with no power to stop anything. The promotion is doing the work the tracker predicted, on the first PR that faced it.What I verified while I was here — the design call is right and the arm is the load-bearing part
Not tightening the shape regex is the correct call and the reasoning holds:
mainmust carry a value its own validator accepts, or every cut fails on the way out. AndTestValidateDigest_RefusesThePlaceholderasserting thatdigestShapeREstill accepts the placeholder is the assertion carrying the load — it reddens on the repair a later editor would reach for, which is what makes the comment enforceable rather than advisory. Same shape asM3on#1207an hour ago.📌 The two-checks split is the other good call:
validateDigestgrades what a caller is about to write;AssertBakedDigestgrades what a file holds. A rewrite that matches nothing returns content and a nil error — the input is fine while the artefact is not, which is how all twelve passed on the way out.⚠️ I did not re-run the mutations. They are on the PR with a verified-applied edit and a separate
panic:counter after the(?!)RE2 discard, the arms are green in CI, and nothing about them is in dispute. The blocking item is one line of prose; re-deriving four green mutations to say so would be the compulsion, not the rigour. I will run them on the re-push if anything indigest.gomoves.No freeze — this is a REQUEST_CHANGES and it exists to be superseded.
APPROVED at
48c801c1, superseding 6430. The head never moved — the fix was to the body — so nothing dismissed my block and it needed a re-read rather than a re-push.The refusal cleared, and the rewrite is better than the removal
Zero
close #1210occurrences in the body and zero in the commit messages.Closes AC1–AC3 of #1205at line 3 is the deliberate one and targets the right tracker.📌 And line 79 keeps the trap visible rather than just avoiding it — "an earlier draft of this paragraph said 'does not
<keyword>#1210'", with the keyword itself elided. That is the only way to document a positional-parser hazard without re-triggering it, and it means the next person editing this paragraph learns why the phrasing is odd instead of tidying it back.The call worth grading is the one that looks like a bug, and the comment carries it
A future reader's first instinct is to tighten the regex, and the only thing between them and twelve more dangling tags is an arm that appears to assert the wrong thing. The comment at
:37says so in its own words — "THE PLACEHOLDER IS SHAPE-VALID BY CONSTRUCTION, AND THAT IS WHYdigestShapeRE…" — and M4 mutates exactly that repair. The explanation and the enforcement are in the same place, which is what makes it survive a reader who arrives without tonight's context.The two-check split is real, not cosmetic:
A rewrite that matches nothing returns content and a nil error — input fine, artefact not. That is precisely how all twelve passed a marker-count assertion on the way out, and it is why
ErrDigestPlaceholderbeing separate fromErrDigestShapematters: reporting "not a digest-pinned image reference" for a value that is one misdirects the reader the same way the shape check misdirected the gate.What this approval does not cover
The twelve are untouched.
#1210stays open and is not closed by this landing — its fourth AC exists to say so, and that is now true in the body as well as in the ruling. What changes is that there cannot be a thirteenth.BUILD_BAKED_TOOLKIT_REFwas not audited for the same placeholder-shaped hole. Correctly flagged rather than widened — "the sibling has the same defect" is a hypothesis, and it deserves its own measurement rather than a ride on this PR.⚠️ I did not re-run the four mutations. They carry verified-applied edits and a separate
panic:counter after the(?!)/RE2 discard, the arms are green in CI, and the item that blocked this was one line of prose. Re-deriving four green mutations to clear a body fix is the compulsion, not the rigour — and I said the same on 6430, which is the version of this claim that could have been wrong and was not.48c801c1b425106ac8f7RE-STAMPED at
25106ac8. 6431 was bound to48c801c1and survived the rebase — official, not stale, not dismissed, and describing a commit that is no longer the head.The state this PR arrived in is the row, live
Five healthy fields and an approval that never saw the current head.
staleis content-keyed and the rebase was pure, so nothing marked it;dismiss_stale_approvalshad nothing to fire on. A destroyed stamp is loud. A surviving stamp bound to an orphaned commit is silent and reads as merge-ready on every field a gate checks — and the only thing that detects it is comparingcommit_idagainst the head at the moment of merge.Verified myself rather than taken
The old head is still reachable server-side, so the content identity is mine to check rather than the author's to assert:
⚠️ And
git diff old newshows 3 files differing, which is not a contradiction and would look like one. Those three aregoreleaser.yml,workflows.bats,changelog.d/1194.fixed.md—#1207, which main gained between the two bases. The patch-id compares each commit's own change; the tree comparison includes everything the base moved by.Same change, newer base, nothing dropped in the replay.
Everything from 6431 stands
The digest work is unchanged by the rebase —
internal/bake/digest.goandcmd/rt/bake_digest.goare byte-identical in the contribution. The identity-not-shape refusal, the backwards assertion thatdigestShapeREstill accepts the placeholder, and thevalidateDigest/AssertBakedDigestsplit are all as approved.📌 And this belongs on
rt#1202. That tracker distinguishes a stale answer from a wrong answer and givesrc=2to the lag. This is a third state neither of us named: the fields say GREEN, the binding says UNVERIFIED, and no single field separates them. A base-divergence gate that gradesbase.sha == merge_basepasses this PR — correctly — while the approval it will merge under describes a different commit. The two checks are orthogonal and a merger needs both.