Skip to content

Commit 051f98e

Browse files
fix(governance): gate the lock-gate's own pin on freshness, and bump it past #946 (#962)
## 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`](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.com/claude-code) https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 3a7c90a commit 051f98e

5 files changed

Lines changed: 383 additions & 8 deletions

File tree

‎.github/workflows/governance-reusable.yml‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,14 +1214,19 @@ jobs:
12141214
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
12151215
with:
12161216
repository: hyperpolymath/standards
1217-
# ⚠ BUMP THIS whenever scripts/check-actions-lock-gate.sh,
1218-
# scripts/update-actions-lock.sh or .machine_readable/lock-allow.txt
1219-
# changes, or callers are judged against a stale gate. Pinned to an
1220-
# immutable commit for the same reason as the dupkey helpers in
1221-
# workflow-lint: following `main` would let an edit in standards
1222-
# change the verdict of every already-pinned caller with no review
1223-
# in their repositories.
1224-
ref: 4f7f02ca528212c578fd56379a202d219d01abe0
1217+
# ⚠ ENFORCED, no longer a request: scripts/check-lock-gate-pin-freshness.sh
1218+
# fails Self Test unless this commit already contains everything on
1219+
# main across the sparse-checkout list below. Change one of those
1220+
# files and the NEXT pull request must bump this line — it cannot be
1221+
# this PR's own merge commit, which does not exist yet, so the pin is
1222+
# one change behind by construction and that is the intended shape.
1223+
# Pinned to an immutable commit for the same reason as the dupkey
1224+
# helpers in workflow-lint: following `main` would let an edit in
1225+
# standards change the verdict of every already-pinned caller with no
1226+
# review in their repositories. This pin is invisible to the caller's
1227+
# `uses:` ref and to actions.lock — it is a third, independent pin,
1228+
# which is precisely how it went stale across standards#946.
1229+
ref: 9c256b67486b4b30c757730e1b65b2c2d2af935b
12251230
path: .standards-lock
12261231
persist-credentials: false
12271232
sparse-checkout: |

‎.github/workflows/self-test.yml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ jobs:
3535
timeout-minutes: 15
3636
steps:
3737
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
38+
with:
39+
# check-lock-gate-pin-freshness.sh compares the lock gate's pinned
40+
# commit against the base ref. A depth-1 clone has neither object, and
41+
# that guard fails rather than skipping on an unresolvable pin, so the
42+
# history is a requirement and not an optimisation.
43+
fetch-depth: 0
3844

3945
# PyYAML is required by the secret-scanner canary. The scorecard
4046
# grounding tests execute the same checks as registry-verify, including
@@ -47,3 +53,17 @@ jobs:
4753
4854
- name: Run tests/*.sh and scripts/tests/*.sh
4955
run: bash scripts/run-shell-test-suite.sh
56+
57+
# Not part of the offline suite: this one needs real git history, so it
58+
# cannot live in scripts/tests/*.sh where a contributor runs it on a
59+
# shallow or detached tree. On a pull request the comparison is the base
60+
# SHA, so a PR that edits a helper is not asked to pin to its own
61+
# unborn merge commit; on main it is HEAD, so the bump owed after such a
62+
# PR lands shows up immediately instead of rotting silently.
63+
- name: Lock-gate pin is not stale
64+
env:
65+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
66+
run: |
67+
set -euo pipefail
68+
COMPARE="${BASE_SHA:-$GITHUB_SHA}"
69+
bash scripts/check-lock-gate-pin-freshness.sh "$COMPARE"
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
#
4+
# check-lock-gate-pin-freshness.sh — the lock gate's own pin must not be stale.
5+
#
6+
# WHY THIS EXISTS
7+
# ---------------
8+
# `governance-reusable.yml` stages the lock-gate tooling from a THIRD pin: not
9+
# the caller's `uses:` ref and not the lockfile's record of it, but a SHA
10+
# hardcoded inside the callee for its own `actions/checkout`. A called reusable
11+
# workflow has no context exposing its own commit, so the hardcode is forced.
12+
#
13+
# That third pin is invisible to every other control. When standards#946 fixed
14+
# `scripts/update-actions-lock.sh`, this pin still pointed at the commit BEFORE
15+
# the fix, so every caller kept being judged by the broken verifier — including
16+
# callers that had just bumped specifically to pick the fix up. The file already
17+
# carried a comment saying "BUMP THIS", but a comment is not a gate.
18+
#
19+
# THE PREDICATE, AND WHY IT IS THIS ONE
20+
# -------------------------------------
21+
# The obvious assertion — "the pin contains the working tree's helpers" —
22+
# DEADLOCKS: a PR that edits a helper would have to pin to its own merge commit,
23+
# which does not exist yet. Unsatisfiable in-PR is the same failure class as a
24+
# required check that can never report.
25+
#
26+
# So the predicate is:
27+
#
28+
# the pinned commit must already contain everything that is on the
29+
# COMPARE ref (main), over exactly the paths the step stages.
30+
#
31+
# * on a pull request, COMPARE is the PR's base SHA. A PR that edits a helper
32+
# PASSES — its edit is not on base yet. A PR opened while main is already
33+
# stale is FORCED to bump, and can, because the needed commit exists.
34+
# * on a push to main, COMPARE is HEAD. Red exactly when a helper change has
35+
# just landed and the bump is owed; healed by the next PR, which the
36+
# pull_request run will not let through unbumped.
37+
#
38+
# The comparison is path-scoped, so a rebase or any unrelated commit cannot fail
39+
# it — only a real divergence in the staged tooling can.
40+
#
41+
# SCOPE IS TAKEN FROM THE STEP, NOT HARDCODED
42+
# -------------------------------------------
43+
# The paths compared are read out of the step's own `sparse-checkout:` list, so
44+
# adding a file to what the gate stages automatically extends what this guard
45+
# protects. A hardcoded list here would be a guard asking a different question
46+
# than its consumer, which is the exact trap this file belongs to.
47+
#
48+
# Usage: check-lock-gate-pin-freshness.sh [COMPARE_REF] (default: origin/main)
49+
50+
set -uo pipefail
51+
52+
STEP_NAME='Checkout standards for the lock gate'
53+
54+
fail() {
55+
echo "::error::lock-gate pin freshness: $*" >&2
56+
exit 1
57+
}
58+
59+
# Resolve the workflow path INSIDE the function, never at script load: a fixture
60+
# override exported by a test after the top-level assignment would otherwise be
61+
# ignored and every mutant would silently read the real tree and "pass".
62+
workflow_path() {
63+
local root
64+
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
65+
printf '%s\n' "${LOCK_GATE_WORKFLOW:-$root/.github/workflows/governance-reusable.yml}"
66+
}
67+
68+
# Print the step's YAML block: from its `- name:` line to the next `- name:` at
69+
# the same indent, exclusive. `grep -A N` cannot do this — the block is 19 lines
70+
# today and any fixed N is either short of the `ref:` or long enough to capture
71+
# the NEXT step's `ref:` and assert against the wrong pin.
72+
step_block() {
73+
awk -v want="- name: $STEP_NAME" '
74+
index($0, want) { inblock = 1; indent = match($0, /-/); next }
75+
inblock && /^[[:space:]]*- name:/ && match($0, /-/) == indent { exit }
76+
inblock { print }
77+
' "$(workflow_path)"
78+
}
79+
80+
main() {
81+
local compare="${1:-origin/main}"
82+
local wf block pin paths diverged
83+
84+
wf="$(workflow_path)"
85+
[ -f "$wf" ] || fail "workflow not found: $wf"
86+
87+
block="$(step_block)"
88+
[ -n "$block" ] ||
89+
fail "no step named '$STEP_NAME' in $wf — the guard has lost its subject; \
90+
rename it here too rather than deleting the assertion"
91+
92+
pin="$(printf '%s\n' "$block" | sed -n 's/^[[:space:]]*ref:[[:space:]]*\([^[:space:]#]*\).*/\1/p' | head -1)"
93+
[ -n "$pin" ] || fail "step '$STEP_NAME' has no 'ref:' — it would follow the default branch"
94+
printf '%s' "$pin" | grep -Eq '^[0-9a-f]{40}$' ||
95+
fail "step '$STEP_NAME' is pinned to '$pin', not an immutable 40-hex commit"
96+
97+
# The staged scope IS the guarded scope.
98+
paths="$(printf '%s\n' "$block" | awk '
99+
/^[[:space:]]*sparse-checkout:[[:space:]]*\|/ { inlist = 1; next }
100+
inlist && /^[[:space:]]*[a-z-]+:/ { inlist = 0 }
101+
inlist && NF { gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print }
102+
')"
103+
[ -n "$paths" ] || fail "step '$STEP_NAME' stages no sparse-checkout paths — nothing to compare"
104+
105+
git rev-parse --verify --quiet "$compare^{commit}" >/dev/null ||
106+
fail "compare ref '$compare' is not resolvable in this clone"
107+
108+
# A missing pin object must FAIL, never skip: a skip is indistinguishable from
109+
# a pass and this guard exists because an unasserted pin rotted unnoticed.
110+
if ! git cat-file -e "$pin^{commit}" 2>/dev/null; then
111+
git fetch --quiet --depth=1 origin "$pin" 2>/dev/null || true
112+
git cat-file -e "$pin^{commit}" 2>/dev/null ||
113+
fail "pinned commit $pin is not present and could not be fetched — \
114+
give the checkout 'fetch-depth: 0' or grant the fetch network access; \
115+
this guard does not pass on an unverifiable pin"
116+
fi
117+
118+
# shellcheck disable=SC2086
119+
diverged="$(git diff --name-only "$pin" "$compare" -- $paths)"
120+
121+
if [ -n "$diverged" ]; then
122+
echo "::error::The lock gate is staged from $pin, which does NOT contain what is already on $compare." >&2
123+
echo "Stale in the pinned tree:" >&2
124+
printf ' %s\n' $diverged >&2
125+
echo >&2
126+
echo "Every caller of governance-reusable.yml is being judged by that older tooling," >&2
127+
echo "including callers that bumped their own pin specifically to pick up the fix." >&2
128+
echo "Cure: set 'ref:' under '$STEP_NAME' to a commit containing the above" >&2
129+
echo "(usually the current tip of main), in this PR." >&2
130+
exit 1
131+
fi
132+
133+
echo "PASS: lock-gate pin $pin contains $compare over: $(printf '%s ' $paths)"
134+
}
135+
136+
main "$@"
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
#
4+
# Mutants for scripts/check-lock-gate-pin-freshness.sh.
5+
#
6+
# Every control builds a THROWAWAY git repository with real commits, so the
7+
# freshness comparison is exercised against genuine history with no network and
8+
# no dependence on this repo's own state. A control that asserted against the
9+
# real tree would go green or red for reasons unrelated to the mutation.
10+
11+
set -uo pipefail
12+
13+
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
14+
GUARD="$ROOT/scripts/check-lock-gate-pin-freshness.sh"
15+
[ -f "$GUARD" ] || { echo "FAIL: guard not found at $GUARD" >&2; exit 1; }
16+
17+
pass=0
18+
fail=0
19+
20+
check() { # name expected_rc actual_rc [haystack needle]
21+
local name="$1" want="$2" got="$3"
22+
if [ "$got" != "$want" ]; then
23+
echo "FAIL: $name — expected rc=$want, got rc=$got" >&2
24+
fail=$((fail + 1))
25+
return
26+
fi
27+
if [ "$#" -ge 5 ] && ! printf '%s' "$4" | grep -Fq -- "$5"; then
28+
echo "FAIL: $name — rc was right but the message never mentioned '$5'" >&2
29+
echo "----- output -----" >&2; printf '%s\n' "$4" >&2; echo "------------------" >&2
30+
fail=$((fail + 1))
31+
return
32+
fi
33+
echo "ok: $name"
34+
pass=$((pass + 1))
35+
}
36+
37+
refute() { # name haystack needle
38+
local name="$1"
39+
if printf '%s' "$2" | grep -Fq -- "$3"; then
40+
echo "FAIL: $name — output mentioned '$3' and must not" >&2
41+
fail=$((fail + 1))
42+
return
43+
fi
44+
echo "ok: $name"
45+
pass=$((pass + 1))
46+
}
47+
48+
# Build a repo whose helper changed in the SECOND commit, plus an unrelated file
49+
# that also changed, so path-scoping can be told apart from "any divergence".
50+
WORK="$(mktemp -d)"
51+
trap 'rm -rf "$WORK"' EXIT
52+
cd "$WORK" || exit 1
53+
git init --quiet -b main .
54+
git config user.email t@example.invalid
55+
git config user.name t
56+
mkdir -p scripts .machine_readable
57+
echo v1 > scripts/update-actions-lock.sh
58+
echo v1 > scripts/check-actions-lock-gate.sh
59+
echo v1 > .machine_readable/lock-allow.txt
60+
echo v1 > UNRELATED.md
61+
git add -A && git commit --quiet -m c1
62+
OLD="$(git rev-parse HEAD)"
63+
echo v2 > scripts/update-actions-lock.sh
64+
echo v2 > UNRELATED.md
65+
git add -A && git commit --quiet -m c2
66+
NEW="$(git rev-parse HEAD)"
67+
# A third commit touching ONLY the unrelated file, to prove path-scoping.
68+
echo v3 > UNRELATED.md
69+
git add -A && git commit --quiet -m c3
70+
NEWEST="$(git rev-parse HEAD)"
71+
72+
# $1 = the `ref:` value; $2 (optional) = step name override.
73+
write_fixture() {
74+
local ref="$1" name="${2:-Checkout standards for the lock gate}"
75+
mkdir -p "$WORK/.github/workflows"
76+
cat > "$WORK/.github/workflows/governance-reusable.yml" <<YAML
77+
jobs:
78+
gate:
79+
steps:
80+
- name: A preceding step that also has a ref
81+
uses: actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
82+
with:
83+
ref: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
84+
- name: $name
85+
uses: actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
86+
with:
87+
repository: hyperpolymath/standards
88+
ref: $ref
89+
path: .standards-lock
90+
sparse-checkout: |
91+
scripts/check-actions-lock-gate.sh
92+
scripts/update-actions-lock.sh
93+
.machine_readable/lock-allow.txt
94+
sparse-checkout-cone-mode: false
95+
- name: A following step with a decoy ref
96+
uses: actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
97+
with:
98+
ref: cccccccccccccccccccccccccccccccccccccccc
99+
YAML
100+
export LOCK_GATE_WORKFLOW="$WORK/.github/workflows/governance-reusable.yml"
101+
}
102+
103+
run() { bash "$GUARD" "$@" 2>&1; }
104+
105+
# 1. The real defect: the pin predates a helper change that is already on main.
106+
write_fixture "$OLD"
107+
out="$(run "$NEW")"; rc=$?
108+
check "stale pin is refused" 1 "$rc" "$out" "scripts/update-actions-lock.sh"
109+
110+
# 2. And it must not name files outside the staged scope. UNRELATED.md differs
111+
# between the two commits too; if it appears, the guard is diffing the whole
112+
# tree and every rebase would redden it.
113+
refute "stale report is path-scoped (never names UNRELATED.md)" "$out" "UNRELATED.md"
114+
115+
# 3. The cured state passes.
116+
write_fixture "$NEW"
117+
out="$(run "$NEW")"; rc=$?
118+
check "fresh pin is accepted" 0 "$rc" "$out" "PASS"
119+
120+
# 4. Path-scoping: an unrelated commit on top must NOT fail a fresh pin.
121+
# Without this, every rebase would redden the gate and the guard would be
122+
# turned off rather than obeyed.
123+
write_fixture "$NEW"
124+
out="$(run "$NEWEST")"; rc=$?
125+
check "unrelated divergence does not fail it" 0 "$rc" "$out" "PASS"
126+
127+
# 5. A moving ref is refused — the whole point of pinning.
128+
write_fixture "main"
129+
out="$(run "$NEW")"; rc=$?
130+
check "ref: main is refused" 1 "$rc" "$out" "not an immutable 40-hex commit"
131+
132+
# 6. A short/abbreviated SHA is refused.
133+
write_fixture "${NEW:0:12}"
134+
out="$(run "$NEW")"; rc=$?
135+
check "abbreviated sha is refused" 1 "$rc" "$out" "not an immutable 40-hex commit"
136+
137+
# 7. Renaming the step must FAIL, not vacuously pass. This is the exact way the
138+
# existing contract test lost its subject: it greps a step name, and a step
139+
# that no longer matches simply stops being checked.
140+
write_fixture "$NEW" "Checkout standards for something else"
141+
out="$(run "$NEW")"; rc=$?
142+
check "renamed step fails loudly" 1 "$rc" "$out" "lost its subject"
143+
144+
# 8. A pin that cannot be resolved must FAIL, never skip.
145+
write_fixture "dddddddddddddddddddddddddddddddddddddddd"
146+
out="$(run "$NEW")"; rc=$?
147+
check "unresolvable pin fails, not skips" 1 "$rc" "$out" "does not pass on an unverifiable pin"
148+
149+
# 9. No ref: at all.
150+
mkdir -p "$WORK/.github/workflows"
151+
cat > "$WORK/.github/workflows/governance-reusable.yml" <<'YAML'
152+
jobs:
153+
gate:
154+
steps:
155+
- name: Checkout standards for the lock gate
156+
uses: actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
157+
with:
158+
repository: hyperpolymath/standards
159+
sparse-checkout: |
160+
scripts/update-actions-lock.sh
161+
YAML
162+
export LOCK_GATE_WORKFLOW="$WORK/.github/workflows/governance-reusable.yml"
163+
out="$(run "$NEW")"; rc=$?
164+
check "missing ref: is refused" 1 "$rc" "$out" "would follow the default branch"
165+
166+
# 10. Staging nothing must not be a free pass.
167+
cat > "$WORK/.github/workflows/governance-reusable.yml" <<YAML
168+
jobs:
169+
gate:
170+
steps:
171+
- name: Checkout standards for the lock gate
172+
uses: actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
173+
with:
174+
ref: $NEW
175+
path: .standards-lock
176+
YAML
177+
out="$(run "$NEW")"; rc=$?
178+
check "empty staged scope is refused" 1 "$rc" "$out" "stages no sparse-checkout paths"
179+
180+
echo
181+
echo "$pass passed, $fail failed"
182+
[ "$fail" -eq 0 ] || exit 1
183+
echo "PASS: lock-gate pin freshness guard refuses a stale pin and cannot be silenced by renaming its subject"

0 commit comments

Comments
 (0)