From 3bb0cb796ee79b83f6c4006cb9bee95c9cf23fa7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 12:12:26 +0000 Subject: [PATCH 1/4] ci: standardize actions/checkout and setup-node on v7.0.0 across workflows/composites Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019Jc1ZYHFjXjn6mE6U6riVU --- .github/actions/setup-node-cached/action.yml | 2 +- .github/workflows/ci-triage.yml | 2 +- .github/workflows/dependency-report.yml | 2 +- .github/workflows/ingestion-autopilot.yml | 2 +- .github/workflows/ops-digest.yml | 2 +- .github/workflows/staging-tenancy.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/actions/setup-node-cached/action.yml b/.github/actions/setup-node-cached/action.yml index 7edfa9c77..0d0fe1b19 100644 --- a/.github/actions/setup-node-cached/action.yml +++ b/.github/actions/setup-node-cached/action.yml @@ -8,7 +8,7 @@ runs: using: composite steps: - name: Setup Node.js - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: ".nvmrc" cache: npm diff --git a/.github/workflows/ci-triage.yml b/.github/workflows/ci-triage.yml index 6366a0acc..4e422c485 100644 --- a/.github/workflows/ci-triage.yml +++ b/.github/workflows/ci-triage.yml @@ -37,7 +37,7 @@ jobs: github.event.workflow_run.event == 'pull_request' steps: - name: Checkout default branch (trusted) for triage policy - uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.repository.default_branch }} persist-credentials: false diff --git a/.github/workflows/dependency-report.yml b/.github/workflows/dependency-report.yml index f5c12c6ed..cf90dc355 100644 --- a/.github/workflows/dependency-report.yml +++ b/.github/workflows/dependency-report.yml @@ -26,7 +26,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/ingestion-autopilot.yml b/.github/workflows/ingestion-autopilot.yml index 6c6b82f40..09b5b6588 100644 --- a/.github/workflows/ingestion-autopilot.yml +++ b/.github/workflows/ingestion-autopilot.yml @@ -41,7 +41,7 @@ jobs: timeout-minutes: 20 steps: - name: Checkout - uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/ops-digest.yml b/.github/workflows/ops-digest.yml index fdae41ffc..1865ee2c6 100644 --- a/.github/workflows/ops-digest.yml +++ b/.github/workflows/ops-digest.yml @@ -30,7 +30,7 @@ jobs: HEALTH_DEEP_PROBE_SECRET: ${{ secrets.HEALTH_DEEP_PROBE_SECRET }} steps: - name: Checkout - uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/staging-tenancy.yml b/.github/workflows/staging-tenancy.yml index 5b1fbe1e0..99239c1ee 100644 --- a/.github/workflows/staging-tenancy.yml +++ b/.github/workflows/staging-tenancy.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 20 steps: - name: Checkout - uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false From 2ff200ff3532d9e08c51bad8a4724dbf575fdd52 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 12:12:27 +0000 Subject: [PATCH 2/4] ci: enforce one SHA per action in the pin checker (now scans composites) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019Jc1ZYHFjXjn6mE6U6riVU --- scripts/check-github-action-pins.mjs | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/scripts/check-github-action-pins.mjs b/scripts/check-github-action-pins.mjs index 82d195306..b17cb3246 100644 --- a/scripts/check-github-action-pins.mjs +++ b/scripts/check-github-action-pins.mjs @@ -76,6 +76,51 @@ if (!/^ src worker scripts supabase\/functions\s*$/m.test(semgrepScanSt failures.push("sast.yml: the Semgrep scan command must target src, worker, scripts, and supabase/functions."); } +// One SHA per action across every workflow AND composite action. Dependabot bumps +// one file at a time, so a laggard can sit on an old major indefinitely; because +// the per-line validation above only covers workflows, a composite skew (e.g. +// setup-node v5 vs v7) was previously invisible. Assert each action name resolves +// to a single SHA everywhere it is used. +function discoverCompositeActionFiles(workflowRoot) { + const actionsRoot = path.join(workflowRoot, ".github", "actions"); + if (!existsSync(actionsRoot)) return []; + const files = []; + for (const entry of readdirSync(actionsRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + for (const name of ["action.yml", "action.yaml"]) { + const candidate = path.join(actionsRoot, entry.name, name); + if (existsSync(candidate)) files.push(candidate); + } + } + return files; +} + +const actionPinPattern = /uses:\s*([^@\s]+)@([0-9a-f]{40})(?:\s*#\s*(\S+))?/; +const shasByAction = new Map(); +for (const filePath of [...discoverGitHubActionFiles(process.cwd()), ...discoverCompositeActionFiles(process.cwd())]) { + const fileName = path.relative(process.cwd(), filePath).replaceAll("\\", "/"); + readFileSync(filePath, "utf8") + .split(/\r?\n/) + .forEach((line, index) => { + const match = actionPinPattern.exec(line); + if (!match) return; + const [, name, sha, version] = match; + if (!shasByAction.has(name)) shasByAction.set(name, new Map()); + const bySha = shasByAction.get(name); + if (!bySha.has(sha)) bySha.set(sha, { version: version ?? "(no version)", locations: [] }); + bySha.get(sha).locations.push(`${fileName}:${index + 1}`); + }); +} +for (const [name, bySha] of shasByAction) { + if (bySha.size <= 1) continue; + const detail = [...bySha.values()] + .map(({ version, locations }) => `${version} (${locations.join(", ")})`) + .join(" vs "); + failures.push( + `${name} is pinned to ${bySha.size} different SHAs across workflows/composites — standardize on one: ${detail}`, + ); +} + if (failures.length > 0) { console.error("GitHub Actions pin check failed:"); for (const failure of failures) console.error(`- ${failure}`); From 2c30c78ec23919e58defa662c4de2567ac68d4ef Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 12:12:27 +0000 Subject: [PATCH 3/4] docs: mark maturity backlog L2 (action-SHA uniformity) done Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019Jc1ZYHFjXjn6mE6U6riVU --- docs/maturity-backlog-workorders.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/maturity-backlog-workorders.md b/docs/maturity-backlog-workorders.md index 981f9c1ee..815922cfe 100644 --- a/docs/maturity-backlog-workorders.md +++ b/docs/maturity-backlog-workorders.md @@ -128,14 +128,16 @@ confirmation) · `SATISFIED` (already true in the repo; no work needed). of this item — a curated script map — ships now as [`docs/scripts-index.md`](scripts-index.md). -### L2 · Single-SHA-per-action uniformity — `OPEN` +### L2 · Single-SHA-per-action uniformity — `DONE` - **Outcome:** every third-party action pinned to one SHA across all workflow + composite files. -- **Approach:** extend `scripts/check-github-action-pins.mjs` (it already parses every workflow) - to assert one SHA per action name. Partly mitigated already by N1 (grouped action bumps land - together). -- **Files:** `scripts/check-github-action-pins.mjs`. -- **Verification:** `npm run check:github-actions`. +- **Landed:** aligned the laggards (`actions/checkout` v6.0.3 → v7.0.0 in 5 workflows, + `actions/setup-node` v5.0.0 → v7.0.0 in the `setup-node-cached` composite) and extended + `scripts/check-github-action-pins.mjs` to assert one SHA per action across **workflows and + composites** — the checker previously scanned only workflows, so the composite skew was invisible. +- **Files:** `scripts/check-github-action-pins.mjs`, `.github/actions/setup-node-cached/action.yml`, + 5 workflow files. +- **Verification:** `npm run check:github-actions` passes; a reintroduced skew is correctly rejected. ### L3 · Single gate manifest — `OPEN` @@ -186,7 +188,7 @@ collaborators join — `AGENTS.md` + the PR template already carry that load. | X5 ACL-migration consolidation | Next | PROVIDER-GATED (DB owner) | | X6 Coverage floors | Next | OPEN | | L1 Archive one-shot scripts | Later | OPEN (index shipped) | -| L2 Action-SHA uniformity | Later | OPEN (mitigated by N1) | +| L2 Action-SHA uniformity | Later | **DONE** (this PR) | | L3 Single gate manifest | Later | OPEN | | L4 Ledger rotation | Later | OPEN | | L5 AI map / WCAG / RPO-RTO | Later | **DONE / SATISFIED** | From 9ff524b9ab8beffa6b05235eb255336886895f3c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 13:50:46 +0000 Subject: [PATCH 4/4] fix: validate composite action references in the pin checker The single-SHA uniformity pass scans workflow and composite files with a 40-hex-only pattern, and only the workflow pre-pass runs validateActionReference. A non-SHA reference in a composite action file (e.g. vendor/action@v1) therefore matched neither path and slipped through unpinned. Run validateActionReference over composite-file lines too; local `./` refs are still ignored, so all current composites pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019Jc1ZYHFjXjn6mE6U6riVU --- scripts/check-github-action-pins.mjs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/check-github-action-pins.mjs b/scripts/check-github-action-pins.mjs index b17cb3246..52040405e 100644 --- a/scripts/check-github-action-pins.mjs +++ b/scripts/check-github-action-pins.mjs @@ -99,9 +99,19 @@ const actionPinPattern = /uses:\s*([^@\s]+)@([0-9a-f]{40})(?:\s*#\s*(\S+))?/; const shasByAction = new Map(); for (const filePath of [...discoverGitHubActionFiles(process.cwd()), ...discoverCompositeActionFiles(process.cwd())]) { const fileName = path.relative(process.cwd(), filePath).replaceAll("\\", "/"); + // Workflow lines are already run through validateActionReference in the first + // pass; composite files are not, so validate them here. Without this, a + // non-SHA composite reference (e.g. vendor/action@v1) matches neither the + // 40-hex actionPinPattern below nor the first pass, so it would slip through + // unpinned. Local `./` refs are correctly ignored by validateActionReference. + const isComposite = fileName.startsWith(".github/actions/"); readFileSync(filePath, "utf8") .split(/\r?\n/) .forEach((line, index) => { + if (isComposite) { + const actionFailure = validateActionReference(line); + if (actionFailure) failures.push(`${fileName}:${index + 1}: ${actionFailure}`); + } const match = actionPinPattern.exec(line); if (!match) return; const [, name, sha, version] = match;