I set up Playwright CI integrations frequently. What looks like a 20-minute task ("just add a GitHub Actions workflow") usually takes two to three hours when you account for dependency caching, parallel execution configuration, artifact upload, and the inevitable debugging of the first few CI failures that don't reproduce locally.
This post covers the setup I use for most engagements, with the decisions explained so you understand why, not just what to copy.
The Basic GitHub Actions Workflow
The minimum viable Playwright CI configuration looks like this:
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
This works, but it has two problems: Playwright browser installation is slow (~2 minutes), and it runs all tests sequentially. For a suite of any meaningful size, this is too slow for a PR gate.
Caching Playwright Browsers
Playwright downloads browser binaries on install. These binaries are large and slow to download. Caching them is essential for CI performance.
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- run: npx playwright install --with-deps
if: steps.playwright-cache.outputs.cache-hit != 'true'
- run: npx playwright install-deps
if: steps.playwright-cache.outputs.cache-hit == 'true'
The cache key includes the package-lock.json hash, so the cache invalidates when Playwright is updated. The conditional install handles both cache hit (only install system dependencies, not browsers) and cache miss (install everything).
The install-deps command on cache hit is important and often overlooked. System dependencies (libglib, libnss, etc.) are not cached. If you skip them, Playwright will fail to launch the browser even when the binaries are cached.
Parallel Execution with Sharding
GitHub Actions matrix strategy lets you run test shards in parallel across multiple runners. For a suite of 100 tests, splitting into four shards runs in roughly one-quarter the time.
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- run: npx playwright install --with-deps
if: steps.playwright-cache.outputs.cache-hit != 'true'
- run: npx playwright install-deps
if: steps.playwright-cache.outputs.cache-hit == 'true'
- run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-${{ matrix.shardIndex }}
path: playwright-report/
retention-days: 30
Note fail-fast: false. Without this, GitHub Actions cancels remaining shards when one fails. That means a failure in shard 1 prevents you from seeing failures in shard 3. In most cases, seeing all failures in a CI run is more useful than stopping early.
Handling Flaky Tests in CI
Flaky tests in CI are a tax on developer productivity. Every false positive erodes trust in the suite. Here is how I handle them.
Retries for known intermittent infrastructure: Playwright has a built-in retry mechanism. For tests that interact with external services that have legitimate intermittent failures, retries are appropriate. In playwright.config.ts:
export default defineConfig({
retries: process.env.CI ? 2 : 0,
// ...
});
This retries failed tests twice in CI and not at all locally. The local behavior matters – a test that passes on retry locally but fails consistently in a clean environment is a flakiness problem, not a legitimate retry scenario.
Never retry instead of fix: Retries are a circuit breaker for infrastructure flakiness, not a solution for test design problems. If a test fails consistently enough that retries are the only thing keeping it green, it needs to be fixed or quarantined. Retries that mask systematic flakiness create false confidence.
Quarantine process: When a test becomes reliably flaky and fixing it is not immediately possible, we quarantine it: tag it with @flaky, run it in a separate non-blocking job, and create a ticket to fix it. Quarantined tests are not ignored – they are tracked and scheduled for repair. A test that stays in quarantine for more than two sprints is a test that should probably be deleted.
The PR Gate vs Full Suite Distinction
Not all tests should run on every PR. The PR gate should be: fast (under 10 minutes), reliable (near-zero flakiness), and covering the critical paths that a PR could plausibly break.
In playwright.config.ts, use projects to separate the PR gate subset from the full suite:
projects: [
{
name: 'pr-gate',
testMatch: '**/critical/**/*.spec.ts',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'full-suite',
testMatch: '**/*.spec.ts',
use: { ...devices['Desktop Chrome'] },
},
],
The PR gate runs the pr-gate project. The full suite runs on a schedule against staging. This keeps PR feedback fast without sacrificing coverage.
Want Playwright built into your CI/CD pipeline?
We set up Playwright CI integrations as part of our automation engagements. Tell us what you are working with and we will scope the right setup.
Book a Free CallMerging Shard Reports
When using sharding, you get one report per shard. Playwright's blob reporter and merge-reports command let you combine them into a single HTML report. Add a merge job after the test matrix completes:
merge-reports:
if: always()
needs: [test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- uses: actions/download-artifact@v4
with:
path: all-reports/
pattern: playwright-report-*
- run: npx playwright merge-reports --reporter html ./all-reports
- uses: actions/upload-artifact@v4
with:
name: playwright-merged-report
path: playwright-report/
retention-days: 30
The merged report is the most useful artifact for debugging CI failures. A single report with all test results, grouped by test file, with screenshots and traces for failed tests, is worth the configuration overhead.