diff --git a/.github/workflows/hourly-commercial-readiness.yml b/.github/workflows/hourly-commercial-readiness.yml index 44d236977..fd86cf78a 100644 --- a/.github/workflows/hourly-commercial-readiness.yml +++ b/.github/workflows/hourly-commercial-readiness.yml @@ -34,6 +34,22 @@ jobs: ref: ${{ github.event.repository.default_branch }} persist-credentials: false + - name: setup node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "24.19.0" + cache: npm + + - name: verify package-manager toolchain + shell: bash + run: | + set -euo pipefail + test "$(node --version)" = "v24.19.0" + test "$(npm --version)" = "11.17.0" + + - name: install lockfile dependencies + run: npm ci --legacy-peer-deps=false --install-links=false + - name: mint dedicated maintainer App token id: maintainer_app uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 @@ -49,15 +65,6 @@ jobs: permission-pull-requests: write permission-statuses: read - - name: setup node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: "24" - cache: npm - - - name: install lockfile dependencies - run: npm ci - - name: verify active main governance before any write id: governance env: @@ -125,5 +132,5 @@ jobs: noema-kpi-evidence.json exchange-30d.ndjson.provenance.json noema-smoke-evidence.json - if-no-files-found: warn + if-no-files-found: error retention-days: 90 diff --git a/test/hourly-commercial-readiness-toolchain-integrity.test.ts b/test/hourly-commercial-readiness-toolchain-integrity.test.ts new file mode 100644 index 000000000..e648bd6c6 --- /dev/null +++ b/test/hourly-commercial-readiness-toolchain-integrity.test.ts @@ -0,0 +1,103 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const workflow = readFileSync( + ".github/workflows/hourly-commercial-readiness.yml", + "utf8", +); + +type WorkflowStep = { + readonly name: string; + readonly block: string; + readonly index: number; +}; + +function workflowSteps(): readonly WorkflowStep[] { + const lines = workflow.split("\n"); + const starts = lines.flatMap((line, index) => { + const match = /^ - name: (.+)$/.exec(line); + return match ? [{ name: match[1], index }] : []; + }); + + return starts.map(({ name, index }, position) => { + const nextIndex = starts[position + 1]?.index ?? lines.length; + return { + name, + block: lines.slice(index, nextIndex).join("\n"), + index, + }; + }); +} + +function uniqueStep(name: string): WorkflowStep { + const matches = workflowSteps().filter((step) => step.name === name); + expect(matches, `expected exactly one workflow step named ${name}`).toHaveLength(1); + return matches[0]; +} + +describe("commercial writer toolchain integrity", () => { + it("uses the exact protected-CI Node/npm execution contract in the intended steps", () => { + const setup = uniqueStep("setup node"); + const verify = uniqueStep("verify package-manager toolchain"); + const install = uniqueStep("install lockfile dependencies"); + const mint = uniqueStep("mint dedicated maintainer App token"); + + expect(setup.block).toContain( + "uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0", + ); + expect(setup.block).toContain('node-version: "24.19.0"'); + expect(setup.block).not.toContain('node-version: "24"'); + + expect(verify.block).toContain('test "$(node --version)" = "v24.19.0"'); + expect(verify.block).toContain('test "$(npm --version)" = "11.17.0"'); + + expect(install.block).toContain( + "run: npm ci --legacy-peer-deps=false --install-links=false", + ); + expect(install.block).not.toMatch(/run:\s+npm ci\s*(?:\n|$)/); + + expect(setup.index).toBeLessThan(verify.index); + expect(verify.index).toBeLessThan(install.index); + expect(install.index).toBeLessThan(mint.index); + }); + + it("retains exact credential isolation and bounded maintainer authority", () => { + const checkout = uniqueStep("checkout trusted default branch"); + const mint = uniqueStep("mint dedicated maintainer App token"); + + expect(checkout.block).toContain("persist-credentials: false"); + expect(mint.block).toContain( + "uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0", + ); + + const permissionLines = mint.block + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.startsWith("permission-")); + expect(permissionLines).toEqual([ + "permission-actions: read", + "permission-checks: read", + "permission-contents: write", + "permission-metadata: read", + "permission-pull-requests: write", + "permission-statuses: read", + ]); + + const tokenConsumers = workflowSteps() + .filter((step) => + step.block.includes("GH_TOKEN: ${{ steps.maintainer_app.outputs.token }}"), + ) + .map((step) => step.name); + expect(tokenConsumers).toEqual([ + "verify active main governance before any write", + "inspect, dispatch, and merge exact-head pull requests", + ]); + expect(workflow).not.toContain("GH_TOKEN: ${{ github.token }}"); + }); + + it("fails closed when no-PR commercial-readiness evidence is missing", () => { + const upload = uniqueStep("upload no-PR commercial-readiness evidence"); + expect(upload.block).toContain("if-no-files-found: error"); + expect(upload.block).not.toContain("if-no-files-found: warn"); + }); +});