Quick answer
Add a workflow that installs Playwright, runs the suite in a shard matrix, uploads blob reports, then merges them into one HTML report in a final job. Add the github reporter for inline PR annotations.
1. The minimal workflow
.github/workflows/e2e.yml
yaml
name: E2E
on:
pull_request:
push:
branches: [main]
jobs:
e2e:
timeout-minutes: 20
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 chromium
- run: npx playwright test
env:
BASE_URL: ${{ secrets.STAGING_URL }}
USER_EMAIL: ${{ secrets.USER_EMAIL }}
USER_PWD: ${{ secrets.USER_PWD }}
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 73. Merge the shards into one report
merge-reports:
if: ${{ !cancelled() }}
needs: e2e
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-blobs, pattern: blob-report-* }
- run: npx playwright merge-reports --reporter=html ./all-blobs
- uses: actions/upload-artifact@v4
with:
name: html-report
path: playwright-report4. Cache Playwright browsers
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('package-lock.json') }}
- name: Install browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- name: Install OS deps only
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: npx playwright install-deps chromium5. Secrets & environment strategy
- Store secrets in Repo → Settings → Secrets and variables → Actions. Reference with
${{ secrets.NAME }}. - Split per environment:
STAGING_URL,PROD_URL, mapped to workflow inputs. - Never
echoa secret — GitHub redacts but logs at other layers can leak. - Rotate tokens quarterly. Use a service account, not a real user.
6. PR annotations at failing lines
playwright.config.ts
ts
reporter: process.env.CI
? [['github'], ['html', { open: 'never' }], ['blob']]
: [['list'], ['html', { open: 'on-failure' }]],You now have production CI
Push a PR that breaks a test. You should see: (1) a red status check, (2) an inline annotation at the failing
expect(...) line, (3) a downloadable HTML report artifact with trace, screenshot and video.7. Hands-on task (35 minutes)
- 1
Add the basic workflow
Copy the minimal YAML into
.github/workflows/e2e.yml. Push a PR — confirm the suite runs. - 2
Shard into 4 jobs
Add the matrix + blob reporter + merge job. Watch runtime drop by ~4×.
- 3
Wire secrets
Move your staging URL and credentials into GitHub Secrets. Delete them from the config file.
Finally, sharpen your test-design instincts: Module 10 — Test Design Techniques.
Frequently asked questions
1.How do I run Playwright in GitHub Actions?
Use the official Playwright Docker image or `npx playwright install --with-deps` on ubuntu-latest. Run `npx playwright test` and upload the HTML report as an artifact on failure.
2.How do I shard a Playwright suite in CI?
Use a matrix strategy (`shard: [1/4, 2/4, 3/4, 4/4]`) and pass `--shard=${shard}` to Playwright. Use the `blob` reporter on each shard and merge with `npx playwright merge-reports` in a final job.
3.Should I cache node_modules and browsers?
Cache `~/.npm` and Playwright browsers at `~/.cache/ms-playwright`. Skip the install step when the cache hits. Trims 40–60 seconds per run.
4.How do I store secrets like API tokens?
GitHub → Repo Settings → Secrets and variables → Actions. Reference in a step as `env: { API_TOKEN: ${{ secrets.API_TOKEN }} }`. Never echo secrets to logs.
5.How do I comment test results on the PR?
Add the `github` reporter — Playwright will post annotations at failing line numbers. For a summary comment, upload the HTML report to GitHub Pages or use a marketplace action like `daun/playwright-report-summary`.
6.What about flaky test retries?
Set `retries: 2` only when `process.env.CI` is truthy. Never retry locally — that hides real bugs from developers.