GitHub Actions 自动化测试流水线踩坑实录:一个 `&&` 符号,折腾了 4 小时,但前端事故率降为 0

凌晨 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-unite2e-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 --- 备注"前端流水线"优先通过。

相关推荐
用户600071819101 小时前
【翻译】CSS 与 JavaScript:动画性能该怎么选
前端
还有多久拿退休金1 小时前
一行命令切换 Claude Code 的 AI 大脑:告别繁琐的 provider 切换流程
前端·ai编程
明月_清风1 小时前
爆破前端生态!Cloudflare 收购 Vite 背后,前端开发者会迎来什么变化?
前端·vite
光影少年1 小时前
react的useMemo 如何优化?
前端·react.js·掘金·金石计划
星栈1 小时前
Makepad、egui、Dioxus、Tauri:Rust GUI 到底怎么选
前端·rust
ai_coder_ai1 小时前
如何在自动化脚本中实现定时操作?
java·前端·javascript
努力早日退休1 小时前
一个 9999px 引发的跨平台血案:小程序离屏隐藏元素的滚动兼容性问题
前端·javascript
嘟嘟07172 小时前
前端异步编程完全指南:从json-server到DeepSeek大模型接口调用
前端
用户059540174462 小时前
大模型多轮对话“失忆”踩坑实录:一次线上事故让我排查了48小时,最终靠 Playwright + Pytest 把记忆锁死
前端·css