Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions .github/workflows/governance-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1214,14 +1214,19 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: hyperpolymath/standards
# ⚠ BUMP THIS whenever scripts/check-actions-lock-gate.sh,
# scripts/update-actions-lock.sh or .machine_readable/lock-allow.txt
# changes, or callers are judged against a stale gate. Pinned to an
# immutable commit for the same reason as the dupkey helpers in
# workflow-lint: following `main` would let an edit in standards
# change the verdict of every already-pinned caller with no review
# in their repositories.
ref: 4f7f02ca528212c578fd56379a202d219d01abe0
# ⚠ ENFORCED, no longer a request: scripts/check-lock-gate-pin-freshness.sh
# fails Self Test unless this commit already contains everything on
# main across the sparse-checkout list below. Change one of those
# files and the NEXT pull request must bump this line — it cannot be
# this PR's own merge commit, which does not exist yet, so the pin is
# one change behind by construction and that is the intended shape.
# Pinned to an immutable commit for the same reason as the dupkey
# helpers in workflow-lint: following `main` would let an edit in
# standards change the verdict of every already-pinned caller with no
# review in their repositories. This pin is invisible to the caller's
# `uses:` ref and to actions.lock — it is a third, independent pin,
# which is precisely how it went stale across standards#946.
ref: 9c256b67486b4b30c757730e1b65b2c2d2af935b
path: .standards-lock
persist-credentials: false
sparse-checkout: |
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/self-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# check-lock-gate-pin-freshness.sh compares the lock gate's pinned
# commit against the base ref. A depth-1 clone has neither object, and
# that guard fails rather than skipping on an unresolvable pin, so the
# history is a requirement and not an optimisation.
fetch-depth: 0

# PyYAML is required by the secret-scanner canary. The scorecard
# grounding tests execute the same checks as registry-verify, including
Expand All @@ -47,3 +53,17 @@ jobs:

- name: Run tests/*.sh and scripts/tests/*.sh
run: bash scripts/run-shell-test-suite.sh

# Not part of the offline suite: this one needs real git history, so it
# cannot live in scripts/tests/*.sh where a contributor runs it on a
# shallow or detached tree. On a pull request the comparison is the base
# SHA, so a PR that edits a helper is not asked to pin to its own
# unborn merge commit; on main it is HEAD, so the bump owed after such a
# PR lands shows up immediately instead of rotting silently.
- name: Lock-gate pin is not stale
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
COMPARE="${BASE_SHA:-$GITHUB_SHA}"
bash scripts/check-lock-gate-pin-freshness.sh "$COMPARE"
136 changes: 136 additions & 0 deletions scripts/check-lock-gate-pin-freshness.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
#
# check-lock-gate-pin-freshness.sh — the lock gate's own pin must not be stale.
#
# WHY THIS EXISTS
# ---------------
# `governance-reusable.yml` stages the lock-gate tooling from a THIRD pin: not
# the caller's `uses:` ref and not the lockfile's record of it, but a SHA
# hardcoded inside the callee for its own `actions/checkout`. A called reusable
# workflow has no context exposing its own commit, so the hardcode is forced.
#
# That third pin is invisible to every other control. When standards#946 fixed
# `scripts/update-actions-lock.sh`, this pin still pointed at the commit BEFORE
# the fix, so every caller kept being judged by the broken verifier — including
# callers that had just bumped specifically to pick the fix up. The file already
# carried a comment saying "BUMP THIS", but a comment is not a gate.
#
# THE PREDICATE, AND WHY IT IS THIS 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 predicate is:
#
# the pinned commit must already contain everything that is on the
# COMPARE ref (main), over exactly the paths the step stages.
#
# * on a 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.
# * on a push to main, COMPARE is HEAD. Red exactly when a helper change has
# just landed and the bump is owed; healed by the next PR, which the
# pull_request run will not let through unbumped.
#
# The 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 TAKEN FROM THE STEP, NOT HARDCODED
# -------------------------------------------
# The paths compared are read out of the step's own `sparse-checkout:` list, so
# adding a file to what the gate stages automatically extends what this guard
# protects. A hardcoded list here would be a guard asking a different question
# than its consumer, which is the exact trap this file belongs to.
#
# Usage: check-lock-gate-pin-freshness.sh [COMPARE_REF] (default: origin/main)

set -uo pipefail

STEP_NAME='Checkout standards for the lock gate'

fail() {
echo "::error::lock-gate pin freshness: $*" >&2
exit 1
}

# Resolve the workflow path INSIDE the function, never at script load: a fixture
# override exported by a test after the top-level assignment would otherwise be
# ignored and every mutant would silently read the real tree and "pass".
workflow_path() {

Check warning on line 62 in scripts/check-lock-gate-pin-freshness.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrkOkIF99j5HEC_&open=AaDIscrkOkIF99j5HEC_&pullRequest=962
local root
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
printf '%s\n' "${LOCK_GATE_WORKFLOW:-$root/.github/workflows/governance-reusable.yml}"
}

# Print the step's YAML block: from its `- name:` line to the next `- name:` at
# the same indent, exclusive. `grep -A N` cannot do this — the block is 19 lines
# today and 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.
step_block() {

Check warning on line 72 in scripts/check-lock-gate-pin-freshness.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrkOkIF99j5HEDA&open=AaDIscrkOkIF99j5HEDA&pullRequest=962
awk -v want="- name: $STEP_NAME" '
index($0, want) { inblock = 1; indent = match($0, /-/); next }
inblock && /^[[:space:]]*- name:/ && match($0, /-/) == indent { exit }
inblock { print }
' "$(workflow_path)"
}

main() {
local compare="${1:-origin/main}"
local wf block pin paths diverged

wf="$(workflow_path)"
[ -f "$wf" ] || fail "workflow not found: $wf"

Check failure on line 85 in scripts/check-lock-gate-pin-freshness.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrkOkIF99j5HEDB&open=AaDIscrkOkIF99j5HEDB&pullRequest=962

block="$(step_block)"
[ -n "$block" ] ||

Check failure on line 88 in scripts/check-lock-gate-pin-freshness.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrkOkIF99j5HEDC&open=AaDIscrkOkIF99j5HEDC&pullRequest=962
fail "no step named '$STEP_NAME' in $wf — the guard has lost its subject; \
rename it here too rather than deleting the assertion"

pin="$(printf '%s\n' "$block" | sed -n 's/^[[:space:]]*ref:[[:space:]]*\([^[:space:]#]*\).*/\1/p' | head -1)"
[ -n "$pin" ] || fail "step '$STEP_NAME' has no 'ref:' — it would follow the default branch"

Check failure on line 93 in scripts/check-lock-gate-pin-freshness.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrkOkIF99j5HEDD&open=AaDIscrkOkIF99j5HEDD&pullRequest=962
printf '%s' "$pin" | grep -Eq '^[0-9a-f]{40}$' ||
fail "step '$STEP_NAME' is pinned to '$pin', not an immutable 40-hex commit"

# The staged scope IS the guarded scope.
paths="$(printf '%s\n' "$block" | awk '
/^[[:space:]]*sparse-checkout:[[:space:]]*\|/ { inlist = 1; next }
inlist && /^[[:space:]]*[a-z-]+:/ { inlist = 0 }
inlist && NF { gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print }
')"
[ -n "$paths" ] || fail "step '$STEP_NAME' stages no sparse-checkout paths — nothing to compare"

Check failure on line 103 in scripts/check-lock-gate-pin-freshness.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrkOkIF99j5HEDE&open=AaDIscrkOkIF99j5HEDE&pullRequest=962

git rev-parse --verify --quiet "$compare^{commit}" >/dev/null ||
fail "compare ref '$compare' is not resolvable in this clone"

# A missing pin object must FAIL, never skip: a skip is indistinguishable from
# a pass and this guard exists because an unasserted pin rotted unnoticed.
if ! git cat-file -e "$pin^{commit}" 2>/dev/null; then
git fetch --quiet --depth=1 origin "$pin" 2>/dev/null || true
git cat-file -e "$pin^{commit}" 2>/dev/null ||
fail "pinned commit $pin is not present and could not be fetched — \
give the checkout 'fetch-depth: 0' or grant the fetch network access; \
this guard does not pass on an unverifiable pin"
fi

# shellcheck disable=SC2086
diverged="$(git diff --name-only "$pin" "$compare" -- $paths)"

if [ -n "$diverged" ]; then

Check failure on line 121 in scripts/check-lock-gate-pin-freshness.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrkOkIF99j5HEDF&open=AaDIscrkOkIF99j5HEDF&pullRequest=962
echo "::error::The lock gate is staged from $pin, which does NOT contain what is already on $compare." >&2
echo "Stale in the pinned tree:" >&2
printf ' %s\n' $diverged >&2
echo >&2
echo "Every caller of governance-reusable.yml is being judged by that older tooling," >&2
echo "including callers that bumped their own pin specifically to pick up the fix." >&2
echo "Cure: set 'ref:' under '$STEP_NAME' to a commit containing the above" >&2
echo "(usually the current tip of main), in this PR." >&2
exit 1
fi

echo "PASS: lock-gate pin $pin contains $compare over: $(printf '%s ' $paths)"
}

main "$@"
183 changes: 183 additions & 0 deletions scripts/tests/check-lock-gate-pin-freshness-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
#
# Mutants for scripts/check-lock-gate-pin-freshness.sh.
#
# Every control builds a THROWAWAY git repository with real commits, so the
# freshness comparison is exercised against genuine history with no network and
# no dependence on this repo's own state. A control that asserted against the
# real tree would go green or red for reasons unrelated to the mutation.

set -uo pipefail

ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
GUARD="$ROOT/scripts/check-lock-gate-pin-freshness.sh"
[ -f "$GUARD" ] || { echo "FAIL: guard not found at $GUARD" >&2; exit 1; }

Check failure on line 15 in scripts/tests/check-lock-gate-pin-freshness-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrbOkIF99j5HEC1&open=AaDIscrbOkIF99j5HEC1&pullRequest=962

pass=0
fail=0

check() { # name expected_rc actual_rc [haystack needle]
local name="$1" want="$2" got="$3"
if [ "$got" != "$want" ]; then

Check failure on line 22 in scripts/tests/check-lock-gate-pin-freshness-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrbOkIF99j5HEC2&open=AaDIscrbOkIF99j5HEC2&pullRequest=962
echo "FAIL: $name — expected rc=$want, got rc=$got" >&2
fail=$((fail + 1))
return
fi
if [ "$#" -ge 5 ] && ! printf '%s' "$4" | grep -Fq -- "$5"; then

Check failure on line 27 in scripts/tests/check-lock-gate-pin-freshness-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrbOkIF99j5HEC3&open=AaDIscrbOkIF99j5HEC3&pullRequest=962

Check warning on line 27 in scripts/tests/check-lock-gate-pin-freshness-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrbOkIF99j5HEC5&open=AaDIscrbOkIF99j5HEC5&pullRequest=962

Check warning on line 27 in scripts/tests/check-lock-gate-pin-freshness-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrbOkIF99j5HEC4&open=AaDIscrbOkIF99j5HEC4&pullRequest=962
echo "FAIL: $name — rc was right but the message never mentioned '$5'" >&2
echo "----- output -----" >&2; printf '%s\n' "$4" >&2; echo "------------------" >&2
fail=$((fail + 1))
return
fi
echo "ok: $name"
pass=$((pass + 1))
}

refute() { # name haystack needle
local name="$1"
if printf '%s' "$2" | grep -Fq -- "$3"; then

Check warning on line 39 in scripts/tests/check-lock-gate-pin-freshness-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrbOkIF99j5HEC6&open=AaDIscrbOkIF99j5HEC6&pullRequest=962

Check warning on line 39 in scripts/tests/check-lock-gate-pin-freshness-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrbOkIF99j5HEC7&open=AaDIscrbOkIF99j5HEC7&pullRequest=962
echo "FAIL: $name — output mentioned '$3' and must not" >&2
fail=$((fail + 1))
return
fi
echo "ok: $name"
pass=$((pass + 1))
}

# Build a repo whose helper changed in the SECOND commit, plus an unrelated file
# that also changed, so path-scoping can be told apart from "any divergence".
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
cd "$WORK" || exit 1
git init --quiet -b main .
git config user.email t@example.invalid
git config user.name t
mkdir -p scripts .machine_readable
echo v1 > scripts/update-actions-lock.sh
echo v1 > scripts/check-actions-lock-gate.sh
echo v1 > .machine_readable/lock-allow.txt
echo v1 > UNRELATED.md
git add -A && git commit --quiet -m c1
OLD="$(git rev-parse HEAD)"
echo v2 > scripts/update-actions-lock.sh
echo v2 > UNRELATED.md
git add -A && git commit --quiet -m c2
NEW="$(git rev-parse HEAD)"
# A third commit touching ONLY the unrelated file, to prove path-scoping.
echo v3 > UNRELATED.md
git add -A && git commit --quiet -m c3
NEWEST="$(git rev-parse HEAD)"

# $1 = the `ref:` value; $2 (optional) = step name override.
write_fixture() {

Check warning on line 73 in scripts/tests/check-lock-gate-pin-freshness-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrbOkIF99j5HEC8&open=AaDIscrbOkIF99j5HEC8&pullRequest=962
local ref="$1" name="${2:-Checkout standards for the lock gate}"
mkdir -p "$WORK/.github/workflows"
cat > "$WORK/.github/workflows/governance-reusable.yml" <<YAML
jobs:
gate:
steps:
- name: A preceding step that also has a ref
uses: actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
with:
ref: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
- name: $name
uses: actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
with:
repository: hyperpolymath/standards
ref: $ref
path: .standards-lock
sparse-checkout: |
scripts/check-actions-lock-gate.sh
scripts/update-actions-lock.sh
.machine_readable/lock-allow.txt
sparse-checkout-cone-mode: false
- name: A following step with a decoy ref
uses: actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
with:
ref: cccccccccccccccccccccccccccccccccccccccc
YAML
export LOCK_GATE_WORKFLOW="$WORK/.github/workflows/governance-reusable.yml"
}

run() { bash "$GUARD" "$@" 2>&1; }

Check warning on line 103 in scripts/tests/check-lock-gate-pin-freshness-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrbOkIF99j5HEC9&open=AaDIscrbOkIF99j5HEC9&pullRequest=962

# 1. The real defect: the pin predates a helper change that is already on main.
write_fixture "$OLD"
out="$(run "$NEW")"; rc=$?
check "stale pin is refused" 1 "$rc" "$out" "scripts/update-actions-lock.sh"

# 2. And it must not name files outside the staged scope. UNRELATED.md differs
# between the two commits too; if it appears, the guard is diffing the whole
# tree and every rebase would redden it.
refute "stale report is path-scoped (never names UNRELATED.md)" "$out" "UNRELATED.md"

# 3. The cured state passes.
write_fixture "$NEW"
out="$(run "$NEW")"; rc=$?
check "fresh pin is accepted" 0 "$rc" "$out" "PASS"

# 4. Path-scoping: an unrelated commit on top must NOT fail a fresh pin.
# Without this, every rebase would redden the gate and the guard would be
# turned off rather than obeyed.
write_fixture "$NEW"
out="$(run "$NEWEST")"; rc=$?
check "unrelated divergence does not fail it" 0 "$rc" "$out" "PASS"

# 5. A moving ref is refused — the whole point of pinning.
write_fixture "main"
out="$(run "$NEW")"; rc=$?
check "ref: main is refused" 1 "$rc" "$out" "not an immutable 40-hex commit"

# 6. A short/abbreviated SHA is refused.
write_fixture "${NEW:0:12}"
out="$(run "$NEW")"; rc=$?
check "abbreviated sha is refused" 1 "$rc" "$out" "not an immutable 40-hex commit"

# 7. Renaming the step must FAIL, not vacuously pass. This is the exact way the
# existing contract test lost its subject: it greps a step name, and a step
# that no longer matches simply stops being checked.
write_fixture "$NEW" "Checkout standards for something else"
out="$(run "$NEW")"; rc=$?
check "renamed step fails loudly" 1 "$rc" "$out" "lost its subject"

# 8. A pin that cannot be resolved must FAIL, never skip.
write_fixture "dddddddddddddddddddddddddddddddddddddddd"
out="$(run "$NEW")"; rc=$?
check "unresolvable pin fails, not skips" 1 "$rc" "$out" "does not pass on an unverifiable pin"

# 9. No ref: at all.
mkdir -p "$WORK/.github/workflows"
cat > "$WORK/.github/workflows/governance-reusable.yml" <<'YAML'
jobs:
gate:
steps:
- name: Checkout standards for the lock gate
uses: actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
with:
repository: hyperpolymath/standards
sparse-checkout: |
scripts/update-actions-lock.sh
YAML
export LOCK_GATE_WORKFLOW="$WORK/.github/workflows/governance-reusable.yml"
out="$(run "$NEW")"; rc=$?
check "missing ref: is refused" 1 "$rc" "$out" "would follow the default branch"

# 10. Staging nothing must not be a free pass.
cat > "$WORK/.github/workflows/governance-reusable.yml" <<YAML
jobs:
gate:
steps:
- name: Checkout standards for the lock gate
uses: actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
with:
ref: $NEW
path: .standards-lock
YAML
out="$(run "$NEW")"; rc=$?
check "empty staged scope is refused" 1 "$rc" "$out" "stages no sparse-checkout paths"

echo
echo "$pass passed, $fail failed"
[ "$fail" -eq 0 ] || exit 1

Check failure on line 182 in scripts/tests/check-lock-gate-pin-freshness-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDIscrbOkIF99j5HEC-&open=AaDIscrbOkIF99j5HEC-&pullRequest=962
echo "PASS: lock-gate pin freshness guard refuses a stale pin and cannot be silenced by renaming its subject"
Loading
Loading