Running Playwright Tests in GitHub Actions with Sharding
A suite that takes twelve minutes on one runner takes about four when split across three runners that work at the same time, because the merge is gated by the slowest job, not the sum of all jobs. GitHub Actions makes this split a matrix of identical jobs, and Playwright slices the test set with --shard=index/total. The complication is the report: three jobs produce three partial results, so without a merge step you get three inconclusive runs instead of one verdict. The blob reporter solves it — each shard writes a machine-readable blob, a final job downloads them all and runs merge-reports to fold them into a single HTML report and a single exit code. This page builds the full workflow step by step.
Why a plain matrix is not enough
A GitHub Actions matrix already runs jobs in parallel, so it is tempting to think the only missing piece is passing a shard index. The problem is the result. Each shard exits with its own status and writes its own report, and the pull request check then shows three separate green-or-red marks with no combined HTML report to open. Worse, the standard list or HTML reporter is not designed to be merged after the fact. The blob reporter exists for exactly this: it serializes the full result of a shard — every test, attachment, and trace pointer — into a format that merge-reports can recombine losslessly into the report you would have gotten from a single run.
Configure the blob reporter
Switch the reporter to blob in CI while keeping a readable reporter locally. The blob reporter writes to blob-report/ by default and embeds the shard index in the file name so merging never collides.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
// 'blob' in CI is mergeable; 'list' locally stays human-readable.
reporter: process.env.CI ? 'blob' : 'list',
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
});
This config is the same one described in Playwright Config & Fixtures; the only CI-specific addition is the conditional reporter.
The workflow file
The workflow below defines two jobs. The test job is a matrix over shard values that each run one slice and upload a blob artifact. The merge job depends on test, downloads every blob, and produces the combined report. This is the one allowed non-TypeScript code block.
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
# Keep all shards running even if one fails, so you see the full picture.
fail-fast: false
matrix:
shard: [1, 2, 3]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
# Cache browsers keyed on the resolved Playwright version.
- name: Get Playwright version
id: pw
run: echo "version=$(npm ls @playwright/test --depth=0 --json | npx --yes json @playwright/test.version)" >> "$GITHUB_OUTPUT"
- name: Cache browsers
id: cache
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: pw-${{ runner.os }}-${{ steps.pw.outputs.version }}
- name: Install browsers
if: steps.cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps
- name: Install OS deps on cache hit
if: steps.cache.outputs.cache-hit == 'true'
run: npx playwright install-deps
# The shard flag slices the suite: index/total.
- name: Run shard ${{ matrix.shard }}
run: npx playwright test --shard=${{ matrix.shard }}/${{ strategy.job-total }}
# Each shard uploads its blob under a unique name.
- name: Upload blob report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shard }}
path: blob-report/
retention-days: 7
merge:
if: ${{ !cancelled() }}
needs: [test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
# Pull every shard's blob into one folder.
- name: Download blob reports
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
# Fold the blobs into one HTML report and a single exit code.
- name: Merge into HTML report
run: npx playwright merge-reports --reporter=html ./all-blob-reports
- name: Upload merged report
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 14
Step-by-step fix
- Switch the reporter to blob in CI. Set
reporter: process.env.CI ? 'blob' : 'list'so each shard writes a mergeable artifact while local runs stay readable. - Define the shard matrix. Add
strategy.matrix.shard: [1, 2, 3]withfail-fast: falseso every shard runs to completion and reports its own failures. - Cache browsers by Playwright version. Restore
~/.cache/ms-playwrightkeyed on the resolved version, install browsers only on a cache miss, and install just the OS deps on a hit. - Pass the shard flag. Run
npx playwright test --shard=${{ matrix.shard }}/${{ strategy.job-total }}so each job executes exactly one slice of the suite. - Upload each blob with a unique name. Use
actions/upload-artifactwithname: blob-report-${{ matrix.shard }}andif: ${{ !cancelled() }}so reports survive failures. - Add a dependent merge job. Declare
needs: [test], download everyblob-report-*artifact withmerge-multiple: true, and runnpx playwright merge-reports --reporter=htmlto produce one report and one verdict.
Troubleshooting variants
Shards pass individually but the merge job is skipped
The merge job only runs when its dependency completes, and a failed shard with default settings can short-circuit the matrix. Set fail-fast: false on the matrix and if: ${{ !cancelled() }} on the merge job so the report is built even when a shard fails — a red report you can open beats no report at all.
Tests are unevenly distributed across shards
Playwright assigns whole test files to shards, so a single very large file lands entirely on one shard and skews timing. Split oversized spec files, or move long-running scenarios into their own files so the runner can balance them. Sharding distributes files, not individual tests within a file.
Traces are missing from the merged report
The blob artifact must include the attachments, which means trace has to be enabled in the config and the blob upload must point at the whole blob-report/ directory. Confirm trace: 'on-first-retry' is set and inspect failures in the Playwright Trace Viewer from the merged report.
Verification
Open a pull request and confirm three things. First, the Actions run shows three parallel test jobs followed by one merge job, and the merged wall-clock time is close to a third of a single run. Second, download the playwright-report artifact and open index.html: it lists every test from all shards in one place, with traces attached to failures. Third, force a failure in one shard and confirm the merge job still produces a report marked red — proving the gate reports a single, correct verdict. The broader pipeline context lives in the CI/CD Integration guide, under Playwright Setup & Core Architecture.
Frequently Asked Questions
How do I choose the number of shards?
Pick a shard count that brings the slowest job under your tolerance for feedback time, then stop adding shards once per-job overhead — checkout, install, browser restore — eats the time you save. For most suites three to four shards is the sweet spot; beyond that the fixed startup cost of each runner dominates.
What does merge-reports actually do?
It reads the blob artifacts from every shard and recombines them losslessly into a single report in the format you request, such as HTML, with one overall exit code. It is the only step where the parallel jobs become one pass-or-fail result, so the pull request check reflects the whole suite rather than three separate slices.
Do I need the blob reporter, or can I merge HTML reports?
You need the blob reporter. The HTML reporter is a final rendering and is not designed to be recombined, whereas the blob reporter serializes the full structured result specifically so merge-reports can fold the shards together without losing tests, attachments, or traces.