From 0ffdc2069b810983348a9bd06e1cd0cc8dc1dd8d Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" Date: Sun, 20 Sep 2026 23:45:32 +0000 Subject: [PATCH 1/2] fix(hooks): make validate-sha-pins.sh able to fail Two independent defects left 26 unpinned action refs uncertified. 1. find -print bound to the last -o clause only, so the *.yaml branch alone was printed. This repo has 53 root .yml workflows and no .yaml ones: the scan emitted 0 paths and the hook announced "All workflow actions are SHA-pinned" over the whole tree. Parenthesising the -o chain emits 166. 2. validate_file() complained only when a file had an unpinned ref AND no pinned ref anywhere, so one actions/checkout@ whitened every other action in the same workflow. Validation is now per line, requires a full 40-hex SHA, and skips local ./ paths and docker:// refs (neither is modelled by actions.lock). The scan also refuses to certify a tree containing no workflow files at all, which is the guard that keeps defect 1 from recurring. Scope is the root .github/{workflows,actions} tree plus nested copies, minus rhodium-standard-repositories/** -- measured as debt in the Debtfile rather than skipped silently. scripts/tests/validate-sha-pins-test.sh covers each failure mode, with a planted positive first so a gate that passes everything is caught too; run-shell-test-suite.sh discovers it in CI. --- .githooks/validate-sha-pins.sh | 106 +++++++++++++++++++----- scripts/tests/validate-sha-pins-test.sh | 81 ++++++++++++++++++ 2 files changed, 166 insertions(+), 21 deletions(-) create mode 100755 scripts/tests/validate-sha-pins-test.sh diff --git a/.githooks/validate-sha-pins.sh b/.githooks/validate-sha-pins.sh index 8a7b38924..6e7e84381 100755 --- a/.githooks/validate-sha-pins.sh +++ b/.githooks/validate-sha-pins.sh @@ -1,41 +1,105 @@ #!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 -# SHA-Pinning Validation - +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +# +# SHA-Pinning Validation -- canon rule 10: every third-party `uses:` ref names a +# full commit SHA, never a moving tag or a branch. +# +# TWO BUGS IN THE VERSION THIS REPLACES, both measured 2026-09-21 on main: +# +# 1. The fallback scan was +# find "$SCAN_PATH" -path '*/.git/*' -prune -o \ +# -path '*/.github/workflows/*.yml' -o -path '*/.github/workflows/*.yaml' -print +# `find` applies a bare action to the last test only, so -print bound to the +# *.yaml clause alone. This repo has 53 root .yml workflows and no .yaml ones, +# so find emitted 0 paths, the loop validated nothing, and the hook announced +# "All workflow actions are SHA-pinned" over 26 unpinned refs. Parenthesising +# the -o chain would have emitted 166. A scan that finds nothing must say so, +# hence the empty-tree refusal below -- that guard is the part that keeps this +# class of silent all-clear from coming back. +# +# 2. validate_file() whitened whole files: it complained only when a file had an +# unpinned ref AND no pinned ref anywhere, so one `actions/checkout@` +# excused every other action in the same workflow. Validation is per line now. +# +# SCOPE. Root .github/{workflows,actions} plus those directories in nested trees, +# minus rhodium-standard-repositories/**: that subtree is a vendored mirror of the +# RSR canon (gitlab.com/hyperpolymath/rhodium-standard-repositories), other +# repositories' workflows reproduced here as templates. Re-pinning it here fixes no +# live runner and desynchronises the mirror, so its unpinned refs are measured and +# tolerated in .machine_readable/Debtfile.a2ml (vendored-mirror-unpinned-actions) +# rather than skipped silently. The scope matches .githooks/validate-actions-lock.sh, +# the sibling hook this repo also runs in CI (.github/workflows/actions-lock-gate.yml). set -euo pipefail SCAN_PATH="${INPUT_PATH:-.}" STAGED_FILES="${INPUT_STAGED_FILES:-}" +VENDORED_MARK="rhodium-standard-repositories/" ERRORS=0 +SCANNED=0 +SKIPPED=0 + +is_vendored() { case "$1" in *"${VENDORED_MARK}"*) return 0 ;; *) return 1 ;; esac; } + +# One line of a workflow, emitted per unpinned ref. Local paths (`./`, `../`) are +# this repo's own composite actions and `docker://` refs are container images, not +# actions -- neither is modelled by actions.lock, which keys actions only. +UNPINNED_FILTER() { + grep -nE '^[[:space:]]*(-[[:space:]]*)?uses:[[:space:]]+[A-Za-z0-9]' \ + | grep -vE 'uses:[[:space:]]+[./]' \ + | grep -vE 'uses:[[:space:]]+docker://' \ + | grep -vE 'uses:[[:space:]]+[^[:space:]@]+@[0-9a-f]{40}([^0-9a-f]|$)' \ + || true +} validate_file() { - local file="$1" - - # Check for unpinned actions (uses: without SHA) - if grep -qE 'uses:[[:space:]]+[a-zA-Z]' "$file" && ! grep -qE 'uses:[[:space:]]+[a-zA-Z].*@[a-f0-9]' "$file"; then - echo "[validate-sha-pins] ERROR: $file has unpinned actions" >&2 + local file="$1" rec lineno body + while IFS= read -r rec; do + [ -n "$rec" ] || continue + lineno="${rec%%:*}" + body="${rec#*:}" + body="${body#"${body%%[![:space:]]*}"}" + echo "[validate-sha-pins] ERROR: $file:$lineno: ${body}" >&2 + echo " unpinned ref: canon rule 10 wants owner/repo@<40-hex> plus a '# ' comment," >&2 + echo " and a matching key in .github/workflows/actions.lock" >&2 ERRORS=$((ERRORS + 1)) - fi + done < <(UNPINNED_FILTER < "$file") } -# If staged files provided, only check those if [ -n "$STAGED_FILES" ]; then while IFS=$'\n' read -r file; do - [ -z "$file" ] && continue - # Only check workflow files - [[ "$file" == *.yml || "$file" == *.yaml ]] || continue - [[ "$file" == *".github/workflows/"* ]] || continue + [ -n "$file" ] || continue + case "$file" in *.yml|*.yaml) ;; *) continue ;; esac + case "$file" in *".github/workflows/"*|*".github/actions/"*) ;; *) continue ;; esac + is_vendored "$file" && { SKIPPED=$((SKIPPED + 1)); continue; } [ -f "$file" ] || continue + SCANNED=$((SCANNED + 1)) validate_file "$file" done <<< "$STAGED_FILES" else - while IFS= read -r workflow; do - [ -f "$workflow" ] || continue - validate_file "$workflow" - done < <(find "$SCAN_PATH" -path '*/.git/*' -prune -o \ - -path '*/.github/workflows/*.yml' -o -path '*/.github/workflows/*.yaml' \ - -print 2>/dev/null || true) + ALL=() + while IFS= read -r f; do + [ -n "$f" ] && ALL+=("$f") + done < <(find "$SCAN_PATH" \ + -path '*/.git/*' -prune -o \ + \( -path '*/.github/workflows/*.yml' -o -path '*/.github/workflows/*.yaml' \ + -o -path '*/.github/actions/*.yml' -o -path '*/.github/actions/*.yaml' \) \ + -print 2>/dev/null | LC_ALL=C sort) + # An empty scan is not a pass -- see bug 1 above. + if [ "${#ALL[@]}" -eq 0 ]; then + echo "[validate-sha-pins] ERROR: found 0 workflow files under '$SCAN_PATH'; refusing to certify a tree it never scanned" >&2 + exit 1 + fi + for f in "${ALL[@]}"; do + if is_vendored "$f"; then SKIPPED=$((SKIPPED + 1)); continue; fi + SCANNED=$((SCANNED + 1)) + validate_file "$f" + done fi -[ $ERRORS -gt 0 ] && exit 1 -echo "[validate-sha-pins] All workflow actions are SHA-pinned" +echo "[validate-sha-pins] ${SCANNED} workflow file(s) scanned, ${SKIPPED} vendored mirror file(s) excluded, ${ERRORS} unpinned ref(s)" +if [ "$ERRORS" -gt 0 ]; then + echo "[validate-sha-pins] FAIL: $ERRORS unpinned action ref(s)" >&2 + exit 1 +fi +echo "[validate-sha-pins] OK: every third-party uses: ref in scope is pinned to a full SHA" exit 0 diff --git a/scripts/tests/validate-sha-pins-test.sh b/scripts/tests/validate-sha-pins-test.sh new file mode 100755 index 000000000..a7917ce70 --- /dev/null +++ b/scripts/tests/validate-sha-pins-test.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +# +# Tests for .githooks/validate-sha-pins.sh. +# +# ⚠ TESTS 1 AND 6 ARE THE POINT. Test 1 is the planted positive -- a fully pinned +# workflow must exit 0, which is the only way to tell a working gate from one that +# dies (or passes) on everything. Test 6 is the regression this file exists for: a +# fixture directory holding ONLY root-level *.yml workflows. The old find() bound +# -print to the *.yaml clause alone, so that exact shape printed zero paths and +# the hook certified "All workflow actions are SHA-pinned" while 26 refs in this +# repo floated on tags. Any gate that can scan nothing must fail loudly. +set -uo pipefail +HOOK="$(cd "$(dirname "$0")/../.." && pwd)/.githooks/validate-sha-pins.sh" +T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT +mkdir -p "$T/.github/workflows" "$T/.github/actions" +pass=0; fail=0 + +ck() { # name expected_exit staged_files + local out rc + out="$(cd "$T" && INPUT_STAGED_FILES="$3" bash "$HOOK" 2>&1)"; rc=$? + if [ "$rc" = "$2" ]; then printf ' ok %s (exit %s)\n' "$1" "$rc"; pass=$((pass+1)) + else printf ' FAIL %s (expected exit %s, got %s) output=%s\n' "$1" "$2" "$rc" "${out:-}"; fail=$((fail+1)); fi +} +ckf() { # name expected_exit dir -- fallback mode (no staged list), scanning $3 + local out rc + out="$(cd "$3" && INPUT_PATH="$3" bash "$HOOK" 2>&1)"; rc=$? + if [ "$rc" = "$2" ]; then printf ' ok %s (exit %s)\n' "$1" "$rc"; pass=$((pass+1)) + else printf ' FAIL %s (expected exit %s, got %s) output=%s\n' "$1" "$2" "$rc" "${out:-}"; fail=$((fail+1)); fi +} + +S40=3d3c42e5aac5ba805825da76410c181273ba90b1 + +printf 'name: pinned\non: push\njobs:\n a:\n steps:\n - uses: actions/checkout@%s # v7.0.1\n' "$S40" \ + > "$T/.github/workflows/pinned.yml" +printf 'name: tag\non: push\njobs:\n a:\n steps:\n - uses: actions/checkout@v7.0.1\n' \ + > "$T/.github/workflows/tag.yml" +printf 'name: mixed\non: push\njobs:\n a:\n steps:\n - uses: actions/checkout@%s # v7.0.1\n - uses: actions/cache@v6.1.0\n' "$S40" \ + > "$T/.github/workflows/mixed.yml" +printf 'name: branch\non: push\njobs:\n a:\n steps:\n - uses: hyperpolymath/thing@main\n' \ + > "$T/.github/workflows/branch.yml" +printf 'name: local\non: push\njobs:\n a:\n steps:\n - uses: ./local-action\n - uses: ./.github/actions/signed-push\n' \ + > "$T/.github/workflows/local.yml" +printf 'name: docker\non:\n a:\n container:\n image: node:20\n steps:\n - uses: docker://ghcr.io/org/img:1.2.3\n' \ + > "$T/.github/workflows/docker.yml" +printf 'name: short\non: push\njobs:\n a:\n steps:\n - uses: actions/checkout@abc123 # not a full SHA\n' \ + > "$T/.github/workflows/short.yml" + +echo "validate-sha-pins.sh" +ck "PLANTED POSITIVE: fully pinned workflow must PASS" 0 ".github/workflows/pinned.yml" +ck "moving tag FAILS" 1 ".github/workflows/tag.yml" +ck "one unpinned among pinned FAILS (whole-file whitening)" 1 ".github/workflows/mixed.yml" +ck "branch ref FAILS" 1 ".github/workflows/branch.yml" +ck "short hex ref FAILS (40 required)" 1 ".github/workflows/short.yml" +ck "local ./ action ignored" 0 ".github/workflows/local.yml" +ck "docker:// ref ignored" 0 ".github/workflows/docker.yml" +ck "non-workflow staged file ignored" 0 "README.adoc" +ck "two valid files pass together" 0 ".github/workflows/pinned.yml +.github/workflows/local.yml" +ck "one bad among good still fails" 1 ".github/workflows/pinned.yml +.github/workflows/tag.yml" +printf 'name: c\nruns:\n using: composite\n steps:\n - uses: actions/github-script@v9.0.0\n' > "$T/.github/actions/c.yml" +ck "composite action under .github/actions is checked" 1 ".github/actions/c.yml" + +# Test 6: the empty-scan regression -- root-level .yml only, fallback mode. +mkdir -p "$T/ymlonly/.github/workflows" +printf 'name: t\non: push\njobs:\n a:\n steps:\n - uses: actions/checkout@v7.0.1\n' > "$T/ymlonly/.github/workflows/a.yml" +printf 'name: t\non: push\njobs:\n a:\n steps:\n - uses: actions/checkout@%s # v7.0.1\n' "$S40" > "$T/ymlonly/.github/workflows/b.yml" +ckf "root-level *.yml IS scanned (find -print precedence)" 1 "$T/ymlonly" +mkdir -p "$T/nested/sub/.github/workflows" +printf 'name: t\non: push\njobs:\n a:\n steps:\n - uses: actions/checkout@v7.0.1\n' > "$T/nested/sub/.github/workflows/a.yml" +ckf "nested .github/workflows is scanned too" 1 "$T/nested" +mkdir -p "$T/vend/rhodium-standard-repositories/satellites/x/.github/workflows" +printf 'name: t\non: push\njobs:\n a:\n steps:\n - uses: actions/checkout@v7.0.1\n' > "$T/vend/rhodium-standard-repositories/satellites/x/.github/workflows/a.yml" +ckf "vendored RSR mirror is scoped out (see Debtfile)" 0 "$T/vend" +mkdir -p "$T/empty" +ckf "refuses to certify a tree with 0 workflow files" 1 "$T/empty" + +printf '\n%s passed, %s failed\n' "$pass" "$fail" +[ "$fail" -eq 0 ] From 03a99f28bba034045eafa142c5cc6e3f55c1d400 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" Date: Sun, 20 Sep 2026 23:45:32 +0000 Subject: [PATCH 2/2] fix(ci): pin the last 26 unpinned refs in root workflows Canon rule 10 requires owner/repo@<40-hex> with a '# ' comment and a matching actions.lock key. 8 files, 26 refs: actions/checkout v7.0.1 3d3c42e5aac5 actions/upload-artifact v7.0.1 043fb46d1a93 actions/cache v6.1.0 55cc8345863c actions/github-script v9.0.0 3a2844b7e9c4 actions/configure-pages v6.0.0 45bfe0192ca1 actions/upload-pages-artifact v5.0.0 fc324d354710 actions/deploy-pages v5.0.1 368f82528645 haskell-actions/setup v2.12.0 6037f33647c3 actions/create-github-app-token v3.2.0 bcd2ba492189 hyperpolymath/a2ml-ecosystem/secrets-check-action main -> f7a40a4d5cc8 Each SHA is the commit its tag names, resolved via the API and cross-checked against actions.lock, where nine of the ten were ALREADY pinned keys used by other workflows in this repo -- no new, unreviewed code is being introduced. The A2ML ref is pinned to f7a40a4d5cc8, the exact value .githooks/validate-actions-lock.sh EXPECTED_ABSENT already names. That entry was printing "stale exception, no workflow uses hyperpolymath/a2ml-ecosystem@f7a40a4d..." on main; using the pin it names clears the warning and keeps a retired action frozen instead of floating on main. It stays out of the lock sections, which is what the exception is for. actions.lock: the ten tag keys in the affected file sections become SHA keys (spec 6.4: same PR, never a follow-up), and 11 dependencies: blocks for tags no workflow now references are dropped, since the lockfile is the union of refs the workflows use. Mechanical conversion -- please re-run gh actions-lock to confirm it agrees byte for byte. --- .github/workflows/actions.lock | 92 ++++--------------- .github/workflows/apply-workflow-pins.yml | 8 +- .github/workflows/deed-conformance.yml | 2 +- .github/workflows/pages-archive.yml | 14 +-- .github/workflows/propagate-hooks.yml | 6 +- .github/workflows/security-gate-pr-target.yml | 10 +- .github/workflows/settings-drift-detect.yml | 4 +- .github/workflows/tag-ruleset-canon.yml | 8 +- .machine_readable/Debtfile.a2ml | 12 +++ 9 files changed, 56 insertions(+), 100 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index be7da4299..6f4afd421 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -11,9 +11,9 @@ workflows: '.github/workflows/allowlist-preflight-reusable.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/apply-workflow-pins.yml': - - 'actions/checkout@v7.0.1' - - 'actions/create-github-app-token@v3.2.0' - - 'actions/upload-artifact@v7.0.1' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' + - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' '.github/workflows/boj-build.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/canon-spine-lockstep.yml': @@ -38,7 +38,7 @@ workflows: '.github/workflows/debt-measure.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/deed-conformance.yml': - - 'actions/checkout@v7.0.1' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/doc-format.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/dyadt-verify.yml': @@ -89,18 +89,18 @@ workflows: '.github/workflows/no-js-scan.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/pages-archive.yml': - - 'actions/cache@v6.1.0' - - 'actions/checkout@v7.0.1' - - 'actions/configure-pages@v6.0.0' - - 'actions/deploy-pages@v5.0.1' - - 'actions/upload-pages-artifact@v5.0.0' - - 'haskell-actions/setup@v2.12.0' + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d' + - 'actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346' + - 'actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9' + - 'haskell-actions/setup@6037f33647c3f17758a2356c80fc4a53d7e0685d' '.github/workflows/pages.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - 'actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346' - 'actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9' '.github/workflows/propagate-hooks.yml': - - 'actions/checkout@v7.0.1' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/readme-derive-reusable.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/readme-derive.yml': [] @@ -124,14 +124,13 @@ workflows: '.github/workflows/secret-scanner-reusable.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/security-gate-pr-target.yml': - - 'actions/checkout@v7.0.1' - - 'actions/github-script@v9.0.0' - - 'hyperpolymath/a2ml-ecosystem@main' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3' '.github/workflows/self-test.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/settings-drift-detect.yml': - - 'actions/checkout@v7.0.1' - - 'actions/upload-artifact@v7.0.1' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' '.github/workflows/signed-push-smoke.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' @@ -141,60 +140,35 @@ workflows: '.github/workflows/tailscale-connect-reusable.yml': - 'tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888' '.github/workflows/tag-ruleset-canon.yml': - - 'actions/checkout@v7.0.1' - - 'actions/create-github-app-token@v3.2.0' - - 'actions/upload-artifact@v7.0.1' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' + - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' dependencies: 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9': ref: '55cc8345863c7cc4c66a329aec7e433d2d1c52a9' commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' owner_id: 44036562 repo_id: 215566462 - 'actions/cache@v6.1.0': - ref: 'v6.1.0' - commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' - owner_id: 44036562 - repo_id: 215566462 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1': ref: 'v7.0.1' commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' owner_id: 44036562 repo_id: 197814629 - 'actions/checkout@v7.0.1': - ref: 'v7.0.1' - commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' - owner_id: 44036562 - repo_id: 197814629 'actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d': ref: '45bfe0192ca1faeb007ade9deae92b16b8254a0d' commit: 'sha1-45bfe0192ca1faeb007ade9deae92b16b8254a0d' owner_id: 44036562 repo_id: 513659658 - 'actions/configure-pages@v6.0.0': - ref: 'v6.0.0' - commit: 'sha1-45bfe0192ca1faeb007ade9deae92b16b8254a0d' - owner_id: 44036562 - repo_id: 513659658 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1': ref: 'v3.2.0' commit: 'sha1-bcd2ba49218906704ab6c1aa796996da409d3eb1' owner_id: 44036562 repo_id: 642580244 - 'actions/create-github-app-token@v3.2.0': - ref: 'v3.2.0' - commit: 'sha1-bcd2ba49218906704ab6c1aa796996da409d3eb1' - owner_id: 44036562 - repo_id: 642580244 'actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346': ref: '368f82528645a54fb793d4d04e342629a3f51346' commit: 'sha1-368f82528645a54fb793d4d04e342629a3f51346' owner_id: 44036562 repo_id: 438112499 - 'actions/deploy-pages@v5.0.1': - ref: 'v5.0.1' - commit: 'sha1-368f82528645a54fb793d4d04e342629a3f51346' - owner_id: 44036562 - repo_id: 438112499 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c': ref: 'v8.0.1' commit: 'sha1-3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' @@ -205,16 +179,6 @@ dependencies: commit: 'sha1-3a2844b7e9c422d3c10d287c895573f7108da1b3' owner_id: 44036562 repo_id: 205262760 - 'actions/github-script@v9.0.0': - ref: 'v9.0.0' - commit: 'sha1-3a2844b7e9c422d3c10d287c895573f7108da1b3' - owner_id: 44036562 - repo_id: 205262760 - 'actions/setup-python@v2': - ref: 'v2' - commit: 'sha1-e9aba2c848f5ebd159c070c61ea2c4e2b122355e' - owner_id: 44036562 - repo_id: 192625525 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a': ref: '043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' commit: 'sha1-043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' @@ -225,11 +189,6 @@ dependencies: commit: 'sha1-bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' owner_id: 44036562 repo_id: 192625955 - 'actions/upload-artifact@v7.0.1': - ref: 'v7.0.1' - commit: 'sha1-043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' - owner_id: 44036562 - repo_id: 192625955 'actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9': ref: 'fc324d3547104276b827a68afc52ff2a11cc49c9' commit: 'sha1-fc324d3547104276b827a68afc52ff2a11cc49c9' @@ -237,11 +196,6 @@ dependencies: repo_id: 496012378 uses: - 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' - 'actions/upload-pages-artifact@v5.0.0': - ref: 'v5.0.0' - commit: 'sha1-fc324d3547104276b827a68afc52ff2a11cc49c9' - owner_id: 44036562 - repo_id: 496012378 'asana/push-signed-commits@d615ca88d8e1a946734c24970d1e7a6c56f34897': ref: 'v1.3' commit: 'sha1-d615ca88d8e1a946734c24970d1e7a6c56f34897' @@ -279,16 +233,6 @@ dependencies: commit: 'sha1-6037f33647c3f17758a2356c80fc4a53d7e0685d' owner_id: 75048950 repo_id: 623796603 - 'haskell-actions/setup@v2.12.0': - ref: 'v2.12.0' - commit: 'sha1-6037f33647c3f17758a2356c80fc4a53d7e0685d' - owner_id: 75048950 - repo_id: 623796603 - 'hyperpolymath/a2ml-ecosystem@main': - ref: 'main' - commit: 'sha1-f7a40a4d5cc82b2e73f861119baa6818d77a448d' - owner_id: 6759885 - repo_id: 1275649586 'ocaml/setup-ocaml@e89b2ded52a6e13f50162220cf5fe47290162032': ref: 'v3.8.0' commit: 'sha1-e89b2ded52a6e13f50162220cf5fe47290162032' diff --git a/.github/workflows/apply-workflow-pins.yml b/.github/workflows/apply-workflow-pins.yml index 8f6785e73..3172093e9 100644 --- a/.github/workflows/apply-workflow-pins.yml +++ b/.github/workflows/apply-workflow-pins.yml @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # WHICH App's credentials belong in vars.APP_ID / secrets.APP_PRIVATE_KEY: # a DEDICATED App for this applier, explicitly NOT OikosBot. Owner ruling @@ -71,7 +71,7 @@ jobs: - name: Mint an App installation token for hyperpolymath id: tok-user if: vars.APP_ID != '' - uses: actions/create-github-app-token@v3.2.0 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 continue-on-error: true with: app-id: ${{ vars.APP_ID }} @@ -81,7 +81,7 @@ jobs: - name: Mint an App installation token for metadatastician id: tok-org if: vars.APP_ID != '' - uses: actions/create-github-app-token@v3.2.0 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 continue-on-error: true with: app-id: ${{ vars.APP_ID }} @@ -146,7 +146,7 @@ jobs: - name: Upload the census if: always() - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: workflow-pin-census path: census.tsv diff --git a/.github/workflows/deed-conformance.yml b/.github/workflows/deed-conformance.yml index c6003c84e..a3d6f7ae1 100644 --- a/.github/workflows/deed-conformance.yml +++ b/.github/workflows/deed-conformance.yml @@ -23,7 +23,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: validator self-test diff --git a/.github/workflows/pages-archive.yml b/.github/workflows/pages-archive.yml index acb633587..4c5503499 100644 --- a/.github/workflows/pages-archive.yml +++ b/.github/workflows/pages-archive.yml @@ -24,22 +24,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Checkout casket-ssg - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: hyperpolymath/casket-ssg path: .casket-ssg - name: Setup GHCup - uses: haskell-actions/setup@v2.12.0 + uses: haskell-actions/setup@6037f33647c3f17758a2356c80fc4a53d7e0685d # v2.12.0 with: ghc-version: '9.8.2' cabal-version: '3.10' - name: Cache Cabal - uses: actions/cache@v6.1.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cabal/packages @@ -83,10 +83,10 @@ jobs: cd .casket-ssg && cabal run casket-ssg -- build ../site ../_site - name: Setup Pages - uses: actions/configure-pages@v6.0.0 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Upload artifact - uses: actions/upload-pages-artifact@v5.0.0 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: '_site' @@ -100,4 +100,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v5.0.1 + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/.github/workflows/propagate-hooks.yml b/.github/workflows/propagate-hooks.yml index 91656da3b..8fbba2d51 100644 --- a/.github/workflows/propagate-hooks.yml +++ b/.github/workflows/propagate-hooks.yml @@ -50,7 +50,7 @@ jobs: steps: - name: Checkout standards repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 @@ -173,7 +173,7 @@ jobs: steps: - name: Checkout standards repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Determine source and target id: config @@ -192,7 +192,7 @@ jobs: echo "target_name=$TARGET_NAME" >> $GITHUB_OUTPUT - name: Checkout target repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: ${{ matrix.repo }} token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/security-gate-pr-target.yml b/.github/workflows/security-gate-pr-target.yml index ac35c2fd3..784f9963b 100644 --- a/.github/workflows/security-gate-pr-target.yml +++ b/.github/workflows/security-gate-pr-target.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout base repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Explicitly checkout the base branch, not the PR branch ref: ${{ github.base_ref }} @@ -89,7 +89,7 @@ jobs: - name: Security Scan - Secrets Detection if: steps.fork-check.outputs.is_fork == 'true' && steps.pr-checkout.outputs.pr_checked_out == 'true' id: secrets-scan - uses: hyperpolymath/a2ml-ecosystem/secrets-check-action@main + uses: hyperpolymath/a2ml-ecosystem/secrets-check-action@f7a40a4d5cc82b2e73f861119baa6818d77a448d # frozen: A2ML retired (see EXPECTED_ABSENT in .githooks/validate-actions-lock.sh) with: path: '.' strict: 'true' @@ -179,7 +179,7 @@ jobs: - name: Post Security Scan Comment if: steps.fork-check.outputs.is_fork == 'true' - uses: actions/github-script@v9.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const prNumber = context.issue.number; @@ -211,7 +211,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 @@ -228,7 +228,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Generate Summary run: | diff --git a/.github/workflows/settings-drift-detect.yml b/.github/workflows/settings-drift-detect.yml index d08c92b9b..ab4d6170a 100644 --- a/.github/workflows/settings-drift-detect.yml +++ b/.github/workflows/settings-drift-detect.yml @@ -59,7 +59,7 @@ jobs: timeout-minutes: 45 steps: - name: Checkout - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Scan estate for settings drift id: scan @@ -90,7 +90,7 @@ jobs: - name: Upload report if: always() - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: settings-drift path: drift.tsv diff --git a/.github/workflows/tag-ruleset-canon.yml b/.github/workflows/tag-ruleset-canon.yml index 6dcce234e..756dae883 100644 --- a/.github/workflows/tag-ruleset-canon.yml +++ b/.github/workflows/tag-ruleset-canon.yml @@ -42,7 +42,7 @@ jobs: converge: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # WHICH App's credentials belong in vars.APP_ID / secrets.APP_PRIVATE_KEY: # a DEDICATED App for this applier -- explicitly NOT OikosBot. Owner ruling @@ -62,7 +62,7 @@ jobs: - name: Mint an App installation token for hyperpolymath id: tok-user if: vars.APP_ID != '' - uses: actions/create-github-app-token@v3.2.0 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 continue-on-error: true with: app-id: ${{ vars.APP_ID }} @@ -72,7 +72,7 @@ jobs: - name: Mint an App installation token for metadatastician id: tok-org if: vars.APP_ID != '' - uses: actions/create-github-app-token@v3.2.0 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 continue-on-error: true with: app-id: ${{ vars.APP_ID }} @@ -161,7 +161,7 @@ jobs: - name: Publish the report if: always() - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: tag-ruleset-canon-report path: canon-*.tsv diff --git a/.machine_readable/Debtfile.a2ml b/.machine_readable/Debtfile.a2ml index 86dfcc12a..6c19672c1 100644 --- a/.machine_readable/Debtfile.a2ml +++ b/.machine_readable/Debtfile.a2ml @@ -68,6 +68,18 @@ forgotten. - tri: control - accepted-until: 2027-06-01 +## Vendored mirrors + +### vendored-mirror-unpinned-actions +- description: Unpinned `uses:` refs inside `rhodium-standard-repositories/**` -- the vendored mirror of the RSR canon (gitlab.com/hyperpolymath/rhodium-standard-repositories). These are OTHER repositories' workflows reproduced here as templates, and this repo's own CI lock gate (.github/workflows/actions-lock-gate.yml -> .githooks/validate-actions-lock.sh) covers only the root .github tree, so `.githooks/validate-sha-pins.sh` matches that scope and skips the mirror rather than silently passing it. Re-pinning the mirror here fixes no live runner and desynchronises the copy; it is fixed upstream, or the mirror retires. MEASURED 2026-09-21 by the probe. +- probe: n=$(git ls-files 'rhodium-standard-repositories' | grep -E '/\.github/(workflows|actions)/.*\.ya?ml$' | xargs -r grep -hE '^[[:space:]]*(-[[:space:]]*)?uses:[[:space:]]+[A-Za-z0-9]' | grep -vcE '@[0-9a-f]{40}([^0-9a-f]|$)' || true); echo "${n:-0}" +- count: 42 +- ceiling: 42 +- severity: low +- policy: flag-only +- tri: eliminate +- accepted-until: 2027-03-01 + ## Licensing — FLAG ONLY ### pmpl-licence-headers