你以为只改一个包,CI 就要把所有包跑一遍?今天这件事被 affected-only 执行彻底变了
配 monorepo 三年了,团队越做越大,包越拆越多,直到有一天——你只是想改一行 README 文档,结果 CI 跑了 12 分钟,整个 PR 等你审完的时候咖啡都凉了。
这不是你代码写得烂,这是 monorepo 的结构性矛盾:你的包之间有依赖图,CI 没有读懂它。
monorepo CI 的根本问题:组合爆炸
一个 30 个包的 repo,naive CI 配置是这样的:
- 每次 push PR,30 个包的测试全部跑一遍
- 加上面向多 Node 版本的 matrix 测试,60~90 个 job
- 一个 README 的修改,和一个核心库的重写,消耗完全一样
这不是工程问题,是数学问题:包的依赖关系是单向图,naive CI 当它是平面的。
同时,GitHub-hosted runner 有并发上限:免费版 20 jobs,Team 60 jobs。90 个 job 的 PR 要排队等资源,时间不是花在构建上,是花在了队列里。
affected-only 执行:只跑改了的,加上依赖它的
affected-only 逻辑很简单:这次 diff 改了哪些包,就只跑这些包的 CI,然后递归地把下游依赖包也跑一遍。
用 Turborepo:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 必须全量 history,否则比较不了
- run: pnpm install
- run: pnpm turbo test --filter='...[origin/main]'
--filter='...[origin/main]' 做了两件事:
- 找出 main 到当前 HEAD 改了哪些包
- 自动加上这些包的下游消费者(你改了 shared-ui,所有用了 shared-ui 的 app 都要重测)
用 Nx:
- uses: nrwl/nx-set-shas@v4
- run: npx nx affected -t test --parallel=3
Nx 会自动构建依赖图,然后跑受影响的任务图。
效果:90 个 job → 4~8 个 job,这才是真正的 order-of-magnitude 改善。
remote cache:别人跑过的,直接拿来用
affected-only 解决了「跑什么」的问题,但 CI 的第二个瓶颈是冷缓存:每个 job 第一次跑都要完整构建。
Turborepo remote cache 把构建产物缓存到 Vercel(免费),同一个 PR 内部不同 job 之间可以复用——张三跑过的构建结果,李四的 runner 直接下载使用。
- name: Build with remote cache
run: pnpm turbo run build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
Remote cache 叠加 affected-only,典型效果:
| 阶段 | naive | + affected | + remote cache |
|---|---|---|---|
| 安装依赖 | 2 min | 2 min | 30s(缓存命中) |
| 构建 | 5 min | 45s | 10s(缓存命中) |
| 测试 | 3 min | 30s | 10s(缓存命中) |
| 总计 | 12 min | 2 min | 50s |
两个坑:你以为装了 Turborepo 就完事了
坑一:fetch-depth: 0 不是默认的
actions/checkout@v4 默认 fetch-depth: 1(只拉最新一个 commit)。affected 检测需要完整 git history 来比较 origin/main 和当前分支。不加这个参数,Turborepo/Nx 都会 fallback 到跑全部包,等于没装。
坑二:根目录变更要触发全量 CI
package.json、turbo.json、pnpm-workspace.yaml 改了,所有包都可能受影响。这三个文件改了要强制全量 CI:
id: check-root
run: |
if git diff --name-only origin/main...HEAD | grep -qE '^(package.json|turbo.json|pnpm-workspace.yaml)$'; then
echo "root_changed=true" >> $GITHUB_OUTPUT
fi
name: Full CI
if: steps.check-root.outputs.root_changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: pnpm turbo run build test lint
下一步:两个检查清单
检查一:你的 monorepo 真实影响范围有多大?
# 看看你的包数量
ls packages/ apps/ | wc -l
# 手动模拟 affected 检测(改一个包看会触发哪些)
pnpm turbo run build --filter='...[origin/main]' --dry-run
如果包数量 > 20,装 Turborepo 的收益就很明显。
检查二:你的 CI 现在跑多少个 job?
去 GitHub Actions 页面看最近一次 PR 的 workflow run 数。如果 > 30,affected-only 执行是你能做的最高 ROI 的优化——不需要改一行业务代码,纯工程改善,10x 时间节省是常态。
monorepo 的问题不是「代码管理」,是「构建系统」。affected-only 把 CI 从「无脑全跑」变成「只跑需要的」,这是 monorepo 规模化的必备能力。12 分钟变成 2 分钟,团队等 CI 的成本降下来,迭代速度才能真正上去。
评论区
登录后可评论。