fix(governance): gate the lock-gate's own pin on freshness, and bump it past #946 - #962
Conversation
The lock gate is staged from a THIRD pin. Not the caller's `uses:` ref and not actions.lock's record of it, but a SHA hardcoded inside governance-reusable.yml for its own `actions/checkout`. A called reusable workflow has no context exposing its own commit, so the hardcode is forced. standards#946 fixed `scripts/update-actions-lock.sh` and did not bump that pin. Every caller therefore kept being judged by the pre-#946 verifier, including metadatastician/burble#226, which had bumped its own pin specifically to pick the fix up and still went red on `governance / Actions lockfile verify`. Measured: the failing job logged `HEAD is now at 4f7f02c`; that wrapper returns rc=1 where the fixed one returns rc=0. The pin's SHAPE was already guarded — tests/test_governance_reusable_shape.sh asserts 40-hex and refuses `ref: main`, job-scoped and aimed correctly. Its CURRENCY was guarded by nothing. A well-formed SHA can point at stale tooling, and did. That is the guard/consumer trap in its usual form: the guard asks "is this 40 hex characters?", the consumer needs "does this contain today's verifier?". The obvious predicate deadlocks. "The pin contains the working tree" would force a PR that edits a helper to pin to its own merge commit, which does not exist yet. So the assertion is that the pin already contains everything on the COMPARE ref, path-scoped to the step's own sparse-checkout list: * pull_request -> base SHA. A PR editing a helper passes; a PR opened while main is already stale is forced to bump, and can. * push to main -> HEAD. Red exactly when the bump is owed, healed by the next PR, which pull_request will not let through unbumped. The pin is one change behind by construction. That is intended, and the comment at the pin now says so instead of asking a human to remember. Scope is read out of the step's `sparse-checkout:` list rather than hardcoded, so the guard cannot drift from what the step actually stages. - scripts/check-lock-gate-pin-freshness.sh — the guard. Fails, never skips, on an unresolvable pin or a renamed subject. - scripts/tests/check-lock-gate-pin-freshness-test.sh — 10 controls against a throwaway git repo, offline. Meta-mutant: removing the path scoping kills exactly the two controls that assert it. - governance-reusable.yml — pin 4f7f02c -> 9c256b6; comment rewritten. - self-test.yml — fetch-depth: 0 (the guard needs both objects) and the step. - governance-reusable-contract-test.sh — bind an assertion to `Checkout standards for the lock gate`. It bound only to the DUPKEY step, whose name shares the nouns "checkout", "pinned" and "standards"; the block is delimited by awk range, because `grep -A N` either stops short of the `ref:` or reaches the next step's. Does NOT close burble#226 on its own: #226 pins e977cc6, whose copy of this workflow still carries 4f7f02c. #226 must re-bump to the SHA this PR produces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
|
Warning Review limit reachedNext included review available in 34 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (5)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The two reds on this PR are inherited from
|
|



What this fixes
The lock gate is staged from a third pin.
There are not two pins in this system, there are three:
uses: hyperpolymath/standards/...@<sha>ref,actions.lock's record of that ref, andgovernance-reusable.ymlfor its ownactions/checkoutof the lock-gate tooling.Bumping a caller cannot reach the third one. A called reusable workflow has no
reliable context exposing its own commit (
github.workflow_sharesolves to thecaller's), which is what forces the hardcode in the first place.
standards#946fixedscripts/update-actions-lock.shso advisory findings stopcounting toward the blocking tally — and did not bump pin 3. So every caller
kept being judged by the pre-#946 verifier, including
metadatastician/burble#226,which had bumped its own pin specifically to pick that fix up and still went
red on
governance / Actions lockfile verify.Measured, not inferred: the failing job logs
HEAD is now at 4f7f02c, and thetwo wrappers disagree on the same tree —
--verify-localrcgrep -c is_advisory_category4f7f02ca(what CI ran)e977cc67(post-#946)Why no existing control caught it
The pin's shape was already guarded, correctly:
tests/test_governance_reusable_shape.sh:63-64assertsref: [0-9a-f]{40}andrefuses
ref: main, scoped to theactions-lock-verifyjob.Its currency was guarded by nothing. A perfectly well-formed 40-hex SHA can
point at stale tooling, and this one did for the whole life of #946.
That is the guard/consumer trap in its plainest form: the guard asks "is this
40 hex characters?", the consumer needs "does this contain today's verifier?"
The file already carried a
⚠ BUMP THIS whenever ... changescomment. A commentis not a gate, and this PR is the difference.
Separately,
scripts/tests/governance-reusable-contract-test.shbound itscheckout assertions only to the step named
Checkout the pinned Standards policy helpers— the dupkey step. The lock gate is a different step,Checkout standards for the lock gate, and the two share the nouns "checkout","pinned" and "standards", so a name-match guard written for one proves nothing
about the other. It now names the lock-gate step too.
The predicate, and why it is not the obvious one
The obvious assertion — the pin contains the working tree's helpers —
deadlocks. A PR that edits a helper would have to pin to its own merge
commit, which does not exist yet. Unsatisfiable-in-PR is the same failure class
as a required check that can never report.
So the assertion is:
pull_request→ compare is the PR's base SHA. A PR that edits a helperpasses (its edit is not on base yet). A PR opened while
mainis alreadystale is forced to bump, and can, because the needed commit exists.
pushtomain→ compare isHEAD. Red exactly when a helper change hasjust landed and the bump is owed; healed by the very next PR, which the
pull_requestrun will not let through unbumped.Under this predicate, #954 would have been forced to bump after #946 landed,
and burble#226 would have gone green on its first attempt.
The pin is therefore one change behind by construction. That is inherent, it
is acceptable, and the comment at the pin now says so rather than asking a human
to remember.
Comparison is path-scoped, so a rebase or any unrelated commit cannot fail
it — only a real divergence in the staged tooling can.
Scope is read out of the step's own
sparse-checkout:list, never hardcoded,so adding a file to what the gate stages automatically extends what the guard
protects. A hardcoded list here would itself be a guard asking a different
question than its consumer.
Verification
scripts/tests/check-lock-gate-pin-freshness-test.sh— 10 controls, eachagainst a throwaway git repo with real commits, fully offline:
scripts/update-actions-lock.shref: mainis refusedref:is refusedMeta-mutant. Removing the path scoping from the guard
(
git diff --name-only $pin $compare -- $paths→ without-- $paths) killsexactly the two controls that assert it, 8 passed / 2 failed. Restored, 10/10.
Contract-test mutants.
ref: main→FAIL: the lock gate is not staged from an immutable 40-hex commit. Renaming the step →FAIL: governance workflow has no step named 'Checkout standards for the lock gate'. Both rc=1.Full suite: all 51 test files pass on this branch.
The guard fails the job. It is not
continue-on-errorand it is not a::warning::, which cannot fail a job.Notes
self-test.ymlgainsfetch-depth: 0. The guard compares two commits andfails rather than skips on an unresolvable pin, so the history is a
requirement, not an optimisation. This is a
with:change only — nouses:ref moves, so
actions.lockis untouched.env:, not byinterpolating an expression into the
run:body. The repo's own injectionscanner (
tests/test_tag_ruleset_canon.sh) still passes.grep -A Ncannot delimit the step block: it is 19 lines today, so any fixedNis either short of theref:or long enough to capture the next step'sref:and assert against the wrong pin. Both the guard and the contract testtake the range from
- name:to- name:with awk.⚠ This does not turn burble#226 green on its own
metadatastician/burble#226pins standards ate977cc67, and that copy ofgovernance-reusable.ymlstill carries4f7f02caat the lock-gate step. Pin 3travels with the pinned YAML.
Sequence: merge this → take the resulting SHA → re-bump burble#226 to it
(all 9 sites plus
actions.lock, transitiveuses:list re-extracted against apositive control) → then #226 can go green.
Out of scope, filed separately
The dupkey pin
317101e0is also stale — 45 files differ underscripts/versus
main. Itssparse-checkoutis the wholescriptsdirectory, so thesame predicate applied verbatim would be permanently red and useless; it needs a
scope narrowed to what that step actually executes. Per the stopping rule that is
an issue with acceptance criteria, not scope for this PR.
🤖 Generated with Claude Code
https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm