diff --git a/.github/workflows/runner-full-stack-e2e.yml b/.github/workflows/runner-full-stack-e2e.yml index 2e8859971d..b9dbcf34ca 100644 --- a/.github/workflows/runner-full-stack-e2e.yml +++ b/.github/workflows/runner-full-stack-e2e.yml @@ -1060,6 +1060,17 @@ jobs: node-version: 24 cache: pnpm + - name: Resolve trusted reporting lockfile without lifecycle scripts + run: | + set -euo pipefail + # Resolve only this trusted checkout's lock, never the target artifact. + test -z "$(git status --porcelain)" + pnpm install --ignore-scripts --no-frozen-lockfile --lockfile-only + test -s pnpm-lock.yaml + git diff --exit-code HEAD -- . ':!pnpm-lock.yaml' + test -z "$(git ls-files --others --exclude-standard)" + sha256sum pnpm-lock.yaml + - run: pnpm install --frozen-lockfile - name: Resolve workflow job attempts @@ -1202,6 +1213,17 @@ jobs: with: version: 9.15.4 + - name: Resolve trusted reporting lockfile without lifecycle scripts + run: | + set -euo pipefail + # Resolve only this trusted checkout's lock, never the target artifact. + test -z "$(git status --porcelain)" + pnpm install --ignore-scripts --no-frozen-lockfile --lockfile-only + test -s pnpm-lock.yaml + git diff --exit-code HEAD -- . ':!pnpm-lock.yaml' + test -z "$(git ls-files --others --exclude-standard)" + sha256sum pnpm-lock.yaml + - run: pnpm install --frozen-lockfile - name: Install publisher-only Chromium diff --git a/tests/runner-e2e/workflow-security.test.ts b/tests/runner-e2e/workflow-security.test.ts index 6f58700018..93844db5eb 100644 --- a/tests/runner-e2e/workflow-security.test.ts +++ b/tests/runner-e2e/workflow-security.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; const repositoryRoot = path.resolve(import.meta.dirname, "../.."); const ordinaryPrTrustedWorkflowRevision = - "03609aa6ecc9a047ed53d6b6469d8be554fbc46d"; + "44dde2dec42a22746a2f36b595acacc9ccfa1df6"; const fullStackTestNeeds = /needs:\s*\[\s*authorize,\s*target_lock,\s*catalog,\s*daytona_image,\s*build_runner_artifacts,\s*build_remote_provider_pack,?\s*\]/u; const buildRunnerNeeds = @@ -77,7 +77,7 @@ describe("public repository paid workflow security", () => { }, { name: "pr-trusted.yml", - expectedCachedSetupNodeSteps: 8, + expectedCachedSetupNodeSteps: 0, }, ]; @@ -99,6 +99,9 @@ describe("public repository paid workflow security", () => { const nodeBootstrapStep = steps[pnpmSetupStepIndex - 1]!; expect(nodeBootstrapStep, name).toContain("uses: actions/setup-node@"); expect(nodeBootstrapStep, name).not.toContain("cache: pnpm"); + if (name === "pr-trusted.yml") { + expect(nodeBootstrapStep, name).toContain("package-manager-cache: false"); + } const nodeVersionMatch = nodeBootstrapStep.match( /^\s*node-version:\s*["']?(\d+)(?:\.(\d+))?/mu, @@ -117,9 +120,15 @@ describe("public repository paid workflow security", () => { ); } - expect(workflow.match(/^\s+cache: pnpm$/gmu), name).toHaveLength( + expect(workflow.match(/^\s+cache: pnpm$/gmu) ?? [], name).toHaveLength( expectedCachedSetupNodeSteps, ); + if (name === "pr-trusted.yml") { + // Upstream #13300/#13302 deliberately made PR stores restore-only: + // seven install jobs reuse caches; the policy resolver has no cache. + expect(workflow.match(/uses: actions\/cache\/restore@caa296126883cff596d87d8935842f9db880ef25/gmu)).toHaveLength(7); + expect(workflow).not.toMatch(/uses: actions\/cache(?:\/save)?@/u); + } } }); diff --git a/tests/runner-e2e/workflow-trusted-lock.test.ts b/tests/runner-e2e/workflow-trusted-lock.test.ts new file mode 100644 index 0000000000..0daa8332a7 --- /dev/null +++ b/tests/runner-e2e/workflow-trusted-lock.test.ts @@ -0,0 +1,68 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const workflow = readFileSync(path.resolve(import.meta.dirname, "../../.github/workflows/runner-full-stack-e2e.yml"), "utf8"); + +for (const [job, next] of [["report", "publish_history"], ["publish_history", "pages"]]) { + describe(`${job} trusted lock resolution`, () => { + const block = workflow.slice(workflow.indexOf(` ${job}:`), workflow.indexOf(` ${next}:`)); + const resolution = block.match(/- name: Resolve trusted reporting lockfile without lifecycle scripts\n run: \|\n((?: .*\n|\n)+)/)?.[1] + .split("\n").map((line) => line.slice(10)).join("\n"); + + it("keeps trusted checkout and resolves its own lock before frozen installation", () => { + expect(block).toContain("ref: ${{ github.sha }}"); + expect(block).not.toContain("needs.target_lock.outputs"); + expect(resolution).toBeTruthy(); + expect(block.indexOf("Resolve trusted reporting lockfile")).toBeLessThan(block.indexOf("- run: pnpm install --frozen-lockfile")); + if (job === "publish_history") { + expect(block.indexOf("- run: pnpm install --frozen-lockfile")).toBeLessThan(block.indexOf("Exchange GitHub OIDC")); + } + }); + + it.each(["stale-lock", "manifest-change", "new-file", "resolve-failed", "empty-lock"])("handles %s without accepting unrelated checkout changes", (scenario) => { + expect(resolution).toBeTruthy(); + const dir = mkdtempSync(path.join(os.tmpdir(), "trusted-report-lock-")); + try { + writeFileSync(path.join(dir, "pnpm-lock.yaml"), "stale\n"); + writeFileSync(path.join(dir, "package.json"), "{}\n"); + execFileSync("git", ["init", "--quiet"], { cwd: dir }); + execFileSync("git", ["add", "."], { cwd: dir }); + execFileSync("git", ["-c", "user.name=Test", "-c", "user.email=test@example.invalid", "commit", "--quiet", "-m", "fixture"], { cwd: dir }); + const bin = path.join(dir, ".git", "test-bin"); + mkdirSync(bin); + writeFileSync(path.join(bin, "pnpm"), `#!/bin/bash +set -eu +if [ "$*" = 'install --ignore-scripts --no-frozen-lockfile --lockfile-only' ]; then + case "$SCENARIO" in + resolve-failed) exit 19 ;; + manifest-change) echo changed > package.json ;; + new-file) echo unexpected > unexpected.txt ;; + empty-lock) : > pnpm-lock.yaml; exit 0 ;; + esac + echo resolved > pnpm-lock.yaml +elif [ "$*" = 'install --frozen-lockfile' ]; then + test "$(cat pnpm-lock.yaml)" = resolved + echo installed > .git/frozen-install-completed +else + exit 20 +fi +`, { mode: 0o700 }); + const result = spawnSync("bash", ["-c", `${resolution}\npnpm install --frozen-lockfile\n`], { + cwd: dir, env: { ...process.env, SCENARIO: scenario, PATH: `${bin}:${process.env.PATH}` }, encoding: "utf8", + }); + if (scenario === "stale-lock") { + expect(result.status, result.stderr).toBe(0); + expect(readFileSync(path.join(dir, ".git/frozen-install-completed"), "utf8")).toBe("installed\n"); + } else { + expect(result.status).not.toBe(0); + expect(() => readFileSync(path.join(dir, ".git/frozen-install-completed"))).toThrow(); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + }); +}