Runnable workflows for Playwright in CI, from single-job to sharded fleets. Adapt paths/commands for other CI providers — the shape is identical.
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} # upload report on failure too
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Notes:
chromium above) — saves minutes per run.if: ${{ !cancelled() }} keeps the report when tests fail; that's when you need it.env: on the test step (E2E_USER: ${{ secrets.E2E_USER }}), never committed.| Approach | Pros | Cons |
|---|---|---|
npx playwright install --with-deps on the runner |
Simple; matches local dev | OS-level rendering drifts with runner image updates — visual baselines can churn |
container: mcr.microsoft.com/playwright:v1.52.0-jammy |
Pinned browser + OS rendering; reproducible visual tests; no install step | Slightly slower job start; must bump tag with @playwright/test |
Always pin the container tag to your exact @playwright/test version — a mismatch produces
"Executable doesn't exist" or subtle behavior skew.
jobs:
test:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.52.0-jammy
steps:
- uses: actions/checkout@v5
- run: npm ci
- run: npx playwright test
env:
HOME: /root # workaround for firefox in containers
- name: Get Playwright version
id: pw-version
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
id: pw-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
- run: npx playwright install --with-deps chromium
if: steps.pw-cache.outputs.cache-hit != 'true'
- run: npx playwright install-deps chromium # OS deps aren't cached
if: steps.pw-cache.outputs.cache-hit == 'true'
Config side — blob on CI shards, html locally:
// playwright.config.ts
reporter: process.env.CI ? 'blob' : 'html',
fullyParallel: true, // shards split per-test instead of per-file -> better balance
jobs:
playwright-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false # let every shard finish; see ALL failures
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: lts/* }
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: blob-report-${{ matrix.shardIndex }}
path: blob-report
retention-days: 1
merge-reports:
if: ${{ !cancelled() }}
needs: [playwright-tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: lts/* }
- run: npm ci
- uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- run: npx playwright merge-reports --reporter html ./all-blob-reports
- uses: actions/upload-artifact@v4
with:
name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report
retention-days: 14
merge-reports accepts multiple reporters: --reporter html,github annotates the PR while also
producing the browsable report.
| Context | Strategy |
|---|---|
| PR validation | fail-fast: false on the matrix + maxFailures: 10 (or --max-failures) per shard. Developers fix everything in one round-trip instead of whack-a-mole |
| Smoke gate before deploy | Fail fast — --grep @smoke, no retries, abort pipeline on first failure |
| Nightly full regression | Full suite, retries on, no fail-fast; route the merged report to the team channel |
| Reporter | Use |
|---|---|
html |
Local + merged CI artifact — the daily driver |
blob |
Shard intermediate; only input for merge-reports |
junit |
Test-management ingestion (Jenkins, Azure DevOps, TestRail): ['junit', { outputFile: 'results.xml' }] |
github |
Inline PR annotations on failures |
list / dot / line |
Console verbosity choices |
Multiple at once:
reporter: process.env.CI
? [['blob'], ['github']]
: [['html', { open: 'on-failure' }]],
webServer: {
command: 'npm run build && npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI, // CI always boots fresh
timeout: 120_000,
stdout: 'pipe', // surface server logs in CI output
},
Playwright waits for url to respond before running tests — no sleep 10 hacks. Multiple
servers (API + frontend) can be given as an array.
forbidOnly: !!process.env.CI — a stray test.only fails the build instead of silently skipping the suiteretries: 2 on CI + trace: 'on-first-retry'workers: 1 per shard on small runners (2-core GitHub runners thrash above that); scale via shardsif: ${{ !cancelled() }}