From efa4b41d904601deefada14318d21be1892258d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:03:42 +0000 Subject: [PATCH 1/2] fix(deonboard): one implementation, so the language-gate rollout gets the schema guard and the case fix scripts/deonboard-workflow.sh (#54) is a copy of the estate-audit de-onboard script taken before #52 and #53 landed, and it carried neither fix: * it accepted any actions.lock schema (rc=0 on a v9.9.9 lock), and * it compared dependency keys byte for byte, so it pruned 'Swatinem/rust-cache@' while the standards record still reached it as 'swatinem/rust-cache@', leaving a dangling edge that startup-fails the run. rollout_language_gates.sh de-onboards every Rust CI caller through it, and Rust callers are exactly the ones whose locks hold that edge. Its header and the README already said the estate-audit script delegates to it; it did not. deonboard-workflow.sh now holds the guard and the case-insensitive prune, and deonboard-estate-audit.sh is a two-line entry point that execs it. Both test suites run every check through both entry points and apply their mutants to the one implementation; a mutant must be killed through the wrapper too, which fails if the wrapper ever stops delegating. tests/deonboard-estate-audit.sh (#52) was not run in CI; it now runs in the Composite shell contract job, and the hygiene self-test triggers on deonboard-workflow.sh. Against main's scripts the new suites fail 4 + 4 checks (dangling edge, entry points disagree, v9.9.9 accepted, wrapper mutant survives); on this branch they pass 19/19 and 63/63. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VTeVWDEJgdYKsUNr4uhnXX --- .github/workflows/code-hygiene-self-test.yml | 12 +- ARCHITECTURE.adoc | 4 +- README.adoc | 11 +- scripts/deonboard-estate-audit.sh | 62 +-------- scripts/deonboard-workflow.sh | 35 ++++- tests/deonboard-estate-audit.sh | 106 +++++++++------ tests/deonboard-lock-schemas.sh | 135 +++++++++++-------- 7 files changed, 201 insertions(+), 164 deletions(-) diff --git a/.github/workflows/code-hygiene-self-test.yml b/.github/workflows/code-hygiene-self-test.yml index 60e3218..24fddf8 100644 --- a/.github/workflows/code-hygiene-self-test.yml +++ b/.github/workflows/code-hygiene-self-test.yml @@ -20,6 +20,7 @@ "actions/formatting-check/**", "tests/**", "scripts/deonboard-estate-audit.sh", + "scripts/deonboard-workflow.sh", ".github/workflows/code-hygiene-self-test.yml", ".github/workflows/actions.lock", # lock-transitive-closure.sh reads the literal cicd-suite SHA pins in @@ -40,6 +41,7 @@ "actions/formatting-check/**", "tests/**", "scripts/deonboard-estate-audit.sh", + "scripts/deonboard-workflow.sh", ".github/workflows/code-hygiene-self-test.yml", ".github/workflows/actions.lock", # lock-transitive-closure.sh reads the literal cicd-suite SHA pins in @@ -85,10 +87,16 @@ run: "bash tests/lock-transitive-closure.sh", }, { - # scripts/deonboard-estate-audit.sh against every actions.lock schema it - # claims to support; an untested schema must fail closed. + # scripts/deonboard-workflow.sh, through both entry points, against + # every actions.lock schema it claims to support; an untested schema + # must fail closed. run: "bash tests/deonboard-lock-schemas.sh", }, + { + # The same script must not prune a dependency still reached by a + # lower-case nested `uses:` edge, through either entry point. + run: "bash tests/deonboard-estate-audit.sh", + }, ], }, test: { diff --git a/ARCHITECTURE.adoc b/ARCHITECTURE.adoc index 89c8798..a78cdf0 100644 --- a/ARCHITECTURE.adoc +++ b/ARCHITECTURE.adoc @@ -12,7 +12,7 @@ actions/-check/action.yml one composite action per gate (26; 24 run) .github/workflows/main-estate-audit.yml the reusable audit: five component jobs templates/estate-audit-caller.yml the ten-line caller each repository holds templates/track-estate-audit.yml weekly bot: moves the caller's pin to HEAD -scripts/deonboard-estate-audit.sh removes the caller from a repo's actions.lock +scripts/deonboard-estate-audit.sh removes the caller from a repo's actions.lock (delegates) rollout_estate.sh installs the three above in every estate clone actions/rust-ci-check/ the strict Rust gate (resolve.sh, check.sh, test.sh) actions/creusot-check/ Creusot: detect · theatre · prove (planted controls) @@ -25,7 +25,7 @@ actions/kanren-check/ miniKanren: detect · run · purity .github/workflows/language-gates-self-test.yml each gate's test.sh, every PR templates/*-caller.yml the thin callers for the four gates scripts/verify-suite-checkout.sh proves the self-checkout is the caller's pin -scripts/deonboard-workflow.sh general form of deonboard-estate-audit.sh +scripts/deonboard-workflow.sh the one de-onboard implementation, every caller rollout_language_gates.sh installs the callers by what a tree contains .... diff --git a/README.adoc b/README.adoc index be602b5..f53600c 100644 --- a/README.adoc +++ b/README.adoc @@ -175,10 +175,13 @@ every consuming repository at once. . The caller must be *de-onboarded* from the repository's `+actions.lock+`. Onboarded, a cross-repository reusable-workflow call startup-fails with zero jobs. `+scripts/deonboard-estate-audit.sh +` -removes the entry and prunes the dependencies only it reached. It needs -mikefarah `+yq+` v4, and it is idempotent. It refuses any `+actions.lock+` -schema it has not been tested on; `+tests/deonboard-lock-schemas.sh+` holds -one fixture per supported schema (v0.0.2, v0.0.3). +removes the entry and prunes the dependencies only it reached, comparing +owner and repository names case-insensitively as GitHub does. It is the +estate-audit entry point to `+scripts/deonboard-workflow.sh+`, the one +implementation every de-onboard goes through. It needs mikefarah `+yq+` v4, +and it is idempotent. It refuses any `+actions.lock+` schema it has not been +tested on; `+tests/deonboard-lock-schemas.sh+` holds one fixture per +supported schema (v0.0.2, v0.0.3) and runs each through both entry points. . `+templates/track-estate-audit.yml+` becomes `+.github/workflows/track-estate-audit.yml+`. Weekly, it opens a PR that moves the pin to cicd-suite HEAD; that PR's own audit run is the check. diff --git a/scripts/deonboard-estate-audit.sh b/scripts/deonboard-estate-audit.sh index b6c29d6..02e4e2b 100755 --- a/scripts/deonboard-estate-audit.sh +++ b/scripts/deonboard-estate-audit.sh @@ -17,63 +17,11 @@ # # Idempotent: a lock that does not list the workflow is left untouched. # Fails closed (exit 3) on an actions.lock schema it has not been tested on. +# +# The work is done by deonboard-workflow.sh, the one implementation, which +# the language-gate rollout also uses. Two copies drifted once: the schema +# guard and the case-insensitive prune landed here and not there. set -euo pipefail repo="${1:?usage: deonboard-estate-audit.sh }" -lock="$repo/.github/workflows/actions.lock" -wf=".github/workflows/main-estate-audit.yml" - -if ! yq --version 2>/dev/null | grep -q mikefarah; then - echo "deonboard-estate-audit.sh needs mikefarah yq v4 (https://github.com/mikefarah/yq)" >&2 - exit 2 -fi -if [ ! -f "$lock" ]; then - echo "$repo: no actions.lock; nothing to de-onboard" - exit 0 -fi -if ! w="$wf" yq -e '.workflows | has(strenv(w))' "$lock" >/dev/null 2>&1; then - echo "$repo: $wf is not onboarded; nothing to do" - exit 0 -fi - -# Lockfile schemas this script is tested against, one fixture each in -# tests/fixtures/actions-lock/ (tests/deonboard-lock-schemas.sh). An untested -# schema fails closed: add its fixture first, then its version here. -known_versions="v0.0.2 v0.0.3" -ver=$(yq '.version' "$lock") -case " $known_versions " in - *" $ver "*) ;; - *) echo "$repo: actions.lock schema '$ver' is untested by this script (tested: $known_versions); see tests/deonboard-lock-schemas.sh" >&2 - exit 3 ;; -esac - -w="$wf" yq -i -I4 'del(.workflows[strenv(w)])' "$lock" - -# owner/repo@ref -> lower-case owner/repo, ref as written. GitHub owner and -# repository names are case-insensitive, and gh actions-lock writes the -# nested `uses:` of a dependency in lower case while keeping the record key's -# original case: a lock can hold the edge 'swatinem/rust-cache@' and -# the record 'Swatinem/rust-cache@' (measured on hyperpolymath/januskey, -# 2026-10-14, under its hyperpolymath/standards record). Compared byte for -# byte, that record looks orphaned, and pruning it leaves a dangling edge, -# which kills the run at startup (standards scripts/close-lock.sh). Refs keep -# their case: git refs are case-sensitive. -lower_repo() { awk '{ i = index($0, "@"); print tolower(substr($0, 1, i - 1)) substr($0, i) }'; } - -pruned=() -while :; do - used=$( { yq '.workflows[][]' "$lock" - yq '.dependencies[] | select(has("uses")) | .uses[]' "$lock" - } | lower_repo | sort -u ) - unused=$(yq '.dependencies | keys | .[]' "$lock" | while IFS= read -r k; do - printf '%s\n' "$k" | lower_repo | grep -qxF -f <(printf '%s\n' "$used") || printf '%s\n' "$k" - done) - [ -n "$unused" ] || break - while IFS= read -r k; do - k="$k" yq -i -I4 'del(.dependencies[strenv(k)])' "$lock" - pruned+=("$k") - done <<< "$unused" -done - -echo "$repo: de-onboarded $wf; pruned ${#pruned[@]} orphaned dependenc(ies):" -if [ "${#pruned[@]}" -gt 0 ]; then printf ' %s\n' "${pruned[@]}"; fi +exec "$(dirname "${BASH_SOURCE[0]}")/deonboard-workflow.sh" "$repo" .github/workflows/main-estate-audit.yml diff --git a/scripts/deonboard-workflow.sh b/scripts/deonboard-workflow.sh index b5e21a2..a7269fd 100755 --- a/scripts/deonboard-workflow.sh +++ b/scripts/deonboard-workflow.sh @@ -16,8 +16,11 @@ # SPARK and miniKanren gate callers are all of that shape. # # Idempotent: a lock that does not list the workflow is left untouched. -# deonboard-estate-audit.sh is the original single-purpose form and now -# delegates here. +# Fails closed (exit 3) on an actions.lock schema it has not been tested on. +# This is the one implementation: deonboard-estate-audit.sh is its +# estate-audit entry point and delegates here, so a fix lands for every +# caller at once (tests/deonboard-estate-audit.sh, +# tests/deonboard-lock-schemas.sh). set -euo pipefail repo="${1:?usage: deonboard-workflow.sh []}" @@ -42,14 +45,38 @@ if ! w="$wf" yq -e '.workflows | has(strenv(w))' "$lock" >/dev/null 2>&1; then exit 0 fi +# Lockfile schemas this script is tested against, one fixture each in +# tests/fixtures/actions-lock/ (tests/deonboard-lock-schemas.sh). An untested +# schema fails closed: add its fixture first, then its version here. +known_versions="v0.0.2 v0.0.3" +ver=$(yq '.version' "$lock") +case " $known_versions " in + *" $ver "*) ;; + *) echo "$repo: actions.lock schema '$ver' is untested by this script (tested: $known_versions); see tests/deonboard-lock-schemas.sh" >&2 + exit 3 ;; +esac + w="$wf" yq -i -I4 'del(.workflows[strenv(w)])' "$lock" +# owner/repo@ref -> lower-case owner/repo, ref as written. GitHub owner and +# repository names are case-insensitive, and gh actions-lock writes the +# nested `uses:` of a dependency in lower case while keeping the record key's +# original case: a lock can hold the edge 'swatinem/rust-cache@' and +# the record 'Swatinem/rust-cache@' (measured on hyperpolymath/januskey, +# 2026-10-14, under its hyperpolymath/standards record). Compared byte for +# byte, that record looks orphaned, and pruning it leaves a dangling edge, +# which kills the run at startup (standards scripts/close-lock.sh). Refs keep +# their case: git refs are case-sensitive. +lower_repo() { awk '{ i = index($0, "@"); print tolower(substr($0, 1, i - 1)) substr($0, i) }'; } + pruned=() while :; do used=$( { yq '.workflows[][]' "$lock" yq '.dependencies[] | select(has("uses")) | .uses[]' "$lock" - } | sort -u ) - unused=$(yq '.dependencies | keys | .[]' "$lock" | grep -vxF -f <(printf '%s\n' "$used") || true) + } | lower_repo | sort -u ) + unused=$(yq '.dependencies | keys | .[]' "$lock" | while IFS= read -r k; do + printf '%s\n' "$k" | lower_repo | grep -qxF -f <(printf '%s\n' "$used") || printf '%s\n' "$k" + done) [ -n "$unused" ] || break while IFS= read -r k; do k="$k" yq -i -I4 'del(.dependencies[strenv(k)])' "$lock" diff --git a/tests/deonboard-estate-audit.sh b/tests/deonboard-estate-audit.sh index 0111aa1..4469e94 100755 --- a/tests/deonboard-estate-audit.sh +++ b/tests/deonboard-estate-audit.sh @@ -1,24 +1,28 @@ #!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 # -# scripts/deonboard-estate-audit.sh test. +# De-onboard prune test: scripts/deonboard-workflow.sh and its estate-audit +# entry point, scripts/deonboard-estate-audit.sh. # -# The script removes main-estate-audit.yml from a consumer's actions.lock and -# prunes the dependencies nothing else reaches. Pruning one that IS still -# reached leaves a dangling edge, and a dangling edge kills the run at startup -# (standards scripts/close-lock.sh). That is the defect this test pins down: +# The script removes a workflow from a consumer's actions.lock and prunes the +# dependencies nothing else reaches. Pruning one that IS still reached leaves +# a dangling edge, and a dangling edge kills the run at startup (standards +# scripts/close-lock.sh). That is the defect this test pins down: # gh actions-lock writes nested `uses:` in lower case, so a record key # 'Swatinem/rust-cache@' is reached by the edge # 'swatinem/rust-cache@', and a byte-for-byte compare called it orphaned # (measured on hyperpolymath/januskey's lock, 2026-10-14). # -# A mutant with the byte-for-byte compare restored must fail, or this suite -# proves nothing. +# Both entry points are run, because the fix once landed in one copy and not +# the other. A mutant of deonboard-workflow.sh with the byte-for-byte compare +# restored must fail through BOTH: through the wrapper too, or the wrapper is +# not delegating and this suite proves nothing about it. set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -SCRIPT="$ROOT/scripts/deonboard-estate-audit.sh" +WF=".github/workflows/main-estate-audit.yml" +RUST_CACHE='Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6' PASS=0; FAIL=0 ok() { printf ' ok %s\n' "$1"; PASS=$((PASS+1)); } bad() { printf ' FAIL %s\n' "$1"; FAIL=$((FAIL+1)); } @@ -74,6 +78,15 @@ dependencies: EOF } +# De-onboard main-estate-audit.yml from through one entry point of +# the scripts in . +deonboard() { # + case "$2" in + wrapper) bash "$1/deonboard-estate-audit.sh" "$3" ;; + generic) bash "$1/deonboard-workflow.sh" "$3" "$WF" ;; + esac +} + has_dep() { k="$2" yq -e '.dependencies | has(strenv(k))' "$1/.github/workflows/actions.lock" >/dev/null 2>&1; } # Every edge (a workflows: ref or a nested uses:) must name a record, owner and @@ -85,44 +98,57 @@ dangling() { # -> prints dangling edges | lower | sort -u | grep -vxF -f <(yq '.dependencies | keys | .[]' "$lock" | lower) || true } -echo "== the fixed script ==" -FIX="$WORK/fixed"; make_fixture "$FIX" -if out="$(bash "$SCRIPT" "$FIX" 2>&1)"; then ok "exits 0"; else bad "exits non-zero: $out"; fi -if w='.github/workflows/main-estate-audit.yml' yq -e '.workflows | has(strenv(w))' "$FIX/.github/workflows/actions.lock" >/dev/null 2>&1; then - bad "main-estate-audit.yml is still onboarded" -else - ok "main-estate-audit.yml is de-onboarded" -fi -for k in 'hyperpolymath/cicd-suite@main' 'hyperpolymath/deed-ecosystem@f9d999b60cb5f383679ea19912bcdc49c944973a' 'actions/checkout@v7.0.1'; do - if has_dep "$FIX" "$k"; then bad "kept $k, which nothing reaches any more"; else ok "pruned $k"; fi +for entry in wrapper generic; do + echo "== the fixed script, through the $entry entry point ==" + FIX="$WORK/fixed-$entry"; make_fixture "$FIX" + if out="$(deonboard "$ROOT/scripts" "$entry" "$FIX" 2>&1)"; then ok "exits 0"; else bad "exits non-zero: $out"; fi + if w="$WF" yq -e '.workflows | has(strenv(w))' "$FIX/.github/workflows/actions.lock" >/dev/null 2>&1; then + bad "main-estate-audit.yml is still onboarded" + else + ok "main-estate-audit.yml is de-onboarded" + fi + for k in 'hyperpolymath/cicd-suite@main' 'hyperpolymath/deed-ecosystem@f9d999b60cb5f383679ea19912bcdc49c944973a' 'actions/checkout@v7.0.1'; do + if has_dep "$FIX" "$k"; then bad "kept $k, which nothing reaches any more"; else ok "pruned $k"; fi + done + if has_dep "$FIX" "$RUST_CACHE"; then + ok "kept Swatinem/rust-cache, still reached as swatinem/rust-cache" + else + bad "pruned Swatinem/rust-cache, which the standards record still reaches" + fi + d="$(dangling "$FIX")" + if [ -z "$d" ]; then ok "no dangling edge"; else bad "dangling edge(s): $d"; fi + cp "$FIX/.github/workflows/actions.lock" "$WORK/after-first" + if deonboard "$ROOT/scripts" "$entry" "$FIX" >/dev/null 2>&1 && cmp -s "$WORK/after-first" "$FIX/.github/workflows/actions.lock"; then + ok "a second run changes nothing" + else + bad "a second run changed the lock or failed" + fi done -if has_dep "$FIX" 'Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6'; then - ok "kept Swatinem/rust-cache, still reached as swatinem/rust-cache" -else - bad "pruned Swatinem/rust-cache, which the standards record still reaches" -fi -d="$(dangling "$FIX")" -if [ -z "$d" ]; then ok "no dangling edge"; else bad "dangling edge(s): $d"; fi -cp "$FIX/.github/workflows/actions.lock" "$WORK/after-first" -if bash "$SCRIPT" "$FIX" >/dev/null 2>&1 && cmp -s "$WORK/after-first" "$FIX/.github/workflows/actions.lock"; then - ok "a second run changes nothing" + +echo "== both entry points give the same lock ==" +if cmp -s "$WORK/fixed-wrapper/.github/workflows/actions.lock" "$WORK/fixed-generic/.github/workflows/actions.lock"; then + ok "identical results" else - bad "a second run changed the lock or failed" + bad "the entry points disagree: $(diff "$WORK/fixed-wrapper/.github/workflows/actions.lock" "$WORK/fixed-generic/.github/workflows/actions.lock" | tr '\n' ' ')" fi -echo "== a mutant with the byte-for-byte compare must fail ==" -MUT="$WORK/mutant.sh" -sed 's/ | lower_repo//g' "$SCRIPT" > "$MUT" -if cmp -s "$SCRIPT" "$MUT"; then +echo "== a mutant with the byte-for-byte compare must fail through both entry points ==" +MUT="$WORK/mutant/scripts"; mkdir -p "$MUT" +cp "$ROOT/scripts/deonboard-estate-audit.sh" "$MUT/" +sed 's/ | lower_repo//g' "$ROOT/scripts/deonboard-workflow.sh" > "$MUT/deonboard-workflow.sh" +chmod +x "$MUT/deonboard-estate-audit.sh" "$MUT/deonboard-workflow.sh" +if cmp -s "$ROOT/scripts/deonboard-workflow.sh" "$MUT/deonboard-workflow.sh"; then bad "mutant is identical to the script: the case-folding the test exists for is gone" else - MFIX="$WORK/mutated"; make_fixture "$MFIX" - bash "$MUT" "$MFIX" >/dev/null 2>&1 || true - if has_dep "$MFIX" 'Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6' && [ -z "$(dangling "$MFIX")" ]; then - bad "mutant survived: the checks above do not detect the defect" - else - ok "mutant killed: it prunes Swatinem/rust-cache and leaves a dangling edge" - fi + for entry in wrapper generic; do + MFIX="$WORK/mutated-$entry"; make_fixture "$MFIX" + deonboard "$MUT" "$entry" "$MFIX" >/dev/null 2>&1 || true + if has_dep "$MFIX" "$RUST_CACHE" && [ -z "$(dangling "$MFIX")" ]; then + bad "mutant survived through the $entry entry point: the checks above do not detect the defect" + else + ok "mutant killed through the $entry entry point: it prunes Swatinem/rust-cache and leaves a dangling edge" + fi + done fi echo diff --git a/tests/deonboard-lock-schemas.sh b/tests/deonboard-lock-schemas.sh index d7a535f..564a76d 100755 --- a/tests/deonboard-lock-schemas.sh +++ b/tests/deonboard-lock-schemas.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 # -# scripts/deonboard-estate-audit.sh against every actions.lock schema it -# claims to support. +# scripts/deonboard-workflow.sh, and its estate-audit entry point +# scripts/deonboard-estate-audit.sh, against every actions.lock schema they +# claim to support. # # The de-onboard script edits a lockfile that gh-actions-lock owns. When # gh-actions-lock moved from schema v0.0.2 to v0.0.3 (v0.2.0, "Upgraded @@ -20,6 +21,11 @@ # * all fixtures are the same lock in different schemas, so all must prune # the same dependency keys. # +# Every check runs through both entry points. The guard once lived in the +# estate-audit copy only, and the generic script the language-gate rollout +# uses accepted any schema; one implementation, tested through both doors, +# keeps that from recurring. +# # The fixtures were also parsed, before and after, by the canonical parser # (github.com/github/actions-lockfile/go v0.0.6, the one gh-actions-lock v0.2.0 # uses). That parser rejects unknown fields and future versions, so it is the @@ -30,12 +36,12 @@ # refused with the lock untouched, and a mutant script that skips the prune # must FAIL the orphan check. # -# Usage: deonboard-lock-schemas.sh [script] (a path lets the mutant run) +# Usage: deonboard-lock-schemas.sh [scripts-dir] (default: this repo's scripts/) set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -SCRIPT="${1:-$ROOT/scripts/deonboard-estate-audit.sh}" +SCRIPTS="${1:-$ROOT/scripts}" FIXTURES="$ROOT/tests/fixtures/actions-lock" WF=".github/workflows/main-estate-audit.yml" PASS=0; FAIL=0 @@ -43,7 +49,9 @@ ok() { printf ' ok %s\n' "$1"; PASS=$((PASS+1)); } bad() { printf ' FAIL %s\n' "$1"; FAIL=$((FAIL+1)); } yq --version 2>/dev/null | grep -q mikefarah || { echo "mikefarah yq v4 is required"; exit 2; } -[ -f "$SCRIPT" ] || { echo "no script at $SCRIPT"; exit 2; } +for s in deonboard-workflow.sh deonboard-estate-audit.sh; do + [ -f "$SCRIPTS/$s" ] || { echo "no script at $SCRIPTS/$s"; exit 2; } +done work=$(mktemp -d); trap 'rm -rf "$work"' EXIT @@ -55,6 +63,14 @@ stage() { # printf '%s\n' "$r" } +# De-onboard $WF from through one entry point of the scripts in . +deonboard() { # + case "$2" in + wrapper) bash "$1/deonboard-estate-audit.sh" "$3" ;; + generic) bash "$1/deonboard-workflow.sh" "$3" "$WF" ;; + esac +} + # References that do not resolve to a dependency key (expect none). dangling() { # { yq '.workflows[][]' "$1" @@ -79,7 +95,7 @@ removed_keys() { # } echo "== every supported schema has a fixture" -known=$(sed -n 's/^known_versions="\(.*\)"$/\1/p' "$SCRIPT") +known=$(sed -n 's/^known_versions="\(.*\)"$/\1/p' "$SCRIPTS/deonboard-workflow.sh") [ -n "$known" ] && ok "script declares known_versions: $known" || bad "script declares no known_versions" for v in $known; do [ -f "$FIXTURES/$v.lock" ] && ok "fixture for $v" || bad "no fixture $FIXTURES/$v.lock for supported schema $v" @@ -88,60 +104,69 @@ done reference="" for fx in "$FIXTURES"/*.lock; do name=$(basename "$fx" .lock) - echo "== $name" before_ver=$(yq '.version' "$fx") - w="$WF" yq -e '.workflows | has(strenv(w))' "$fx" >/dev/null \ - && ok "fixture onboards $WF" || bad "fixture does not onboard $WF, so it tests nothing" + for entry in wrapper generic; do + echo "== $name, through the $entry entry point" + w="$WF" yq -e '.workflows | has(strenv(w))' "$fx" >/dev/null \ + && ok "fixture onboards $WF" || bad "fixture does not onboard $WF, so it tests nothing" + + r=$(stage "$fx" "$name-$entry") + lock="$r/.github/workflows/actions.lock" + if out=$(deonboard "$SCRIPTS" "$entry" "$r" 2>&1); then ok "script exits 0"; else bad "script failed: $out"; continue; fi + + w="$WF" yq -e '.workflows | has(strenv(w)) | not' "$lock" >/dev/null \ + && ok "$WF entry removed" || bad "$WF entry still present" + [ "$(yq '.version' "$lock")" = "$before_ver" ] \ + && ok "version line kept ($before_ver)" || bad "version changed to $(yq '.version' "$lock")" + added=$(diff "$fx" "$lock" | grep -c '^>' || true) + [ "$added" -eq 0 ] && ok "pure removals" || bad "$added line(s) added or rewritten" + d=$(dangling "$lock"); [ -z "$d" ] && ok "every reference resolves" || bad "dangling: $d" + o=$(orphans "$lock"); [ -z "$o" ] && ok "no orphaned dependencies" || bad "orphaned: $o" + + cp "$lock" "$work/once" + deonboard "$SCRIPTS" "$entry" "$r" >/dev/null 2>&1 + cmp -s "$work/once" "$lock" && ok "idempotent" || bad "second run changed the lock" + + keys=$(removed_keys "$fx" "$lock") + if [ -z "$reference" ]; then + reference="$keys" + [ -n "$keys" ] && ok "pruned: $(oneline "$keys")" || bad "pruned nothing" + else + [ "$keys" = "$reference" ] && ok "same keys pruned as the first run" \ + || bad "pruned set differs: $(oneline "$keys")" + fi + done +done - r=$(stage "$fx" "$name") +for entry in wrapper generic; do + echo "== negative control: an untested schema is refused, lock untouched ($entry)" + r=$(stage "$FIXTURES/v0.0.2.lock" "future-$entry") lock="$r/.github/workflows/actions.lock" - if out=$(bash "$SCRIPT" "$r" 2>&1); then ok "script exits 0"; else bad "script failed: $out"; continue; fi - - w="$WF" yq -e '.workflows | has(strenv(w)) | not' "$lock" >/dev/null \ - && ok "$WF entry removed" || bad "$WF entry still present" - [ "$(yq '.version' "$lock")" = "$before_ver" ] \ - && ok "version line kept ($before_ver)" || bad "version changed to $(yq '.version' "$lock")" - added=$(diff "$fx" "$lock" | grep -c '^>' || true) - [ "$added" -eq 0 ] && ok "pure removals" || bad "$added line(s) added or rewritten" - d=$(dangling "$lock"); [ -z "$d" ] && ok "every reference resolves" || bad "dangling: $d" - o=$(orphans "$lock"); [ -z "$o" ] && ok "no orphaned dependencies" || bad "orphaned: $o" - - cp "$lock" "$work/once" - bash "$SCRIPT" "$r" >/dev/null 2>&1 - cmp -s "$work/once" "$lock" && ok "idempotent" || bad "second run changed the lock" - - keys=$(removed_keys "$fx" "$lock") - if [ -z "$reference" ]; then - reference="$keys" - [ -n "$keys" ] && ok "pruned: $(oneline "$keys")" || bad "pruned nothing" - else - [ "$keys" = "$reference" ] && ok "same keys pruned as the first fixture" \ - || bad "pruned set differs: $(oneline "$keys")" - fi + sed -i "s/^version: 'v0\.0\.2'$/version: 'v9.9.9'/" "$lock" + cp "$lock" "$work/future-before" + deonboard "$SCRIPTS" "$entry" "$r" >/dev/null 2>&1; rc=$? + [ "$rc" -eq 3 ] && ok "exit 3 on schema v9.9.9" || bad "exit $rc on schema v9.9.9 (want 3)" + cmp -s "$work/future-before" "$lock" && ok "lock untouched" || bad "lock modified despite refusal" done -echo "== negative control: an untested schema is refused, lock untouched" -r=$(stage "$FIXTURES/v0.0.2.lock" future) -lock="$r/.github/workflows/actions.lock" -sed -i "s/^version: 'v0\.0\.2'$/version: 'v9.9.9'/" "$lock" -cp "$lock" "$work/future-before" -bash "$SCRIPT" "$r" >/dev/null 2>&1; rc=$? -[ "$rc" -eq 3 ] && ok "exit 3 on schema v9.9.9" || bad "exit $rc on schema v9.9.9 (want 3)" -cmp -s "$work/future-before" "$lock" && ok "lock untouched" || bad "lock modified despite refusal" - -echo "== negative control: a mutant that skips the prune fails the orphan check" -if [ "$SCRIPT" = "$ROOT/scripts/deonboard-estate-audit.sh" ]; then - mutant="$work/mutant.sh" - sed 's/^ \[ -n "\$unused" \] || break$/ break/' "$SCRIPT" > "$mutant" - if cmp -s "$SCRIPT" "$mutant"; then - bad "mutation did not apply; the prune loop changed shape, update this control" - else - r=$(stage "$FIXTURES/v0.0.3.lock" mutant) - bash "$mutant" "$r" >/dev/null 2>&1 - o=$(orphans "$r/.github/workflows/actions.lock") - [ -n "$o" ] && ok "mutant leaves orphans, and the check sees them: $(oneline "$o")" \ - || bad "orphan check passed a mutant that does not prune" - fi +# Run against this repo's own scripts only: a mutant of a mutant tests nothing. +if [ "$SCRIPTS" = "$ROOT/scripts" ]; then + mutant="$work/mutant/scripts"; mkdir -p "$mutant" + cp "$SCRIPTS/deonboard-estate-audit.sh" "$mutant/" + sed 's/^ \[ -n "\$unused" \] || break$/ break/' "$SCRIPTS/deonboard-workflow.sh" > "$mutant/deonboard-workflow.sh" + chmod +x "$mutant/deonboard-estate-audit.sh" "$mutant/deonboard-workflow.sh" + for entry in wrapper generic; do + echo "== negative control: a mutant that skips the prune fails the orphan check ($entry)" + if cmp -s "$SCRIPTS/deonboard-workflow.sh" "$mutant/deonboard-workflow.sh"; then + bad "mutation did not apply; the prune loop changed shape, update this control" + else + r=$(stage "$FIXTURES/v0.0.3.lock" "mutant-$entry") + deonboard "$mutant" "$entry" "$r" >/dev/null 2>&1 + o=$(orphans "$r/.github/workflows/actions.lock") + [ -n "$o" ] && ok "mutant leaves orphans, and the check sees them: $(oneline "$o")" \ + || bad "orphan check passed a mutant that does not prune" + fi + done fi echo From 3325927364067e64a7bf027a0d268027d63f0f39 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 11:51:16 +0000 Subject: [PATCH 2/2] chore(ci): repin estate audit composites to #54 (follow-up to #51) Move all 24 hyperpolymath/cicd-suite/actions/* refs in .github/workflows/main-estate-audit.yml from 1aa6bfb (#48) to 9a947fa, the current main HEAD (#54's merge commit). #51 changed actions/manifest-check and left the pins at 1aa6bfb, so main's "Composite shell contract" has been red since 8bf6d92: tests/lock-transitive-closure.sh reports that manifest-check is pinned at 1aa6bfb, which differs from HEAD's. The pin moves to 9a947fa rather than 8bf6d92 so it serves everything on main, including #54's additions. This also makes #51's manifest-deed-paths-ignore and manifest-k9-paths-ignore inputs take effect: manifest-check@1aa6bfb does not declare them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kjdf4Mf4Z41bNQh2TrsEp9 (cherry picked from commit 80bea8c1956fa736e01cfa623ab39abaeff9fdd5) --- .github/workflows/main-estate-audit.yml | 48 ++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/main-estate-audit.yml b/.github/workflows/main-estate-audit.yml index 8019d36..4f4ac55 100755 --- a/.github/workflows/main-estate-audit.yml +++ b/.github/workflows/main-estate-audit.yml @@ -84,16 +84,16 @@ jobs: persist-credentials: false - name: Required Files Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/required-files-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/required-files-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: SPDX License Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/spdx-license-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/spdx-license-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Affirmation Document Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/affirmation-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/affirmation-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Academic Referencing Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/referencing-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/referencing-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) hygiene: name: hygiene @@ -105,19 +105,19 @@ jobs: persist-credentials: false - name: Code Hygiene Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/code-hygiene-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/code-hygiene-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Formatting Validation Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/formatting-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/formatting-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Linguist & Banned Languages Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/linguist-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/linguist-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Are We UnAPI Gate (Secret Scanning) if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/secrets-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/secrets-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Reasonably Good Token Validation Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/vaulted-tokens-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/vaulted-tokens-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) manifests: name: manifests @@ -129,13 +129,13 @@ jobs: persist-credentials: false - name: Manifest Validation Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/manifest-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/manifest-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) with: deed-paths-ignore: ${{ inputs.manifest-deed-paths-ignore }} k9-paths-ignore: ${{ inputs.manifest-k9-paths-ignore }} - name: Recipes Set Validation Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/recipes-set-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/recipes-set-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) languages: name: languages @@ -147,16 +147,16 @@ jobs: persist-credentials: false - name: Idris2 ABI Purity Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/idris2-abi-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/idris2-abi-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Zig UnifiedApiAdapter API Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/zig-unified-api-adapter-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/zig-unified-api-adapter-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Proof Runner Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/proof-runner-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/proof-runner-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: BoJ Cartridge Validation Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/boj-cartridge-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/boj-cartridge-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) advisory: name: advisory @@ -168,28 +168,28 @@ jobs: persist-credentials: false - name: Contractile Validation Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/contractile-validation-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/contractile-validation-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Panic Attack & Pons Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/custom-tools-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/custom-tools-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: WWW & Well-Known Compliance Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/www-compliance-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/www-compliance-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Accreditations & Badges Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/badges-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/badges-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Metrics Extraction Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/metrics-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/metrics-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Test & Benchmarks Dashboard Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/tests-benches-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/tests-benches-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Hosting & Site Status Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/hosting-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/hosting-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Git-Sea Analytics Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/gitsea-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/gitsea-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54) - name: Trust & Humans Validation Gate if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} - uses: hyperpolymath/cicd-suite/actions/trust-humans-check@1aa6bfb657f8240db089a8eef1ad4a7ac198bcf0 # 2026-10-09 (#48) + uses: hyperpolymath/cicd-suite/actions/trust-humans-check@9a947faad4f7f6640148980dd892b39f216fd091 # 2026-10-14 (#54)