Configure GitHub Actions to run Playwright tests with sharding, upload trace artifacts on failure, merge distributed reports, and gate pull request merges on passing E2E tests.
Running Playwright tests locally is straightforward. Running them reliably in GitHub Actions requires orchestrating the application startup, browser installation, test sharding, and artifact collection — all while keeping the workflow fast enough to provide timely feedback on pull requests.
This lesson builds a production-ready GitHub Actions workflow for Playwright E2E tests. The workflow runs the full test suite in under 10 minutes using test sharding, collects detailed failure artifacts, and gates pull request merges on the results.
1# .github/workflows/e2e.yml
2name: E2E Tests
3
4on:
5 push:
6 branches: [main, develop]
7 pull_request:
8 branches: [main]
9
10jobs:
11 e2e:
12 name: Playwright E2E Tests
13 runs-on: ubuntu-latest
14 timeout-minutes: 30
15
16 services:
17 postgres:
18 image: postgres:16-alpine
19 env:
20 POSTGRES_USER: test
21 POSTGRES_PASSWORD: test
22 POSTGRES_DB: testdb
23 ports: ['5433:5432']
24 options: --health-cmd pg_isready --health-interval 5s --health-retries 10
25
26 env:
27 DATABASE_URL: postgresql://test:test@localhost:5433/testdb
28 JWT_SECRET: ci-e2e-jwt-secret-minimum-32-chars
29 NODE_ENV: test
30
31 steps:
32 - uses: actions/checkout@v4
33
34 - uses: actions/setup-node@v4
35 with:
36 node-version: '20'
37 cache: 'npm'
38
39 - name: Install dependencies
40 run: npm ci
41
42 - name: Install Playwright browsers
43 run: npx playwright install --with-deps chromium webkit
44 # Install only the browsers you test — saves ~2 minutes
45
46 - name: Run database migrations
47 run: npx prisma migrate deploy
48
49 - name: Build the application
50 run: npm run build
51 env:
52 NEXT_PUBLIC_API_URL: http://localhost:3001
53
54 - name: Run E2E tests
55 run: npx playwright test
56 env:
57 PLAYWRIGHT_BASE_URL: http://localhost:3000
58 CI: 'true'
59
60 - name: Upload Playwright report
61 uses: actions/upload-artifact@v4
62 if: always() # upload even when tests fail
63 with:
64 name: playwright-report
65 path: playwright-report/
66 retention-days: 71# .github/workflows/e2e-sharded.yml
2name: E2E Tests (Sharded)
3
4on:
5 pull_request:
6 branches: [main]
7
8jobs:
9 e2e-sharded:
10 name: E2E Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
11 runs-on: ubuntu-latest
12 timeout-minutes: 20
13 strategy:
14 fail-fast: false # don't cancel other shards if one fails
15 matrix:
16 shardIndex: [1, 2, 3, 4]
17 shardTotal: [4]
18
19 services:
20 postgres:
21 image: postgres:16-alpine
22 env: { POSTGRES_USER: test, POSTGRES_PASSWORD: test, POSTGRES_DB: testdb }
23 ports: ['5433:5432']
24 options: --health-cmd pg_isready --health-interval 5s --health-retries 10
25
26 steps:
27 - uses: actions/checkout@v4
28 - uses: actions/setup-node@v4
29 with: { node-version: '20', cache: 'npm' }
30 - run: npm ci
31 - run: npx playwright install --with-deps chromium
32 - run: npx prisma migrate deploy
33 env: { DATABASE_URL: postgresql://test:test@localhost:5433/testdb }
34 - run: npm run build
35 - name: Run shard
36 run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
37 env:
38 DATABASE_URL: postgresql://test:test@localhost:5433/testdb
39 JWT_SECRET: ci-jwt-secret-min-32-chars-here
40 CI: 'true'
41
42 - name: Upload shard blob report
43 uses: actions/upload-artifact@v4
44 if: always()
45 with:
46 name: playwright-blob-${{ matrix.shardIndex }}
47 path: blob-report/
48 retention-days: 1
49
50 merge-reports:
51 name: Merge E2E Reports
52 needs: [e2e-sharded]
53 runs-on: ubuntu-latest
54 if: always()
55 steps:
56 - uses: actions/checkout@v4
57 - uses: actions/setup-node@v4
58 with: { node-version: '20', cache: 'npm' }
59 - run: npm ci
60
61 - uses: actions/download-artifact@v4
62 with:
63 path: all-blob-reports
64 pattern: playwright-blob-*
65 merge-multiple: true
66
67 - name: Merge reports
68 run: npx playwright merge-reports --reporter html ./all-blob-reports
69
70 - uses: actions/upload-artifact@v4
71 with:
72 name: playwright-html-report
73 path: playwright-report/
74 retention-days: 71// playwright.config.ts — blob reporter for sharding
2reporter: process.env['CI']
3 ? [['blob'], ['github']] // blob = machine-readable shard output; github = PR annotations
4 : [['html', { open: 'on-failure' }]],1 # Always upload traces, screenshots, and videos for failed tests
2 - name: Upload test artifacts
3 uses: actions/upload-artifact@v4
4 if: failure() # only on failure to save storage
5 with:
6 name: playwright-artifacts-shard-${{ matrix.shardIndex }}
7 path: |
8 test-results/
9 playwright-report/
10 retention-days: 3
11 if-no-files-found: ignore1// playwright.config.ts — collect traces and video on CI failure
2use: {
3 trace: 'on-first-retry', // trace file (.zip) — open in trace viewer
4 screenshot: 'only-on-failure', // screenshot at point of failure
5 video: 'on-first-retry', // video recording (.webm)
6},
7
8// Test result output structure:
9// test-results/
10// {test-name}/
11// trace.zip → npx playwright show-trace trace.zip
12// screenshot.png → view directly
13// video.webm → view directly
14// test-failed-1.png → additional failure screenshot1 # Publish test results as PR comment
2 - name: Publish test results
3 uses: EnricoMi/publish-unit-test-result-action@v2
4 if: always()
5 with:
6 files: 'test-results/**/*.xml'
7 check_name: 'E2E Test Results'
8 comment_mode: always
9 fail_on: 'test failures'1 # Cache Playwright browser binaries between runs (~200MB each)
2 - name: Cache Playwright browsers
3 uses: actions/cache@v4
4 id: playwright-cache
5 with:
6 path: ~/.cache/ms-playwright
7 key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
8 restore-keys: |
9 playwright-${{ runner.os }}-
10
11 - name: Install Playwright browsers
12 # Only download browsers if cache was not restored
13 if: steps.playwright-cache.outputs.cache-hit != 'true'
14 run: npx playwright install --with-deps chromium webkit
15
16 - name: Install system dependencies only
17 # Even with cached browsers, system deps may be missing on fresh runner
18 if: steps.playwright-cache.outputs.cache-hit == 'true'
19 run: npx playwright install-deps chromium webkit
20
21# Alternative: use the official Playwright GitHub Action
22 - uses: microsoft/playwright-github-action@v1
23 with:
24 browsers: 'chromium,webkit'1 - name: Build Next.js application
2 run: npm run build
3 env:
4 NEXT_PUBLIC_API_URL: http://localhost:3001
5
6 - name: Start Next.js server in background
7 run: npm run start &
8 env:
9 PORT: 3000
10
11 - name: Start NestJS API in background
12 run: npm run start:api &
13 env:
14 PORT: 3001
15 DATABASE_URL: postgresql://test:test@localhost:5433/testdb
16
17 - name: Wait for servers to be ready
18 run: |
19 npx wait-on http://localhost:3000 http://localhost:3001/health \
20 --timeout 60000 \
21 --interval 1000
22 # wait-on polls the URLs until they return 200 or timeout
23
24 - name: Run Playwright tests
25 run: npx playwright test
26
27# Alternatively, use Playwright's built-in webServer config:
28# playwright.config.ts:
29# webServer: [
30# { command: 'npm run start', url: 'http://localhost:3000', timeout: 120_000, reuseExistingServer: false },
31# { command: 'npm run start:api', url: 'http://localhost:3001/health', timeout: 120_000 },
32# ],A production-ready GitHub Actions workflow with 4-shard parallel execution, browser caching, trace collection, report merging, and branch protection integration.
1# .github/workflows/e2e.yml
2name: E2E Tests
3
4on:
5 push:
6 branches: [main, develop]
7 pull_request:
8 branches: [main]
9
10concurrency:
11 group: e2e-${{ github.ref }}
12 cancel-in-progress: true
13
14jobs:
15 e2e:
16 name: 'E2E [${{ matrix.shardIndex }}/${{ matrix.shardTotal }}]'
17 runs-on: ubuntu-latest
18 timeout-minutes: 20
19 strategy:
20 fail-fast: false
21 matrix:
22 shardIndex: [1, 2, 3, 4]
23 shardTotal: [4]
24
25 services:
26 postgres:
27 image: postgres:16-alpine
28 env:
29 POSTGRES_USER: testuser
30 POSTGRES_PASSWORD: testpass
31 POSTGRES_DB: e2e_testdb
32 ports: ['5433:5432']
33 options: >-
34 --health-cmd pg_isready
35 --health-interval 5s
36 --health-timeout 5s
37 --health-retries 10
38
39 env:
40 DATABASE_URL: postgresql://testuser:testpass@localhost:5433/e2e_testdb
41 JWT_SECRET: ${{ secrets.CI_JWT_SECRET }}
42 JWT_REFRESH_SECRET: ${{ secrets.CI_JWT_REFRESH_SECRET }}
43 NODE_ENV: test
44 PLAYWRIGHT_BASE_URL: http://localhost:3000
45
46 steps:
47 - uses: actions/checkout@v4
48
49 - uses: actions/setup-node@v4
50 with: { node-version: '20', cache: 'npm' }
51
52 - run: npm ci
53
54 - name: Cache Playwright browsers
55 id: pw-cache
56 uses: actions/cache@v4
57 with:
58 path: ~/.cache/ms-playwright
59 key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
60
61 - name: Install Playwright
62 if: steps.pw-cache.outputs.cache-hit != 'true'
63 run: npx playwright install --with-deps chromium webkit
64
65 - name: Install Playwright system deps (cached)
66 if: steps.pw-cache.outputs.cache-hit == 'true'
67 run: npx playwright install-deps chromium webkit
68
69 - name: Run database migrations
70 run: npx prisma migrate deploy
71
72 - name: Seed E2E test users
73 run: npx tsx scripts/seed-e2e-users.ts
74 # Creates: [email protected], [email protected], [email protected]
75
76 - name: Build application
77 run: npm run build
78
79 - name: Run E2E shard
80 run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
81
82 - name: Upload blob report
83 uses: actions/upload-artifact@v4
84 if: always()
85 with:
86 name: e2e-blob-${{ matrix.shardIndex }}
87 path: blob-report/
88 retention-days: 1
89
90 - name: Upload failure artifacts
91 uses: actions/upload-artifact@v4
92 if: failure()
93 with:
94 name: e2e-failures-shard-${{ matrix.shardIndex }}
95 path: test-results/
96 retention-days: 5
97
98 merge-e2e-reports:
99 name: Merge E2E Reports
100 needs: [e2e]
101 runs-on: ubuntu-latest
102 if: always()
103 steps:
104 - uses: actions/checkout@v4
105 - uses: actions/setup-node@v4
106 with: { node-version: '20', cache: 'npm' }
107 - run: npm ci
108
109 - uses: actions/download-artifact@v4
110 with:
111 path: all-blobs
112 pattern: e2e-blob-*
113 merge-multiple: true
114
115 - run: npx playwright merge-reports --reporter html ./all-blobs
116
117 - uses: actions/upload-artifact@v4
118 with:
119 name: e2e-html-report
120 path: playwright-report/
121 retention-days: 14
122
123 - name: Check for failures
124 run: |
125 if ls all-blobs/*failed* 2>/dev/null; then
126 echo 'E2E tests had failures — check the HTML report artifact'
127 exit 1
128 fiThe 4-shard matrix runs tests in parallel on 4 separate runners — a 16-minute suite completes in 4 minutes. concurrency cancel-in-progress prevents queued runs from stacking up on force-push. Browser caching saves ~2 minutes per run after the first. The seed step creates test users so global-setup.ts can log in via API without a UI form. Failure artifacts (traces, screenshots, videos) are uploaded for 5 days — long enough to diagnose intermittent failures. The merge-e2e-reports job assembles all shard outputs into one HTML report, downloadable from the Actions run page.
The final E2E lesson covers best practices for the entire E2E testing discipline: deciding what to test with E2E vs integration tests, keeping E2E suites lean and fast, and maintaining tests as the application evolves.
Your progress is saved in this browser, on this device only.