你以为只改一个包,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]' 做了两件事:

  1. 找出 main 到当前 HEAD 改了哪些包
  2. 自动加上这些包的下游消费者(你改了 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.jsonturbo.jsonpnpm-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 的成本降下来,迭代速度才能真正上去。

评论区

0 条评论

登录后可评论。