GitHub Actions × Playwright 完整實戰 — yml、PR 留言、Artifact、Sharding
Playwright 預設生出來的 yml 只能算「跑得起來」。要 production-ready 還缺:artifact 上傳、PR 留言、平行 sharding、快取。這篇給你一份能直接 copy 的 yml。
起點:預設 yml 不夠用
npm init playwright@latest 會幫你生這個:
name: Playwright Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
問題: 1. 沒快取 → 每次重裝 Playwright 瀏覽器(>1 GB、~2 分鐘) 2. PR 看不到測試結果 → 要點進 Actions 找 artifact 3. 只跑單一 worker → 慢 4. Trace、screenshot 沒分開 upload → 點開 zip 找半天
Production-ready yml(直接 copy)
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
# 同個 PR 推新 commit 時,取消舊跑的
concurrency:
group: e2e-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
name: E2E (Shard ${{ matrix.shard }}/4)
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false # 一個 shard 壞不影響其他
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install deps
run: npm ci
# ⚡ Playwright 瀏覽器快取(最大時間殺手)
- name: Get Playwright version
id: pw-version
run: |
echo "version=$(npm ls @playwright/test --json | jq -r '.dependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT
- name: Cache Playwright browsers
id: pw-cache
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: pw-browsers-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
- name: Install Playwright browsers
if: steps.pw-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps
- name: Install system deps only (cache hit)
if: steps.pw-cache.outputs.cache-hit == 'true'
run: npx playwright install-deps
# 跑測試(用 shard 平行)
- name: Run Playwright tests
run: npx playwright test --shard=${{ matrix.shard }}/4
env:
BASE_URL: ${{ secrets.STAGING_URL }}
TEST_USER: ${{ secrets.TEST_USER }}
TEST_PASS: ${{ secrets.TEST_PASS }}
# 上傳 HTML report
- name: Upload HTML report
if: always()
uses: actions/upload-artifact@v4
with:
name: html-report-shard-${{ matrix.shard }}
path: playwright-report/
retention-days: 7
# 上傳 traces(失敗 case 的,給 debug 用)
- name: Upload traces
if: failure()
uses: actions/upload-artifact@v4
with:
name: traces-shard-${{ matrix.shard }}
path: test-results/
retention-days: 7
# 合併 4 個 shard 報告 + PR 留言
report:
name: Merge reports & comment PR
if: always()
needs: [test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- name: Install deps
run: npm ci
# 下載所有 shard 的 blob report
- name: Download blob reports
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Merge into single HTML report
run: npx playwright merge-reports --reporter html ./all-blob-reports
- name: Upload merged HTML report
uses: actions/upload-artifact@v4
with:
name: html-report-merged
path: playwright-report/
retention-days: 14
# 從 JSON summary 抽 pass/fail 數字
- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const summary = JSON.parse(fs.readFileSync('./all-blob-reports/summary.json', 'utf8'));
const { passed, failed, skipped, duration } = summary;
const status = failed > 0 ? '❌ Failed' : '✅ Passed';
const body = `## ${status} — Playwright E2E
| Passed | Failed | Skipped | Duration |
|--------|--------|---------|----------|
| ${passed} | ${failed} | ${skipped} | ${(duration/1000).toFixed(1)}s |
📊 [Full HTML report](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body,
});
關鍵設計解釋
1. Concurrency cancel-in-progress
同 PR 推新 commit 時,自動取消正在跑的 CI,省 runner minutes。main 不取消(避免漏跑)。
2. Playwright 瀏覽器快取
最大時間殺手是 playwright install --with-deps(裝 Chromium + Firefox + WebKit 約 1-2 分鐘)。
關鍵:key 要包 Playwright 版本,不然版本升上去快取就壞了。
升版時自動 cache miss、重灌。
3. Shard 平行
npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4
4 個 runner 同時跑、總時長除以 4。Matrix 跑出來 4 個 job 一起。
幾個 shard 合適? 看你的測試數: - < 50 個 test → 不用 shard - 50-200 → shard 2-4 - 200+ → shard 4-8
shard 太多會被 GitHub Actions free tier minute 燒爆。
4. Trace 跟 HTML report 分開傳
- HTML report:永遠傳,給 review 看
- Trace:只有 fail 時傳,因為很大(單個 case 可能 5-50 MB)
Trace viewer 開啟:
npx playwright show-trace trace.zip
直接看到當下 DOM、network、console、step screenshot。Trace 是 debug 神器。
5. PR 自動留言
把測試結果 inline 在 PR 上,review 不用點 Actions。這是 dev 最有感的 QA 投資。
設定要點
playwright.config.ts(搭配上面 yml)
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0, // CI 上 fail retry 2 次
workers: process.env.CI ? 4 : undefined,
reporter: process.env.CI
? [['blob'], ['html', { open: 'never' }], ['json', { outputFile: 'summary.json' }]]
: 'html',
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'retain-on-failure', // fail 才存 trace
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
// 起步先單一瀏覽器、穩定後加
// { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
// { name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
Secrets 設定
GitHub repo → Settings → Secrets and variables → Actions:
STAGING_URL— staging 環境網址TEST_USER/TEST_PASS— 測試用帳號
不要把這些寫死在 yml 或 code。
跑得起來後的優化清單
-
拆 PR vs main 行為
yaml on: pull_request: branches: [main]PR 只跑 smoke + 改動的 spec / main 跑 full -
依改動檔案決定跑什麼
yaml - uses: dorny/paths-filter@v3 id: filter with: filters: | frontend: - 'src/**' - if: steps.filter.outputs.frontend == 'true' run: npx playwright test -
失敗時自動 retry 整個 job
yaml - uses: nick-fields/retry@v3 with: max_attempts: 2 command: npx playwright test -
timeout 設嚴格
yaml jobs: test: timeout-minutes: 30防止 hang job 燒 runner -
失敗時通知 Slack
yaml - if: failure() && github.ref == 'refs/heads/main' uses: slackapi/slack-github-action@v1 with: channel-id: 'qa-alerts' slack-message: 'main E2E failed: ${{ github.event.head_commit.message }}'
常見坑
- 快取沒 invalidate → 升 Playwright 後不更新瀏覽器 → 跑舊版 bug
- shard 不均勻 → 某個 shard 跑 30 分鐘其他 5 分鐘 → 用
--shardPlaywright 自動分配,不要手動分檔案 - Test 改了 baseURL 但 secret 沒改 → 全 fail
- trace 沒上傳 → Debug 時沒料看
- PR 留言每次重發 → 蓋舊的不刪,PR 留言一坨。改用
peter-evans/find-comment找舊的、更新而不是新增
最後
E2E + CI 跑通的那一刻,你會發現每個 PR 都自動驗證,QA 從「全部都要看」變「只看 CI 沒抓的」。剩下時間留給探索性測試、spec review、AI 工具 — 真正能加值的工作。