bug(bootstrap): a SHA-pinned consumer fails at bootstrap rt in 16 of 18 reusable files; resolve-ref emits the SHA and bootstrap-rt.sh has no arm for it #1514
Labels
No labels
bump
major
bump
minor
bump
patch
kind
bug
kind
chore
kind
docs
kind
epic
kind
feature
loop/paused
mirror-drift
priority
critical
priority
high
priority
low
priority
medium
size
L
size
M
size
S
size
XL
status
blocked
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit#1514
Loading…
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?
A consumer that pins a reusable workflow by a full 40-hex commit SHA fails at
bootstrap rtin 8 of the 9 baked reusables, and in their 8.giteatwins. That is the posturedocs/SECURITY.mdrecommends for higher security. #336's resolve-ref arm passes the SHA to the shared bootstrap.scripts/bootstrap-rt.shhas arms only forv[0-9]*andmain, so it refuses the SHA with a message that names neither SHA pinning nor a remedy.Nothing is broken today: no consumer on this instance pins by SHA (see Who is affected). It is a shipped promise that fails on first use.
Measured
At
origin/main4ffde69(2026-10-02). The files involved are byte-identical toe4c0f35except for one line ofdocs/SECURITY.md, which moved in #1512.The chain, link by link:
.forgejo/workflows/reusable-release.yml:332-343, stepresolve toolkit refWRAPPER_REF =~ ^[0-9a-fA-F]{40}$, soREF=<that SHA>andSOURCE="consumer SHA pin (release-toolkit#336)". This arm comes before the baked-value check, so the SHA wins whatever the marker says.reusable-release.yml:357-362,checkout release-toolkit.release-toolkitat the SHA, which is #336's goal.reusable-release.yml:421-424,bootstrap rtref: ${{ steps.resolve-ref.outputs.ref }}. The SHA goes in unmapped. A step's outputs are fixed once it ends, and no step in between remaps the ref.composite/bootstrap-rt/action.yml:51RT_REF: ${{ inputs.ref }}, unmapped. Line 8 documents the input as "a v... release tag or main". The cache step (:42) is skipped because the ref does not start withv.scripts/bootstrap-rt.sh:31-72v[0-9]*)FETCH,main)BUILD, and*)→fail "unsupported toolkit ref '${ref}'; only version tags (v...) and main have bootstrap arms", exit 1.The same arm sits in all 9 baked reusables (
grep 'consumer SHA pin'): ac-closure-check:207, changelog-body-check:204, changelog-fragment-check:260, manifest-check:171, prepared-uncut-check:209, recover-pending-cut:216, register-check:218, release:343, workflow-parse-check:173. Their 9.giteatwins carry it too. Every one of them except workflow-parse-check bootstrapsrtfromsteps.resolve-ref.outputs.refthrough the composite. workflow-parse-check has no bootstrap step and fetches the Forgejo validator instead, so a SHA pin works there.Result: 16 of 18 files refuse a SHA-pinned consumer at
bootstrap rt; 2 are unaffected.Reproduction
Two bats tests were added to
tests/bootstrap-rt.batsin a local LF clone. They are a reproduction, not for merge, and the full text is at the end. The composition test runs the real resolve-ref bash extracted from each of the 18 files. It writes a consumer wrapper per pin shape, feeds the resolved ref to the realscripts/bootstrap-rt.sh, and stubs both arms so each would succeed if reached. Run inrt-bats:local(bats + python3-yaml) on4ffde69:1c84e95isv0.63.1^{commit}: what an adopter gets fromgit rev-parse v0.63.1^{commit}. "Baked" means theBUILD_BAKED_TOOLKIT_REFvalue the reusable carries at the pinned commit. A tag commit carries'v0.63.1', and a main commit carries'main'.The reproduction is not a placebo (AGENTS.md §8 Axis B). Each mutant was run on a throwaway copy:
bootstrap-rt.shgains a[0-9a-fA-F]*)arm that succeedsreusable-release.yml's SHA arm keeps the baked ref (one file only)RT_REFbefore the scriptThe controls (tag pin → FETCH, main pin → BUILD, 16/16 each) pass, so the refusals come from arm selection and not from the harness.
How it got here
e287752(#861)composite/bootstrap-rt+scripts/bootstrap-rt.sh, with an explicit*)refusaltests/bootstrap-rt.bats:93pins "unknown refs refuse instead of silently building or fetching" withfeature/861688002e(#336)tests/workflows.bats:1585-1744executes only the resolve-ref step and checksref=<SHA>Each test is correct for its own step. No test composes resolve-ref with the bootstrap, so a new shape on the resolver side had no consumer-side check. #336's implementation AC records the four mutations it ran, all on the resolver.
Why it matters
docs/SECURITY.md:41: "pinninguses:@<full-commit-SHA>instead of a tag now closes the second-checkout gap end-to-end".:70is headed "SHA-pinned consumers closed", and:80says "the second checkout is now immutable end-to-end". The v0.62.0 CHANGELOG entry announces it: "a SHA-pinned consumer's second checkout now resolves through that SHA". An adopter who follows the recommendation gets a red on every job that bootstraps rt: release, manifest-check, fragment-check and the rest.rtbinary, which any fix has to source from somewhere. The decision below is mostly about what a SHA pin guarantees forrt.Who is affected
.forgejo,.giteaand.githubworkflow on the default branch of every repo this token can see: 36, or 35 besides the toolkit. Six consume the toolkit: cellblock, ember, purser, storey, tic-tac-toe and tmux-tell. Every pin is a tag. Repos this token cannot see were not measured.@main(AGENTS.md §2.1).'main') takes the #1222 wrapper-ref fallback, which emits the short hex, and the bootstrap refuses that too (16/16). A short-hex pin of a tag commit gets the baked tag and works. This issue can take that row or hand it on (AC below).Not established
uses: frankenbit/release-toolkit/.forgejo/workflows/reusable-*.yml@<40-hex>for a called reusable workflow at all.688002e's message says it does, but I found no recorded run. If it does not, the job fails before resolve-ref and this bug is masked, but SECURITY.md is still wrong. Measuring it needs a throwaway consumer repo (the AGENTS.md §14 pattern). Could-not-grade, not assumed.Decision tree: what a SHA ref should do at
bootstrap rtWhat an adopter can pin, measured on v0.63.1:
action.ymlimagevX.Y.Z^{commit}, the #794 digest-pin commit)1c84e95'v0.63.1'@sha256:a94e8eb0…(real)1c84e95^'v0.63.1''main'(A) FETCH the asset named by the marker baked at the pinned SHA. The resolver keeps the SHA for the checkout and also outputs the baked
'vX.Y.Z'it already reads, for the bootstrap and the cache key. The marker is content of the pinned tree, so the SHA-to-version step is immutable, and no tag lookup is needed.rtto the SHA, and adopters pin tag or cut commits.rtbound to the minisign key and the tag name, not the SHA. The fetch isreleases/download/<tag>(fetch-rt.sh:69), and the signedchecksums.txtlistsrt-linux-amd64with no version in the name (fetch-rt.sh:68). A signed manifest from any release therefore verifies any release's binary, and a moved tag or replaced release could serve a different signedrt. SECURITY.md would have to say so. Commits baked'main'have no asset, so they need (D).(B) BUILD
rtfrom.release-toolkit, already checked out at the SHA.rt, and SHA-pinning adopters can be asked to carry Go. It works for any commit.command -v gopreflight's remedy ("pin a release tag",bootstrap-rt.sh:51) needs a SHA variant. Every bootstrapping job pays a build.(C) Run
rtfrom the image digest pinned inaction.ymlat the SHA. On a tag commit,action.ymlpinsdocker://…/release-toolkit@sha256:…. The digest is content-addressed, sortis bound to the pinned tree without Go.(D) Refuse with an actionable message, and narrow SECURITY.md. Either give
bootstrap-rt.sha[0-9a-fA-F]{40}arm that says SHA pinning is unsupported for rt-bootstrapping reusables and names the remedy, or drop the SHA arm from the 8 bootstrapping reusables and their twins and keep it in workflow-parse-check, where it works.Combinations: (A)+(D) or (C)+(D) (function where an asset or digest exists, an honest refusal everywhere else) are coherent, and both keep the
*)refusal for non-hex refs.Lean, for the operator to overrule: (D) now, because it makes the docs true at the smallest cost while no adopter depends on the path. Then (C) when a SHA-pinning adopter appears, since it is the only option that binds
rtto the pinned tree without adding Go to adopter runners. (A) gives function cheaply, but it weakens what a SHA pin means forrtwithout saying so unless SECURITY.md is rewritten alongside it.Acceptance criteria
rtbinary, separately. — DONE in comment 114178: option (D). The second checkout follows the SHA;rtis refused with the remedy.rt, or a refusal that names SHA pinning and its remedy. This holds in all 16 bootstrapping files: the 8.forgejoreusables and their 8.giteatwins. — DONE @ #1515 (0156d84, reviewed as0156d84)scripts/bootstrap-rt.shfor every bootstrapping reusable. The file set is glob-discovered, with tag andmaincontrols. It is mutation-verified: reverting the change reddens it. The reproduction below is a starting point. — DONE @ #1515 (0156d84, reviewed as0156d84)tests/bootstrap-rt.bats:93,feature/861. — DONE @ #1515 (0156d84, reviewed as0156d84)docs/SECURITY.md:35,:41and:70-80match the shipped behaviour, including any runner requirement (Go or docker) the chosen option adds. — DONE @ #1515 (0156d84, reviewed as0156d84)composite/bootstrap-rt/action.yml:8'srefdescription names every accepted shape. — DONE @ #1515 (0156d84, reviewed as0156d84)uses: …/reusable-*.yml@<40-hex>, and the run is linked here. — N/A under option (D): no doc claims SHA pins work; the measurement moved to #1517's first AC0156d84, reviewed as0156d84)0156d84, reviewed as0156d84)Reproduction text
The full test text, against
tests/bootstrap-rt.batsat4ffde69. It is not pushed. Run it withbats -f REPRO tests/bootstrap-rt.batsin an image with bats and python3-yaml.tests/bootstrap-rt.bats additions (two @test blocks)
Decision (operator, 2026-10-02): option (D), kept narrow.
scripts/bootstrap-rt.shgets a hex arm that refuses with an actionable message: commit-SHA pins are not supported by the reusables that installrt, so pin a release tag. The arm covers 7 to 40 hex characters, so the short-hex row (the #1222 fallback, AC 9) gets the same message instead of a separate issue.reusable-workflow-parse-checkkeeps working SHA-pinned, and a later functional option would reuse them.docs/SECURITY.mdand the composite'srefdescription are narrowed to what ships.AC 7 (ADR-0008 amendment) and AC 8 (measure Forgejo's
@<40-hex>resolution) apply only if a doc claims SHA pinning works end to end. Under (D) no doc does, so the PR will mark both not applicable, with this reason.Shutting this by hand (AGENTS.md §15), every acceptance criterion ticked above.
0156d84fe764db2a0b02bae6da91af22b11ecf14onmain, via #1515, fast-forwarded. The merge commit is the head, so what landed is byte-for-byte what was reviewed.APPROVEDat0156d84.0156d84aresuccess(combinedsuccess), read 2026-10-02 after the merge.claude/1514-sha-pin-bootstrap-refusalis gone (404).Follow-ups:
rt. Low priority, to be picked up when a consumer wants it. Its first AC is the Forgejo@<40-hex>measurement that AC 8 here deferred.checksums.txtnames no version, so an older signed release passes verification under a newer tag. Found while narrowingdocs/SECURITY.mdhere.