name: e2e on: pull_request: branches: [main] push: branches: [main] permissions: contents: read # Cancel in-progress runs when a new commit lands on the same PR. concurrency: group: e2e-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: # 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: # Playwright shards, each with its own database and production build, and # a gate job below that carries the exact name branch protection requires. # Two shards grew to 14–19 minutes against the 20-minute cap once the # mobile route sweep landed, and a slow runner tipped them over; three # shards with a 30-minute cap leave room for both. # Every Playwright job runs inside Microsoft's Playwright image, which ships # the browsers with their system libraries: installing them from apt at run # time stalled shards for 29 minutes twice, once for WebKit and once for # Chromium alone. shard: runs-on: ubuntu-24.04 needs: playwright-version timeout-minutes: 30 container: image: mcr.microsoft.com/playwright:v${{ needs.playwright-version.outputs.version }}-noble strategy: fail-fast: false matrix: shard: [1, 2, 3] name: e2e shard ${{ matrix.shard }}/3 services: postgres: image: postgres:18-alpine env: POSTGRES_USER: healthlog POSTGRES_PASSWORD: healthlog POSTGRES_DB: healthlog options: >- --health-cmd "pg_isready -U healthlog" --health-interval 5s --health-timeout 5s --health-retries 10 env: # Inside a job container the service is reached by its name. DATABASE_URL: postgresql://healthlog:healthlog@postgres:5432/healthlog?schema=public ENCRYPTION_KEYS: '{"v1":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' ENCRYPTION_ACTIVE_KEY_ID: v1 API_TOKEN_HMAC_KEY: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb AUTH_RP_NAME: HealthLog AUTH_RP_ID: localhost AUTH_RP_ORIGIN: http://localhost:3000 SESSION_COOKIE_SECURE: "false" NODE_ENV: production # The WebKit specs run in the `webkit` job below. E2E_SKIP_WEBKIT: "1" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # The image runs as root while the checkout belongs to the runner user; # git refuses such a tree, and the prepare hook calls git. - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm db:generate - run: pnpm db:migrate:deploy # `pnpm build` (not bare `next build`): the prebuild hook generates # the versioned service-worker stamp AND the static i18n catalog # assets (public/i18n/.js) the client boots from — without # them every UI interaction waits on missing catalogs. # The repository-wide Next type worker exceeds V8's ~4 GiB default # old-space ceiling. Keep the larger heap scoped to this production # build only; tests and the runtime retain their normal limits. - run: NODE_OPTIONS=--max-old-space-size=8192 pnpm build # Client-bundle budget gate — rides this job because it is the one # place CI already has a production build on disk. Measures the first # load of every page (files deduplicated by content hash) against its # baseline, the p95 and max across pages, and the distinct client code # (modules deduplicated by id); see the script header. Catches a # statically imported message catalog, a duplicated recharts chunk, or # a heavy module (Zod) pulled into a page that had none. - run: pnpm bundle-check - run: pnpm exec playwright test --shard=${{ matrix.shard }}/3 env: E2E_BASE_URL: http://localhost:3000 - name: Upload Playwright HTML report if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: playwright-report-shard-${{ matrix.shard }} path: playwright-report/ retention-days: 7 if-no-files-found: ignore # The machine-readable report, for the skipped-spec check in the gate # job below. Only uploaded when the shard ran to completion, so a missing # artifact there means a shard that did not finish, not an empty suite. - name: Upload Playwright JSON report uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: playwright-json-shard-${{ matrix.shard }} path: playwright-json/results.json retention-days: 7 if-no-files-found: error # The Playwright image must carry the browsers of the exact Playwright the # lockfile resolves, or the run refuses them. Read the version from the # lockfile so a dependency bump moves the image with it. playwright-version: runs-on: ubuntu-24.04 timeout-minutes: 5 outputs: version: ${{ steps.read.outputs.version }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: sparse-checkout: pnpm-lock.yaml sparse-checkout-cone-mode: false - id: read run: | version=$(grep -m1 -E "^ playwright-core@[0-9]" pnpm-lock.yaml | sed -E "s/^ playwright-core@([0-9.]+):.*/\1/") if [ -z "$version" ]; then echo "playwright-core not found in pnpm-lock.yaml"; exit 1; fi echo "version=$version" >> "$GITHUB_OUTPUT" # The WebKit specs, in the same image as the shards: the bottom sheet on an # iPhone (`bottom-sheet.spec.ts`, project `webkit-iphone`) and the app shell # on an iPad (`tablet-shell.spec.ts`, project `webkit-tablet`). webkit: runs-on: ubuntu-24.04 needs: playwright-version timeout-minutes: 30 name: e2e webkit container: image: mcr.microsoft.com/playwright:v${{ needs.playwright-version.outputs.version }}-noble services: postgres: image: postgres:18-alpine env: POSTGRES_USER: healthlog POSTGRES_PASSWORD: healthlog POSTGRES_DB: healthlog options: >- --health-cmd "pg_isready -U healthlog" --health-interval 5s --health-timeout 5s --health-retries 10 env: # Inside a job container the service is reached by its name. DATABASE_URL: postgresql://healthlog:healthlog@postgres:5432/healthlog?schema=public ENCRYPTION_KEYS: '{"v1":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' ENCRYPTION_ACTIVE_KEY_ID: v1 API_TOKEN_HMAC_KEY: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb AUTH_RP_NAME: HealthLog AUTH_RP_ID: localhost AUTH_RP_ORIGIN: http://localhost:3000 SESSION_COOKIE_SECURE: "false" NODE_ENV: production steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # The image runs as root while the checkout belongs to the runner user; # git refuses such a tree, and the prepare hook calls git. - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1 - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm db:generate - run: pnpm db:migrate:deploy - run: NODE_OPTIONS=--max-old-space-size=8192 pnpm build - run: pnpm exec playwright test --project=webkit-iphone --project=webkit-tablet env: E2E_BASE_URL: http://localhost:3000 - name: Upload Playwright HTML report if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: playwright-report-webkit path: playwright-report/ retention-days: 7 if-no-files-found: ignore - name: Upload Playwright JSON report uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: playwright-json-shard-webkit path: playwright-json/results.json retention-days: 7 if-no-files-found: error # Branch protection requires a check named exactly "e2e". This job carries # that name and answers for every shard and the WebKit job: it fails when # any of them failed or was cancelled, so a half-green run cannot satisfy # the gate. e2e: runs-on: ubuntu-24.04 needs: [shard, webkit] if: always() timeout-minutes: 5 steps: - name: Every shard finished green run: | if [ "${{ needs.shard.result }}" != "success" ] || [ "${{ needs.webkit.result }}" != "success" ]; then echo "shard result: ${{ needs.shard.result }}, webkit result: ${{ needs.webkit.result }}" exit 1 fi # A spec file that skipped every one of its tests reports green. The # check aggregates both shards' reports, because Playwright splits a # file's tests across shards; see the script's header for the allowlist. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: sparse-checkout: scripts - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 22 - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: pattern: playwright-json-shard-* path: playwright-json - name: No spec file skipped all of its tests run: node scripts/check-e2e-skipped-specs.mjs playwright-json/*/results.json