Skip to main content
/tayyab/portfolio — zsh
tayyab
TA
// dispatch.read --classified=false --access-level: public

Setting Up Playwright with GitHub Actions CI/CD (Step-by-Step)

March 20, 2026 EST. READ: 14 MIN #Quality Assurance

You've written your Playwright tests. Now the real question: How do you run them automatically on every pull request?

This guide gives you a production-ready GitHub Actions workflow that:

  • ✅ Runs Playwright tests in parallel (12 minutes down to 3-4 minutes)
  • ✅ Blocks pull requests if tests fail (prevents broken code from merging)
  • ✅ Uploads HTML reports automatically
  • ✅ Notifies Slack/teams on failure
  • ✅ Works on first try (copy-paste, no debugging)

This is the workflow I use in production. Copy it exactly and adapt only the values specific to your setup.

The Complete GitHub Actions Workflow

Create this file: .github/workflows/playwright.yml

name: Playwright Tests

on:
  push:
    branches: [main, develop, staging]
  pull_request:
    branches: [main, develop]
  schedule:
    # Run tests nightly at 2 AM UTC
    - cron: '0 2 * * *'

jobs:
  test:
    timeout-minutes: 30
    runs-on: ubuntu-latest
    
    strategy:
      fail-fast: true
      matrix:
        # Run tests on multiple browsers in parallel jobs
        browser: [chromium, firefox]
    
    steps:
      # Step 1: Checkout code
      - name: Checkout repository
        uses: actions/checkout@v4
      
      # Step 2: Setup Node.js
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'
      
      # Step 3: Install dependencies
      - name: Install dependencies
        run: npm ci
      
      # Step 4: Install Playwright browsers
      - name: Install Playwright browsers
        run: npx playwright install --with-deps ${{ matrix.browser }}
      
      # Step 5: Run tests
      - name: Run Playwright tests on ${{ matrix.browser }}
        run: npx playwright test --project=${{ matrix.browser }}
        env:
          # Set environment variables for test configuration
          BASE_URL: ${{ secrets.BASE_URL }}
          API_KEY: ${{ secrets.API_KEY }}
      
      # Step 6: Upload test report
      - name: Upload Playwright report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report-${{ matrix.browser }}
          path: playwright-report/
          retention-days: 30
      
      # Step 7: Comment PR with results
      - name: Comment PR with test results
        if: always() && github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const testResultsFile = 'test-results.json';
            
            if (fs.existsSync(testResultsFile)) {
              const results = JSON.parse(fs.readFileSync(testResultsFile, 'utf8'));
              const passed = results.stats.expected;
              const failed = results.stats.unexpected;
              const skipped = results.stats.skipped;
              
              const comment = `## Playwright Test Results (${{ matrix.browser }})
              
              - ✅ Passed: ${passed}
              - ❌ Failed: ${failed}
              - ⊘ Skipped: ${skipped}
              
              [View detailed report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})`;
              
              github.rest.issues.createComment({
                issue_number: context.issue.number,
                owner: context.repo.owner,
                repo: context.repo.repo,
                body: comment
              });
            }

  # This job ensures the workflow passes only if all tests pass
  test-results:
    if: always()
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Check test results
        if: needs.test.result == 'failure'
        run: exit 1

Step-by-Step Explanation

1. Trigger Events

on:
  push:
    branches: [main, develop, staging]
  pull_request:
    branches: [main, develop]
  schedule:
    - cron: '0 2 * * *'

What this does:

  • ✅ Runs on every push to main/develop/staging
  • ✅ Runs on every pull request to main/develop
  • ✅ Runs nightly at 2 AM UTC (catches production regressions early)

You can also add manual trigger:

on:
  workflow_dispatch: # Allows manual triggering from GitHub UI

2. Browser Matrix Strategy

strategy:
  matrix:
    browser: [chromium, firefox]

What this does:

  • ✅ Runs your tests on both Chromium and Firefox
  • ✅ Creates separate job for each browser
  • ✅ Jobs run in parallel (saves time)
  • ✅ If any job fails, PR is blocked

To add Safari (WebKit):

browser: [chromium, firefox, webkit]

3. Dependency Caching

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: '18'
    cache: 'npm'  # Cache npm dependencies

What this does:

  • ✅ Caches npm_modules folder (saves 2-3 minutes per run)
  • ✅ Automatically detects package-lock.json
  • ✅ Reuses cache across runs unless dependencies change

4. Environment Variables for Secrets

- name: Run tests
  run: npx playwright test
  env:
    BASE_URL: ${{ secrets.BASE_URL }}
    API_KEY: ${{ secrets.API_KEY }}

How to set secrets in GitHub:

  1. Go to Repository → Settings → Secrets and variables → Actions
  2. Click "New repository secret"
  3. Add your secrets (BASE_URL, API_KEY, etc.)
  4. Reference them in workflow as ${{ secrets.SECRET_NAME }}

Your Playwright config then reads them:

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

5. Artifact Upload & Retention

- name: Upload Playwright report
  if: always()  # Upload even if tests fail
  uses: actions/upload-artifact@v4
  with:
    name: playwright-report-${{ matrix.browser }}
    path: playwright-report/
    retention-days: 30

What this does:

  • ✅ Uploads HTML test report after every run
  • ✅ Creates separate artifact per browser
  • ✅ Stores for 30 days (auto-deletes after)
  • ✅ Accessible from Actions tab → click run → Artifacts

Advanced Configurations

Run Only Smoke Tests on PR (Faster Feedback)

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

- name: Run all tests on push to main
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  run: npx playwright test

This speeds up PR feedback (smoke tests in 3 min) while running full suite nightly.

Slack Notification on Failure

- name: Notify Slack on test failure
  if: failure()
  uses: slackapi/slack-github-action@v1.24.0
  with:
    webhook-url: ${{ secrets.SLACK_WEBHOOK }}
    payload: |
      {
        "text": "❌ Playwright tests failed",
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*Test Failure in ${{ github.repository }}*\\nBranch: ${{ github.ref_name }}\\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>"
            }
          }
        ]
      }

To set up Slack webhook:

  1. Go to Slack workspace → Create new Incoming Webhook
  2. Copy webhook URL
  3. Add to GitHub Secrets as SLACK_WEBHOOK
  4. Now you'll get Slack notifications on test failures

Generate & Download Reports as ZIP

- name: Generate test report
  if: always()
  run: npx playwright show-report

- name: Package reports
  if: always()
  run: zip -r playwright-report.zip playwright-report/

- name: Upload packaged report
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: playwright-report-archive
    path: playwright-report.zip

Common Issues & Solutions

"Playwright browsers not found"

Problem: Tests fail because Playwright browsers aren't installed in CI.

Solution: Ensure this step runs:

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

The --with-deps flag installs system dependencies (not just browsers).

"Timeout waiting for element"

Problem: Tests pass locally but timeout in CI (network is slower).

Solution: Increase timeouts in playwright.config.ts:

export default defineConfig({
  use: {
    navigationTimeout: 30000, // 30 seconds
    actionTimeout: 10000,     // 10 seconds
  },
  timeout: 30000, // Per test timeout
});

"Tests flaky in CI but stable locally"

Problem: Environmental differences between local and CI.

Solution: Use recorded API responses instead of real APIs:

// Use mock mode in CI
const context = await browser.newContext({
  offline: true, // Simulate offline environment
});

// OR use Playwright mock API responses
await page.route('**/api/**', route => {
  route.abort();
});

Real Example: Fintech Project CI/CD

On the Wells Fargo project, we configured:

  • ✅ PR tests: Smoke tests only (2 min feedback)
  • ✅ Push to develop: Full test suite (12 min)
  • ✅ Nightly: Full suite + API tests + performance benchmarks (25 min)
  • ✅ Slack notifications: Only on failures
  • ✅ Report retention: 30 days

This approach gave developers instant feedback on PRs while ensuring full test coverage before production deploys.

Frequently Asked Questions

How long should CI/CD tests take?

Aim for 5-15 minutes for full suite. If longer, split into smoke (critical) + extended (full) tests. Developers won't wait 30+ minutes for PR feedback—they'll skip running tests locally.

Should I run tests on every commit or just pull requests?

Both. Run on pull requests for feedback before merging, AND on push to main for post-merge validation. This catches regressions from merge conflicts that weren't caught in the PR.

How do I download and view the HTML report?

After workflow runs: Actions tab → select your run → Artifacts → download playwright-report.zip → extract → open index.html in browser

Can I retry failed tests automatically in CI?

Yes. In playwright.config.ts: retries: process.env.CI ? 2 : 0 This retries failed tests twice in CI (flaky catch), but zero times locally (you fix the root cause).

How do I parallelize tests across multiple runners?

GitHub Actions parallelizes within a single runner. For multi-machine parallelization, you need a more complex setup with sharding. I cover this in the advanced Playwright guide.

Next Steps

You now have a production-grade CI/CD workflow. The next steps:

  1. Copy the workflow above
  2. Update secrets with your BASE_URL, API_KEY, etc.
  3. Push to your repo and watch it run
  4. View reports in Actions → Artifacts
  5. Add Slack notifications (optional but recommended)

That's it. Your tests now run automatically on every pull request, blocking merges if they fail.

Need help setting up CI/CD for your Playwright tests? I offer complete framework setup services including CI/CD configuration.

Let's get your tests running in CI/CD

Related Articles:

Tayyab Akmal
// author

Tayyab Akmal

AI & QA Automation Engineer

I've caught critical bugs in fintech, e-commerce, and SaaS platforms — then built the automation that prevents them from shipping again. 6+ years scaling test automation and AI-driven QA.

// feedback_channel

FOUND THIS USEFUL?

Share your thoughts or let's discuss automation testing strategies.

→ Start Conversation
Available for hire