diff --git a/.github/workflows/pr-trusted.yml b/.github/workflows/pr-trusted.yml index a55c7f597e..edbe655136 100644 --- a/.github/workflows/pr-trusted.yml +++ b/.github/workflows/pr-trusted.yml @@ -280,6 +280,11 @@ jobs: exit 1 fi + - name: Setup Node.js for pnpm bootstrap + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - name: Setup pnpm uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: @@ -373,6 +378,11 @@ jobs: with: persist-credentials: false + - name: Setup Node.js for pnpm bootstrap + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - name: Setup pnpm uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: @@ -464,6 +474,11 @@ jobs: with: persist-credentials: false + - name: Setup Node.js for pnpm bootstrap + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - name: Setup pnpm uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: @@ -546,6 +561,11 @@ jobs: with: persist-credentials: false + - name: Setup Node.js for pnpm bootstrap + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - name: Setup pnpm uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: @@ -615,6 +635,11 @@ jobs: with: persist-credentials: false + - name: Setup Node.js for pnpm bootstrap + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - name: Setup pnpm uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: @@ -656,6 +681,11 @@ jobs: with: persist-credentials: false + - name: Setup Node.js for pnpm bootstrap + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - name: Setup pnpm uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: @@ -738,6 +768,11 @@ jobs: with: persist-credentials: false + - name: Setup Node.js for pnpm bootstrap + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - name: Setup pnpm uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: diff --git a/.github/workflows/runner-full-stack-e2e.yml b/.github/workflows/runner-full-stack-e2e.yml index 177d5d53b5..b7662b4205 100644 --- a/.github/workflows/runner-full-stack-e2e.yml +++ b/.github/workflows/runner-full-stack-e2e.yml @@ -158,6 +158,10 @@ jobs: ref: ${{ needs.authorize.outputs.target_sha }} persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: NPM_CONFIG_AUDIT: "false" @@ -166,10 +170,6 @@ jobs: with: version: 9.15.4 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 - with: - node-version: 24 - - name: Resolve target lockfile without lifecycle scripts id: lock run: | @@ -236,6 +236,10 @@ jobs: cp "$lock" pnpm-lock.yaml test "$(sha256sum pnpm-lock.yaml | cut -d ' ' -f 1)" = "$EXPECTED_LOCK_SHA256" + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: NPM_CONFIG_AUDIT: "false" @@ -490,6 +494,10 @@ jobs: cp "$lock" pnpm-lock.yaml test "$(sha256sum pnpm-lock.yaml | cut -d ' ' -f 1)" = "$EXPECTED_LOCK_SHA256" + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: NPM_CONFIG_AUDIT: "false" @@ -615,6 +623,11 @@ jobs: cp "$lock" pnpm-lock.yaml test "$(sha256sum pnpm-lock.yaml | cut -d ' ' -f 1)" = "$EXPECTED_LOCK_SHA256" + - if: needs.catalog.outputs.needs_remote_provider_pack == 'true' + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - if: needs.catalog.outputs.needs_remote_provider_pack == 'true' uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: @@ -769,6 +782,10 @@ jobs: cp "$lock" pnpm-lock.yaml test "$(sha256sum pnpm-lock.yaml | cut -d ' ' -f 1)" = "$EXPECTED_LOCK_SHA256" + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: NPM_CONFIG_AUDIT: "false" @@ -777,10 +794,6 @@ jobs: with: version: 9.15.4 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 - with: - node-version: 24 - # This job receives provider credentials only in the final paid-test # step. Keep target-selected dependency lifecycle code from running in # the protected environment during setup. @@ -931,6 +944,10 @@ jobs: ref: ${{ github.sha }} persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: NPM_CONFIG_AUDIT: "false" @@ -1041,6 +1058,10 @@ jobs: ref: ${{ github.sha }} persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 env: NPM_CONFIG_AUDIT: "false" @@ -1049,10 +1070,6 @@ jobs: with: version: 9.15.4 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 - with: - node-version: 24 - - run: pnpm install --frozen-lockfile - name: Download access-controlled normalized campaign diff --git a/tests/runner-e2e/workflow-security.test.ts b/tests/runner-e2e/workflow-security.test.ts index f24646f5a8..74e1c5924a 100644 --- a/tests/runner-e2e/workflow-security.test.ts +++ b/tests/runner-e2e/workflow-security.test.ts @@ -45,6 +45,58 @@ describe("public repository paid workflow security", () => { } }); + it("installs a modern Node runtime before every trusted pnpm bootstrap", async () => { + const workflows = [ + { + name: "runner-full-stack-e2e.yml", + expectedCachedSetupNodeSteps: 4, + }, + { + name: "pr-trusted.yml", + expectedCachedSetupNodeSteps: 7, + }, + ]; + + for (const { name, expectedCachedSetupNodeSteps } of workflows) { + const workflow = await readFile( + path.join(repositoryRoot, ".github/workflows", name), + "utf8", + ); + const steps = workflow.split(/\n(?= {6}- )/u); + const pnpmSetupStepIndexes = steps.flatMap((step, index) => + step.includes("uses: pnpm/action-setup@") ? [index] : [], + ); + + expect(pnpmSetupStepIndexes, name).toHaveLength(7); + for (const pnpmSetupStepIndex of pnpmSetupStepIndexes) { + const pnpmSetupStep = steps[pnpmSetupStepIndex]!; + const nodeBootstrapStep = steps[pnpmSetupStepIndex - 1]!; + expect(nodeBootstrapStep, name).toContain("uses: actions/setup-node@"); + expect(nodeBootstrapStep, name).not.toContain("cache: pnpm"); + + const nodeVersionMatch = nodeBootstrapStep.match( + /^\s*node-version:\s*["']?(\d+)(?:\.(\d+))?/mu, + ); + expect(nodeVersionMatch, name).not.toBeNull(); + const nodeMajor = Number(nodeVersionMatch![1]); + const nodeMinor = Number(nodeVersionMatch![2] ?? 0); + expect( + nodeMajor > 22 || (nodeMajor === 22 && nodeMinor >= 13), + `${name} must install Node >=22.13 before pnpm/action-setup`, + ).toBe(true); + + const conditionPattern = /^ {6}- if:\s*(.+)$/mu; + expect(nodeBootstrapStep.match(conditionPattern)?.[1] ?? null).toBe( + pnpmSetupStep.match(conditionPattern)?.[1] ?? null, + ); + } + + expect(workflow.match(/^\s+cache: pnpm$/gmu), name).toHaveLength( + expectedCachedSetupNodeSteps, + ); + } + }); + it("gates every provider-secret job with stable actor IDs", async () => { const workflows = await Promise.all( ["runner-full-stack-e2e.yml", "runner-live-evals.yml", "e2e.yml"].map(