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
2 changes: 2 additions & 0 deletions .github/workflows/actions.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ version: 'v0.0.2'
workflows:
'.github/workflows/code-hygiene-self-test.yml':
- 'actions/checkout@v7.0.1'
- 'hyperpolymath/deed-ecosystem@f9d999b60cb5f383679ea19912bcdc49c944973a'
- 'hyperpolymath/k9-ecosystem@2155aa26a21758f2ba119f61bc7e0e1981c106fb'
'.github/workflows/label-triage.yml': []
'.github/workflows/labels.yml': []
'.github/workflows/main-estate-audit.yml':
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/code-hygiene-self-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on:
- 'actions/spdx-license-check/**'
- 'tests/**'
- '.github/workflows/code-hygiene-self-test.yml'
- '.github/workflows/actions.lock'
pull_request:
paths:
- 'actions/code-hygiene-check/**'
Expand All @@ -29,6 +30,7 @@ on:
- 'actions/spdx-license-check/**'
- 'tests/**'
- '.github/workflows/code-hygiene-self-test.yml'
- '.github/workflows/actions.lock'

permissions:
contents: read
Expand All @@ -44,6 +46,9 @@ jobs:
# those pins goes stale the job dies in `prepare`, before a single step runs —
# which is exactly how the defect this suite covers stayed invisible. This job
# touches nothing but checkout, so its square always reflects the suite.
# For the same reason it is where the actions.lock closure guard runs: a lock
# that is missing a composite-reached pin kills `Gate controls` in `Set up job`,
# so a check living there could never report the very defect it tests for.
shell-contract:
name: Composite shell contract
runs-on: ubuntu-latest
Expand All @@ -53,6 +58,7 @@ jobs:
with:
persist-credentials: false
- run: bash tests/composite-shell-contract.sh
- run: bash tests/lock-transitive-closure.sh

test:
name: Gate controls
Expand Down
118 changes: 118 additions & 0 deletions tests/lock-transitive-closure.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
#
# actions.lock transitive-closure test.
#
# An onboarded workflow may invoke a same-repo composite as `uses: $/actions/x`.
# That ref is inherently pinned (it resolves at the running commit) and needs no
# lockfile entry of its own — but the runner ALSO validates that composite's
# internals against the lock, and it does so against the *calling workflow's*
# entry in the `workflows:` map. A remote ref reached only through a composite
# must therefore be listed under every workflow that reaches it, or the run dies
# in `Set up job` with `lockfile missing pin for ...` before a step executes.
#
# Why this test exists rather than a comment in the lockfile:
#
# ecd0240 (#16) added the two transitive SHA refs for code-hygiene-self-test
# ... they survived #17, #18, #25 — `Gate controls` was green
# 3b4afaf (#28) regenerated the lock and SILENTLY DROPPED both
#
# `gh actions-lock`'s extractor reads step-level `uses:` only. It cannot derive
# a ref reached through a composite, so a regenerate deletes any added by hand —
# and `gh actions-lock --verify` then returns rc=0 on the result. #28's own
# commit message says it "pins the SHA-form transitive deps reached via called
# reusables"; its diff removes exactly those, and its cited proof was that same
# rc=0. The tool cannot see the defect it creates, so the lockfile needs a gate
# that is not the tool. `--no-fix` still reports "Scanning 4 workflows", rc=0,
# against the broken file.
#
# Measured 2026-09-22, run 35786094563 (push, main, fa71ac2):
# ##[error]lockfile missing pin for hyperpolymath/deed-ecosystem@f9d999b6...
# Gate controls: failure (1 steps) <- dead before a single step ran
#
# Usage: lock-transitive-closure.sh [lockfile] (a path lets the mutant run)

set -uo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOCK="${1:-$ROOT/.github/workflows/actions.lock}"
PASS=0; FAIL=0
ok() { printf ' ok %s\n' "$1"; PASS=$((PASS+1)); }

Check warning on line 40 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wb&open=AaDLCtwnPxHANMuoX1Wb&pullRequest=34
bad() { printf ' FAIL %s\n' "$1"; FAIL=$((FAIL+1)); }

Check warning on line 41 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wc&open=AaDLCtwnPxHANMuoX1Wc&pullRequest=34

command -v yq >/dev/null || { echo "yq is required (Y-1: gates read YAML with yq, never grep)"; exit 2; }
[ -f "$LOCK" ] || { echo "no lockfile at $LOCK"; exit 2; }

Check failure on line 44 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wd&open=AaDLCtwnPxHANMuoX1Wd&pullRequest=34

# yq here is mikefarah v4. `--arg` and `// empty` are jq-only and fail as a
# LEXER ERROR, which a stray 2>/dev/null turns into a clean empty result and a
# vacuous pass. Errors stay visible on purpose; the expressions below are the
# mikefarah forms (strenv / select(. != null)).
yqr() { yq -r "$@"; }

Check warning on line 50 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1We&open=AaDLCtwnPxHANMuoX1We&pullRequest=34

# owner/repo/subpath@ref -> owner/repo@ref, the shape lock keys use.
lock_key() { printf '%s\n' "$1" | sed -E 's#^([^/]+/[^/@]+)(/[^@]*)?@(.*)$#\1@\3#'; }

Check warning on line 53 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wf&open=AaDLCtwnPxHANMuoX1Wf&pullRequest=34

Check warning on line 53 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wg&open=AaDLCtwnPxHANMuoX1Wg&pullRequest=34

# Every remote ref a workflow reaches through same-repo ($/) composites,
# following nested composites to a fixpoint.
closure_of_workflow() { # <workflow path relative to ROOT>
local wf="$ROOT/$1" seen=" " queue=() cur act ref
[ -f "$wf" ] || return 0

Check failure on line 59 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wh&open=AaDLCtwnPxHANMuoX1Wh&pullRequest=34
while IFS= read -r cur; do
[ -n "$cur" ] && queue+=("${cur#\$/}")

Check failure on line 61 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wi&open=AaDLCtwnPxHANMuoX1Wi&pullRequest=34
done < <(yqr '.jobs[].steps[].uses | select(. != null)' "$wf" | grep '^\$/' || true)
while [ "${#queue[@]}" -gt 0 ]; do

Check failure on line 63 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wj&open=AaDLCtwnPxHANMuoX1Wj&pullRequest=34
cur="${queue[0]}"; queue=("${queue[@]:1}")
case "$seen" in *" $cur "*) continue ;; esac
seen="$seen$cur "
act="$ROOT/$cur/action.yml"
[ -f "$act" ] || continue

Check failure on line 68 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wk&open=AaDLCtwnPxHANMuoX1Wk&pullRequest=34
while IFS= read -r ref; do
[ -n "$ref" ] || continue

Check failure on line 70 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wl&open=AaDLCtwnPxHANMuoX1Wl&pullRequest=34
case "$ref" in
'$/'*) queue+=("${ref#\$/}") ;; # nested same-repo composite
./*) ;; # legacy local ref: not lockable
*) lock_key "$ref" ;;
esac
done < <(yqr '.runs.steps[].uses | select(. != null)' "$act")
done
}

echo "== every ref reached through a \$/ composite is declared for its workflow =="
checked=0
while IFS= read -r wf; do
[ -n "$wf" ] || continue

Check failure on line 83 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wm&open=AaDLCtwnPxHANMuoX1Wm&pullRequest=34
declared="$(w="$wf" yqr '.workflows[strenv(w)][]' "$LOCK" 2>/dev/null || true)"
while IFS= read -r need; do
[ -n "$need" ] || continue

Check failure on line 86 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wn&open=AaDLCtwnPxHANMuoX1Wn&pullRequest=34
checked=$((checked+1))
if printf '%s\n' "$declared" | grep -qxF "$need"; then
ok "$wf declares $need"
else
bad "$wf reaches $need through a composite but does not declare it"
echo " this is the shape that kills the job in Set up job, 1 step, no gate run"
fi
done < <(closure_of_workflow "$wf" | sort -u)
done < <(yqr '.workflows | keys | .[]' "$LOCK")

# A closure test with nothing to check is vacuous, and this repo HAS such edges
# (manifest-check reaches two). Assert the population is non-empty — this very
# assertion caught a jq-vs-mikefarah dialect bug on the first run of this file.
if [ "$checked" -eq 0 ]; then

Check failure on line 100 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wo&open=AaDLCtwnPxHANMuoX1Wo&pullRequest=34
bad "no composite-reached refs found at all — the test is vacuous"
else
ok "checked $checked composite-reached ref(s) — population is non-empty"
fi

echo "== every declared ref resolves to a dependencies: entry =="
while IFS= read -r ref; do
[ -n "$ref" ] || continue

Check failure on line 108 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wp&open=AaDLCtwnPxHANMuoX1Wp&pullRequest=34
if r="$ref" yq -e '.dependencies[strenv(r)]' "$LOCK" >/dev/null 2>&1; then
ok "dependencies: defines $ref"
else
bad "workflows: names $ref but dependencies: has no such key"
fi
done < <(yqr '.workflows[][]' "$LOCK" | sort -u)

echo
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]

Check failure on line 118 in tests/lock-transitive-closure.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_cicd-suite&issues=AaDLCtwnPxHANMuoX1Wq&open=AaDLCtwnPxHANMuoX1Wq&pullRequest=34
Loading