---
title: GitHub Actions × Playwright 完整實戰 — yml、PR 留言、Artifact、Sharding
description: 從零配出 production-ready 的 Playwright CI。完整 yml、失敗截圖/trace 自動上傳、PR 自動留言報告、平行 sharding 加速。
category: automation
tags: [github-actions, playwright, cicd, e2e, ci-yml]
date: 2026-06-09
---

# GitHub Actions × Playwright 完整實戰 — yml、PR 留言、Artifact、Sharding

Playwright 預設生出來的 yml 只能算「跑得起來」。要 production-ready 還缺：**artifact 上傳**、**PR 留言**、**平行 sharding**、**快取**。這篇給你一份能直接 copy 的 yml。

## 起點：預設 yml 不夠用

`npm init playwright@latest` 會幫你生這個：

```yaml
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）

```yaml
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 平行

```bash
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 開啟：

```bash
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）

```typescript
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 工具 — **真正能加值的工作**。
