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:
- Go to Repository → Settings → Secrets and variables → Actions
- Click "New repository secret"
- Add your secrets (BASE_URL, API_KEY, etc.)
- 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:
- Go to Slack workspace → Create new Incoming Webhook
- Copy webhook URL
- Add to GitHub Secrets as SLACK_WEBHOOK
- 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:
- Copy the workflow above
- Update secrets with your BASE_URL, API_KEY, etc.
- Push to your repo and watch it run
- View reports in Actions → Artifacts
- 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
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.