fix(bootstrap): refuse a commit-SHA pin with a pin-a-release-tag remedy, and stop recommending SHA pins (#1514) #1515

Merged
alex merged 1 commit from claude/1514-sha-pin-bootstrap-refusal into main 2026-10-02 19:47:33 +02:00
Member

Refs #1514

Implements option (D) of #1514, as decided in comment 114178: refuse a commit-SHA pin at bootstrap rt with a remedy, and narrow docs/SECURITY.md to what ships.

What changes

  • scripts/bootstrap-rt.sh: the *) arm now recognises a 7–40 character hex ref as a commit pin. It refuses with the gate's name, the classification and the remedy: "toolkit ref '…' looks like a commit SHA … Pin a release tag instead (uses: ...@vX.Y.Z)". Any other unknown ref keeps the generic unsupported toolkit ref refusal. Exit status stays 1, and neither FETCH nor BUILD is reached.
  • composite/bootstrap-rt/action.yml: the ref input description names all three outcomes.
  • docs/SECURITY.md: it stops recommending uses:@<full-commit-SHA>. The second checkout still follows a SHA pin, but every reusable that installs rt refuses one, and only reusable-workflow-parse-check.yml works SHA-pinned. Lines 7, 35–37, 43 and 72–82 change, and the :72 heading no longer claims the gap is closed for SHA-pinned consumers.
  • tests/bootstrap-rt.bats: three new tests, plus a negative assertion on the existing feature/861 test. Details under Verification.
  • changelog.d/1514-sha-pin-bootstrap-refusal.fixed.md: adopter-visible, because the SECURITY.md recommendation is withdrawn.

Decision tree

Where the refusal lives.

  • In bootstrap-rt.sh (this PR) is right when the resolve-ref SHA arms should keep working where they can. It is one file, the second checkout still follows the SHA, workflow-parse-check keeps running SHA-pinned, and a later functional option (#1514's C, or A/B) reuses the arms unchanged.
  • Removing the SHA arm from the 16 bootstrapping resolve-ref steps is right when no reusable should accept a SHA pin at all. It touches 16 files plus #336's workflows.bats test, and it would have to be put back if a functional option lands.

Which refs get the SHA message.

  • 7–40 hex (this PR) is right because both resolver paths emit hex: the #336 arm passes a full SHA, and the #1222 fallback passes a short one when the baked value is 'main'. #1514's AC 9 asked for the short row to be handled or split off, and one regex range handles it. A branch named, say, deadbeef would get "looks like a commit SHA" instead of "unsupported". It is refused either way, and the remedy is the same.
  • Exactly 40 hex is right if the short row should keep the generic message. It would need a separate issue for AC 9.

What SECURITY.md says about a tag pin. Only what was already there. This PR narrows the SHA claims and adds nothing about what a tag pin guarantees for the rt binary. That is a separate question and is not folded in here.

Deliberately untouched: the comment on each of the 18 resolve-ref SHA arms still says the arm "closes the audit-boundary gap SECURITY.md names". That remains true of the second checkout, which is all the comment describes. Rewording 18 copies (9 .forgejo + 9 regenerated .gitea twins) is churn that (D) does not need. Say so if you want it in this PR.

Self-bootstrap (AGENTS.md section 2)

No re-pin: toolkit-self floats @main (AGENTS.md §2.1). scripts/bootstrap-rt.sh and the composite are not in gates.DefaultComposeScripts, and a toolkit-self run builds from its own workspace. Adopters get the new refusal from the first cut tag that contains this commit, because the composite and the script are read from .release-toolkit, checked out at the resolved ref.

Verification

Run on an LF clone at 0156d84 (base 4ffde69 = live main), in rt-bats:local (golang:1.25 + bats, jq, python3-yaml, shellcheck):

Check Result
bats tests/ (the CI invocation) 215/215 ok
shellcheck scripts/bootstrap-rt.sh, bash -n clean
yaml.safe_load on the composite ok
python3 scripts/anchor-check.py (renderer git.frankenbit.de) 58 files, 25 anchors, 0 unresolved
python3 scripts/dated-examples-check.py OK
FRAGMENT_CHECK_COVERAGE=content rt fragment-check changelog.d PASS, no warnings on the new fragment
rt register-check --stdin on each changed file 0 hits
close-keyword regex (ac_closure_check.go:21 + qualified form) on title, body and commit 0 hits

The composition test (every reusable's resolve-ref -> bootstrap chain …) extracts the real resolve-ref run: from every reusable that uses the bootstrap composite. It runs that bash against a consumer wrapper for six pin shapes and feeds the result to the real script, with both arms stubbed to succeed if reached. It grades 16 files: 8 .forgejo and 8 .gitea. A second, text-based discovery must find the same set, and each provider must contribute at least one file.

Pin shape (baked value at that pin) Resolved ref Bootstrap
tag (v0.63.1) v0.63.1 FETCH (control)
main (main) main BUILD (control)
full SHA (v0.63.1) the SHA refused, remedy named
full SHA (main) the SHA refused, remedy named
12-hex (main) the 12-hex refused, remedy named
12-hex (v0.63.1) v0.63.1 FETCH

Mutation check (AGENTS.md §8 Axis B). Each mutant ran on a throwaway copy, and its diff was confirmed non-empty:

Mutant Turns red
M1 hex check removed (main's script) commit-SHA unit test, composition test
M2 regex widened to any hex length boundary test (6 and 41 hex)
M3 regex narrowed to 40 only commit-SHA unit test, composition test (12-hex row)
M4 remedy dropped from the message commit-SHA unit test
M5 SHA message for every unknown ref feature/861 test, boundary test
M6 one .gitea twin loses its SHA arm composition test, naming that file
M7 composite maps RT_REF composition test
M8 a reusable stops feeding resolve-ref's output to the bootstrap composition test

#1514 acceptance criteria

AC Here
1 decision recorded comment 114178 (option D)
2 every emitted shape reaches an arm or a refusal naming the remedy bootstrap-rt.sh, graded by the composition test across 16 files
3 composition test, glob-discovered, controls, mutation-verified new test, M1–M8
4 non-hex refusal still holds feature/861 test, now also asserting it does not get the SHA message
5 SECURITY.md matches shipped behaviour lines 7, 35–37, 43, 72–82
6 composite ref description action.yml:8-12
7 ADR-0008 amendment not applicable: (D) adds no build or image path
8 measure Forgejo's @<40-hex> resolution before a doc claims SHA pins work not applicable: no doc claims they work now
9 short-hex row handled: 7–40 hex range, composition test row 5
10 fragment changelog.d/1514-sha-pin-bootstrap-refusal.fixed.md

Review request

@alex, please verify at source (AGENTS.md §6). Both halves need checking:

  1. The script half: run bats tests/bootstrap-rt.bats on the head, and read the refusal text a SHA-pinned adopter would see, including whether the remedy is enough on its own.
  2. The doc half: read docs/SECURITY.md against the script and decide whether the narrowed claims at lines 35–37, 43 and 72–82 say exactly what ships. In particular, does anything still imply that a SHA pin works?

Pin your review to the head with rt review-submit-check --owner frankenbit --repo release-toolkit --pr 1515 --reviewed-head <full sha> (§6.2).

🤖 Generated with Claude Code

Refs #1514 Implements option (D) of #1514, as decided in comment 114178: refuse a commit-SHA pin at `bootstrap rt` with a remedy, and narrow `docs/SECURITY.md` to what ships. ## What changes - **`scripts/bootstrap-rt.sh`**: the `*)` arm now recognises a 7–40 character hex ref as a commit pin. It refuses with the gate's name, the classification and the remedy: *"toolkit ref '…' looks like a commit SHA … Pin a release tag instead (uses: ...@vX.Y.Z)"*. Any other unknown ref keeps the generic `unsupported toolkit ref` refusal. Exit status stays 1, and neither FETCH nor BUILD is reached. - **`composite/bootstrap-rt/action.yml`**: the `ref` input description names all three outcomes. - **`docs/SECURITY.md`**: it stops recommending `uses:@<full-commit-SHA>`. The second checkout still follows a SHA pin, but every reusable that installs `rt` refuses one, and only `reusable-workflow-parse-check.yml` works SHA-pinned. Lines 7, 35–37, 43 and 72–82 change, and the `:72` heading no longer claims the gap is closed for SHA-pinned consumers. - **`tests/bootstrap-rt.bats`**: three new tests, plus a negative assertion on the existing `feature/861` test. Details under **Verification**. - **`changelog.d/1514-sha-pin-bootstrap-refusal.fixed.md`**: adopter-visible, because the SECURITY.md recommendation is withdrawn. ## Decision tree **Where the refusal lives.** - *In `bootstrap-rt.sh` (this PR)* is right when the resolve-ref SHA arms should keep working where they can. It is one file, the second checkout still follows the SHA, workflow-parse-check keeps running SHA-pinned, and a later functional option (#1514's C, or A/B) reuses the arms unchanged. - *Removing the SHA arm from the 16 bootstrapping resolve-ref steps* is right when no reusable should accept a SHA pin at all. It touches 16 files plus #336's `workflows.bats` test, and it would have to be put back if a functional option lands. **Which refs get the SHA message.** - *7–40 hex (this PR)* is right because both resolver paths emit hex: the #336 arm passes a full SHA, and the #1222 fallback passes a short one when the baked value is `'main'`. #1514's AC 9 asked for the short row to be handled or split off, and one regex range handles it. A branch named, say, `deadbeef` would get "looks like a commit SHA" instead of "unsupported". It is refused either way, and the remedy is the same. - *Exactly 40 hex* is right if the short row should keep the generic message. It would need a separate issue for AC 9. **What SECURITY.md says about a tag pin.** Only what was already there. This PR narrows the SHA claims and adds nothing about what a tag pin guarantees for the `rt` binary. That is a separate question and is not folded in here. **Deliberately untouched:** the comment on each of the 18 resolve-ref SHA arms still says the arm *"closes the audit-boundary gap SECURITY.md names"*. That remains true of the second checkout, which is all the comment describes. Rewording 18 copies (9 `.forgejo` + 9 regenerated `.gitea` twins) is churn that (D) does not need. Say so if you want it in this PR. ## Self-bootstrap (AGENTS.md section 2) No re-pin: toolkit-self floats `@main` (AGENTS.md §2.1). `scripts/bootstrap-rt.sh` and the composite are not in `gates.DefaultComposeScripts`, and a toolkit-self run builds from its own workspace. Adopters get the new refusal from the first cut tag that contains this commit, because the composite and the script are read from `.release-toolkit`, checked out at the resolved ref. ## Verification Run on an LF clone at `0156d84` (base `4ffde69` = live `main`), in `rt-bats:local` (golang:1.25 + bats, jq, python3-yaml, shellcheck): | Check | Result | |---|---| | `bats tests/` (the CI invocation) | 215/215 ok | | `shellcheck scripts/bootstrap-rt.sh`, `bash -n` | clean | | `yaml.safe_load` on the composite | ok | | `python3 scripts/anchor-check.py` (renderer `git.frankenbit.de`) | 58 files, 25 anchors, 0 unresolved | | `python3 scripts/dated-examples-check.py` | OK | | `FRAGMENT_CHECK_COVERAGE=content rt fragment-check changelog.d` | PASS, no warnings on the new fragment | | `rt register-check --stdin` on each changed file | 0 hits | | close-keyword regex (`ac_closure_check.go:21` + qualified form) on title, body and commit | 0 hits | **The composition test** (`every reusable's resolve-ref -> bootstrap chain …`) extracts the real resolve-ref `run:` from every reusable that uses the bootstrap composite. It runs that bash against a consumer wrapper for six pin shapes and feeds the result to the real script, with both arms stubbed to succeed if reached. It grades 16 files: 8 `.forgejo` and 8 `.gitea`. A second, text-based discovery must find the same set, and each provider must contribute at least one file. | Pin shape (baked value at that pin) | Resolved ref | Bootstrap | |---|---|---| | tag (`v0.63.1`) | `v0.63.1` | FETCH (control) | | `main` (`main`) | `main` | BUILD (control) | | full SHA (`v0.63.1`) | the SHA | refused, remedy named | | full SHA (`main`) | the SHA | refused, remedy named | | 12-hex (`main`) | the 12-hex | refused, remedy named | | 12-hex (`v0.63.1`) | `v0.63.1` | FETCH | **Mutation check** (AGENTS.md §8 Axis B). Each mutant ran on a throwaway copy, and its diff was confirmed non-empty: | Mutant | Turns red | |---|---| | M1 hex check removed (main's script) | commit-SHA unit test, composition test | | M2 regex widened to any hex length | boundary test (6 and 41 hex) | | M3 regex narrowed to 40 only | commit-SHA unit test, composition test (12-hex row) | | M4 remedy dropped from the message | commit-SHA unit test | | M5 SHA message for every unknown ref | `feature/861` test, boundary test | | M6 one `.gitea` twin loses its SHA arm | composition test, naming that file | | M7 composite maps `RT_REF` | composition test | | M8 a reusable stops feeding resolve-ref's output to the bootstrap | composition test | ## #1514 acceptance criteria | AC | Here | |---|---| | 1 decision recorded | comment 114178 (option D) | | 2 every emitted shape reaches an arm or a refusal naming the remedy | `bootstrap-rt.sh`, graded by the composition test across 16 files | | 3 composition test, glob-discovered, controls, mutation-verified | new test, M1–M8 | | 4 non-hex refusal still holds | `feature/861` test, now also asserting it does not get the SHA message | | 5 SECURITY.md matches shipped behaviour | lines 7, 35–37, 43, 72–82 | | 6 composite `ref` description | `action.yml:8-12` | | 7 ADR-0008 amendment | not applicable: (D) adds no build or image path | | 8 measure Forgejo's `@<40-hex>` resolution before a doc claims SHA pins work | not applicable: no doc claims they work now | | 9 short-hex row | handled: 7–40 hex range, composition test row 5 | | 10 fragment | `changelog.d/1514-sha-pin-bootstrap-refusal.fixed.md` | ## Review request @alex, please **verify at source** (AGENTS.md §6). Both halves need checking: 1. **The script half**: run `bats tests/bootstrap-rt.bats` on the head, and read the refusal text a SHA-pinned adopter would see, including whether the remedy is enough on its own. 2. **The doc half**: read `docs/SECURITY.md` against the script and decide whether the narrowed claims at lines 35–37, 43 and 72–82 say exactly what ships. In particular, does anything still imply that a SHA pin works? Pin your review to the head with `rt review-submit-check --owner frankenbit --repo release-toolkit --pr 1515 --reviewed-head <full sha>` (§6.2). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(bootstrap): refuse a commit-SHA pin with a pin-a-release-tag remedy, and stop recommending SHA pins (#1514)
All checks were successful
tests / workflow-schema (pull_request) Successful in 5s
tests / bats (pull_request) Successful in 34s
tests / dated-examples (pull_request) Successful in 5s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 4s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 5s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 4s
workflow-parse-check / check (pull_request) Successful in 0s
ac-closure-check / ac-closure check (pull_request) Successful in 6s
fragment-check / coverage (pull_request) Successful in 7s
fragment-check / density (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 7s
go-ci / page landing-tree failure (pull_request) Has been skipped
check-self-bootstrap / check (push) Successful in 5s
gitea-twin-check / check (push) Successful in 5s
go-ci / lint + build + test (push) Successful in 30s
go-ci / record reviewed vs landed commit (push) Successful in 3s
prepared-uncut-check / toolkit-self prepared-uncut controls (push) Successful in 5s
prepared-uncut-check / prepared-but-uncut release (push) Successful in 7s
prepared-uncut-check / check (push) Successful in 0s
release / decide + act (push) Successful in 12s
release / release (push) Successful in 0s
tests / workflow-schema (push) Successful in 3s
tests / bats (push) Successful in 32s
tests / dated-examples (push) Successful in 5s
tests / contract-paths (push) Successful in 5s
tests / shellcheck (push) Successful in 3s
go-ci / page landing-tree failure (push) Has been skipped
release / fire-cut (push) Has been skipped
0156d84fe7
Since v0.62.0, docs/SECURITY.md has recommended pinning a reusable by
full commit SHA. resolve-ref's #336 arm hands that SHA to the shared
bootstrap, and scripts/bootstrap-rt.sh has arms only for v* and main,
so every reusable that installs rt (8 of 9, plus their .gitea twins)
refused the pin as an "unsupported toolkit ref" with no remedy. The
#1222 fallback hands over a short SHA the same way.

The bootstrap now recognises a 7-40 character hex ref as a commit pin.
It refuses that pin with the gate's name and the remedy: pin a release
tag. rt is published per release tag, not per commit. The resolve-ref
SHA arms stay, so the second checkout still follows the SHA, and
reusable-workflow-parse-check, which runs no rt, still works
SHA-pinned. docs/SECURITY.md and the composite's ref description now
say so.

The new composition test runs every bootstrapping reusable's real
resolve-ref bash into the script, for tag, main, full-SHA and
short-SHA pins. #336's test ran the resolver alone and #861's ran the
script alone, so the commit-pin shape fell between them. Eight
mutants each turn the intended test red.

Refs #1514

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude requested review from alex 2026-10-02 18:43:14 +02:00
alex approved these changes 2026-10-02 19:47:30 +02:00
alex merged commit 0156d84fe7 into main 2026-10-02 19:47:33 +02:00
alex deleted branch claude/1514-sha-pin-bootstrap-refusal 2026-10-02 19:47:33 +02:00

Landing identity record

  • PR: #1515
  • landed commit (server merge_commit_sha): 0156d84fe764db2a0b02bae6da91af22b11ecf14
  • effective official approval(s):
    • @alex, review #7190, stamped commit: 0156d84fe764db2a0b02bae6da91af22b11ecf14
  • replay comparison: no identity change (stamped SHA equals landed SHA)

This is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.

<!-- release-toolkit:landing-review-record-v1 landed=0156d84fe764db2a0b02bae6da91af22b11ecf14 --> ## Landing identity record - PR: #1515 - landed commit (server merge_commit_sha): `0156d84fe764db2a0b02bae6da91af22b11ecf14` - effective official approval(s): - @alex, review #7190, stamped commit: `0156d84fe764db2a0b02bae6da91af22b11ecf14` - replay comparison: no identity change (stamped SHA equals landed SHA) This is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
frankenbit/release-toolkit!1515
No description provided.