CI/CD August 5, 2026 15 min read

Playwright + GitHub Actions: Complete CI/CD Pipeline Guide (2026)

Every Playwright test that only runs on your laptop is a test that will eventually break in production. This guide walks you through building a complete GitHub Actions pipeline — from a minimal 10-line workflow to a production-grade config with sharding, caching, Docker containers, and Slack failure alerts.

You've written a solid Playwright test suite. Tests pass locally, coverage is high, the team is happy. Then someone merges a broken feature because they forgot to run tests before pushing. Sound familiar?

The fix isn't process — it's automation. A GitHub Actions pipeline that runs your Playwright tests on every push and pull request catches regressions before they reach production. And with GitHub Actions' generous free tier (2,000 minutes/month for private repos, unlimited for public), there's no reason not to set one up today.

This guide progresses from a basic pipeline to a fully optimized production config. Each section builds on the previous one, so you can stop wherever your needs are met — or go all the way. (For general test suite hygiene, see our Playwright best practices guide.)


1. Why GitHub Actions for Playwright?

GitHub Actions isn't the only CI/CD option, but it's the best default choice for Playwright in 2026 for three reasons:

  • Zero infrastructure — no Jenkins server to maintain, no Docker registry to manage. Create a YAML file, push, and your pipeline is live
  • Native GitHub integration — PR checks, commit statuses, artifact links, and environment protections are built in. No webhook configuration needed
  • Matrix strategy — split your test suite across multiple parallel machines with a single strategy.matrix block. This is how you turn a 20-minute suite into a 5-minute run

The free tier gives you 2,000 minutes/month on private repos and unlimited minutes on public repos. A typical Playwright pipeline with 4-shard parallelism uses 8–12 minutes per run — that's 160–250 free runs per month before you pay a cent.

Playwright's official recommendation: The Playwright team actively maintains a CI/CD guide with GitHub Actions as the primary example. The official Docker image is built specifically for Actions runners.


2. Basic Pipeline

Let's start with the simplest possible workflow that installs dependencies, runs tests, and uploads the report. Create this file at .github/workflows/playwright.yml:

.github/workflows/playwright.yml
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: 22

      - name: Install dependencies
        run: npm ci

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

      - name: Run Playwright tests
        run: npx playwright test

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

That's it. Commit this file and push — GitHub automatically detects the workflow and starts running it. The key details:

  • npm ci installs exact versions from package-lock.json (faster and more deterministic than npm install)
  • npx playwright install --with-deps downloads browsers and their system-level dependencies (fonts, libraries)
  • if: always() on the upload step ensures the report is saved even when tests fail — which is exactly when you need it most

Common mistake: Forgetting --with-deps on the install command. Without it, Playwright downloads browser binaries but skips system dependencies. Tests will fail with cryptic errors about missing shared libraries like libgbm.so or libatk-1.0.so.


3. Optimizing for Speed

The basic pipeline works, but it's slow. Every run downloads node_modules (~200MB) and Playwright browsers (~500MB). Let's fix that with caching.

Cache node_modules

.github/workflows/playwright.yml (cache steps)
steps:
  - uses: actions/checkout@v4

  - uses: actions/setup-node@v4
    with:
      node-version: 22
      cache: 'npm'

  - name: Install dependencies
    run: npm ci

  # Cache Playwright browsers
  - name: Get Playwright version
    id: playwright-version
    run: |
      echo "version=$(npx playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT

  - name: Cache Playwright browsers
    uses: actions/cache@v4
    id: playwright-cache
    with:
      path: ~/.cache/ms-playwright
      key: playwright-${{ steps.playwright-version.outputs.version }}

  - name: Install Playwright browsers
    if: steps.playwright-cache.outputs.cache-hit != 'true'
    run: npx playwright install --with-deps

  - name: Install system deps only (if cached)
    if: steps.playwright-cache.outputs.cache-hit == 'true'
    run: npx playwright install-deps

This pattern saves 60–90 seconds per run. The cache key includes the Playwright version, so browsers are re-downloaded automatically when you upgrade Playwright. System dependencies (install-deps) still need to run on cache hits because the runner is a fresh VM each time.

Impact: With both npm and browser caching, the install phase drops from ~90 seconds to ~15 seconds. For a team running 20 CI builds per day, that saves 25 minutes daily — roughly 8 hours per month.


4. Sharding

Caching speeds up the install phase, but if your test suite itself takes 15 minutes, that's still too slow for a PR check. Sharding splits your tests across multiple parallel machines using GitHub Actions' matrix strategy.

.github/workflows/playwright.yml (sharded)
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1/4, 2/4, 3/4, 4/4]
    steps:
      # ... checkout, setup, install steps ...

      - name: Run Playwright tests
        run: npx playwright test --shard=${{ matrix.shard }}

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: report-shard-${{ strategy.job-index }}
          path: blob-report/
          retention-days: 7

  merge-reports:
    if: always()
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Install dependencies
        run: npm ci

      - name: Download shard reports
        uses: actions/download-artifact@v4
        with:
          path: all-reports
          pattern: report-shard-*
          merge-multiple: true

      - name: Merge reports
        run: npx playwright merge-reports --reporter html ./all-reports

      - uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

Key details about sharding:

  • fail-fast: false is critical — without it, GitHub cancels remaining shards when one fails, and you lose diagnostic data from other machines
  • Use the blob reporter in playwright.config.ts to produce mergeable reports: reporter: process.env.CI ? 'blob' : 'html'
  • The merge-reports job downloads all shard artifacts and combines them into a single HTML report
  • 4 shards is the sweet spot for most teams. Going beyond 6 shards adds more overhead than it saves unless your suite has 1,000+ tests

Performance math: A 20-minute sequential suite split across 4 shards runs in ~5 minutes (plus ~2 minutes for install + merge). That's a 75% reduction in wall-clock time. The total CI minutes consumed is the same, but developers get feedback 4x faster.


5. Running on PR vs Push

Not every commit needs the full test suite. PRs need fast feedback; the main branch needs comprehensive coverage. Here's how to configure both:

.github/workflows/playwright.yml (conditional)
name: Playwright Tests

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

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: ${{ github.event_name == 'push' && fromJSON('[\"1/4\",\"2/4\",\"3/4\",\"4/4\"]') || fromJSON('[\"1/1\"]') }}
    steps:
      # ... checkout, setup, install steps ...

      - name: Run smoke tests (PR)
        if: github.event_name == 'pull_request'
        run: npx playwright test --grep @smoke

      - name: Run full suite (push to main)
        if: github.event_name == 'push'
        run: npx playwright test --shard=${{ matrix.shard }}

This gives you the best of both worlds:

  • Pull requests run only @smoke-tagged tests on a single machine — fast feedback in 2–3 minutes
  • Pushes to main run the full suite across 4 shards — comprehensive coverage before deployment

To tag tests as smoke tests, add the tag in your test title or use Playwright's tag syntax:

example.spec.ts
test('user can log in @smoke', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@test.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page).toHaveURL(/dashboard/);
});

Rule of thumb: Tag 10–15% of your tests as @smoke. These should cover the critical user paths: login, checkout, main CRUD operations. If your smoke suite takes more than 3 minutes, you've tagged too many tests.


6. Trace & Report Artifacts

When a test fails in CI, you need to know exactly what happened without reproducing it locally. Playwright's trace files capture every action, network request, console log, and DOM snapshot. Here's how to configure them:

Enable traces on failure

playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    // Capture trace on first retry of a failed test
    trace: 'on-first-retry',

    // Capture screenshot on failure
    screenshot: 'only-on-failure',

    // Record video on first retry
    video: 'on-first-retry',
  },

  // Retry failed tests once in CI
  retries: process.env.CI ? 2 : 0,

  // Use blob reporter in CI for shard merging
  reporter: process.env.CI
    ? [[ 'blob' ], [ 'github' ]]
    : [[ 'html' ]],
});

Upload traces as artifacts

.github/workflows/playwright.yml (artifact step)
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results-${{ strategy.job-index }}
          path: |
            test-results/
            blob-report/
          retention-days: 14

After the workflow finishes, go to the Actions tab, click the run, and download the artifacts. To view a trace file locally:

Terminal
npx playwright show-trace test-results/my-test/trace.zip

The Trace Viewer opens in your browser with a full timeline of every action, network request, and console log. You can step through the test frame by frame and see exactly where it diverged from expectations. For more on using traces effectively, see our debugging Playwright tests guide.

Pro tip: The github reporter (included above) adds inline annotations directly to your PR — failed test names appear as annotations on the exact files and lines that failed. No need to dig through logs.


7. Environment Variables & Secrets

Hardcoding URLs, API keys, or test credentials in your workflow file is a security risk and makes your pipeline inflexible. Use GitHub's environment variables and encrypted secrets instead.

Setting up secrets

Go to your repository Settings > Secrets and variables > Actions and add your secrets:

  • BASE_URL — the URL of your staging environment
  • TEST_USER_EMAIL — test account email
  • TEST_USER_PASSWORD — test account password
  • API_KEY — API key for test data seeding

Using secrets in your workflow

.github/workflows/playwright.yml (env section)
      - name: Run Playwright tests
        run: npx playwright test
        env:
          BASE_URL: ${{ secrets.BASE_URL }}
          TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
          TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
          API_KEY: ${{ secrets.API_KEY }}

Accessing secrets in Playwright config

playwright.config.ts
export default defineConfig({
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
  },
});

Security warning: Never log secret values in CI output. Avoid echo ${{ secrets.API_KEY }} in your workflow. GitHub automatically masks secrets in logs, but console.log() in your test code won't be masked. Use .env files locally and secrets in CI — never commit .env files.

Do this
process.env.BASE_URL || 'http://localhost:3000'
${{ secrets.API_KEY }}
Not this
baseURL: 'https://staging.myapp.com'
apiKey: 'sk-12345-hardcoded'

8. Docker Container

The official Playwright Docker image (mcr.microsoft.com/playwright) comes with all browsers and system dependencies pre-installed. This eliminates version mismatches and speeds up the install phase.

.github/workflows/playwright.yml (Docker)
jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: mcr.microsoft.com/playwright:v1.50.0-noble
    strategy:
      fail-fast: false
      matrix:
        shard: [1/4, 2/4, 3/4, 4/4]
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: npm ci

      # No need for 'npx playwright install' — browsers are in the image

      - name: Run Playwright tests
        run: npx playwright test --shard=${{ matrix.shard }}
        env:
          HOME: /root

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: report-shard-${{ strategy.job-index }}
          path: blob-report/

Notice what's missing: no actions/setup-node, no playwright install, no browser cache steps. The Docker image includes everything. This simplifies the workflow significantly.

Version pinning: Always pin the Docker image version (e.g., v1.50.0-noble) to match your @playwright/test version in package.json. Mismatched versions cause tests to fail with "browser revision mismatch" errors. Update both together when upgrading Playwright.

The HOME: /root environment variable is required because GitHub Actions containers run as root, and Playwright looks for browser binaries relative to the home directory.


9. Slack/Teams Notifications

A CI pipeline that fails silently is a CI pipeline that gets ignored. Add Slack or Teams notifications to alert your team the moment tests fail, with a direct link to the report.

Slack notification on failure

.github/workflows/playwright.yml (Slack step)
  notify:
    if: failure()
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - name: Notify Slack on failure
        uses: slackapi/slack-github-action@v2.0.0
        with:
          webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
          webhook-type: incoming-webhook
          payload: |
            {
              "text": "Playwright tests failed on ${{ github.ref_name }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Playwright Tests Failed*\nBranch: `${{ github.ref_name }}`\nCommit: `${{ github.sha }}`\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>"
                  }
                }
              ]
            }

Microsoft Teams notification

.github/workflows/playwright.yml (Teams step)
      - name: Notify Teams on failure
        if: failure()
        run: |
          curl -H "Content-Type: application/json" \
            -d '{
              "title": "Playwright Tests Failed",
              "text": "Branch: ${{ github.ref_name }} | [View Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
            }' \
            ${{ secrets.TEAMS_WEBHOOK_URL }}

Set up the webhook URL:

  • Slack: Create an incoming webhook at api.slack.com/apps, add it to your channel, and save the URL as SLACK_WEBHOOK_URL in GitHub Secrets
  • Teams: In your channel, click the ... menu > Connectors > Incoming Webhook, copy the URL, and save as TEAMS_WEBHOOK_URL

Avoid alert fatigue: Only notify on failure(), not on every run. If you also want success notifications, consider sending them only for the main branch to keep noise low on feature branches.


10. Complete Production Config

Here's everything combined into a single, production-ready workflow file. This is the config used in real-world projects with 500+ Playwright tests:

.github/workflows/playwright.yml (full production config)
name: Playwright Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  workflow_dispatch:
    inputs:
      grep:
        description: 'Test filter (e.g. @smoke, @regression)'
        required: false
        default: ''

env:
  CI: true
  PLAYWRIGHT_VERSION: ''

jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: mcr.microsoft.com/playwright:v1.50.0-noble
    strategy:
      fail-fast: false
      matrix:
        shard: ${{ github.event_name == 'pull_request' && fromJSON('[\"1/1\"]') || fromJSON('[\"1/4\",\"2/4\",\"3/4\",\"4/4\"]') }}
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: npm ci

      # PR: smoke tests only | Push: full suite with sharding
      - name: Run Playwright tests
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            npx playwright test --grep @smoke
          elif [ -n "${{ inputs.grep }}" ]; then
            npx playwright test --grep "${{ inputs.grep }}" --shard=${{ matrix.shard }}
          else
            npx playwright test --shard=${{ matrix.shard }}
          fi
        env:
          HOME: /root
          BASE_URL: ${{ secrets.BASE_URL }}
          TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
          TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: report-shard-${{ strategy.job-index }}
          path: |
            blob-report/
            test-results/
          retention-days: 14

  merge-reports:
    if: always() && github.event_name != 'pull_request'
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Install dependencies
        run: npm ci

      - name: Download shard reports
        uses: actions/download-artifact@v4
        with:
          path: all-reports
          pattern: report-shard-*
          merge-multiple: true

      - name: Merge reports
        run: npx playwright merge-reports --reporter html ./all-reports

      - uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

  notify:
    if: failure() && github.ref == 'refs/heads/main'
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - name: Notify Slack
        uses: slackapi/slack-github-action@v2.0.0
        with:
          webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
          webhook-type: incoming-webhook
          payload: |
            {
              "text": "Playwright tests failed on main",
              "blocks": [{
                "type": "section",
                "text": {
                  "type": "mrkdwn",
                  "text": "*Playwright Tests Failed*\nBranch: `main`\nCommit: `${{ github.sha }}`\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run & Download Report>"
                }
              }]
            }

This config gives you:

What this pipeline delivers

  • Smoke tests on PRs (2-3 min feedback)
  • Full sharded suite on push to main
  • Docker container for consistency
  • Merged HTML report as artifact
  • Trace files for failed test diagnosis (see also: visual regression testing)
  • Secrets for secure credential handling
  • Slack alerts on main branch failures
  • Manual trigger with custom test filter

Frequently Asked Questions

Is GitHub Actions free for Playwright CI/CD?

Yes, for most teams. Public repos get unlimited minutes. Private repos on the Free plan get 2,000 minutes/month. A typical sharded Playwright pipeline uses 8–12 minutes per run, giving you 160–250 free runs per month. Most teams don't exceed this until they scale significantly.

How do I speed up Playwright tests in GitHub Actions?

Four strategies cut CI time by 70–80%: (1) Cache node_modules and browsers to skip 60–90 second installs, (2) Use sharding with matrix strategy across 4+ machines, (3) Run only @smoke tests on PRs, (4) Use the official Docker image to skip browser installation. Combined, these reduce a 20-minute suite to under 5 minutes.

How do I view Playwright reports from GitHub Actions?

Upload the HTML report as an artifact with actions/upload-artifact and if: always(). After the run, go to the Actions tab, click the run, scroll to Artifacts, download the ZIP, extract it, and run npx playwright show-report. The github reporter also adds inline annotations directly on your PR.

Should I use Docker or install browsers directly?

Use Docker (mcr.microsoft.com/playwright) for consistency and speed. It includes all browsers pre-installed, eliminates version mismatches, and simplifies your workflow file. Pin the image version to match your @playwright/test version and update both together.

How do I run different tests on PRs vs main?

Use conditional steps with if: github.event_name == 'pull_request'. Run only @smoke-tagged tests (10–15% of your suite) on PRs for fast 2–3 minute feedback. Run the full suite with sharding on pushes to main for comprehensive coverage before deployment.


Asim Noaman - Playwright and Claude AI course instructor

Asim Noaman

Senior QA Automation Engineer & AI Testing Specialist

With years of hands-on experience building test automation frameworks for production applications, Asim specializes in combining traditional QA methodologies with cutting-edge AI tools. He has helped teams adopt Playwright and AI-driven testing workflows to ship faster with fewer bugs.

Udemy Instructor Published course author
Playwright + AI Expert Specialized in AI-powered QA
Production Experience Enterprise-grade frameworks
Connect on LinkedIn