Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/setup-node-cached/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dependency-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ingestion-autopilot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ops-digest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/staging-tenancy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 9 additions & 7 deletions docs/maturity-backlog-workorders.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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** |
55 changes: 55 additions & 0 deletions scripts/check-github-action-pins.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,61 @@ 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("\\", "/");
// 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;
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}`);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
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}`);
Expand Down
Loading