Exit course
Chapter 8 of 10 Beginner 8 min read 3 quick checks Updated

Run Playwright in CI with GitHub Actions

On this page
  1. The generated workflow
  2. Reading a failure from CI
  3. Secrets and environment
  4. Faster installs with the Playwright container
  5. Sharding across jobs
  6. Other CI systems
  7. When CI browsers are not enough

Tests that only run on a developer's laptop protect nobody. This chapter turns the suite into a check that runs on every push and pull request, keeps the report and traces when something fails, and then scales out with sharding.

The generated workflow#

If you answered "yes" to the GitHub Actions question in chapter 1, you already have .github/workflows/playwright.yml. This is what npm init playwright@latest generates with Playwright 1.65:

name: Playwright Tests
on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: lts/*
    - 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: ${{ !cancelled() }}
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

Step by step:

  • Triggers: every push to, and every pull request into, main or master. Adjust the branch names to your repository.
  • npm ci installs exactly what package-lock.json says. Commit the lock file, or this step fails.
  • npx playwright install --with-deps downloads the browser builds that match the installed @playwright/test version, plus the Linux system libraries they need. Because the version comes from your lock file, CI always gets the browsers your tests were written against.
  • npx playwright test runs the suite. GitHub Actions sets CI=true, so the config's CI branches kick in: forbidOnly fails the run on a stray test.only, failed tests get two retries, and traces are recorded on the first retry.
  • upload-artifact with if: ${{ !cancelled() }} uploads the HTML report even when tests failed, which is exactly when you need it. Skipping only on cancellation avoids uploading half-written reports.

The generator pins v4 of the official actions. Newer major versions exist; upgrading them (for example via Dependabot) is fine, but it is not required for Playwright to work.

The Playwright CI docs suggest one more change: instead of relying on the job-level timeout-minutes, set a globalTimeout in playwright.config.ts. When a GitHub job times out it is killed and you get no report; when Playwright's own global timeout fires, the reporters still write their output.

export default defineConfig({
  // Fail the run after an hour, so reporters still produce a report
  globalTimeout: 60 * 60 * 1000,
});

A slow run hit the job's timeout-minutes and there is no HTML report at all. What change keeps a report next time?

A job killed by GitHub never lets the reporters finish. When Playwright's own global timeout fires first, the run ends cleanly and the report is written and uploaded.

Reading a failure from CI#

When the job goes red:

  1. Open the workflow run and download the playwright-report artifact from the summary page.
  2. Unzip it and serve it locally: npx playwright show-report path/to/playwright-report.
  3. Open the failed test. If it was retried, its trace is attached; open it and work through it as described in chapter 5.

A test marked flaky passed on retry, so the job stayed green. Flaky tests are worth a look on every run; the report lists them separately.

Secrets and environment#

Your tests read credentials and URLs from environment variables, so pass them in from GitHub secrets on the test step:

    - name: Run Playwright tests
      run: npx playwright test
      env:
        BASE_URL: ${{ vars.STAGING_URL }}
        APP_USER: ${{ secrets.APP_USER }}
        APP_PASSWORD: ${{ secrets.APP_PASSWORD }}

Secrets are masked in logs, but they are not masked inside traces, screenshots or videos. If a test types a password, that value can be visible in the trace's DOM snapshots and in the action log. Use dedicated test accounts with no access to real data, and keep artifact retention short for anything sensitive.

Your login test types a password read from a GitHub secret. Where can that password still show up in plain text?

GitHub masks secrets in logs, not in artifacts. Trace DOM snapshots and the action log can contain the typed value, so use dedicated test accounts and short retention.

Faster installs with the Playwright container#

Installing browsers and system dependencies on every run takes time. An alternative is to run the job inside Microsoft's Docker image, which already contains the browsers and their dependencies. The image tag must match your @playwright/test version exactly:

jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: mcr.microsoft.com/playwright:v1.65.0-noble
      options: --user 1001
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: lts/*
    - run: npm ci
    - run: npx playwright test

When you upgrade Playwright, upgrade the image tag in the same commit. A mismatch shows up as an error about missing browser executables.

The Playwright docs also advise against caching the browser binaries between runs: restoring the cache takes about as long as downloading them, and on Linux the system dependencies cannot be cached anyway. If you only test one browser, npx playwright install chromium --with-deps is the cheaper speed-up.

Sharding across jobs#

When a single job gets too slow, split it with a matrix. Each job runs one shard and uploads a blob report; a final job merges them into one HTML report. First, switch reporters on CI in playwright.config.ts:

reporter: process.env.CI ? 'blob' : 'html',

Then the workflow:

name: Playwright Tests
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
jobs:
  playwright-tests:
    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: lts/*
    - run: npm ci
    - run: npx playwright install --with-deps
    - run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
    - uses: actions/upload-artifact@v4
      if: ${{ !cancelled() }}
      with:
        name: blob-report-${{ matrix.shardIndex }}
        path: blob-report
        retention-days: 1

  merge-reports:
    if: ${{ !cancelled() }}
    needs: [playwright-tests]
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: lts/*
    - run: npm ci
    - uses: actions/download-artifact@v4
      with:
        path: all-blob-reports
        pattern: blob-report-*
        merge-multiple: true
    - run: npx playwright merge-reports --reporter html ./all-blob-reports
    - uses: actions/upload-artifact@v4
      with:
        name: html-report--attempt-${{ github.run_attempt }}
        path: playwright-report
        retention-days: 14

Points that are easy to get wrong:

  • fail-fast: false lets the other shards finish when one fails; otherwise you lose most of the results.
  • The merge job uses if: ${{ !cancelled() }} so it runs even when shards failed, which is when the merged report matters.
  • Blob artifacts are only needed until the merge, hence retention-days: 1.
  • Leave fullyParallel: true on so shards are balanced at test level (chapter 6).

Your sharded matrix leaves out fail-fast: false. Shard 2 fails early. What do you lose?

Matrix jobs fail fast by default, so the other shards are cancelled and most of the results never exist. fail-fast: false lets them finish.

Other CI systems#

Nothing here is GitHub-specific apart from the YAML. On any CI provider the recipe is the same: npm ci, npx playwright install --with-deps (or the Docker image), npx playwright test, and keep playwright-report/ and test-results/ as build artifacts. The official CI guide has examples for Azure Pipelines, CircleCI, GitLab CI and Jenkins.

When CI browsers are not enough#

The runner above tests the Linux builds of Chromium, Firefox and WebKit on whatever machine GitHub provides. That covers a great deal, but it does not test branded Chrome or Edge on Windows, or WebKit on macOS, and parallelism is bounded by how many runners you can afford. The next chapter keeps this workflow and points the same tests at remote browsers. Our GitHub Actions integration guide covers the TestingBot-specific parts of that setup, such as storing your API key as a secret.