From 01a5004490322e0004d5feb1aa98ace0687fb064 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:42:45 +0100 Subject: [PATCH] =?UTF-8?q?fix(ci):=20pin=20cicd-suite=20lock=20at=200c1bc?= =?UTF-8?q?9f=20=E2=80=94=20consumers=20were=20still=20running=20old=20gat?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fa71ac2 (#32) cured the composite `-e` kill and 0c1bc9f (#34) restored the lockfile's composite-reached pins. Both are green here. Both were inert for every downstream consumer, and nothing was red to say so. pons-asinorum's estate audit still died at Required Files Gate, 82ms, silently, skipping 25 downstream gates — after being repointed at 0c1bc9f. The reason is that the reusable invokes its composites by BRANCH ref: uses: hyperpolymath/cicd-suite/actions/required-files-check@main A branch ref is trusted from the lockfile, and the runner executes THE COMMIT THE LOCKFILE NAMES, not the branch tip. The lock pinned cicd-suite@main at 9adb3908 — four commits back, with 0 of the three `-e` guards. So no SHA a consumer chooses for the reusable can reach a cured composite; the lock's `@main` entry is the real gate, and re-pinning it is the actual delivery step. This bumps that pin to 0c1bc9f (an ancestor-clean fast-forward, transitive `uses:` unchanged). It carries three composites the old pin lacked: actions/linguist-check/action.yml (#31) actions/required-files-check/action.yml (#32) actions/spdx-license-check/action.yml (#32) The house pattern is a follow-up "pin cicd-suite lock at " commit — f8c8f4a, 4f9a7a4, 373714a all do exactly this. #31, #32 and #34 never got one. Rather than rely on remembering, lock-transitive-closure.sh now asserts it: for any self-referencing branch pin, the actions/ tree at the locked commit must equal HEAD's, and it names the drifted files when it does not. `shell-contract` gains `fetch-depth: 0` so the locked commit is present to compare against. Non-vacuity, twice over. The assertion was written BEFORE the bump and caught this defect live, naming all three files. And its first draft printed its header while checking nothing — a sed without lazy-quantifier support left ".git" on the repo name so no key matched — so the block now carries its own non-vacuity counter. A header is not a check. Local: lock-transitive-closure PASS=9 FAIL=0; composite-shell-contract PASS=7 FAIL=0 with its mutant killed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WRvDivYwLSeVCJUrfjic3f --- .github/workflows/actions.lock | 2 +- .github/workflows/code-hygiene-self-test.yml | 3 ++ tests/lock-transitive-closure.sh | 52 ++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index b061fbf..42bce03 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -20,7 +20,7 @@ dependencies: repo_id: 197814629 'hyperpolymath/cicd-suite@main': ref: 'main' - commit: 'sha1-9adb3908d60690dbd6a302f634b914a279f6812e' + commit: 'sha1-0c1bc9f5aa5857965e50f96021485d5710b6e0ea' owner_id: 6759885 repo_id: 1326697643 uses: diff --git a/.github/workflows/code-hygiene-self-test.yml b/.github/workflows/code-hygiene-self-test.yml index 18568e2..b8ca743 100644 --- a/.github/workflows/code-hygiene-self-test.yml +++ b/.github/workflows/code-hygiene-self-test.yml @@ -57,6 +57,9 @@ jobs: - uses: actions/checkout@v7.0.1 with: persist-credentials: false + # lock-transitive-closure.sh compares the actions/ tree at the commit + # the lockfile pins against HEAD's, so it needs that commit present. + fetch-depth: 0 - run: bash tests/composite-shell-contract.sh - run: bash tests/lock-transitive-closure.sh diff --git a/tests/lock-transitive-closure.sh b/tests/lock-transitive-closure.sh index 7c5a0fa..d785178 100755 --- a/tests/lock-transitive-closure.sh +++ b/tests/lock-transitive-closure.sh @@ -103,6 +103,58 @@ else ok "checked $checked composite-reached ref(s) — population is non-empty" fi +# A self-referencing BRANCH pin is how every downstream consumer reaches these +# composites: `uses: hyperpolymath/cicd-suite/actions/x@main` is resolved by the +# runner to the COMMIT THE LOCKFILE NAMES, not to the branch tip. So if actions/ +# has changed since that locked commit, every consumer silently runs the OLD +# gates while this repo's own CI runs the new ones — the cure lands here and is +# inert everywhere else, with nothing red to show for it. +# +# Measured 2026-09-22: fa71ac2 (#32) and 0c1bc9f (#34) both cured the composite +# `-e` kill, and pons-asinorum's estate audit still died on it at 82ms, because +# the lock still pinned cicd-suite@main at 9adb3908 — four commits back. The +# house pattern is a follow-up "pin cicd-suite lock at " commit (f8c8f4a, +# 4f9a7a4, 373714a); #32 and #34 never got one. This asserts it instead. +# +# Needs full history: the job running this must use fetch-depth: 0. +echo "== a self-referencing branch pin serves the CURRENT composites ==" +# ERE has no lazy quantifiers, so strip .git FIRST and then take owner/repo. +# In Actions $GITHUB_REPOSITORY is authoritative; the remote is the local path. +selfrepo="${GITHUB_REPOSITORY:-$(git -C "$ROOT" remote get-url origin 2>/dev/null \ + | sed -E 's#\.git$##; s#^.*[:/]([^/]+/[^/]+)$#\1#')}" +selfchecked=0 +while IFS= read -r ref; do + [ -n "$ref" ] || continue + case "$ref" in "$selfrepo@"*) ;; *) continue ;; esac + case "${ref#*@}" in *[!0-9a-f]*) ;; *) continue ;; esac # a SHA pin cannot drift + selfchecked=$((selfchecked+1)) + locked="$(r="$ref" yqr '.dependencies[strenv(r)].commit' "$LOCK" | sed 's/^sha1-//')" + if ! git -C "$ROOT" cat-file -e "$locked^{commit}" 2>/dev/null; then + bad "$ref pins $locked, which is not in this clone (need fetch-depth: 0)" + continue + fi + have="$(git -C "$ROOT" rev-parse "$locked:actions" 2>/dev/null || true)" + want="$(git -C "$ROOT" rev-parse "HEAD:actions" 2>/dev/null || true)" + if [ -n "$have" ] && [ "$have" = "$want" ]; then + ok "$ref serves the current actions/ tree ($locked)" + else + bad "$ref pins $locked, whose actions/ tree differs from HEAD" + echo " every consumer using this branch ref runs those OLD composites;" + echo " a cure landed here is inert downstream. Changed since the pin:" + git -C "$ROOT" diff --name-only "$locked" HEAD -- actions/ 2>/dev/null \ + | sed 's/^/ /' | head -8 + fi +done < <(yqr '.dependencies | keys | .[]' "$LOCK") + +# This block printed its header and checked NOTHING on its first run — a broken +# sed left ".git" on the repo name so no key ever matched, and the section above +# only asserts non-vacuity for its own population. A header is not a check. +if [ "$selfchecked" -eq 0 ]; then + bad "no self-referencing branch pin examined (selfrepo='$selfrepo') — this section is vacuous" +else + ok "examined $selfchecked self-referencing branch pin(s)" +fi + echo "== every declared ref resolves to a dependencies: entry ==" while IFS= read -r ref; do [ -n "$ref" ] || continue