diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e2f0601c3..6bc757ed4 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -15,9 +15,19 @@ concurrency: cancel-in-progress: true jobs: - e2e: + # The suite outgrew the runner the same way the integration suite did: the + # single job passed at ~18 minutes against a 20-minute cap, and the settings + # geometry measurements pushed it over. Same remedy, split rather than raise: + # two Playwright shards, each with its own database and production build, and + # a gate job below that carries the exact name branch protection requires. + shard: runs-on: ubuntu-latest timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + shard: [1, 2] + name: e2e shard ${{ matrix.shard }}/2 services: postgres: image: postgres:16-alpine @@ -87,7 +97,7 @@ jobs: # chart-runtime boundary (a duplicated ~92 KB gz recharts chunk). - run: pnpm bundle-check - - run: pnpm exec playwright test + - run: pnpm exec playwright test --shard=${{ matrix.shard }}/2 env: E2E_BASE_URL: http://localhost:3000 @@ -95,7 +105,23 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: playwright-report + name: playwright-report-shard-${{ matrix.shard }} path: playwright-report/ retention-days: 7 if-no-files-found: ignore + + # Branch protection requires a check named exactly "e2e". This job carries + # that name and answers for both shards: it fails when any shard failed or + # was cancelled, so a half-green matrix cannot satisfy the gate. + e2e: + runs-on: ubuntu-latest + needs: shard + if: always() + timeout-minutes: 5 + steps: + - name: Every shard finished green + run: | + if [ "${{ needs.shard.result }}" != "success" ]; then + echo "shard result: ${{ needs.shard.result }}" + exit 1 + fi diff --git a/scripts/__tests__/ci-determinism.test.ts b/scripts/__tests__/ci-determinism.test.ts index 1a8cfac66..34bac3b29 100644 --- a/scripts/__tests__/ci-determinism.test.ts +++ b/scripts/__tests__/ci-determinism.test.ts @@ -77,14 +77,19 @@ describe("Playwright CI determinism", () => { const workflow = parse( readRepoFile(".github/workflows/e2e.yml"), ) as Workflow; - const upload = workflow.jobs.e2e?.steps?.find((step) => + // The suite runs as a shard matrix since the single job outgrew its + // timeout; the running job is `shard`, while `e2e` is the gate that + // carries the required check name and runs no browser. The report + // therefore lives on the shard job, named per shard so the two + // artifacts cannot clobber each other. + const upload = workflow.jobs.shard?.steps?.find((step) => step.uses?.startsWith("actions/upload-artifact@"), ); expect(upload).toBeDefined(); expect(upload?.if).toBe("always()"); expect(upload?.with).toMatchObject({ - name: "playwright-report", + name: "playwright-report-shard-${{ matrix.shard }}", path: "playwright-report/", "retention-days": 7, "if-no-files-found": "ignore", @@ -95,7 +100,7 @@ describe("Playwright CI determinism", () => { const workflow = parse( readRepoFile(".github/workflows/e2e.yml"), ) as Workflow; - const env = workflow.jobs.e2e?.env; + const env = workflow.jobs.shard?.env; expect(env?.API_TOKEN_HMAC_KEY).toMatch(/^[0-9a-f]{64}$/); expect(env?.SESSION_COOKIE_SECURE).toBe("false");