diff --git a/.github/workflows/runner-full-stack-e2e.yml b/.github/workflows/runner-full-stack-e2e.yml index fb341726a5..9c45df4479 100644 --- a/.github/workflows/runner-full-stack-e2e.yml +++ b/.github/workflows/runner-full-stack-e2e.yml @@ -60,6 +60,7 @@ jobs: test_runner: ${{ steps.runner.outputs.runner }} max_parallel_default: ${{ steps.runner.outputs.max_parallel_default }} max_parallel_limit: ${{ steps.runner.outputs.max_parallel_limit }} + playwright_channel: ${{ steps.runner.outputs.playwright_channel }} target_sha: ${{ steps.target.outputs.sha }} target_ref: ${{ steps.target.outputs.ref }} steps: @@ -132,6 +133,7 @@ jobs: echo "runner=$aws_runner" echo "max_parallel_default=100" echo "max_parallel_limit=100" + echo "playwright_channel=chrome" } >> "$GITHUB_OUTPUT" echo '::notice title=Paid runner routing::Using an ephemeral RunsOn Fleet runner' else @@ -139,6 +141,7 @@ jobs: echo "runner=$github_runner" echo "max_parallel_default=32" echo "max_parallel_limit=57" + echo "playwright_channel=" } >> "$GITHUB_OUTPUT" echo '::notice title=Paid runner routing::RUNNER_E2E_AWS_ENABLED is not true; using the proven GitHub-hosted runner' fi @@ -841,6 +844,7 @@ jobs: run: node packages/paperclip-runner/scripts/materialize-opencode-binary.mjs - name: Download immutable campaign outputs + if: startsWith(matrix.profileId, 'runner-') || matrix.suiteId == 'openrouter-model-breadth' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: ${{ needs.build_runner_artifacts.outputs.build_artifact_name }} @@ -854,6 +858,7 @@ jobs: path: runner-e2e-provider-pack - name: Verify and restore campaign outputs + if: startsWith(matrix.profileId, 'runner-') || matrix.suiteId == 'openrouter-model-breadth' env: NEEDS_RUNNER_TYPESCRIPT: ${{ matrix.profileId == 'runner-opencode' || startsWith(matrix.profileId, 'runner-acpx-') || matrix.suiteId == 'openrouter-model-breadth' }} NEEDS_NATIVE_BINARY: ${{ startsWith(matrix.profileId, 'runner-') || matrix.suiteId == 'openrouter-model-breadth' }} @@ -910,7 +915,16 @@ jobs: if: matrix.profileId == 'legacy-claude' run: npm install --global --omit=dev @anthropic-ai/claude-code@2.1.19 - - name: Install Chromium headless shell + - name: Qualify preinstalled Chrome + if: needs.authorize.outputs.playwright_channel == 'chrome' + run: | + set -euo pipefail + chrome_path="$(command -v google-chrome)" + test -x "$chrome_path" + google-chrome --version + + - name: Install Chromium headless shell on GitHub-hosted fallback + if: needs.authorize.outputs.playwright_channel != 'chrome' run: | set -euo pipefail for attempt in 1 2 3; do @@ -935,6 +949,7 @@ jobs: PAPERCLIP_E2E_CAMPAIGN_ID: gha-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.executionId }} PAPERCLIP_RUNNER_E2E_SOURCE_SHA: ${{ needs.authorize.outputs.target_sha }} PAPERCLIP_RUNNER_E2E_SOURCE_REF: ${{ needs.authorize.outputs.target_ref }} + PAPERCLIP_PLAYWRIGHT_CHANNEL: ${{ needs.authorize.outputs.playwright_channel }} run: pnpm test:e2e:runner -- --id "${{ matrix.executionId }}" - name: Upload access-controlled packaged cell evidence diff --git a/tests/runner-e2e/playwright.config.ts b/tests/runner-e2e/playwright.config.ts index 8bc57d0cb2..4a42e7a881 100644 --- a/tests/runner-e2e/playwright.config.ts +++ b/tests/runner-e2e/playwright.config.ts @@ -14,8 +14,14 @@ const privateDir = required("PAPERCLIP_RUNNER_E2E_PRIVATE_DIR"); const paperclipHome = required("PAPERCLIP_HOME"); const configPath = required("PAPERCLIP_CONFIG"); const baseURL = `http://127.0.0.1:${port}`; +const playwrightChannel = process.env.PAPERCLIP_PLAYWRIGHT_CHANNEL?.trim(); const chromiumExecutable = process.env.PAPERCLIP_RUNNER_E2E_CHROMIUM_EXECUTABLE?.trim(); +if (playwrightChannel && chromiumExecutable) { + throw new Error( + "PAPERCLIP_PLAYWRIGHT_CHANNEL and PAPERCLIP_RUNNER_E2E_CHROMIUM_EXECUTABLE are mutually exclusive", + ); +} if (chromiumExecutable && !path.isAbsolute(chromiumExecutable)) { throw new Error( "PAPERCLIP_RUNNER_E2E_CHROMIUM_EXECUTABLE must be an absolute path", @@ -45,6 +51,7 @@ export default defineConfig({ use: { baseURL, browserName: "chromium", + ...(playwrightChannel ? { channel: playwrightChannel } : {}), ...(chromiumExecutable ? { launchOptions: { executablePath: chromiumExecutable } } : {}), diff --git a/tests/runner-e2e/workflow-security.test.ts b/tests/runner-e2e/workflow-security.test.ts index 2ee1ca60e2..ee46454dd1 100644 --- a/tests/runner-e2e/workflow-security.test.ts +++ b/tests/runner-e2e/workflow-security.test.ts @@ -197,6 +197,11 @@ describe("public repository paid workflow security", () => { expect(authorizeJob).toContain( "AWS_PAID_RUNNER_ENABLED: ${{ vars.RUNNER_E2E_AWS_ENABLED }}", ); + expect(authorizeJob).toContain( + "playwright_channel: ${{ steps.runner.outputs.playwright_channel }}", + ); + expect(authorizeJob).toContain('echo "playwright_channel=chrome"'); + expect(authorizeJob).toContain('echo "playwright_channel="'); expect(authorizeJob).toContain( "Resolve requested repository branch to an immutable commit", ); @@ -241,6 +246,17 @@ describe("public repository paid workflow security", () => { expect(paidJob).toContain( "pnpm exec playwright install --with-deps --only-shell chromium", ); + expect(paidJob).toContain("name: Qualify preinstalled Chrome"); + expect(paidJob).toContain( + "if: needs.authorize.outputs.playwright_channel == 'chrome'", + ); + expect(paidJob).toContain("google-chrome --version"); + expect(paidJob).toContain( + "if: needs.authorize.outputs.playwright_channel != 'chrome'", + ); + expect(paidJob).toContain( + "PAPERCLIP_PLAYWRIGHT_CHANNEL: ${{ needs.authorize.outputs.playwright_channel }}", + ); expect(paidJob).not.toContain( "pnpm exec playwright install --with-deps chromium", ); @@ -519,6 +535,11 @@ describe("public repository paid workflow security", () => { expect(testJob).toMatch(fullStackTestNeeds); expect(testJob).toContain("Download immutable campaign outputs"); + expect( + testJob.match( + /if: startsWith\(matrix\.profileId, 'runner-'\) \|\| matrix\.suiteId == 'openrouter-model-breadth'/gu, + ), + ).toHaveLength(2); expect(testJob).toContain("Download immutable remote provider pack"); expect(testJob).toContain( "needs.build_runner_artifacts.outputs.build_artifact_name", @@ -551,6 +572,24 @@ describe("public repository paid workflow security", () => { expect(testJob).not.toContain("build-provider-pack.mjs"); }); + it("uses the reviewed AWS Chrome channel without weakening the local executable override", async () => { + const config = await readFile( + path.join(repositoryRoot, "tests/runner-e2e/playwright.config.ts"), + "utf8", + ); + + expect(config).toContain( + "process.env.PAPERCLIP_PLAYWRIGHT_CHANNEL?.trim()", + ); + expect(config).toContain("{ channel: playwrightChannel }"); + expect(config).toContain( + "process.env.PAPERCLIP_RUNNER_E2E_CHROMIUM_EXECUTABLE?.trim()", + ); + expect(config).toContain( + "PAPERCLIP_PLAYWRIGHT_CHANNEL and PAPERCLIP_RUNNER_E2E_CHROMIUM_EXECUTABLE are mutually exclusive", + ); + }); + it("binds rerun evidence and Pages artifacts to the exact workflow attempt", async () => { const workflow = await readFile( path.join(repositoryRoot, ".github/workflows/runner-full-stack-e2e.yml"),