凌晨 2:17,手机震了三下,产品群连发 9 条消息:"登录页白屏,所有用户进不去!"你光脚坐到电脑前,扫了一眼上次发版的 commit------前端小哥改了一个公共组件,没人跑过测试。
你掏一根烟,在烟雾里想到:这事本来不用发生的。如果合并代码之前,有个机器人自动把测试全跑一遍,不通过就不让合,这个点你应该在睡觉,而不是在修一个"不可能发生"的 JS 错误。
这事其实每个人都知道要做,只是都觉得"下周再上自动化"。但下周往往是下一个 2:17。今天我们就用 0 成本 、纯 GitHub Actions 把这条自动化测试流水线搭起来------我踩过的坑,帮你排干净。
问题拆解:为什么"手动跑测试"等于没跑
前端项目越跑越快,但测试环节始终拉跨。根因很直白:
- 项目没有在 PR 合并前强制跑测试,靠的是口口相传的"你测了没?"。
- 开发机环境千奇百怪,本地能过的测试,CI 上常常炸得一塌糊涂。
- 真要搭 CI,传统 Jenkins 需要服务器、安装配置,小团队没人维护;GitLab CI 虽好,但如果你用 GitHub,还要额外注册 runner,心智负担不小。
为什么不直接用 GitHub Actions?它和仓库深度集成,对公开仓库免费额度极其大方(每个账户每月 2000 分钟),小团队跑前端测试基本不花钱。加上 Branch protection rules 卡死合并条件,测试不过的 PR 根本进不了主分支,这就是一堵免费的质量墙。
但真实落地没那么顺滑。接下来说我选型的思路,以及那两个把我折磨到天亮的坑。
方案设计:为什么选这组技术栈
我们的前端项目:React + TypeScript,使用 pnpm 包管理器,测试分了三个层级:
- ESLint + Prettier 静态检查:拦掉格式错误和不安全写法。
- Vitest 单元/组件测试:快,适合做提交前的快速门禁。
- Playwright 端到端测试:模拟真实浏览器,覆盖核心用户路径(登录、下单等)。
GitHub Actions 的 Workflow 设计为 单一文件,三个 Job 串行再部分并行:
PR 打开/推送 → lint → unit-test → e2e-test → 所有 job 通过 → PR 可合并
为什么不用其他方案?
- CircleCI / Travis CI:曾经很好,但现在免费额度砍得厉害,前端 e2e 时间长,分分钟用完。
- Jenkins:要自己维护服务器,插件多但配置复杂,前端团队没精力伺候它。
- 在 Git hooks 里跑测试 :可以本地卡,但容易被跳过 (
--no-verify),不能作为唯一卡口。
GitHub Actions 最大的优势是 声明式 + 零运维。你只需要一个 YAML 文件,GitHub 承担所有计算资源。对我们这种"没预算养 CI 机器"的团队,它就是最优解。
核心实现:从 YAML 到卡住 PR 的全过程
1. 基础流水线:一碰 PR 就自动开跑
下面这段 Workflow 解决"没人跑测试"的问题------只要有人针对 main 分支开 PR 或推送新 commit,自动触发全套检查。
yaml
# .github/workflows/pr-checks.yml
name: PR Checks
on:
pull_request:
branches: [main] # 所有想合入 main 的 PR 都跑
jobs:
lint-and-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
# 使用 pnpm 必须官方 action,顺带解决缓存问题
- uses: pnpm/action-setup@v3
with:
version: 9
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm run lint
- name: Unit tests
run: pnpm run test:unit
关键决策 :用 pnpm/action-setup 而不是手动 npm i -g pnpm,因为官方 action 会自动处理 pnpm 版本和存储路径,还能和缓存步骤无缝配合。--frozen-lockfile 确保 CI 环境不会偷偷改 lock 文件,防小聪明。
2. 端到端测试:Playwright 上场,浏览器不用装
单元测试跑得快,但挡不住"组件拆了但页面挂掉"的 Bug。Playwright 的 e2e 能模拟真实用户点击,可惜每次安装浏览器很慢。这段代码解决缓存浏览器二进制的问题,让 e2e 跑在合理时间。
yaml
e2e-test:
needs: lint-and-unit # 单元过了再跑贵的 e2e,省分钟
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: pnpm/action-setup@v3
with:
version: 9
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
# 缓存 Playwright 浏览器,跳过每次都下载 500MB
- name: Get installed Playwright version
id: playwright-version
run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('@playwright/test/package.json').version)")" >> $GITHUB_OUTPUT
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}
- name: Install Playwright browsers (if not cached)
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps
- name: Run Playwright tests
run: pnpm exec playwright test
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
3. 卡死合并按钮:测试不过,代码进不来
在 GitHub 仓库 Settings → Branches → Branch protection rules 中,针对 main 分支添加规则,勾选:
- Require status checks to pass before merging
- 在搜索框里勾选
lint-and-unit和e2e-test这两个 job name
这样即便管理员点"Merge",如果上面 Checks 没绿灯,按钮直接灰掉。真正实现"不跑通测试,别想合代码"。
踩坑记录:官方文档没告诉你的事
坑一:&& 符号在 run 命令里的诡异解析
现象 :我把 lint 和 unit test 写在一行:pnpm run lint && pnpm run test:unit。提交后日志报错 sh: 1: pnpm: not found,可明明 setup-node 已经装了 pnpm。同一个 YAML 拆成两个 step 又正常。
原因 :GitHub Actions 的 run 背后是 shell,当你在 run: 里写复杂指令时,如果没指定 shell: bash(或者默认的 bash 被某些 action 篡改),&& 可能导致环境变量污染或提前退出。更致命的是,如果前面某条命令用到了 secrets 或特殊字符,&& 的串联会让报错信息完全错位,根本查不到。
解决 :永远把逻辑上独立的命令拆成单独 step,或者用 run: | 多行写法并显式声明 shell: bash。在 CI 里不要省 step,可观测性比省几行 YAML 重要得多。我拆成 lint、test 两个 step 后,世界清静了。
坑二:pnpm 缓存在自定义 runner 上路径不对
现象 :按照社区文章,配置了 ~/.pnpm-store 缓存,但每次 workflow 都 miss cache,安装依赖耗时 3 分钟。检查发现 pnpm store path 返回的路径是 /home/runner/.local/share/pnpm/store/v3,不是常见的 ~/.pnpm-store/v3。
原因:pnpm v9 变更了存储路径的默认位置,不同操作系统还不一样。很多旧教程的写死路径直接失效。
解决 :必须用动态获取 store path 的方法,我在上面代码里已经给出:pnpm store path --silent 先拿到真实路径,再传给缓存 action。这个细节在 pnpm 官方文档里只有一行小字,首次踩坑大概率中招。
效果验证:用数据说话
上线 GitHub Actions 流水线并强制卡口之后,我们追踪了三个月的指标:
| 指标 | 引入前(半手动) | 引入后(强制门禁) |
|---|---|---|
| 月均前端线上故障次数 | 2 ~ 3 | 0 |
| PR 合并前测试通过率 | 约 65% | 100%(否则合不了) |
| 从开 PR 到合并的反馈时间 | 40 分钟以上 | 8 分钟内 |
| 因环境差异导致的"我电脑能跑" | 每月 4 ~ 5 次 | 0 |
最直观的变化是:再也没人半夜被叫起来修低级前端 Bug。开发开始信任"只有绿灯才能合",代码审查也从讨论格式变成了讨论架构。
可直接抄走的完整模板 + 开箱命令
把上面两份 YAML 合成一个 .github/workflows/pr-checks.yml 放到你的仓库,然后在分支保护里勾选两个 job,前端流水线就算搭完了。
如果你想让效果更炸,再加一行命令,把 Husky 的 pre-push hook 删掉:npm uninstall husky,让本地只管写代码,所有质量警察让 CI 当。
#GitHubActions #前端测试 #CI/CD #零成本
关于作者
我是暴富哥哥,一个天天跟线上故障、CI 流水线和性能天花板搏斗的后端/架构实战派。写的东西都是踩过坑、救过火的,不卖课、不吹水。
GitHub: github.com/baofugege --- 会陆续放出更多"拿来就能用"的模板仓库。
Sponsor: github.com/sponsors/ba... --- 如果这篇文章省了你一晚上的排错时间,请我喝杯咖啡,码字有劲。
服务: Python 后端性能优化 / 内部工具定制 / 技术咨询,用工程的思路解你的痛。联系方式 Telegram @baofugege --- 备注"前端流水线"优先通过。