Skip to content

fix(governance): gate the lock-gate's own pin on freshness, and bump it past #946 - #962

Merged
hyperpolymath merged 2 commits into
mainfrom
fix/lock-gate-pin-freshness-gate
Sep 22, 2026
Merged

hyperpolymath merged 2 commits into
mainfrom
fix/lock-gate-pin-freshness-gate

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

What this fixes

The lock gate is staged from a third pin.

There are not two pins in this system, there are three:

  1. the caller's uses: hyperpolymath/standards/...@<sha> ref,
  2. actions.lock's record of that ref, and
  3. a SHA hardcoded inside governance-reusable.yml for its own
    actions/checkout of 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_sha resolves to the
caller's), which is what forces the hardcode in the first place.

standards#946 fixed scripts/update-actions-lock.sh so advisory findings stop
counting 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 the
two wrappers disagree on the same tree —

wrapper staged from --verify-local rc grep -c is_advisory_category
4f7f02ca (what CI ran) 1 0
e977cc67 (post-#946) 0 present

Why no existing control caught it

The pin's shape was already guarded, correctly:
tests/test_governance_reusable_shape.sh:63-64 asserts ref: [0-9a-f]{40} and
refuses ref: main, scoped to the actions-lock-verify job.

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 ... changes comment. A comment
is not a gate, and this PR is the difference.

Separately, scripts/tests/governance-reusable-contract-test.sh bound its
checkout 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:

the pinned commit must already contain everything on the compare ref,
path-scoped to the step's own sparse-checkout: list.

  • pull_request → compare is the PR's base SHA. A PR that edits a helper
    passes (its edit is not on base yet). A PR opened while main is already
    stale is forced to bump, and can, because the needed commit exists.
  • push to main → compare is HEAD. Red exactly when a helper change has
    just landed and the bump is owed; healed by the very next PR, which the
    pull_request run 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, each
against a throwaway git repo with real commits, fully offline:

control asserts
stale pin is refused rc=1, names scripts/update-actions-lock.sh
stale report is path-scoped never names the unrelated file that also changed
fresh pin is accepted rc=0
unrelated divergence does not fail it a rebase must not redden the gate
ref: main is refused pinning is the point
abbreviated sha is refused 40-hex only
renamed step fails loudly rc=1 — the exact way the contract test lost its subject
unresolvable pin fails, not skips a skip is indistinguishable from a pass
missing ref: is refused would follow the default branch
empty staged scope is refused nothing to compare is not a free pass

Meta-mutant. Removing the path scoping from the guard
(git diff --name-only $pin $compare -- $paths → without -- $paths) kills
exactly 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-error and it is not a
::warning::, which cannot fail a job.

Notes

  • self-test.yml gains fetch-depth: 0. The guard compares two commits and
    fails rather than skips on an unresolvable pin, so the history is a
    requirement, not an optimisation. This is a with: change only — no uses:
    ref moves, so actions.lock is untouched.
  • The new Self Test step passes the base SHA through env:, not by
    interpolating an expression into the run: body. The repo's own injection
    scanner (tests/test_tag_ruleset_canon.sh) still passes.
  • grep -A N cannot delimit the step block: it is 19 lines today, so any fixed
    N is either short of the ref: or long enough to capture the next step's
    ref: and assert against the wrong pin. Both the guard and the contract test
    take the range from - name: to - name: with awk.

⚠ This does not turn burble#226 green on its own

metadatastician/burble#226 pins standards at e977cc67, and that copy of
governance-reusable.yml still carries 4f7f02ca at the lock-gate step. Pin 3
travels with the pinned YAML.

Sequence: merge this → take the resulting SHA → re-bump burble#226 to it
(all 9 sites plus actions.lock, transitive uses: list re-extracted against a
positive control) → then #226 can go green.

Out of scope, filed separately

The dupkey pin 317101e0 is also stale — 45 files differ under scripts/
versus main. Its sparse-checkout is the whole scripts directory, so the
same 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

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
@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 34 minutes.

Check out review usage here.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 9ebe7928-4259-4d64-9baf-4559cce2b891

📥 Commits

Reviewing files that changed from the base of the PR and between 3a7c90a and 04593c8.

📒 Files selected for processing (5)
  • .github/workflows/governance-reusable.yml
  • .github/workflows/self-test.yml
  • scripts/check-lock-gate-pin-freshness.sh
  • scripts/tests/check-lock-gate-pin-freshness-test.sh
  • scripts/tests/governance-reusable-contract-test.sh

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hyperpolymath

Copy link
Copy Markdown
Owner Author

The two reds on this PR are inherited from main, not caused by it

scan / Hypatia Neurosymbolic Analysis and governance / Validate Hypatia Baseline fail on this
branch. They also fail on main at 9c256b67 — the exact commit this branch is based on.

hypatia-scan.yml on main:
  failure  2026-09-22T10:15:10Z  9c256b67   <- this PR's base
  success  2026-09-22T10:10:08Z  be6c9580
  success  2026-09-22T09:59:46Z  e977cc67

The single finding that survives the baseline names a file this PR does not touch:

{
  "type": "invalid_actions_lock",
  "file": ".github/workflows/actions.lock",
  "severity": "high",
  "baseline_status": "new",
  "reason": "actions.lock failed closed: {:transitive_dependencies_missing,
             [{\"asana/push-signed-commits@d615ca88...\", \"actions/setup-python@v2\"}]}"
}

This PR changes governance-reusable.yml, self-test.yml and three files under scripts/.
It moves no uses: ref, so actions.lock is untouched — and uses ⊆ actions.lock and
governance / Actions lockfile verify are both green here.

Filed separately as a main regression rather than carried as a blocker on this PR, per the
stopping rule.

What this PR's own changes did in CI

  • Repo self-tests — success. That is the new guard, its 10 mutant controls and the extended
    contract test, running against real history with fetch-depth: 0.
  • governance / Actions lockfile verify — success.
  • scan / gitleaks, Workflow syntax and action locks, uses ⊆ actions.lock — success.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm

@hyperpolymath
hyperpolymath merged commit 051f98e into main Sep 22, 2026
35 of 36 checks passed
@hyperpolymath
hyperpolymath deleted the fix/lock-gate-pin-freshness-gate branch September 22, 2026 10:53
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant