580 lines
18 KiB
Markdown
580 lines
18 KiB
Markdown
# CI:GitHub Actions
|
||
|
||
> **使用时机**:在拉取请求、合并到主分支或按计划自动运行 Playwright 测试。GitHub Actions 是 Playwright 项目中最常用的 CI。
|
||
|
||
## 快速参考
|
||
|
||
```bash
|
||
# CI 的关键 CLI 标志
|
||
npx playwright install --with-deps # 安装浏览器 + 操作系统依赖
|
||
npx playwright test --shard=1/4 # 运行 4 个分片中的第 1 个
|
||
npx playwright test --reporter=github # 在 PR 上标注失败信息
|
||
npx playwright merge-reports ./blob-report # 合并分片报告
|
||
```
|
||
|
||
## 模式
|
||
|
||
### 模式 1:生产就绪工作流(可直接复制的起步模板)
|
||
|
||
**使用时机**:任何使用 GitHub Actions 的项目。这是一个完整且经过实战检验的工作流。
|
||
|
||
```yaml
|
||
# .github/workflows/playwright.yml
|
||
name: Playwright Tests
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
# 取消同一 PR/分支上的进行中运行
|
||
concurrency:
|
||
group: playwright-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
env:
|
||
CI: true
|
||
|
||
jobs:
|
||
test:
|
||
timeout-minutes: 30
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: 20
|
||
cache: "npm"
|
||
|
||
- name: Install dependencies
|
||
run: npm ci
|
||
|
||
- name: Cache Playwright browsers
|
||
id: playwright-cache
|
||
uses: actions/cache@v4
|
||
with:
|
||
path: ~/.cache/ms-playwright
|
||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||
|
||
- name: Install Playwright browsers
|
||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||
run: npx playwright install --with-deps
|
||
|
||
- name: Install Playwright OS dependencies
|
||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||
run: npx playwright install-deps
|
||
|
||
- name: Run Playwright tests
|
||
run: npx playwright test
|
||
|
||
- name: Upload HTML report
|
||
uses: actions/upload-artifact@v4
|
||
if: ${{ !cancelled() }}
|
||
with:
|
||
name: playwright-report
|
||
path: playwright-report/
|
||
retention-days: 14
|
||
|
||
- name: Upload test traces
|
||
uses: actions/upload-artifact@v4
|
||
if: failure()
|
||
with:
|
||
name: playwright-traces
|
||
path: test-results/
|
||
retention-days: 7
|
||
```
|
||
|
||
### 模式 2:使用矩阵策略的分片执行
|
||
|
||
**使用时机**:测试套件耗时超过 10 分钟。分散到多个并行运行器以缩短实际耗时。
|
||
**避免时机**:测试套件在 5 分钟以内——分片开销(检出、安装、合并)会抵消其带来的好处。
|
||
|
||
```yaml
|
||
# .github/workflows/playwright-sharded.yml
|
||
name: Playwright Tests (Sharded)
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
concurrency:
|
||
group: playwright-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
env:
|
||
CI: true
|
||
|
||
jobs:
|
||
test:
|
||
timeout-minutes: 20
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
shard: [1/4, 2/4, 3/4, 4/4]
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: 20
|
||
cache: "npm"
|
||
|
||
- name: Install dependencies
|
||
run: npm ci
|
||
|
||
- name: Cache Playwright browsers
|
||
id: playwright-cache
|
||
uses: actions/cache@v4
|
||
with:
|
||
path: ~/.cache/ms-playwright
|
||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||
|
||
- name: Install Playwright browsers
|
||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||
run: npx playwright install --with-deps
|
||
|
||
- name: Install Playwright OS dependencies
|
||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||
run: npx playwright install-deps
|
||
|
||
- name: Run Playwright tests (shard ${{ matrix.shard }})
|
||
run: npx playwright test --shard=${{ matrix.shard }}
|
||
|
||
- name: Upload blob report
|
||
uses: actions/upload-artifact@v4
|
||
if: ${{ !cancelled() }}
|
||
with:
|
||
name: blob-report-${{ strategy.job-index }}
|
||
path: blob-report/
|
||
retention-days: 1
|
||
|
||
merge-reports:
|
||
if: ${{ !cancelled() }}
|
||
needs: test
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: 20
|
||
cache: "npm"
|
||
|
||
- name: Install dependencies
|
||
run: npm ci
|
||
|
||
- name: Download all blob reports
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
path: all-blob-reports
|
||
pattern: blob-report-*
|
||
merge-multiple: true
|
||
|
||
- name: Merge reports
|
||
run: npx playwright merge-reports --reporter=html ./all-blob-reports
|
||
|
||
- name: Upload merged HTML report
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: playwright-report
|
||
path: playwright-report/
|
||
retention-days: 14
|
||
```
|
||
|
||
**分片配置**——添加 blob reporter 以便合并分片输出:
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
reporter: process.env.CI ? [["blob"], ["github"]] : [["html", { open: "on-failure" }]],
|
||
})
|
||
```
|
||
|
||
### 模式 3:可复用工作流
|
||
|
||
**使用时机**:多个仓库或多个工作流文件需要相同的 Playwright 配置。
|
||
**避免时机**:只有一个工作流的单个仓库。
|
||
|
||
```yaml
|
||
# .github/workflows/playwright-reusable.yml
|
||
name: Playwright Reusable
|
||
|
||
on:
|
||
workflow_call:
|
||
inputs:
|
||
node-version:
|
||
type: string
|
||
default: "20"
|
||
test-command:
|
||
type: string
|
||
default: "npx playwright test"
|
||
shard-total:
|
||
type: number
|
||
default: 1
|
||
secrets:
|
||
BASE_URL:
|
||
required: false
|
||
TEST_PASSWORD:
|
||
required: false
|
||
|
||
jobs:
|
||
test:
|
||
timeout-minutes: 30
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
shard: ${{ fromJson(format('[{0}]', join(fromJson(format('[{0}]', inputs.shard-total == 1 && '"1/1"' || '"1/4","2/4","3/4","4/4"')), ','))) }}
|
||
|
||
env:
|
||
CI: true
|
||
BASE_URL: ${{ secrets.BASE_URL }}
|
||
TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: ${{ inputs.node-version }}
|
||
cache: "npm"
|
||
|
||
- run: npm ci
|
||
|
||
- name: Cache Playwright browsers
|
||
id: playwright-cache
|
||
uses: actions/cache@v4
|
||
with:
|
||
path: ~/.cache/ms-playwright
|
||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||
|
||
- name: Install Playwright browsers
|
||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||
run: npx playwright install --with-deps
|
||
|
||
- name: Install Playwright OS dependencies
|
||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||
run: npx playwright install-deps
|
||
|
||
- name: Run tests
|
||
run: ${{ inputs.test-command }} --shard=${{ matrix.shard }}
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
if: ${{ !cancelled() }}
|
||
with:
|
||
name: playwright-report-${{ strategy.job-index }}
|
||
path: playwright-report/
|
||
retention-days: 14
|
||
```
|
||
|
||
**调用可复用工作流:**
|
||
|
||
```yaml
|
||
# .github/workflows/ci.yml
|
||
name: CI
|
||
on:
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
jobs:
|
||
e2e:
|
||
uses: ./.github/workflows/playwright-reusable.yml
|
||
with:
|
||
node-version: "20"
|
||
shard-total: 4
|
||
secrets:
|
||
BASE_URL: ${{ secrets.STAGING_URL }}
|
||
TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
|
||
```
|
||
|
||
### 模式 4:在容器中运行
|
||
|
||
**使用时机**:你需要一个与本地 Docker 运行一致的可重现环境,或者运行器的操作系统依赖引起问题。
|
||
**避免时机**:标准的 `ubuntu-latest` 配合 `--with-deps` 已经能正常工作(这是常见情况)。
|
||
|
||
```yaml
|
||
# .github/workflows/playwright-container.yml
|
||
name: Playwright (Container)
|
||
|
||
on:
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
jobs:
|
||
test:
|
||
timeout-minutes: 30
|
||
runs-on: ubuntu-latest
|
||
container:
|
||
image: mcr.microsoft.com/playwright:v1.52.0-noble
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: 20
|
||
|
||
- run: npm ci
|
||
|
||
# 无需安装浏览器——它们已在容器镜像中
|
||
- name: Run Playwright tests
|
||
run: npx playwright test
|
||
env:
|
||
HOME: /root # 在容器中以 root 身份运行时需要
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
if: ${{ !cancelled() }}
|
||
with:
|
||
name: playwright-report
|
||
path: playwright-report/
|
||
retention-days: 14
|
||
```
|
||
|
||
### 模式 5:环境密钥与部署目标
|
||
|
||
**使用时机**:测试针对需要身份验证凭据的预发/生产环境运行。
|
||
**避免时机**:测试只针对本地启动的开发服务器运行。
|
||
|
||
```yaml
|
||
# .github/workflows/playwright-staging.yml
|
||
name: Playwright (Staging)
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
workflow_dispatch:
|
||
|
||
jobs:
|
||
test:
|
||
timeout-minutes: 30
|
||
runs-on: ubuntu-latest
|
||
environment: staging # 包含保护规则的 GitHub Environment
|
||
|
||
env:
|
||
CI: true
|
||
BASE_URL: ${{ vars.STAGING_URL }}
|
||
TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
|
||
API_KEY: ${{ secrets.API_KEY }}
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: 20
|
||
cache: "npm"
|
||
|
||
- run: npm ci
|
||
|
||
- name: Cache Playwright browsers
|
||
id: playwright-cache
|
||
uses: actions/cache@v4
|
||
with:
|
||
path: ~/.cache/ms-playwright
|
||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||
|
||
- name: Install Playwright browsers
|
||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||
run: npx playwright install --with-deps
|
||
|
||
- name: Install Playwright OS dependencies
|
||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||
run: npx playwright install-deps
|
||
|
||
- name: Run tests against staging
|
||
run: npx playwright test --grep @smoke
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
if: ${{ !cancelled() }}
|
||
with:
|
||
name: staging-report
|
||
path: playwright-report/
|
||
retention-days: 14
|
||
```
|
||
|
||
### 模式 6:定时运行(夜间回归)
|
||
|
||
**使用时机**:完整回归套件对每个 PR 来说太慢。改为每晚针对主分支运行一次。
|
||
**避免时机**:测试套件在 15 分钟内跑完,且可以在每个 PR 上运行。
|
||
|
||
```yaml
|
||
# .github/workflows/playwright-nightly.yml
|
||
name: Nightly Regression
|
||
|
||
on:
|
||
schedule:
|
||
- cron: "0 3 * * 1-5" # UTC 时间凌晨 3 点,周一至周五
|
||
workflow_dispatch: # 允许手动触发
|
||
|
||
jobs:
|
||
test:
|
||
timeout-minutes: 60
|
||
runs-on: ubuntu-latest
|
||
|
||
env:
|
||
CI: true
|
||
BASE_URL: ${{ vars.STAGING_URL }}
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: 20
|
||
cache: "npm"
|
||
|
||
- run: npm ci
|
||
|
||
- name: Install Playwright browsers
|
||
run: npx playwright install --with-deps
|
||
|
||
- name: Run full regression suite
|
||
run: npx playwright test --grep @regression
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
if: ${{ !cancelled() }}
|
||
with:
|
||
name: nightly-report-${{ github.run_number }}
|
||
path: playwright-report/
|
||
retention-days: 30
|
||
|
||
- name: Notify on failure
|
||
if: failure()
|
||
uses: slackapi/slack-github-action@v1.27.0
|
||
with:
|
||
payload: |
|
||
{
|
||
"text": "Nightly Playwright regression failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||
}
|
||
env:
|
||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||
```
|
||
|
||
## 决策指南
|
||
|
||
| 场景 | 做法 | 原因 |
|
||
| -------------------------- | ------------------------------------------ | ------------------------------------------ |
|
||
| 小型套件(< 5 分钟) | 单个 job,不分片 | 分片开销超过节省的时间 |
|
||
| 中型套件(5-20 分钟) | 使用矩阵策略分为 2-4 个分片 | 将实际耗时缩短约 60-75% |
|
||
| 大型套件(20+ 分钟) | 4-8 个分片 + blob report 合并 | 将 PR 反馈时间控制在 10 分钟以内 |
|
||
| PR 上的跨浏览器测试 | PR 上仅 Chromium;主分支上测试所有浏览器 | PR 上的耗时减少 3 倍 |
|
||
| 预发/生产冒烟测试 | 使用 `environment:` 的独立工作流 | 隔离密钥,添加审批关卡 |
|
||
| 夜间完整回归测试 | `schedule` 触发器 + `workflow_dispatch` | 全面覆盖,不阻塞 PR |
|
||
| 多个仓库,相同配置 | 使用 `workflow_call` 的可复用工作流 | DRY 原则;更新一个文件,所有仓库受益 |
|
||
| 需要可重现环境 | 使用 Playwright 镜像的容器 job | 与本地 Docker 环境完全一致 |
|
||
|
||
## 反模式
|
||
|
||
| 反模式 | 问题 | 应改为 |
|
||
| ------------------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------- |
|
||
| 没有 `concurrency` 分组 | 每次推送都会产生重复运行,浪费分钟数 | 添加 `concurrency: { group: ..., cancel-in-progress: true }` |
|
||
| 分片时使用 `fail-fast: true` | 一个分片失败会取消其他分片,丢失它们的测试结果 | 设置 `fail-fast: false` 以收集所有失败 |
|
||
| 安装浏览器时未使用缓存 | 每次运行浪费 60-90 秒 | 以 lockfile 哈希为键,缓存 `~/.cache/ms-playwright` |
|
||
| 未设置 `timeout-minutes` | 卡住的 job 将运行 6 小时(GitHub 默认值) | 设置明确的超时时间:20-30 分钟 |
|
||
| 仅在失败时上传构建产物 | 测试通过时没有报告,无法验证结果 | 使用 `if: ${{ !cancelled() }}` 始终上传 |
|
||
| 在工作流文件中硬编码密钥 | 安全漏洞 | 使用 GitHub Secrets 和 Environments |
|
||
| 在每个 PR 上运行所有浏览器 | 成本增加 3 倍,收益甚微 | PR 上仅 Chromium;合并到主分支时做跨浏览器测试 |
|
||
| `actions/upload-artifact` 未设置保留期限 | 默认 90 天保留期会填满存储 | 为报告设置 `retention-days: 7-14` |
|
||
| 安装浏览器时未使用 `--with-deps` | 缺少操作系统库导致浏览器启动失败 | 始终使用 `npx playwright install --with-deps` |
|
||
|
||
## 故障排查
|
||
|
||
### 浏览器启动失败:"Missing dependencies"
|
||
|
||
**原因**:浏览器从缓存安装,但操作系统依赖未被缓存(它们位于系统目录中,而非 `~/.cache`)。
|
||
|
||
**修复**:在缓存命中时始终运行 `npx playwright install-deps`:
|
||
|
||
```yaml
|
||
- name: Install Playwright OS dependencies
|
||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||
run: npx playwright install-deps
|
||
```
|
||
|
||
### 测试本地通过但 CI 中超时失败
|
||
|
||
**原因**:CI 运行器的 CPU 核心数和 RAM 少于你的开发机器。默认的 worker 数和超时时间设置得太激进。
|
||
|
||
**修复**:在配置中为 CI 减少 worker 数并增加超时时间:
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
workers: process.env.CI ? "50%" : undefined,
|
||
use: {
|
||
actionTimeout: process.env.CI ? 15_000 : 10_000,
|
||
navigationTimeout: process.env.CI ? 30_000 : 15_000,
|
||
},
|
||
})
|
||
```
|
||
|
||
### 分片报告不完整——合并报告中缺少部分分片
|
||
|
||
**原因**:使用 `actions/download-artifact@v4` 时未设置 `merge-multiple: true`,或者分片之间的构建产物名称冲突。
|
||
|
||
**修复**:为每个分片赋予唯一的构建产物名称,并使用 `merge-multiple`:
|
||
|
||
```yaml
|
||
# 在每个分片 job 中上传
|
||
- uses: actions/upload-artifact@v4
|
||
with:
|
||
name: blob-report-${{ strategy.job-index }}
|
||
path: blob-report/
|
||
|
||
# 在合并 job 中下载
|
||
- uses: actions/download-artifact@v4
|
||
with:
|
||
path: all-blob-reports
|
||
pattern: blob-report-*
|
||
merge-multiple: true
|
||
```
|
||
|
||
### `webServer` 在 CI 中失败:"port 3000 already in use"
|
||
|
||
**原因**:前一次运行留下了僵尸进程,或者其他 job 步骤正在使用该端口。
|
||
|
||
**修复**:确保在 CI 中设置 `reuseExistingServer: false`,并添加一个前置步骤来杀死过期进程:
|
||
|
||
```yaml
|
||
- name: Kill stale processes
|
||
run: lsof -ti:3000 | xargs kill -9 2>/dev/null || true
|
||
```
|
||
|
||
### GitHub 注释未在 PR 上显示
|
||
|
||
**原因**:未配置 `github` reporter。
|
||
|
||
**修复**:为 CI 运行添加 `github` reporter:
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
reporter: process.env.CI
|
||
? [["html", { open: "never" }], ["github"]]
|
||
: [["html", { open: "on-failure" }]],
|
||
})
|
||
```
|
||
|
||
## 相关文档
|
||
|
||
- [ci/parallel-and-sharding.md](parallel-and-sharding.md) —— 分片策略与 blob report 合并
|
||
- [ci/reporting-and-artifacts.md](reporting-and-artifacts.md) —— reporter 配置与构建产物管理
|
||
- [ci/docker-and-containers.md](docker-and-containers.md) —— CI 容器镜像
|
||
- [ci/ci-gitlab.md](ci-gitlab.md) —— GitLab CI 对应方案
|
||
- [ci/ci-other.md](ci-other.md) —— CircleCI、Azure DevOps、Jenkins
|
||
- [core/configuration.md](../core/configuration.md) —— 感知 CI 的配置设置
|