Monorepo 同一次 push 触发两条 CI,Web 白跑一遍的根因与修法
项目是一个 Monorepo,同时产出两类构建产物:一个 Web 前端(部署到 CDN),一个桌面客户端(Tauri / Electron 打包 dmg/exe)。为了分别控制发布节奏,建了两条 GitHub Actions 工作流:deploy-web.yml 由分支推送触发,desktop-build.yml 由 tag 触发。
某次只是给桌面端做版本号 bump,推送 commit + tag。打开 Actions 看到:Web 的 CI 也在跑,而且跑完了,再看 deploy 日志——Web bundle 内容和上一次完全一样,这次根本没有任何前端源码被改动。
现象
git commit -m "chore: bump desktop to v0.1.13"
git push # → deploy-web.yml 开始运行(浪费)
git tag desktop-v0.1.13
git push origin desktop-v0.1.13 # → desktop-build.yml 开始运行(正常)Actions 面板里同一个 commit SHA 上同时出现两个绿色进度圈。Web 这条从头到尾 rebuild + deploy,耗时 3 分钟,输出和上一次毫无二致。
根因
push.branches: [main] 这个触发条件对 所有 推送到 main 的 commit 无差别触发,不管这次 commit 改了什么文件。桌面版本 bump 的 commit 只动了 desktop/src-tauri/tauri.conf.json、update.json 和几个桌面专属脚本,和 Web 的 src/、public/ 一个字节都没碰,但 GitHub Actions 不知道这些——它只认触发条件,不看 diff。
push.branches: [main] 只是一个 ref 过滤器,不是 path 过滤器。只要有 commit 推到 main,不管改了哪些文件,都会触发。Monorepo 里多产物共享同一条主干时,这个默认行为会造成大量 CI 浪费——改桌面跑 Web build,改文档跑 Web build,改发布脚本跑 Web build。
正解
给 deploy-web.yml(即 branch-triggered 那条)加 paths-ignore,把属于另一个产物域的所有文件列进去:
# .github/workflows/deploy-web.yml
on:
push:
branches: [main]
paths-ignore:
# 桌面端源码树
- 'desktop/**'
# 桌面端专属 CI 工作流(改它不应该重新部署 Web)
- '.github/workflows/desktop-build.yml'
# 桌面端专属发布脚本
- 'scripts/upload-desktop-installers.mjs'
- 'scripts/upload-update-json.mjs'
- 'scripts/build-update-json.sh'
# 桌面端生成的 release manifest
- 'update.json'
# 纯文档,不影响任何构建
- '**.md'
workflow_dispatch: # ← 必须保留,用于手动强制重跑paths-ignore 的核心判断原则:
| 放进去 | 不能放进去 |
|--------|-----------|
| 另一产物域的源码树 desktop/** | 共享源码 src/**、lib/** |
| 另一产物域的 CI 文件 | 当前工作流自身文件(要能立即验证 CI 配置修改) |
| 另一产物域的发布脚本 | package.json、lockfile(影响所有产物) |
| 生成的 release manifest | |
| **.md | |
只在 branch-triggered 工作流上加 paths-ignore,tag-triggered 那条不用动——tag 本来就只在你主动创建 tag 时才触发,不存在误触发问题。同时一定要保留 workflow_dispatch:,这样当你确实需要在桌面 bump 之后强制重跑 Web deploy(比如改了环境变量),可以用 gh workflow run deploy-web.yml 手动触发,而不需要 push 一个假的 commit。
排查:如果已经在跑,先取消
发现时两条流水线都在运行,先取消那条浪费的:
# 列出最近几条运行,找到要取消的 ID
gh run list --limit 5 --json databaseId,workflowName,headSha,status
# 取消
gh run cancel <DATABASE_ID>然后在 follow-up commit 里加上 paths-ignore,之后就不会再误触发。
一句话外卖
Monorepo 多产物共主干时,branch-triggered CI 默认是无差别广播,
paths-ignore是给它装上产物域过滤器的最小代价方案——但别忘了同时留一个workflow_dispatch出口。