From c57122047d6c9c3892992a7a3789428e896af680 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 16:42:11 +0000 Subject: [PATCH 1/8] =?UTF-8?q?ci:=20fold=20runtime-policy.yml=20into=20go?= =?UTF-8?q?vernance=20(cicd-regularisation=20=C2=A714)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The downstream half of rsr-template-repo#244. This repository's copy of runtime-policy.yml is the pre-2026-10-14 version ("Deno is RETIRED"), whose `total -gt 1` test fails any repository holding bun.lock, deno.lock and pnpm-lock.yaml together, which is what js-runtimes regen writes. Governance at 934b879 already covers the check: "Language / package anti-pattern policy" fails any tracked package-lock.json, and the "JS runtimes" jobs check each lock against package.json. - delete .github/workflows/runtime-policy.yml and its actions.lock record - validate-template.sh and validate_workflows_test.sh require governance.yml in its place Checks: check-lock-sync.sh in sync; validate-template.sh 0 errors; validate_workflows_test.sh, actions_lock_sha_refs_test.sh and scorecard_permissions_test.sh exit 0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6FD63UrriRBHqmhhEca9t --- .github/workflows/actions.lock | 2 - .github/workflows/runtime-policy.yml | 72 ---------------------- CHANGELOG.adoc | 14 +++++ scripts/validate-template.sh | 6 +- tests/workflows/validate_workflows_test.sh | 8 +-- 5 files changed, 23 insertions(+), 79 deletions(-) delete mode 100644 .github/workflows/runtime-policy.yml diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 746dbf0..c53e0d3 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -71,8 +71,6 @@ workflows: - 'softprops/action-gh-release@v3.0.3' '.github/workflows/rsr-compliance-canary.yml': - 'actions/checkout@v7.0.1' - '.github/workflows/runtime-policy.yml': - - 'actions/checkout@v7.0.1' '.github/workflows/rust-ci.yml': - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82' '.github/workflows/scorecard.yml': diff --git a/.github/workflows/runtime-policy.yml b/.github/workflows/runtime-policy.yml deleted file mode 100644 index 7319b77..0000000 --- a/.github/workflows/runtime-policy.yml +++ /dev/null @@ -1,72 +0,0 @@ -# This workflow is managed by gh actions-lock. -# SPDX-License-Identifier: MPL-2.0 -# Runtime and package-manager policy check. -# -# Authority: hyperpolymath/standards LANGUAGE-POLICY.adoc §1. -# Ordering: Bun (1st) > pnpm (2nd) > npm (last resort). Deno is RETIRED, not a tier. -# -# REPLACES npm-bun-blocker.yml, which failed any build carrying `bun.lockb` with -# the message "npm/bun artifacts detected. Use Deno instead." That gate blocked -# what is now the FIRST-choice runtime and mandated the second. It was present in -# 55 repositories. -# -# What this fails on, deliberately: -# MIXED TOOLCHAINS -- two different package managers' lockfiles in one repo. -# That is real, actionable drift: two dependency graphs that can disagree. -# What it does NOT fail on: -# Using bun, pnpm or npm. Deno is RETIRED (2026-08-26). npm is LAST but PERMITTED; the check reports -# the tier in use so drift is visible without blocking legitimate work. -name: Runtime Policy -on: - push: - branches: [main, master] - pull_request: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - runtime-policy: - name: Runtime Policy - runs-on: ubuntu-24.04 - timeout-minutes: 10 - permissions: - contents: read - steps: - - uses: actions/checkout@v7.0.1 - - - name: Report runtime tier and reject mixed toolchains - run: | - set -euo pipefail - - bun=0; deno=0; pnpm=0; npm=0 - [ -f bun.lockb ] || [ -f bun.lock ] && bun=1 || true - [ -f deno.lock ] || [ -f deno.json ] || [ -f deno.jsonc ] && deno=1 || true - [ -f pnpm-lock.yaml ] && pnpm=1 || true - [ -f package-lock.json ] && npm=1 || true - - total=$((bun + deno + pnpm + npm)) - - if [ "$total" -eq 0 ]; then - echo "::notice::No JS/TS package manager in use — nothing to check." - exit 0 - fi - - # Report the tier actually in use (LANGUAGE-POLICY.adoc §1). - [ "$bun" -eq 1 ] && echo "Bun — tier 1 (preferred)" - [ "$deno" -eq 1 ] && echo "::warning::Deno lockfile/config present. Deno is BEING REMOVED, not grandfathered (owner ruling 2026-08-26, canon standards#655). Migrate to Bun — package.json + bun.lock. This is a warning rather than an error only while the estate migration is in flight; it escalates to an error when that completes." - [ "$pnpm" -eq 1 ] && echo "pnpm — tier 3" - [ "$npm" -eq 1 ] && echo "::warning::npm lockfile present. npm is tier 4, the last resort — permitted, never preferred. See LANGUAGE-POLICY.adoc §1." - - if [ "$total" -gt 1 ]; then - echo "::error::Mixed toolchains: $total package managers have lockfiles in this repository." - echo "Two dependency graphs that can disagree is real drift. Pick one — preferring the" - echo "highest tier present — and delete the others' lockfiles." - exit 1 - fi - - echo "✅ Single package manager in use." diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 6f5d239..7c36eda 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -1,5 +1,19 @@ == Changelog +== 2026-10-19 — runtime-policy.yml folded into governance + +* `.github/workflows/runtime-policy.yml` is removed, with its + `actions.lock` record, as rsr-template-repo#244 does at the source. This + repository's copy still said Deno was retired. It failed any repository + holding `bun.lock`, `deno.lock` and `pnpm-lock.yaml` together, which is + what `js-runtimes regen` writes. Governance at `934b879` already covers the + check. "Language / package anti-pattern policy" fails any tracked + `package-lock.json`, and the "JS runtimes" jobs check each lock against + `package.json` (standards cicd-regularisation spec §14). +* `scripts/validate-template.sh` and + `tests/workflows/validate_workflows_test.sh` require `governance.yml` in + its place. + == 2026-10-14 — Scorecard grants `actions: read`; four lock refs kept immutable (resource-algebra#7) diff --git a/scripts/validate-template.sh b/scripts/validate-template.sh index 06222d4..09f0de8 100755 --- a/scripts/validate-template.sh +++ b/scripts/validate-template.sh @@ -222,7 +222,11 @@ REQUIRED_WORKFLOWS=( # none of the 55 repos carrying it ever adopted Bun. This list was not # updated, so it has required two files the template deliberately no longer # ships — which is why validate-template.sh has been red since that merge. - "runtime-policy.yml" + # runtime-policy.yml is folded into governance in turn (standards + # cicd-regularisation spec §14): "Language / package anti-pattern policy" + # fails any tracked package-lock.json and the "JS runtimes" jobs check each + # lock against package.json. So governance.yml is what this list requires. + "governance.yml" "secret-scanner.yml" ) diff --git a/tests/workflows/validate_workflows_test.sh b/tests/workflows/validate_workflows_test.sh index 4e6e321..88553df 100755 --- a/tests/workflows/validate_workflows_test.sh +++ b/tests/workflows/validate_workflows_test.sh @@ -99,10 +99,10 @@ REQUIRED_WORKFLOWS=( "security-policy.yml" "wellknown-enforcement.yml" "workflow-linter.yml" - # Retired in #14, replaced by runtime-policy.yml — see the note in - # scripts/validate-template.sh. Keeping the old names here required two - # files the template no longer ships. - "runtime-policy.yml" + # npm-bun-blocker.yml and ts-blocker.yml were retired in #14 for + # runtime-policy.yml, which governance has since absorbed — see the note in + # scripts/validate-template.sh. + "governance.yml" "secret-scanner.yml" ) From 6fffbc4ba9d483a920aa7d30fd69c5bbf725bd87 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:04:44 +0000 Subject: [PATCH 2/8] fix(ci): re-pin standards to 75bd599 and give lock records tag refs Ported unchanged from resource-algebra#12 (3f435aa) so this PR's workflows get past "Set up job". It no-ops once #12 merges. GitHub's runner now rejects a lock record whose ref is its own SHA ("invalid lockfile pin"; standards#1271). Since 2026-10-15 main has had Governance, Rust CI, Mirror, Code Quality and Pages fail at startup. The six standards reusables move from 934b879 to 75bd599, whose own lock carries tag refs (standards#1271, #1285); lock-selfcheck.sh reports it self-consistent. relock-standards-pin.sh at 75bd599 re-keyed actions.lock and gave the twelve self-SHA action records the tags standards records for the same commits, after checking commit and repository ids match. No commit value changes. Each tag was checked with git ls-remote; 14 of 15 point at the recorded commit. dtolnay/rust-toolchain's 'v1' has since moved off e2a55d2, in standards' own lock too; the runner does not refuse it. This reverses #7's four SHA refs (upload-artifact, setup-deno, rust-toolchain, setup-bun). actions_lock_sha_refs_test.sh asserted the form the runner now refuses; it now requires a symbolic ref with the SHA in key and commit, and plants the self-SHA, upper-case and empty refs as negative controls. It passes on this lock and fails on main's. (cherry picked from commit 3f435aa905c94a64105156dc45e2b3e08311b5ef) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6FD63UrriRBHqmhhEca9t --- .github/workflows/actions.lock | 42 +++++++++--------- .github/workflows/governance.yml | 2 +- .github/workflows/hypatia-scan.yml | 2 +- .github/workflows/mirror.yml | 2 +- .github/workflows/rust-ci.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/secret-scanner.yml | 2 +- CHANGELOG.adoc | 14 ++++++ tests/workflows/actions_lock_sha_refs_test.sh | 43 +++++++++++-------- 9 files changed, 66 insertions(+), 45 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index c53e0d3..0288335 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -31,13 +31,13 @@ workflows: '.github/workflows/estate-rules.yml': - 'actions/checkout@v7.0.1' '.github/workflows/governance.yml': - - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/groove-check.yml': - 'actions/checkout@v7.0.1' '.github/workflows/guix-policy.yml': - 'actions/checkout@v7.0.1' '.github/workflows/hypatia-scan.yml': - - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/k9-validate.yml': - 'actions/checkout@v7.0.1' - 'hyperpolymath/k9-ecosystem@main' @@ -47,7 +47,7 @@ workflows: '.github/workflows/main-estate-audit.yml': - 'hyperpolymath/cicd-suite@55556cf5ba71ba744695861503c13148d3915a5d' '.github/workflows/mirror.yml': - - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/ossf-best-practices.yml': - 'actions/checkout@v7.0.1' '.github/workflows/pages.yml': @@ -72,11 +72,11 @@ workflows: '.github/workflows/rsr-compliance-canary.yml': - 'actions/checkout@v7.0.1' '.github/workflows/rust-ci.yml': - - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/scorecard.yml': - - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/secret-scanner.yml': - - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82' + - 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66' '.github/workflows/security-policy.yml': - 'actions/checkout@v7.0.1' '.github/workflows/sonarqube.yml': @@ -104,7 +104,7 @@ dependencies: owner_id: 44036562 repo_id: 760701061 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9': - ref: '55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + ref: 'v6.1.0' commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' owner_id: 44036562 repo_id: 215566462 @@ -114,7 +114,7 @@ dependencies: owner_id: 44036562 repo_id: 215566462 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1': - ref: '3d3c42e5aac5ba805825da76410c181273ba90b1' + ref: 'v7.0.1' commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' owner_id: 44036562 repo_id: 197814629 @@ -144,7 +144,7 @@ dependencies: owner_id: 44036562 repo_id: 192625955 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9': - ref: 'cf430e030ddbb5b0abf93d22962f4752f3646cd9' + ref: 'v7.0.2' commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9' owner_id: 44036562 repo_id: 192625955 @@ -161,7 +161,7 @@ dependencies: uses: - 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed': - ref: '22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' + ref: 'v2.0.5' commit: 'sha1-22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' owner_id: 42048915 repo_id: 356423100 @@ -171,12 +171,12 @@ dependencies: owner_id: 27347476 repo_id: 371068214 'dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f': - ref: 'e2a55d2ffb04f378e9626c28d38b36d230d1e12f' + ref: 'v1' commit: 'sha1-e2a55d2ffb04f378e9626c28d38b36d230d1e12f' owner_id: 1940490 repo_id: 260749683 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393': - ref: '51f63319f592f97930c73d9c46184d20bd206393' + ref: 'v3.0.0' commit: 'sha1-51f63319f592f97930c73d9c46184d20bd206393' owner_id: 26415196 repo_id: 297874902 @@ -186,7 +186,7 @@ dependencies: owner_id: 26415196 repo_id: 297874902 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124': - ref: '54075bcc5e249e4758d363f27d099f55d843f124' + ref: 'v1.24.1' commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' owner_id: 47606891 repo_id: 331103973 @@ -201,7 +201,7 @@ dependencies: owner_id: 9919 repo_id: 259445878 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406': - ref: 'abea47f85e598557f500fa1fd2ab7464fcb39406' + ref: 'v2.2.1' commit: 'sha1-abea47f85e598557f500fa1fd2ab7464fcb39406' owner_id: 1006268 repo_id: 212984112 @@ -238,9 +238,9 @@ dependencies: commit: 'sha1-c1c9fa07992a02c1fd3d67a0dc1b08cccb852aef' owner_id: 6759885 repo_id: 1352485172 - 'hyperpolymath/standards@934b879f6068295e42dac20d80e010d555ebec82': - ref: '934b879f6068295e42dac20d80e010d555ebec82' - commit: 'sha1-934b879f6068295e42dac20d80e010d555ebec82' + 'hyperpolymath/standards@75bd5994470661a099df562d6d5ad2fe744dbd66': + ref: '75bd5994470661a099df562d6d5ad2fe744dbd66' + commit: 'sha1-75bd5994470661a099df562d6d5ad2fe744dbd66' owner_id: 6759885 repo_id: 1116521501 uses: @@ -258,12 +258,12 @@ dependencies: - 'swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6' - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555' 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc': - ref: '2d1146689b8cda280b9bc96326124645441f03bc' + ref: 'v2.4.4' commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' owner_id: 67707773 repo_id: 421101922 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6': - ref: '0c5077e51419868618aeaa5fe8019c62421857d6' + ref: 'v2.2.0' commit: 'sha1-0c5077e51419868618aeaa5fe8019c62421857d6' owner_id: 108928776 repo_id: 512644635 @@ -283,12 +283,12 @@ dependencies: owner_id: 545988 repo_id: 366408409 'swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6': - ref: '6323deb102c322ba6fcbdcafc7e3dddab59af2b6' + ref: 'v2.9.2' commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' owner_id: 580492 repo_id: 298565987 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555': - ref: 'e83874834305fe9a4a2997156cb26c5de65a8555' + ref: 'v0.10.0' commit: 'sha1-e83874834305fe9a4a2997156cb26c5de65a8555' owner_id: 135788 repo_id: 208510314 diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 8ef98da..4609772 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -18,4 +18,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@934b879f6068295e42dac20d80e010d555ebec82 + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index e95d4ed..734f602 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -27,4 +27,4 @@ permissions: jobs: hypatia: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@934b879f6068295e42dac20d80e010d555ebec82 + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index c118db0..5e19042 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -13,7 +13,7 @@ permissions: contents: read jobs: mirror: - uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@934b879f6068295e42dac20d80e010d555ebec82 + uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 # Explicit secrets map — no `secrets: inherit` (Hypatia WH008, alert #131). # All seven are the callee's complete optional contract (standards # mirror-reusable.yml@da2c748); behaviour is unchanged, future secrets diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 4f27132..d4d39e5 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -16,4 +16,4 @@ permissions: contents: read jobs: rust-ci: - uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@934b879f6068295e42dac20d80e010d555ebec82 + uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 77510c4..bd93efe 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -16,7 +16,7 @@ permissions: jobs: scorecard: - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@934b879f6068295e42dac20d80e010d555ebec82 + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 # Reusable called-workflow permissions are CAPPED by the caller's grants; # without security-events: write here the scorecard SARIF upload fails with # startup_failure (hypatia WF018). id-token: write enables OIDC publish. diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 403f0b3..2adeddc 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -26,4 +26,4 @@ jobs: # is therefore the cap GitHub applies to the called workflow. permissions: contents: read - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@934b879f6068295e42dac20d80e010d555ebec82 + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 7c36eda..6acd95a 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -15,6 +15,20 @@ its place. +== 2026-10-19 — lock records carry tag refs; standards re-pinned to `75bd599` + +* Since 2026-10-15 GitHub's runner rejects a lock record whose `ref` is its + own SHA ("invalid lockfile pin"). Governance, Rust CI, Mirror, Code Quality + and Pages died at startup on main. +* The six standards reusables move from `934b879` to `75bd599`, whose own lock + carries tag refs (hyperpolymath/standards#1271, + hyperpolymath/standards#1285). `relock-standards-pin.sh` at `75bd599` gave + the twelve self-SHA action records in `actions.lock` the same tags. No + commit changes: the key and `commit` still pin each action by SHA. +* This reverses #7's four SHA refs (upload-artifact, setup-deno, + rust-toolchain, setup-bun). `tests/workflows/actions_lock_sha_refs_test.sh` + now requires a tag ref and rejects the self-SHA form as a negative control. + == 2026-10-14 — Scorecard grants `actions: read`; four lock refs kept immutable (resource-algebra#7) * `.github/workflows/scorecard.yml`: the calling job grants `actions: read`. diff --git a/tests/workflows/actions_lock_sha_refs_test.sh b/tests/workflows/actions_lock_sha_refs_test.sh index 20218c0..8ef5ad6 100755 --- a/tests/workflows/actions_lock_sha_refs_test.sh +++ b/tests/workflows/actions_lock_sha_refs_test.sh @@ -1,8 +1,11 @@ #!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 # -# PR #7 regression: the four SHA-keyed leaves added by standards must keep -# their immutable ref, rather than the tag gh-actions-lock assigned to them. +# The four SHA-keyed leaves added by standards must carry a symbolic (tag) +# ref, with the SHA in their key and commit. GitHub's runner rejects a record +# whose ref is its own SHA ("invalid lockfile pin", enforced from 2026-10-15; +# standards#1271), so the SHA ref PR #7 asked for stopped every job that +# resolved such a record. The commit, not the ref, pins the action. # Separate tag-keyed records remain valid. This test is offline. # Usage: bash tests/workflows/actions_lock_sha_refs_test.sh [workflow-directory] set -euo pipefail @@ -28,14 +31,15 @@ STANDARDS=$(jq -er ' # Assertions on parsed data, independent of the sync gate (which checks edges # but deliberately does not inspect ref/commit metadata). -immutable_record() { +pinned_record() { local key="$1" file="$2" jq -e --arg key "$key" ' ($key | split("@")[1]) as $sha | .dependencies[$key] as $record | ($sha | test("^[0-9a-f]{40}$")) - and ($record.ref == $sha) and ($record.commit == ("sha1-" + $sha)) + and ($record.ref | type == "string" and . != "") + and ($record.ref | test("^[0-9a-fA-F]{40}$") | not) ' "$file" >/dev/null } @@ -47,32 +51,35 @@ mkdir -p "$FIXTURE/workflows" cp -a "$WF_DIR/." "$FIXTURE/workflows/" checked=0 -while IFS='|' read -r action former_ref owner_id repo_id; do +while IFS='|' read -r action tag_ref owner_id repo_id; do key=$(jq -er --arg standards "$STANDARDS" --arg action "$action" ' [.dependencies[$standards].uses[] | select(startswith($action + "@"))] | select(length == 1) | .[0] ' "$FIXTURE/lock.json") || fail "$action must occur exactly once in standards transitive coverage" - immutable_record "$key" "$FIXTURE/lock.json" \ - || fail "$key must have matching immutable ref and commit" + pinned_record "$key" "$FIXTURE/lock.json" \ + || fail "$key must have a symbolic ref and its own SHA as commit" jq -e --arg key "$key" --argjson owner "$owner_id" --argjson repo "$repo_id" ' .dependencies[$key] | .owner_id == $owner and .repo_id == $repo ' "$FIXTURE/lock.json" >/dev/null || fail "$key lost its repository identity" - # Reproduce each of the four exact pre-PR tag substitutions. Even though - # the key and commit still carry the SHA, ref must not float to a tag. - jq --arg key "$key" --arg tag "$former_ref" ' - .dependencies[$key].ref = $tag - ' "$FIXTURE/lock.json" > "$FIXTURE/mutated.json" - if immutable_record "$key" "$FIXTURE/mutated.json"; then - fail "$key accepted its former floating ref $former_ref" - fi + # Reproduce the record GitHub's runner refuses: the key's own SHA as ref + # (the shape PR #7 wrote), and the same SHA in upper case. + sha=${key##*@} + for bad_ref in "$sha" "${sha^^}" ""; do + jq --arg key "$key" --arg ref "$bad_ref" ' + .dependencies[$key].ref = $ref + ' "$FIXTURE/lock.json" > "$FIXTURE/mutated.json" + if pinned_record "$key" "$FIXTURE/mutated.json"; then + fail "$key accepted the refused ref '$bad_ref'" + fi + done - # A valid-looking, different SHA is still a broken key/ref/commit binding. + # A valid-looking, different SHA is still a broken key/commit binding. jq --arg key "$key" ' .dependencies[$key].commit = "sha1-0000000000000000000000000000000000000000" ' "$FIXTURE/lock.json" > "$FIXTURE/mutated.json" - if immutable_record "$key" "$FIXTURE/mutated.json"; then + if pinned_record "$key" "$FIXTURE/mutated.json"; then fail "$key accepted a mismatched commit" fi @@ -96,7 +103,7 @@ while IFS='|' read -r action former_ref owner_id repo_id; do # upload-artifact and setup-bun also have direct tag users. Their records # must coexist with the SHA records; the repair must not globally ban tags. if [[ "$action" == actions/upload-artifact || "$action" == oven-sh/setup-bun ]]; then - jq -e --arg key "$key" --arg alias "$action@$former_ref" --arg tag "$former_ref" ' + jq -e --arg key "$key" --arg alias "$action@$tag_ref" --arg tag "$tag_ref" ' .dependencies[$alias].ref == $tag and (.dependencies[$alias].commit == .dependencies[$key].commit) and ([.workflows[][] | select(. == $alias)] | length > 0) From 87243782a4f08bc9345002de8e0faae8b95f4b6e Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:19:37 +0000 Subject: [PATCH 3/8] test(ci): Expand action lock SHA regression tests Ported unchanged from resource-algebra#12 (c0b5883) so this PR's workflows get past "Set up job". It no-ops once #12 merges. (cherry picked from commit c0b5883b42304745aaf5f848dc629b6911601921) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6FD63UrriRBHqmhhEca9t --- tests/workflows/actions_lock_sha_refs_test.sh | 87 +++++++++++++++++-- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/tests/workflows/actions_lock_sha_refs_test.sh b/tests/workflows/actions_lock_sha_refs_test.sh index 8ef5ad6..986f94e 100755 --- a/tests/workflows/actions_lock_sha_refs_test.sh +++ b/tests/workflows/actions_lock_sha_refs_test.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 # -# The four SHA-keyed leaves added by standards must carry a symbolic (tag) +# The twelve SHA-keyed leaves relocked in PR #12 must carry a symbolic (tag) # ref, with the SHA in their key and commit. GitHub's runner rejects a record # whose ref is its own SHA ("invalid lockfile pin", enforced from 2026-10-15; # standards#1271), so the SHA ref PR #7 asked for stopped every job that @@ -29,6 +29,30 @@ STANDARDS=$(jq -er ' | select(length == 1) | .[0] ' "$FIXTURE/lock.json") || fail "governance must lock exactly one standards reusable" +# All six callers must use the same immutable standards revision, including +# the reusable's path (a lock key alone loses that path information). +standards_sha=${STANDARDS##*@} +[[ "$standards_sha" =~ ^[0-9a-f]{40}$ ]] || fail "standards must be pinned by SHA" +for workflow in governance hypatia-scan mirror rust-ci scorecard secret-scanner; do + jq -e --arg workflow ".github/workflows/$workflow.yml" --arg standards "$STANDARDS" ' + .workflows[$workflow] == [$standards] + ' "$FIXTURE/lock.json" >/dev/null || fail "$workflow must lock the shared standards pin" + expected="hyperpolymath/standards/.github/workflows/$workflow-reusable.yml@$standards_sha" + yq -o=json '.jobs' "$WF_DIR/$workflow.yml" | jq -e --arg expected "$expected" ' + [.[].uses | select(. != null)] == [$expected] + ' >/dev/null || fail "$workflow must call $expected" +done + +# Reusable-workflow records still use a SHA ref. The symbolic-ref requirement +# applies to action leaves, so do not reject this intentional exception. +jq -e --arg standards "$STANDARDS" --arg sha "$standards_sha" ' + .dependencies[$standards] + | .ref == $sha and .commit == ("sha1-" + $sha) + and .owner_id == 6759885 and .repo_id == 1116521501 + and (.uses | type == "array" and length > 0) +' "$FIXTURE/lock.json" >/dev/null || fail "standards lost its SHA binding or repository identity" +echo "PASS: six standards callers share a locked SHA and the correct reusable paths" + # Assertions on parsed data, independent of the sync gate (which checks edges # but deliberately does not inspect ref/commit metadata). pinned_record() { @@ -63,10 +87,11 @@ while IFS='|' read -r action tag_ref owner_id repo_id; do .dependencies[$key] | .owner_id == $owner and .repo_id == $repo ' "$FIXTURE/lock.json" >/dev/null || fail "$key lost its repository identity" - # Reproduce the record GitHub's runner refuses: the key's own SHA as ref - # (the shape PR #7 wrote), and the same SHA in upper case. + # Any full SHA ref is refused, even one unrelated to the commit, and hex + # case must not bypass the check (including mixed case). sha=${key##*@} - for bad_ref in "$sha" "${sha^^}" ""; do + for bad_ref in "$sha" "${sha^^}" 'aBcDeF0123456789aBcDeF0123456789aBcDeF01' \ + '0000000000000000000000000000000000000000' ""; do jq --arg key "$key" --arg ref "$bad_ref" ' .dependencies[$key].ref = $ref ' "$FIXTURE/lock.json" > "$FIXTURE/mutated.json" @@ -75,6 +100,23 @@ while IFS='|' read -r action tag_ref owner_id repo_id; do fi done + # Missing and non-string metadata must not masquerade as a symbolic ref. + # jq may report a type error for these inputs; either way the gate must + # reject the record rather than treating its truthiness as a valid pin. + for bad_ref in null false 42 '[]' '{}'; do + jq --arg key "$key" --argjson ref "$bad_ref" ' + .dependencies[$key].ref = $ref + ' "$FIXTURE/lock.json" > "$FIXTURE/mutated.json" + if pinned_record "$key" "$FIXTURE/mutated.json" 2> "$FIXTURE/ref.err"; then + fail "$key accepted a non-string ref $bad_ref" + fi + done + jq --arg key "$key" 'del(.dependencies[$key].ref)' \ + "$FIXTURE/lock.json" > "$FIXTURE/mutated.json" + if pinned_record "$key" "$FIXTURE/mutated.json" 2> "$FIXTURE/ref.err"; then + fail "$key accepted a missing ref" + fi + # A valid-looking, different SHA is still a broken key/commit binding. jq --arg key "$key" ' .dependencies[$key].commit = "sha1-0000000000000000000000000000000000000000" @@ -83,6 +125,21 @@ while IFS='|' read -r action tag_ref owner_id repo_id; do fail "$key accepted a mismatched commit" fi + # A raw SHA, absent value, or JSON null cannot replace the sha1- binding. + for bad_commit in "\"$sha\"" null false '""'; do + jq --arg key "$key" --argjson commit "$bad_commit" ' + .dependencies[$key].commit = $commit + ' "$FIXTURE/lock.json" > "$FIXTURE/mutated.json" + if pinned_record "$key" "$FIXTURE/mutated.json"; then + fail "$key accepted an invalid commit $bad_commit" + fi + done + jq --arg key "$key" 'del(.dependencies[$key].commit)' \ + "$FIXTURE/lock.json" > "$FIXTURE/mutated.json" + if pinned_record "$key" "$FIXTURE/mutated.json"; then + fail "$key accepted a missing commit" + fi + # Delete only this leaf record, keeping its incoming standards edge and # all other workflow/lock data intact. Preserve gh-actions-lock layout: # the production gate parses that layout, not yq's reformatted output. @@ -100,12 +157,18 @@ while IFS='|' read -r action tag_ref owner_id repo_id; do fail "sync gate did not identify the missing $key" fi - # upload-artifact and setup-bun also have direct tag users. Their records - # must coexist with the SHA records; the repair must not globally ban tags. - if [[ "$action" == actions/upload-artifact || "$action" == oven-sh/setup-bun ]]; then + # Some actions also have direct tag users (notably Pages' cache and Code + # Quality's editorconfig checker). Their tag and SHA records must coexist + # with identical commits and repository identities. + if [[ "$action" == actions/cache || "$action" == actions/checkout \ + || "$action" == actions/upload-artifact \ + || "$action" == editorconfig-checker/action-editorconfig-checker \ + || "$action" == erlef/setup-beam || "$action" == oven-sh/setup-bun ]]; then jq -e --arg key "$key" --arg alias "$action@$tag_ref" --arg tag "$tag_ref" ' .dependencies[$alias].ref == $tag and (.dependencies[$alias].commit == .dependencies[$key].commit) + and (.dependencies[$alias].owner_id == .dependencies[$key].owner_id) + and (.dependencies[$alias].repo_id == .dependencies[$key].repo_id) and ([.workflows[][] | select(. == $alias)] | length > 0) ' "$FIXTURE/lock.json" >/dev/null || fail "$action lost its separate referenced tag record" fi @@ -113,11 +176,19 @@ while IFS='|' read -r action tag_ref owner_id repo_id; do checked=$((checked + 1)) echo "PASS: $action SHA binding, identity, negative controls and closure" done <<'CASES' +actions/cache|v6.1.0|44036562|215566462 +actions/checkout|v7.0.1|44036562|197814629 actions/upload-artifact|v7.0.2|44036562|192625955 denoland/setup-deno|v2.0.5|42048915|356423100 dtolnay/rust-toolchain|v1|1940490|260749683 +editorconfig-checker/action-editorconfig-checker|v3.0.0|26415196|297874902 +erlef/setup-beam|v1.24.1|47606891|331103973 +goto-bus-stop/setup-zig|v2.2.1|1006268|212984112 +ossf/scorecard-action|v2.4.4|67707773|421101922 oven-sh/setup-bun|v2.2.0|108928776|512644635 +swatinem/rust-cache|v2.9.2|580492|298565987 +webfactory/ssh-agent|v0.10.0|135788|208510314 CASES -[[ "$checked" == 4 ]] || fail "expected to check all four changed dependency records" +[[ "$checked" == 12 ]] || fail "expected to check all twelve changed dependency records" echo "PASS: actions.lock SHA-ref regression controls ($checked dependencies)" From 678141b08f32f7b25efae766f4a7d9eb4e0e02fe Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:49:19 +0000 Subject: [PATCH 4/8] test(ci): Cover policy migration and lock tag refs --- tests/workflows/actions_lock_sha_refs_test.sh | 18 ++ .../runtime_policy_migration_test.sh | 157 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100755 tests/workflows/runtime_policy_migration_test.sh diff --git a/tests/workflows/actions_lock_sha_refs_test.sh b/tests/workflows/actions_lock_sha_refs_test.sh index 986f94e..34d0c33 100755 --- a/tests/workflows/actions_lock_sha_refs_test.sh +++ b/tests/workflows/actions_lock_sha_refs_test.sh @@ -21,6 +21,14 @@ command -v jq >/dev/null || fail "jq is required" yq --version | grep -q 'github.com/mikefarah/yq/' || fail "mikefarah yq v4 is required" yq -o=json '.' "$LOCK" > "$FIXTURE/lock.json" +# Folding runtime policy into governance must remove both the workflow and +# its lock entry. A valid, fully locked legacy workflow would pass sync alone. +[[ ! -e "$WF_DIR/runtime-policy.yml" && ! -L "$WF_DIR/runtime-policy.yml" ]] \ + || fail "runtime-policy.yml must remain folded into governance" +jq -e '.workflows | has(".github/workflows/runtime-policy.yml") | not' \ + "$FIXTURE/lock.json" >/dev/null || fail "runtime-policy.yml must not retain a lock entry" +echo "PASS: retired runtime-policy workflow and lock entry are absent" + # Use the standards pin actually called by governance, so a later standards # update does not require copying its commit into this test. STANDARDS=$(jq -er ' @@ -83,6 +91,11 @@ while IFS='|' read -r action tag_ref owner_id repo_id; do pinned_record "$key" "$FIXTURE/lock.json" \ || fail "$key must have a symbolic ref and its own SHA as commit" + # Another nonempty symbolic ref also passes pinned_record, but would no + # longer identify the tag assigned to this action by the relock operation. + jq -e --arg key "$key" --arg tag "$tag_ref" ' + .dependencies[$key].ref == $tag + ' "$FIXTURE/lock.json" >/dev/null || fail "$key must retain its relocked tag $tag_ref" jq -e --arg key "$key" --argjson owner "$owner_id" --argjson repo "$repo_id" ' .dependencies[$key] | .owner_id == $owner and .repo_id == $repo ' "$FIXTURE/lock.json" >/dev/null || fail "$key lost its repository identity" @@ -192,3 +205,8 @@ CASES [[ "$checked" == 12 ]] || fail "expected to check all twelve changed dependency records" echo "PASS: actions.lock SHA-ref regression controls ($checked dependencies)" + +# This regression gate already runs in Estate Rules CI. Include the validators' +# migration controls so requiring governance in place of runtime-policy cannot +# regress while the workflow and its lock entry still look correct. +bash "$REPO/tests/workflows/runtime_policy_migration_test.sh" diff --git a/tests/workflows/runtime_policy_migration_test.sh b/tests/workflows/runtime_policy_migration_test.sh new file mode 100755 index 0000000..dad8992 --- /dev/null +++ b/tests/workflows/runtime_policy_migration_test.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# PR #13: both validators must require governance in place of runtime-policy. +# Exercise the real scripts against an isolated repository, including negative +# controls. The template validator treats missing workflows as errors; the +# workflow validator reports them as recommendations and still exits zero. +# Usage: bash tests/workflows/runtime_policy_migration_test.sh +set -euo pipefail + +repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT +fixture="$scratch/repository with spaces" +workflows="$fixture/.github/workflows" + +fail() { + echo "FAIL: $*" >&2 + for log in "$scratch/template.log" "$scratch/workflows.log"; do + if [[ -f "$log" ]]; then cat "$log" >&2; fi + done + exit 1 +} + +# Supply the unrelated structure checks with a minimal complete repository. +# Copy the real deed readers so an early setup error cannot look like the +# expected rejection of a missing governance workflow. +mkdir -p "$workflows" "$fixture/.machine_readable" "$fixture/scripts" \ + "$fixture/src/interface/Abi" "$fixture/src/interface/ffi/src" \ + "$fixture/src/interface/ffi/test" "$fixture/src/interface/generated/abi" \ + "$fixture/docs/developer" "$fixture/docs/governance" +cp "$repo/scripts/deed-field.sh" "$repo/scripts/check-no-a2ml.sh" "$fixture/scripts/" +cat > "$fixture/fixture_chora.deed" <<'DEED' +(repo-deed + (status :phase active) + (maturity :level experimental) + (meta :type library) + (ecosystem :project "fixture") + (anchor :purpose "workflow validation fixture") + (policies (maintenance-axes :corrective-first #t))) +DEED +for file in README.adoc EXPLAINME.adoc LICENSE Justfile AUDIT.adoc \ + TOPOLOGY.adoc CONTRIBUTING.md docs/developer/ABI-FFI-README.adoc \ + src/interface/Abi/Types.idr src/interface/Abi/Layout.idr \ + src/interface/Abi/Foreign.idr src/interface/abi.ipkg \ + src/interface/ffi/build.zig src/interface/ffi/src/main.zig \ + src/interface/ffi/test/integration_test.zig; do + printf '%s\n' '# SPDX-License-Identifier: MPL-2.0' > "$fixture/$file" +done + +# Compiler calls are outside this regression's scope. Bash functions keep the +# test independent of locally installed compilers and create no build outputs. +zig() { [[ $# == 1 && $1 == build ]]; } +idris2() { [[ $# == 2 && $1 == --typecheck && $2 == src/interface/abi.ipkg ]]; } +export -f zig idris2 + +write_workflow() { + printf '%s\n' '# SPDX-License-Identifier: MPL-2.0' 'name: Fixture' > "$1" +} + +# Keep the fixture's contract independent of the validators' arrays: deriving +# it from their implementation would also copy a regressed requirement. +for workflow in hypatia-scan codeql scorecard quality mirror instant-sync \ + guix-policy security-policy wellknown-enforcement workflow-linter \ + governance secret-scanner; do + write_workflow "$workflows/$workflow.yml" +done +cp "$workflows/governance.yml" "$scratch/governance.yml" + +run_case() { + local label=$1 governance_present=$2 template_status=0 workflow_status=0 + local validators=${3:-both} + local expected_status=0 expected_errors=0 + if [[ "$governance_present" == no ]]; then + expected_status=1 + expected_errors=1 + fi + + # Verbose mode exposes which required workflow was actually resolved. + bash "$repo/scripts/validate-template.sh" "$fixture" 1 \ + > "$scratch/template.log" 2>&1 || template_status=$? + [[ "$template_status" == "$expected_status" ]] \ + || fail "$label: template validator exited $template_status, expected $expected_status" + [[ $(grep -c 'ERROR' "$scratch/template.log" || true) == "$expected_errors" ]] \ + || fail "$label: template validator reported unrelated errors" + + if [[ "$governance_present" == yes ]]; then + grep -Fq 'Workflow found: governance.yml' "$scratch/template.log" \ + || fail "$label: template validator did not recognize governance" + grep -Fq 'Validation PASSED' "$scratch/template.log" \ + || fail "$label: template validator did not reach its passing summary" + else + grep -Fq 'Required workflow missing: governance.yml' "$scratch/template.log" \ + || fail "$label: template rejection did not identify governance" + grep -Fq 'Validation FAILED' "$scratch/template.log" \ + || fail "$label: template validator did not reach its failing summary" + fi + if grep -Eq '(Required workflow missing:|Workflow found:) runtime-policy\.yml' \ + "$scratch/template.log"; then + fail "$label: template validator still requires runtime-policy.yml" + fi + if [[ "$validators" == template ]]; then + echo "PASS: $label (template validator)" + return + fi + + bash "$repo/tests/workflows/validate_workflows_test.sh" "$workflows" \ + > "$scratch/workflows.log" 2>&1 || workflow_status=$? + [[ "$workflow_status" == 0 ]] \ + || fail "$label: workflow recommendations unexpectedly failed validation" + grep -Fq 'Workflow validation PASSED' "$scratch/workflows.log" \ + || fail "$label: workflow validator did not reach its summary" + + if [[ "$governance_present" == yes ]]; then + grep -Fq 'Found: governance.yml' "$scratch/workflows.log" \ + || fail "$label: workflow validator did not recognize governance" + if grep -Fq 'Missing: governance.yml' "$scratch/workflows.log"; then + fail "$label: workflow validator also reported governance missing" + fi + else + grep -Fq 'Missing: governance.yml' "$scratch/workflows.log" \ + || fail "$label: workflow validator did not recommend governance" + if grep -Fq 'Found: governance.yml' "$scratch/workflows.log"; then + fail "$label: workflow validator accepted a substitute for governance" + fi + fi + + if grep -Eq '(Missing:|Found:) runtime-policy\.yml' "$scratch/workflows.log"; then + fail "$label: workflow validator still requires runtime-policy.yml" + fi + echo "PASS: $label (both validators)" +} + +run_case 'governance alone satisfies the migrated requirement' yes + +rm "$workflows/governance.yml" +run_case 'neither workflow leaves governance missing' no + +write_workflow "$workflows/runtime-policy.yml" +run_case 'legacy runtime policy cannot substitute for governance' no + +cp "$scratch/governance.yml" "$workflows/governance.yml" +run_case 'governance is recognized when a legacy workflow is also present' yes + +rm "$workflows/runtime-policy.yml" "$workflows/governance.yml" +mkdir "$workflows/governance.yml" +# The workflow validator's generic scan also treats named directories as +# malformed YAML files; isolate this presence boundary to template validation. +run_case 'a directory named governance.yml is not a workflow file' no template +rmdir "$workflows/governance.yml" + +cp "$scratch/governance.yml" "$workflows/governance.yaml" +run_case 'governance.yaml does not satisfy the required governance.yml path' no +rm "$workflows/governance.yaml" + +cp "$scratch/governance.yml" "$workflows/governance.yml" +run_case 'restoring governance restores passing validation' yes From 9d4bedfb27f7ec60e0e684453c1d984f913d3b7c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 18:39:25 +0000 Subject: [PATCH 5/8] docs(changelog): governance is no longer at 934b879; name the wired migration test The runtime-policy entry said governance at 934b879 covers the check. This branch re-pins it to 75bd599, so the pin is dropped from the sentence. It also records that actions_lock_sha_refs_test.sh, which Estate Rules runs, requires runtime-policy.yml and its lock record to stay gone and runs runtime_policy_migration_test.sh. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6FD63UrriRBHqmhhEca9t --- CHANGELOG.adoc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 6acd95a..82e4649 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,14 +6,16 @@ `actions.lock` record, as rsr-template-repo#244 does at the source. This repository's copy still said Deno was retired. It failed any repository holding `bun.lock`, `deno.lock` and `pnpm-lock.yaml` together, which is - what `js-runtimes regen` writes. Governance at `934b879` already covers the - check. "Language / package anti-pattern policy" fails any tracked + what `js-runtimes regen` writes. Governance already covers the check. + "Language / package anti-pattern policy" fails any tracked `package-lock.json`, and the "JS runtimes" jobs check each lock against `package.json` (standards cicd-regularisation spec §14). * `scripts/validate-template.sh` and `tests/workflows/validate_workflows_test.sh` require `governance.yml` in - its place. - + its place. `tests/workflows/runtime_policy_migration_test.sh` checks both + validators reject a tree without `governance.yml`, and + `actions_lock_sha_refs_test.sh`, which Estate Rules runs, requires the + workflow and its lock record to stay gone and runs that test. == 2026-10-19 — lock records carry tag refs; standards re-pinned to `75bd599` From 469e65c64d2c1e29c7be1fbb8a459808be947491 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:48:40 +0000 Subject: [PATCH 6/8] docs(test): Document runtime policy migration test helpers --- tests/workflows/runtime_policy_migration_test.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/workflows/runtime_policy_migration_test.sh b/tests/workflows/runtime_policy_migration_test.sh index dad8992..6e4f73b 100755 --- a/tests/workflows/runtime_policy_migration_test.sh +++ b/tests/workflows/runtime_policy_migration_test.sh @@ -14,6 +14,8 @@ trap 'rm -rf "$scratch"' EXIT fixture="$scratch/repository with spaces" workflows="$fixture/.github/workflows" +# Report all arguments as a failure message and print any validator logs from +# $scratch to stderr. Terminates the test script with exit status 1. fail() { echo "FAIL: $*" >&2 for log in "$scratch/template.log" "$scratch/workflows.log"; do @@ -50,10 +52,18 @@ done # Compiler calls are outside this regression's scope. Bash functions keep the # test independent of locally installed compilers and create no build outputs. + +# Stub the Zig compiler: return 0 only for the single argument "build"; +# return 1 for any other invocation without compiling or writing files. zig() { [[ $# == 1 && $1 == build ]]; } + +# Stub Idris2: return 0 only for "--typecheck src/interface/abi.ipkg"; +# return 1 for any other invocation without invoking the real compiler. idris2() { [[ $# == 2 && $1 == --typecheck && $2 == src/interface/abi.ipkg ]]; } export -f zig idris2 +# Write a minimal workflow with an SPDX header and name to the path in $1, +# overwriting any existing file. Returns the status of the printf command. write_workflow() { printf '%s\n' '# SPDX-License-Identifier: MPL-2.0' 'name: Fixture' > "$1" } @@ -67,6 +77,11 @@ for workflow in hypatia-scan codeql scorecard quality mirror instant-sync \ done cp "$workflows/governance.yml" "$scratch/governance.yml" +# Validate the current $fixture against the expected governance presence. +# Arguments: $1 case label, $2 "yes" or "no" for governance.yml presence, +# $3 "both" (default) or "template" to select the validators to check. +# Writes validator logs under $scratch and prints PASS, returning 0 on success; +# calls fail to print diagnostics and exit 1 on any unexpected result. run_case() { local label=$1 governance_present=$2 template_status=0 workflow_status=0 local validators=${3:-both} From df190357d9da36f916520d8dd147969482f1cef7 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:30:26 +0000 Subject: [PATCH 7/8] test(ci): Cover runtime policy retirement guards and governance path edge cases --- .../runtime_policy_migration_test.sh | 28 +++++ .../runtime_policy_retirement_test.sh | 108 ++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100755 tests/workflows/runtime_policy_retirement_test.sh diff --git a/tests/workflows/runtime_policy_migration_test.sh b/tests/workflows/runtime_policy_migration_test.sh index 6e4f73b..bc8bc0f 100755 --- a/tests/workflows/runtime_policy_migration_test.sh +++ b/tests/workflows/runtime_policy_migration_test.sh @@ -125,6 +125,10 @@ run_case() { || fail "$label: workflow recommendations unexpectedly failed validation" grep -Fq 'Workflow validation PASSED' "$scratch/workflows.log" \ || fail "$label: workflow validator did not reach its summary" + local expected_found=12 + if [[ "$governance_present" == no ]]; then expected_found=11; fi + grep -Fq "Found $expected_found/12 required workflows" "$scratch/workflows.log" \ + || fail "$label: workflow validator reported an unexpected required-workflow count" if [[ "$governance_present" == yes ]]; then grep -Fq 'Found: governance.yml' "$scratch/workflows.log" \ @@ -168,5 +172,29 @@ cp "$scratch/governance.yml" "$workflows/governance.yaml" run_case 'governance.yaml does not satisfy the required governance.yml path' no rm "$workflows/governance.yaml" +# The presence check must resolve the exact top-level path, even though the +# workflow validator also scans nested files for generic formatting checks. +mkdir "$workflows/nested" +cp "$scratch/governance.yml" "$workflows/nested/governance.yml" +run_case 'nested governance cannot satisfy the top-level requirement' no +rm -r "$workflows/nested" + +cp "$scratch/governance.yml" "$workflows/Governance.yml" +run_case 'governance workflow names are case-sensitive' no +rm "$workflows/Governance.yml" + +# Match the validators' regular-file contract: a dangling link is missing, +# while a link resolving to a regular workflow file satisfies the requirement. +# As with the directory case, the generic workflow scan rejects the unreadable +# link separately, so isolate the missing-file boundary to template validation. +ln -s missing-governance.yml "$workflows/governance.yml" +run_case 'a dangling governance symlink is missing' no template +rm "$workflows/governance.yml" + +cp "$scratch/governance.yml" "$workflows/governance-target.yml" +ln -s governance-target.yml "$workflows/governance.yml" +run_case 'a governance symlink resolving to a workflow file is accepted' yes +rm "$workflows/governance.yml" "$workflows/governance-target.yml" + cp "$scratch/governance.yml" "$workflows/governance.yml" run_case 'restoring governance restores passing validation' yes diff --git a/tests/workflows/runtime_policy_retirement_test.sh b/tests/workflows/runtime_policy_retirement_test.sh new file mode 100755 index 0000000..c7af8b5 --- /dev/null +++ b/tests/workflows/runtime_policy_retirement_test.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# Exercise PR #13's retirement and relocked-tag guards through the real gate. +# A synchronized legacy workflow must still fail the retirement requirement. +# Usage: bash tests/workflows/runtime_policy_retirement_test.sh +# Requires the gate's existing dependencies: mikefarah yq v4, jq and GNU awk. +set -euo pipefail + +repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +gate="$repo/tests/workflows/actions_lock_sha_refs_test.sh" +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT +workflows="$scratch/workflows with spaces" +mkdir -p "$workflows" +cp -a "$repo/.github/workflows/." "$workflows/" +original_lock="$repo/.github/workflows/actions.lock" + +# Print the captured gate output so a setup failure cannot masquerade as an +# expected rejection from the specific guard being tested. +fail() { + echo "FAIL: $*" >&2 + if [[ -f "$scratch/gate.log" ]]; then cat "$scratch/gate.log" >&2; fi + exit 1 +} + +# Exercise the complete passing gate before introducing one defect at a time. +bash "$gate" "$workflows" > "$scratch/gate.log" 2>&1 \ + || fail 'unmodified workflows must pass the regression gate' +grep -Fq 'PASS: restoring governance restores passing validation' "$scratch/gate.log" \ + || fail 'the lock regression gate must run the governance migration controls' +echo 'PASS: current workflows and lock pass the complete regression gate' + +# Require the intended exit status and diagnostic, rather than accepting any +# failure (for example, an unrelated lock-sync or fixture error). +reject_case() { + local label=$1 diagnostic=$2 status=0 + bash "$gate" "$workflows" > "$scratch/gate.log" 2>&1 || status=$? + [[ "$status" == 1 ]] || fail "$label: expected exit 1, got $status" + grep -Fq "FAIL: $diagnostic" "$scratch/gate.log" \ + || fail "$label: the gate did not identify the intended regression" + echo "PASS: $label rejected by the regression gate" +} + +workflow_error='runtime-policy.yml must remain folded into governance' +lock_error='runtime-policy.yml must not retain a lock entry' + +printf '%s\n' '# SPDX-License-Identifier: MPL-2.0' 'name: Retired workflow' \ + > "$workflows/runtime-policy.yml" +reject_case 'restored regular workflow' "$workflow_error" +rm "$workflows/runtime-policy.yml" + +mkdir "$workflows/runtime-policy.yml" +reject_case 'directory occupying the retired workflow path' "$workflow_error" +rmdir "$workflows/runtime-policy.yml" + +ln -s governance.yml "$workflows/runtime-policy.yml" +reject_case 'retired workflow symlink to an existing workflow' "$workflow_error" +rm "$workflows/runtime-policy.yml" + +# -e alone cannot detect this path; the explicit -L guard must also hold. +ln -s missing-workflow.yml "$workflows/runtime-policy.yml" +reject_case 'dangling retired workflow symlink' "$workflow_error" +rm "$workflows/runtime-policy.yml" + +# A key is stale regardless of its value, including a null value that a +# truthiness check could overlook. Preserve the lock generator's formatting. +for value in '[]' 'null' "['actions/checkout@v7.0.1']"; do + awk -v entry=" '.github/workflows/runtime-policy.yml': $value" ' + { print } + /^workflows:$/ { print entry } + ' "$original_lock" > "$workflows/actions.lock" + reject_case "retired lock entry with value $value" "$lock_error" +done + +# Both sides can be restored consistently, so lock synchronization alone is +# insufficient. This fixture passes the real sync gate before being rejected. +awk ' + { print } + /^workflows:$/ { print " \047.github/workflows/runtime-policy.yml\047: []" } +' "$original_lock" > "$workflows/actions.lock" +printf '%s\n' '# SPDX-License-Identifier: MPL-2.0' 'name: Retired workflow' \ + > "$workflows/runtime-policy.yml" +bash "$repo/scripts/check-lock-sync.sh" "$workflows" > "$scratch/gate.log" 2>&1 \ + || fail 'the synchronized legacy fixture must pass lock synchronization' +reject_case 'synchronized restored workflow and lock entry' "$workflow_error" +rm "$workflows/runtime-policy.yml" + +# A different symbolic ref still satisfies the SHA-ref safety predicate but +# must fail the additional exact-tag assertion introduced by this PR. Select +# the first action checked by the gate so this control stays fast. +cache_key=$(yq -o=json '.' "$original_lock" | jq -er ' + [.dependencies | keys[] | select(test("^actions/cache@[0-9a-f]{40}$"))] + | select(length == 1) | .[0] +') +cache_tag=$(yq -o=json '.' "$original_lock" | jq -er --arg key "$cache_key" \ + '.dependencies[$key].ref') +awk -v header=" '$cache_key':" ' + $0 == header { in_record = 1; print; next } + in_record && /^ ref:/ { + print " ref: \047unexpected-symbolic-ref\047" + in_record = 0 + next + } + { print } +' "$original_lock" > "$workflows/actions.lock" +reject_case 'different nonempty symbolic action ref' \ + "$cache_key must retain its relocked tag $cache_tag" From e06c645a994d907070965ce385a6bc020c083341 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 22:44:06 +0000 Subject: [PATCH 8/8] ci(estate-rules): run the runtime policy retirement test CodeRabbit's df19035 added tests/workflows/runtime_policy_retirement_test.sh, which restores the retired runtime-policy workflow or its lock record in each way the SHA-ref gate must catch and checks the gate rejects every one. It tests this PR's own change but no workflow ran it. Estate Rules now runs it after the gate, and the changelog names it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6FD63UrriRBHqmhhEca9t --- .github/workflows/estate-rules.yml | 4 ++++ CHANGELOG.adoc | 3 +++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/estate-rules.yml b/.github/workflows/estate-rules.yml index 8c8f44d..1726316 100644 --- a/.github/workflows/estate-rules.yml +++ b/.github/workflows/estate-rules.yml @@ -98,6 +98,10 @@ jobs: # and the sync gate names each one if its record goes missing. - name: actions.lock SHA-ref regression controls run: bash tests/workflows/actions_lock_sha_refs_test.sh + # The gate above must reject each way the retired runtime policy could + # come back, including a workflow and lock record restored together. + - name: Runtime policy retirement guards reject each regression + run: bash tests/workflows/runtime_policy_retirement_test.sh # The Scorecard calling job's own permissions map must grant actions: read; # the workflow-level grant does not reach a job that declares its own map. - name: Scorecard caller grants diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 82e4649..b75f617 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -16,6 +16,9 @@ validators reject a tree without `governance.yml`, and `actions_lock_sha_refs_test.sh`, which Estate Rules runs, requires the workflow and its lock record to stay gone and runs that test. + `tests/workflows/runtime_policy_retirement_test.sh`, also in Estate Rules, + restores the workflow or its lock record in each way the gate must catch + and checks the gate rejects every one. == 2026-10-19 — lock records carry tag refs; standards re-pinned to `75bd599`