From 3b19d0241a6c85d8eab7e81810c9674b0b47136b Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 15 Sep 2026 03:21:32 +0100 Subject: [PATCH 1/3] fix(hooks): give validate-spdx staged mode the filter its scan mode has validate-spdx.sh scan mode filters by extension; staged mode did not. Any commit touching an extensionless or non-source staged file was therefore judged against a rule the scan path would never have applied to it -- and in particular NO commit touching .github/workflows/actions.lock could pass pre-commit, because the lockfile is not a source file and carries no SPDX header by design. This is instance 15 of the estate's recurring trap: a guard asking a different question than its consumer. Adds scripts/tests/validate-spdx-test.sh covering both modes, including the lockfile case that motivated the fix. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0168Bgpez8mFBcAqYAj8VgEx --- .githooks/validate-spdx.sh | 37 ++++++++++++--- scripts/tests/validate-spdx-test.sh | 72 +++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 7 deletions(-) create mode 100755 scripts/tests/validate-spdx-test.sh diff --git a/.githooks/validate-spdx.sh b/.githooks/validate-spdx.sh index 49d0baaa..53a194b9 100755 --- a/.githooks/validate-spdx.sh +++ b/.githooks/validate-spdx.sh @@ -7,24 +7,47 @@ SCAN_PATH="${INPUT_PATH:-.}" STAGED_FILES="${INPUT_STAGED_FILES:-}" ERRORS=0 +# The single authority for "does this path need an SPDX header". +# +# ⚠ THIS FUNCTION EXISTS BECAUSE THE TWO MODES BELOW USED TO ASK DIFFERENT +# QUESTIONS. Full-scan mode filtered by source extension inside `find`; staged +# mode had NO filter at all (`FILES_TO_CHECK=$STAGED_FILES`) and so demanded a +# header on EVERY staged path. That is not a cosmetic split, because only the +# staged branch is reachable: this validator is called from .githooks/pre-commit +# and from no workflow at all, so the mode that HAD the filter never ran and the +# mode that ran had none. +# +# The concrete casualty was .github/workflows/actions.lock. It is generated by +# `gh actions-lock` ("Do not edit by hand"), carries no header, and is restamped +# on every regeneration — so a header added by hand does not survive. No commit +# touching the lockfile could pass pre-commit, which is why a Dependabot-caused +# lockfile desync could sit unrepaired long enough to recur (see #746). +# +# Keep ONE list. If a mode ever needs a different rule, that is a new function +# with a name saying so, never a second copy of these patterns. +is_source_file() { + case "$1" in + *.rs|*.res|*.js|*.ts|*.sh|*.bash|*.zig|*.ex|*.exs|*.gleam|\ + *.ml|*.mli|*.adb|*.ads|*.ncl|*.toml|*.json|*.yaml|*.yml) return 0 ;; + *) return 1 ;; + esac +} + # If staged files provided, only check those if [ -n "$STAGED_FILES" ]; then FILES_TO_CHECK=$STAGED_FILES else - # Check all source files + # Every candidate file; is_source_file below decides which ones count. FILES_TO_CHECK=$(find "$SCAN_PATH" -path '*/.git/*' -prune -o -path '*/node_modules/*' -prune -o \ - -type f \( -name '*.rs' -o -name '*.res' -o -name '*.js' -o -name '*.ts' -o -name '*.sh' \ - -o -name '*.bash' -o -name '*.zig' -o -name '*.ex' -o -name '*.exs' -o -name '*.gleam' \ - -o -name '*.ml' -o -name '*.mli' -o -name '*.adb' -o -name '*.ads' -o -name '*.ncl' \ - -o -name '*.toml' -o -name '*.json' -o -name '*.yaml' -o -name '*.yml' \ - \) -print 2>/dev/null || true) + -type f -print 2>/dev/null || true) fi [ -z "$FILES_TO_CHECK" ] && exit 0 for file in $FILES_TO_CHECK; do [ -f "$file" ] || continue - + is_source_file "$file" || continue + # Check for SPDX header in first 10 lines if ! head -10 "$file" | grep -qE '^# SPDX-License-Identifier:'; then echo "[validate-spdx] ERROR: $file missing SPDX header" >&2 diff --git a/scripts/tests/validate-spdx-test.sh b/scripts/tests/validate-spdx-test.sh new file mode 100755 index 00000000..76949b6e --- /dev/null +++ b/scripts/tests/validate-spdx-test.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +# +# Tests for .githooks/validate-spdx.sh, the pre-commit gate. +# +# ⚠ TEST 1 IS THE REASON THIS EXISTS, and it FAILS against the previous version. +# +# The validator had two modes asking two different questions. Full-scan mode +# filtered by source extension inside `find`; staged mode did not filter at all: +# +# FILES_TO_CHECK=$STAGED_FILES # every staged path, whatever it is +# +# Only the staged branch is reachable in practice — this validator is invoked by +# .githooks/pre-commit and by no workflow — so the mode that HAD the filter never +# ran, and the mode that ran had none. +# +# The casualty was .github/workflows/actions.lock: machine-generated by +# `gh actions-lock`, headerless by construction, and restamped on every +# regeneration so a hand-added header does not survive. Every commit touching the +# lockfile was therefore unpassable, which is how a Dependabot-caused lockfile +# desync survived long enough to recur (precedent: #746). +# +# Test 1 is the planted positive for that: staging a headerless .lock file must +# PASS. Test 4 is the parity check that the two modes now ask the same question, +# because a filter that exists in only one mode is what caused this. +set -uo pipefail +HOOK="$(cd "$(dirname "$0")/../.." && pwd)/.githooks/validate-spdx.sh" +T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT +mkdir -p "$T/.github/workflows" +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 +} + +ck_scan() { # name expected_exit path + local out rc + out="$(cd "$T" && 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 +} + +# A real actions.lock preamble: the tool's own banner, and no SPDX line anywhere. +printf "# This file is machine-generated by \`gh actions-lock\`.\n# Do not edit by hand; run \`gh actions-lock\` to update.\nversion: 'v0.0.2'\n" \ + > "$T/.github/workflows/actions.lock" +printf '# SPDX-License-Identifier: MPL-2.0\necho ok\n' > "$T/good.sh" +printf 'echo bad\n' > "$T/bad.sh" +printf 'name: bad\non: push\n' > "$T/.github/workflows/bad.yml" +printf 'binary-ish payload, not source\n' > "$T/README.md" + +echo "validate-spdx.sh" +ck "PLANTED POSITIVE: headerless actions.lock must PASS" 0 ".github/workflows/actions.lock" +ck "negative control: headerless .sh must FAIL" 1 "good.sh bad.sh" +ck "negative control: headerless .yml must FAIL" 1 ".github/workflows/bad.yml" +ck "valid header must PASS" 0 "good.sh" +ck "non-source README.md must PASS" 0 "README.md" +ck "lockfile alongside a valid source file must PASS" 0 ".github/workflows/actions.lock good.sh" + +# PARITY: both modes must agree about the same tree. A tree whose only headerless +# files are non-source must pass a full scan exactly as it passes staged mode. +mkdir -p "$T/parity" +cp "$T/.github/workflows/actions.lock" "$T/parity/actions.lock" +cp "$T/README.md" "$T/parity/README.md" +cp "$T/good.sh" "$T/parity/good.sh" +ck_scan "PARITY: full-scan mode ignores the same non-source files" 0 "parity" + +printf '\n%s passed, %s failed\n' "$pass" "$fail" +[ "$fail" -eq 0 ] || exit 1 From bbf9b5eca0db6ca88e6b55dade1518bcdb6348a7 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 15 Sep 2026 03:22:12 +0100 Subject: [PATCH 2/3] fix(workflows): resync actions.lock after codeql-action bump Dependabot bumped codeql-action in the workflow files; actions.lock was not resynced, so the lockfile named a ref the workflows no longer used and the actions-lock gate reported 3 stale findings. Six lines, hand-edited: cdf488f595d80d6e07e03d4674febd5ab45fa938 -> b96794f015dfd88f77b49b1c93e0fa7110f94c63 (the three codeql-action refs in the workflows: stanzas, plus the dependencies: block key, ref: and commit:). owner_id 9919 / repo_id 259445878 unchanged. The lockfile was edited BY HAND, deliberately. gh actions-lock fix mode was measured to rewrite 16 .yml files (42 with --no-narrow), de-pinning correct SHAs back to mutable tags and inventing invalid local action refs -- straight into a sha_pinning_required ruleset and startup death. Only --no-fix --json is safe to read. This change touches ZERO .yml bytes. ref: stays a bare SHA, per the estate lock regime. Verification: stale findings 3 -> 0; gate rc 1 -> 0; git diff confirms no workflow file was modified. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0168Bgpez8mFBcAqYAj8VgEx --- .github/workflows/actions.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 5a7db6cb..11a035c0 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -22,7 +22,7 @@ workflows: '.github/workflows/changelog.yml': [] '.github/workflows/codeql-reusable.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - - 'github/codeql-action@cdf488f595d80d6e07e03d4674febd5ab45fa938' + - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63' '.github/workflows/codeql.yml': [] '.github/workflows/debt-measure.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' @@ -50,7 +50,7 @@ workflows: - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' - - 'github/codeql-action@cdf488f595d80d6e07e03d4674febd5ab45fa938' + - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63' '.github/workflows/hypatia-scan.yml': [] '.github/workflows/instant-sync.yml': - 'peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697' @@ -94,7 +94,7 @@ workflows: '.github/workflows/scorecard-reusable.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' - - 'github/codeql-action@cdf488f595d80d6e07e03d4674febd5ab45fa938' + - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63' - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc' '.github/workflows/secret-scanner-reusable.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' @@ -181,9 +181,9 @@ dependencies: commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' owner_id: 47606891 repo_id: 331103973 - 'github/codeql-action@cdf488f595d80d6e07e03d4674febd5ab45fa938': - ref: 'cdf488f595d80d6e07e03d4674febd5ab45fa938' - commit: 'sha1-cdf488f595d80d6e07e03d4674febd5ab45fa938' + 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63': + ref: 'b96794f015dfd88f77b49b1c93e0fa7110f94c63' + commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63' owner_id: 9919 repo_id: 259445878 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406': From adf4273a2fced2277dcaadb99f3ecbdd551e3071 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 15 Sep 2026 03:22:13 +0100 Subject: [PATCH 3/3] chore(registry): refresh three stale source_hash lines REGISTRY.a2ml is a GENERATED artefact (scripts/build-registry.sh). Three source_hash values had drifted from the file tree, so build-registry.sh --check fails on main today -- which means registry-verify.yml is red before this branch touches anything. Regenerated with the generator, not hand-edited. The diff is exactly three source_hash lines; the file's shape is UNCHANGED (owner ruling R-H4). Reshaping the registry away from its TOML-shaped record dialect is a separate, deliberate job against build-registry.sh, and is not coupled to this branch. Verification: build-registry.sh --check rc 1 (at HEAD) -> 0 (with this). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0168Bgpez8mFBcAqYAj8VgEx --- .machine_readable/REGISTRY.a2ml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.machine_readable/REGISTRY.a2ml b/.machine_readable/REGISTRY.a2ml index 984a2171..cb39c921 100644 --- a/.machine_readable/REGISTRY.a2ml +++ b/.machine_readable/REGISTRY.a2ml @@ -63,7 +63,7 @@ name = "META.a2ml spec" stream = "foundation" home = "meta-a2ml/" canonical_doc = "meta-a2ml/README.adoc" -source_hash = "sha256:1c5337a9782e37cf24d869109d798d0bf82c67fe62c24f94314b235ea8f173b4" +source_hash = "sha256:a058855d1c8019ccf1814a9386ba406b7e8df3698fd17c9fd342a5134f6a2eb0" route = "architecture decisions / governance metadata format" [[spec]] @@ -126,7 +126,7 @@ name = "0-AI Gatekeeper Protocol" stream = "protocol" home = "0-ai-gatekeeper-protocol/" canonical_doc = "0-ai-gatekeeper-protocol/README.adoc" -source_hash = "sha256:369bd762d903006a10f75e924be5c07d082554d88824fbf035ed32f3f67d05c2" +source_hash = "sha256:41f60acfb75bc32b0a3fc13cf3642f2e3f553bb4b7ddc9911b1e23d17e205ef3" route = "the AI-agent entry/gating protocol behind 0-AI-MANIFEST" [[spec]] @@ -207,7 +207,7 @@ name = "RSR — Rhodium Standard Repositories" stream = "governance" home = "rhodium-standard-repositories/" canonical_doc = "rhodium-standard-repositories/README.adoc" -source_hash = "sha256:dbd52c26f0ca4964683db06045feda29710e44289baf5eb573ebf195a3ed44a9" +source_hash = "sha256:35b2a2d8a9b4e33d7f73c663f0d811d054a16e8601f62a20c9ec54114cd43b54" route = "the repository-compliance standard every repo is graded against" [[spec]]