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.matrixblock. 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:
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 ciinstalls exact versions frompackage-lock.json(faster and more deterministic thannpm install)npx playwright install --with-depsdownloads 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
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.
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: falseis critical — without it, GitHub cancels remaining shards when one fails, and you lose diagnostic data from other machines- Use the
blobreporter inplaywright.config.tsto produce mergeable reports:reporter: process.env.CI ? 'blob' : 'html' - The
merge-reportsjob 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:
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:
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
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
- 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:
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 environmentTEST_USER_EMAIL— test account emailTEST_USER_PASSWORD— test account passwordAPI_KEY— API key for test data seeding
Using secrets in your workflow
- 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
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.
process.env.BASE_URL || 'http://localhost:3000'${{ secrets.API_KEY }}
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.
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
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
- 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_URLin GitHub Secrets - Teams: In your channel, click the
...menu > Connectors > Incoming Webhook, copy the URL, and save asTEAMS_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:
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
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.