The baseline workflow

Start with one workflow that runs on pull requests and on pushes to main. The version below already includes the three decisions most teams retrofit later: matrix sharding, blob-report merging, and artifact upload scoped to failures. We’ll unpack each in the following sections.

Code example
name: e2e
on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium

      - name: Run tests (shard)
        run: npx playwright test --shard=${{ matrix.shard }}/4 --reporter=blob

      - name: Upload report
        uses: actions/upload-artifact@v4
        if: ${{ always() }}
        with:
          name: blob-report-${{ matrix.shard }}
          path: blob-report/
          retention-days: 7

  merge-reports:
    if: ${{ always() }}
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - uses: actions/download-artifact@v4
        with:
          path: all-reports
          pattern: blob-report-*
          merge-multiple: true
      - run: npx playwright merge-reports --reporter=html ./all-reports
      - uses: actions/upload-artifact@v4
        if: ${{ always() }}
        with:
          name: html-report
          path: playwright-report/
          retention-days: 7

Note fail-fast: false: with it unset, one flaky shard cancels the others mid-run and you lose half your evidence. The tradeoff is paying for full execution even after an early failure — worth it, because parallel evidence from the other shards often contains the trace that explains the first one.

CHANGE

GitHub Actions run

Start with the paths affected by the change.

scope the run
CHECKS

Signup · checkout · access

Each scenario reports its own assertion and captured route.

one blocked
DECISION

Ready or blocked

A reviewer sees why the release can proceed or what needs attention.

reviewable result
A release gate narrows the decisionRun the checks that match the change, attach the failed route, and leave the merge decision reviewable.

Cache what’s slow

Two things dominate cold-start time: dependency installation and browser binaries. The cache: npm input above handles dependencies by hashing the lockfile. Browsers need explicit handling — browser binaries and system dependencies can make cold starts expensive:

Code example
- uses: actions/cache@v4
  with:
    path: ~/.cache/ms-playwright
    key: pw-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

Keying on the lockfile works because Playwright versions are pinned there; bump the version, the key changes, the cache rebuilds once, and every subsequent run restores instantly. Keep npx playwright install --with-deps chromium in the workflow regardless — cache hits make it near-instant, misses make it correct. Only install browsers you actually use; installing all three engines triples download time for zero benefit.

Split independent tests across CI jobs

Sharding splits the suite across matrix jobs. Pick the target wall-clock time first, then derive shard count: a serial suite split across four shards can still be limited by balance and setup overhead. Playwright balances shards by test count; if durations vary wildly, sort specs into size buckets or split known-slow files so no single shard dominates. Watch for two scaling ceilings: shared backend contention (parallel shards hammering one database turn into a different kind of red), and per-shard fixed costs for checkout, install, and browser boot can dominate once shards are small.

If sharding exposes order dependence — tests failing only under some shard arrangements — fix the dependence, not the arrangement. That class of bug is covered in the isolation section of our E2E practices guide.

Traces and artifacts on failure

Configure capture in playwright.config.ts so every CI failure arrives debuggable:

Code example
export default defineConfig({
  retries: process.env.CI ? 1 : 0,
  use: {
    trace: 'retain-on-failure',
    video: 'retain-on-failure',
    screenshot: 'only-on-failure',
  },
});

A retry can reveal an intermittent failure, but a passing retry still needs to be reported. Open a captured trace with npx playwright show-trace trace.zip, or retain it as a CI artifact. Set retention to match your debugging needs. The Molar Trace comparison explains how to inspect the captured data available from each type of run.

Make the check required — carefully

A green-required gate starts with naming. Branch protection references check names exactly, and a matrix job produces one check per shard named like e2e / test (1), (2), and so on. Require either each shard explicitly or add a final aggregate job whose success means “all shards passed” — the latter survives future shard-count changes without editing protection rules. The exact configuration steps are in our required status checks guide, and the GitHub Actions integration page documents the configured Guard path for reporting verdicts as native check runs.

As more code is generated, machine-verified gates carry proportionally more of the safety burden — a gate that can be trained-around is a liability at exactly the moment usage patterns are drifting that way.

Concurrency saves real money on busy repos. Add concurrency: { group: e2e-${{ github.ref }}, cancel-in-progress: true } so new pushes cancel superseded runs instead of queueing behind them.

Keep the gate honest as the suite grows

CI quality decays through retries, not failures. Cap retries at one in CI, review which tests consume them weekly, and quarantine repeat offenders out of the blocking set with an owner and expiry — the full policy lives in our flaky-tests guide. Run the complete suite nightly rather than forcing every edge case onto PRs; a fast required smoke on every PR plus a thorough scheduled run catches more regressions per minute of engineer attention than a monolithic blocking suite ever will. This tiered shape is the core of continuous testing in the CI/CD pipeline.

The end state to aim for: every pull request gets sub-10-minute E2E feedback it can trust, every failure arrives with enough evidence to diagnose without a local reproduction, and the merge gate stays green for exactly one reason — the app actually works.

Reference: Playwright’s CI sharding and report-merging guide.