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

跑得起來後的優化清單

  1. 拆 PR vs main 行為 yaml on: pull_request: branches: [main] PR 只跑 smoke + 改動的 spec / main 跑 full

  2. 依改動檔案決定跑什麼 yaml - uses: dorny/paths-filter@v3 id: filter with: filters: | frontend: - 'src/**' - if: steps.filter.outputs.frontend == 'true' run: npx playwright test

  3. 失敗時自動 retry 整個 job yaml - uses: nick-fields/retry@v3 with: max_attempts: 2 command: npx playwright test

  4. timeout 設嚴格 yaml jobs: test: timeout-minutes: 30 防止 hang job 燒 runner

  5. 失敗時通知 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 }}'

常見坑

  1. 快取沒 invalidate → 升 Playwright 後不更新瀏覽器 → 跑舊版 bug
  2. shard 不均勻 → 某個 shard 跑 30 分鐘其他 5 分鐘 → 用 --shard Playwright 自動分配,不要手動分檔案
  3. Test 改了 baseURL 但 secret 沒改 → 全 fail
  4. trace 沒上傳 → Debug 時沒料看
  5. PR 留言每次重發 → 蓋舊的不刪,PR 留言一坨。改用 peter-evans/find-comment 找舊的、更新而不是新增

最後

E2E + CI 跑通的那一刻,你會發現每個 PR 都自動驗證,QA 從「全部都要看」變「只看 CI 沒抓的」。剩下時間留給探索性測試、spec review、AI 工具 — 真正能加值的工作