# CI: GitHub Actions > **When to use**: Running Playwright tests automatically on pull requests, merges to main, or on a schedule. GitHub Actions is the most common CI for Playwright projects. ## Quick Reference ```bash # Key CLI flags for CI npx playwright install --with-deps # install browsers + OS deps npx playwright test --shard=1/4 # run 1 of 4 shards npx playwright test --reporter=github # annotate PR with failures npx playwright merge-reports ./blob-report # merge shard reports ``` ## Patterns ### Pattern 1: Production-Ready Workflow (Copy-Paste Starter) **Use when**: Any project using GitHub Actions. This is the complete, battle-tested workflow. ```yaml # .github/workflows/playwright.yml name: Playwright Tests on: push: branches: [main] pull_request: branches: [main] # Cancel in-progress runs for the same PR/branch concurrency: group: playwright-${{ github.ref }} cancel-in-progress: true env: CI: true jobs: test: timeout-minutes: 30 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - name: Install dependencies run: npm ci - name: Cache Playwright browsers id: playwright-cache uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} - name: Install Playwright browsers if: steps.playwright-cache.outputs.cache-hit != 'true' run: npx playwright install --with-deps - name: Install Playwright OS dependencies if: steps.playwright-cache.outputs.cache-hit == 'true' run: npx playwright install-deps - name: Run Playwright tests run: npx playwright test - name: Upload HTML report uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: playwright-report path: playwright-report/ retention-days: 14 - name: Upload test traces uses: actions/upload-artifact@v4 if: failure() with: name: playwright-traces path: test-results/ retention-days: 7 ``` ### Pattern 2: Sharded Execution with Matrix Strategy **Use when**: Test suite takes more than 10 minutes. Split across parallel runners to cut wall-clock time. **Avoid when**: Suite runs under 5 minutes -- sharding overhead (checkout, install, merge) negates the benefit. ```yaml # .github/workflows/playwright-sharded.yml name: Playwright Tests (Sharded) on: push: branches: [main] pull_request: branches: [main] concurrency: group: playwright-${{ github.ref }} cancel-in-progress: true env: CI: true jobs: test: timeout-minutes: 20 runs-on: ubuntu-latest strategy: fail-fast: false matrix: shard: [1/4, 2/4, 3/4, 4/4] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - name: Install dependencies run: npm ci - name: Cache Playwright browsers id: playwright-cache uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} - name: Install Playwright browsers if: steps.playwright-cache.outputs.cache-hit != 'true' run: npx playwright install --with-deps - name: Install Playwright OS dependencies if: steps.playwright-cache.outputs.cache-hit == 'true' run: npx playwright install-deps - name: Run Playwright tests (shard ${{ matrix.shard }}) run: npx playwright test --shard=${{ matrix.shard }} - name: Upload blob report uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: blob-report-${{ strategy.job-index }} path: blob-report/ retention-days: 1 merge-reports: if: ${{ !cancelled() }} needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - name: Install dependencies run: npm ci - name: Download all blob reports uses: actions/download-artifact@v4 with: path: all-blob-reports pattern: blob-report-* merge-multiple: true - name: Merge reports run: npx playwright merge-reports --reporter=html ./all-blob-reports - name: Upload merged HTML report uses: actions/upload-artifact@v4 with: name: playwright-report path: playwright-report/ retention-days: 14 ``` **Config for sharding** -- add blob reporter so shard output can be merged: ```ts // playwright.config.ts import { defineConfig } from "@playwright/test" export default defineConfig({ reporter: process.env.CI ? [["blob"], ["github"]] : [["html", { open: "on-failure" }]], }) ``` ### Pattern 3: Reusable Workflow **Use when**: Multiple repositories or multiple workflow files need the same Playwright setup. **Avoid when**: Single repo with one workflow. ```yaml # .github/workflows/playwright-reusable.yml name: Playwright Reusable on: workflow_call: inputs: node-version: type: string default: "20" test-command: type: string default: "npx playwright test" shard-total: type: number default: 1 secrets: BASE_URL: required: false TEST_PASSWORD: required: false jobs: test: timeout-minutes: 30 runs-on: ubuntu-latest strategy: fail-fast: false matrix: shard: ${{ fromJson(format('[{0}]', join(fromJson(format('[{0}]', inputs.shard-total == 1 && '"1/1"' || '"1/4","2/4","3/4","4/4"')), ','))) }} env: CI: true BASE_URL: ${{ secrets.BASE_URL }} TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} cache: "npm" - run: npm ci - name: Cache Playwright browsers id: playwright-cache uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} - name: Install Playwright browsers if: steps.playwright-cache.outputs.cache-hit != 'true' run: npx playwright install --with-deps - name: Install Playwright OS dependencies if: steps.playwright-cache.outputs.cache-hit == 'true' run: npx playwright install-deps - name: Run tests run: ${{ inputs.test-command }} --shard=${{ matrix.shard }} - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: playwright-report-${{ strategy.job-index }} path: playwright-report/ retention-days: 14 ``` **Calling the reusable workflow:** ```yaml # .github/workflows/ci.yml name: CI on: pull_request: branches: [main] jobs: e2e: uses: ./.github/workflows/playwright-reusable.yml with: node-version: "20" shard-total: 4 secrets: BASE_URL: ${{ secrets.STAGING_URL }} TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} ``` ### Pattern 4: Running in a Container **Use when**: You need a reproducible environment identical to local Docker runs, or the runner's OS dependencies cause issues. **Avoid when**: Standard `ubuntu-latest` with `--with-deps` works fine (the common case). ```yaml # .github/workflows/playwright-container.yml name: Playwright (Container) on: pull_request: branches: [main] jobs: test: timeout-minutes: 30 runs-on: ubuntu-latest container: image: mcr.microsoft.com/playwright:v1.52.0-noble steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci # No browser install needed -- they're in the container image - name: Run Playwright tests run: npx playwright test env: HOME: /root # required when running as root in container - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: playwright-report path: playwright-report/ retention-days: 14 ``` ### Pattern 5: Environment Secrets and Deployment Targets **Use when**: Tests run against staging/production environments that require authentication credentials. **Avoid when**: Tests only run against a locally started dev server. ```yaml # .github/workflows/playwright-staging.yml name: Playwright (Staging) on: push: branches: [main] workflow_dispatch: jobs: test: timeout-minutes: 30 runs-on: ubuntu-latest environment: staging # GitHub Environment with protection rules env: CI: true BASE_URL: ${{ vars.STAGING_URL }} TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} API_KEY: ${{ secrets.API_KEY }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - run: npm ci - name: Cache Playwright browsers id: playwright-cache uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} - name: Install Playwright browsers if: steps.playwright-cache.outputs.cache-hit != 'true' run: npx playwright install --with-deps - name: Install Playwright OS dependencies if: steps.playwright-cache.outputs.cache-hit == 'true' run: npx playwright install-deps - name: Run tests against staging run: npx playwright test --grep @smoke - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: staging-report path: playwright-report/ retention-days: 14 ``` ### Pattern 6: Scheduled Runs (Nightly Regression) **Use when**: Full regression suite is too slow for every PR. Run it nightly against main. **Avoid when**: Suite runs in under 15 minutes and can run on every PR. ```yaml # .github/workflows/playwright-nightly.yml name: Nightly Regression on: schedule: - cron: "0 3 * * 1-5" # 3 AM UTC, Mon-Fri workflow_dispatch: # allow manual trigger jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest env: CI: true BASE_URL: ${{ vars.STAGING_URL }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps - name: Run full regression suite run: npx playwright test --grep @regression - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: nightly-report-${{ github.run_number }} path: playwright-report/ retention-days: 30 - name: Notify on failure if: failure() uses: slackapi/slack-github-action@v1.27.0 with: payload: | { "text": "Nightly Playwright regression failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} ``` ## Decision Guide | Scenario | Approach | Why | | -------------------------- | ------------------------------------------ | --------------------------------------- | | Small suite (< 5 min) | Single job, no sharding | Overhead of sharding exceeds time saved | | Medium suite (5-20 min) | 2-4 shards with matrix | Cut wall-clock time by ~60-75% | | Large suite (20+ min) | 4-8 shards + blob report merge | Keep PR feedback under 10 minutes | | Cross-browser on PRs | Chromium only on PRs; all browsers on main | 3x fewer minutes burned on PRs | | Staging/prod smoke tests | Separate workflow with `environment:` | Isolate secrets, add approval gates | | Nightly full regression | `schedule` trigger + `workflow_dispatch` | Full coverage without blocking PRs | | Multiple repos, same setup | Reusable workflow with `workflow_call` | DRY; update one file, all repos benefit | | Reproducible env needed | Container job with Playwright image | Identical to local Docker environment | ## Anti-Patterns | Anti-Pattern | Problem | Do This Instead | | ------------------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------- | | No `concurrency` group | Duplicate runs waste minutes on every push | Add `concurrency: { group: ..., cancel-in-progress: true }` | | `fail-fast: true` with sharding | One shard failure cancels others; you lose their results | Set `fail-fast: false` to collect all failures | | Installing browsers without caching | 60-90 seconds wasted every run | Cache `~/.cache/ms-playwright` keyed on lockfile hash | | `timeout-minutes` not set | Stuck jobs run for 6 hours (GitHub default) | Set explicit timeout: 20-30 minutes | | Uploading artifacts only on failure | No report when tests pass; can't verify results | Use `if: ${{ !cancelled() }}` to always upload | | Hardcoding secrets in workflow files | Security breach | Use GitHub Secrets and Environments | | Running all browsers on every PR | 3x CI cost for marginal benefit | Chromium on PR; cross-browser on main merge | | `actions/upload-artifact` with no retention | Default 90-day retention fills storage | Set `retention-days: 7-14` for reports | | No `--with-deps` on browser install | Missing OS libraries cause browser launch failures | Always use `npx playwright install --with-deps` | ## Troubleshooting ### Browser launch fails: "Missing dependencies" **Cause**: Browsers installed from cache but OS dependencies were not cached (they live in system directories, not `~/.cache`). **Fix**: Always run `npx playwright install-deps` on cache hit: ```yaml - name: Install Playwright OS dependencies if: steps.playwright-cache.outputs.cache-hit == 'true' run: npx playwright install-deps ``` ### Tests pass locally but fail in CI with timeouts **Cause**: CI runners have fewer CPU cores and less RAM than your dev machine. Default workers and timeouts are too aggressive. **Fix**: Reduce workers and increase timeouts for CI in your config: ```ts // playwright.config.ts import { defineConfig } from "@playwright/test" export default defineConfig({ workers: process.env.CI ? "50%" : undefined, use: { actionTimeout: process.env.CI ? 15_000 : 10_000, navigationTimeout: process.env.CI ? 30_000 : 15_000, }, }) ``` ### Sharded reports are incomplete -- some shards missing from merged report **Cause**: Using `actions/download-artifact@v4` without `merge-multiple: true`, or artifact names collide across shards. **Fix**: Give each shard a unique artifact name and use `merge-multiple`: ```yaml # Upload in each shard job - uses: actions/upload-artifact@v4 with: name: blob-report-${{ strategy.job-index }} path: blob-report/ # Download in merge job - uses: actions/download-artifact@v4 with: path: all-blob-reports pattern: blob-report-* merge-multiple: true ``` ### `webServer` fails in CI: "port 3000 already in use" **Cause**: Previous run left a zombie process, or another job step is using the port. **Fix**: Ensure `reuseExistingServer: false` in CI and add a pre-step to kill stale processes: ```yaml - name: Kill stale processes run: lsof -ti:3000 | xargs kill -9 2>/dev/null || true ``` ### GitHub annotations not appearing on PR **Cause**: The `github` reporter is not configured. **Fix**: Add `github` reporter for CI runs: ```ts // playwright.config.ts import { defineConfig } from "@playwright/test" export default defineConfig({ reporter: process.env.CI ? [["html", { open: "never" }], ["github"]] : [["html", { open: "on-failure" }]], }) ``` ## Related - [ci/parallel-and-sharding.md](parallel-and-sharding.md) -- sharding strategies and blob report merging - [ci/reporting-and-artifacts.md](reporting-and-artifacts.md) -- reporter configuration and artifact management - [ci/docker-and-containers.md](docker-and-containers.md) -- container images for CI - [ci/ci-gitlab.md](ci-gitlab.md) -- GitLab CI equivalent - [ci/ci-other.md](ci-other.md) -- CircleCI, Azure DevOps, Jenkins - [core/configuration.md](../core/configuration.md) -- CI-aware config settings