Skip to main content

Running Playwright E2E Tests in CI/CD: GitHub Actions Setup

Running Playwright tests on your local machine is useful, but the real power comes from running them automatically on every push and pull request via CI/CD. GitHub Actions (free for public repos) makes this simple: every commit triggers your test suite, and PRs can't be merged without passing. This article covers GitHub Actions configuration, parallel test execution, artifact collection, and troubleshooting test failures in CI.

Why Run Playwright Tests in CI?

Running tests in CI ensures that: every code change is tested automatically (no human can forget to run tests before pushing), tests run in a consistent environment (same OS, browser versions, network conditions), and bugs are caught before code reaches production. Without CI, developers might ship untested code; with CI, tests are a non-negotiable gate.

Setting Up a Basic GitHub Actions Workflow

Create .github/workflows/playwright.yml:

name: Playwright Tests

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
# Check out the code
- uses: actions/checkout@v4

# Set up Node.js
- uses: actions/setup-node@v4
with:
node-version: 18

# Install dependencies
- run: npm install

# Install Playwright browsers
- run: npx playwright install --with-deps

# Run Playwright tests
- run: npm run test:e2e

# Upload test report (even if tests fail)
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

This workflow runs on every push to main or develop and every PR to main. It installs dependencies, downloads browser binaries, runs tests, and uploads the HTML report as an artifact.

Running Tests in Parallel Across Multiple Workers

By default, Playwright uses all available CPU cores. For GitHub Actions, increase parallelism by creating multiple job instances:

name: Playwright Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

strategy:
# Create 4 separate job instances, each running a shard of tests
matrix:
shard: [1, 2, 3, 4]

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18

- run: npm install
- run: npx playwright install --with-deps

# Run a shard of the tests
- run: npm run test:e2e -- --shard=${{ matrix.shard }}/4

# Upload reports from each shard
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report-${{ matrix.shard }}
path: playwright-report/

This creates 4 parallel jobs, each running 1/4 of your tests. For a suite that takes 60 seconds sequentially, parallel sharding reduces CI time to ~15 seconds.

Handling Test Retries and Flaky Tests

Some tests are flaky (occasionally fail due to timing, network issues). Configure retries:

- run: npm run test:e2e -- --retries=2

Or in playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
retries: process.env.CI ? 2 : 0, // retry twice in CI, no retries locally
workers: process.env.CI ? 1 : undefined, // single worker in CI for stability

// ... rest of config
});

Retries help with flakiness, but don't mask real bugs. If a test fails 3+ times in a row, investigate the root cause instead of increasing retries.

Configuring Playwright for CI

Update your playwright.config.ts for CI:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './tests',

// In CI, run slower, with fewer workers
workers: process.env.CI ? 1 : undefined,

// Set a reasonable timeout for CI (longer than local)
timeout: process.env.CI ? 60000 : 30000,

// Retries in CI, not locally
retries: process.env.CI ? 2 : 0,

// Always record videos of failures in CI for debugging
recordVideo: process.env.CI ? 'retain-on-failure' : 'off',

// Trace for debugging (larger file, save on failure)
trace: process.env.CI ? 'on-first-retry' : 'off',

reporter: 'html',

use: {
baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
},

// Start dev server if running tests
webServer: process.env.CI
? undefined // In CI, assume server is already running
: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: true,
},

projects: [
{ name: 'chromium', use: { ...devices.chromium } },
{ name: 'firefox', use: { ...devices.firefox } },
{ name: 'webkit', use: { ...devices.webkit } },
],
});

Starting the Dev Server in CI

If your tests need a running dev server, start it in CI:

name: Playwright Tests

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18

- run: npm install
- run: npx playwright install --with-deps

# Start the dev server in the background
- run: npm run dev &

# Wait for the server to be ready
- run: npx wait-on http://localhost:3000 --timeout 30000

# Run tests
- run: npm run test:e2e

# Upload reports
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/

Or use a dedicated action to start the server:

- uses: BertalanD/desktop-headless-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
port: 3000
build-script: npm run build
run-script: npm run preview

Collecting and Reporting Test Results

Upload test reports and artifacts for easy debugging:

# Upload HTML report as artifact
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

# Upload videos of failed tests
- uses: actions/upload-artifact@v3
if: always()
with:
name: test-videos
path: test-results/
retention-days: 7

# Publish test results as GitHub check
- uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: test-results.xml
check_name: Playwright Test Results

In the GitHub Actions UI, you can see the report as an artifact. Click "playwright-report" to download and open the interactive HTML report.

Conditional Test Runs

Only run E2E tests when relevant code changes:

on:
push:
branches: [main]
paths:
- 'src/**' # Run on source changes
- 'tests/**' # Run on test changes
- '.github/workflows/playwright.yml' # Run on workflow changes
- '!**.md' # Don't run on README changes

jobs:
test:
# ... rest of config

This prevents unnecessary test runs on documentation updates.

Handling Environment Variables and Secrets

Store sensitive data (API keys, test credentials) in GitHub Secrets:

# In GitHub Settings → Secrets, create PLAYWRIGHT_TEST_API_KEY

env:
PLAYWRIGHT_TEST_API_KEY: ${{ secrets.PLAYWRIGHT_TEST_API_KEY }}
PLAYWRIGHT_TEST_BASE_URL: https://staging.example.com

jobs:
test:
runs-on: ubuntu-latest
steps:
# ...
- run: npm run test:e2e

In your tests, access environment variables:

test('api request with auth', async ({ page }) => {
const apiKey = process.env.PLAYWRIGHT_TEST_API_KEY;

await page.route('**/api/**', route => {
route.fulfill({
status: 200,
headers: { 'Authorization': `Bearer ${apiKey}` },
body: JSON.stringify({ success: true }),
});
});
});

Testing Against Multiple Node Versions

Test compatibility across Node versions:

strategy:
matrix:
node-version: [18, 20, 22]

steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

# ... rest of steps

This creates separate jobs for each Node version, ensuring your tests pass on all supported versions.

Failure Notification and Debugging

Receive notifications when tests fail:

# Send Slack notification on failure
- uses: slackapi/slack-github-action@v1
if: failure()
with:
payload: |
{
"text": "Playwright tests failed on ${{ github.ref }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Playwright tests failed\n*Repo:* ${{ github.repository }}\n*Branch:* ${{ github.ref }}\n*Commit:* ${{ github.sha }}"
}
}
]
}

Or post a comment on the PR:

- uses: actions/github-script@v7
if: failure()
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Playwright tests failed. Check the artifacts for details.'
})

Optimizing CI Performance

Tips for faster CI:

  1. Cache dependencies: Store node_modules between runs
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
  1. Shard tests across multiple jobs (covered above)

  2. Only test changed code:

- run: npm run test:e2e -- --testPathPattern=$(git diff --name-only HEAD~1 | grep -oP 'tests/\K[^/]+' | sort -u | paste -sd '|')
  1. Use smaller test dataset (mock APIs, use test fixtures)

  2. Parallelize across OS: Test on Windows, macOS, Linux simultaneously

Key Takeaways

  • GitHub Actions runs Playwright tests automatically on every push and PR, preventing untested code from reaching production.
  • Shard tests across multiple jobs to reduce CI time: a 60-second suite becomes 15 seconds with 4 shards.
  • Upload HTML reports and videos as artifacts for easy debugging.
  • Configure timeouts and retries differently for CI vs. local (CI should be more lenient).
  • Store secrets in GitHub Secrets and pass them as environment variables.

Frequently Asked Questions

How long should Playwright tests take in CI?

A typical suite of 50 tests takes 20–60 seconds with 4 parallel shards. If slower, investigate: are tests waiting for slow APIs? Can you mock them? Are tests running sequentially instead of in parallel?

Can I run Playwright tests in Docker?

Yes, use the Playwright Docker image: mcr.microsoft.com/playwright:v1.48.2-focal. This ensures consistent browser rendering across environments.

How do I debug a test that passes locally but fails in CI?

Use video recordings and traces:

export default defineConfig({
recordVideo: process.env.CI ? 'retain-on-failure' : 'off',
trace: process.env.CI ? 'on-first-retry' : 'off',
});

Download the video/trace from the artifact and play it locally.

Should I run tests on every branch or only main?

Run on all branches. Catch bugs early before PRs are merged. Use on: [push, pull_request] in your workflow.

How do I test against a staging environment in CI?

Set baseURL via environment variable:

env:
PLAYWRIGHT_TEST_BASE_URL: https://staging.example.com

steps:
- run: npm run test:e2e

Can I trigger tests manually?

Yes, add workflow_dispatch:

on:
push:
pull_request:
workflow_dispatch: # Allows manual trigger from Actions tab

Further Reading