CI/CD 集成
要点
- 前面几篇把 Turborepo 的本地工作流打通了
- Turborepo 的远程缓存(Remote Caching)能把构建产物上传到云端,让任何机器——包括其他开发者的本地和 CI——都能共享同一份缓存
- 当 monorepo 里有很多 app 时,每次 CI 跑全量构建往往没必要
- monorepo 里有多个 app,每个 app 需要的环境变量不同,但也有一些是共享的
内容
1. 在本地跑顺了,在 CI 里还要再想一遍
前面几篇把 Turborepo 的本地工作流打通了。
到这里,你的开发体验已经相当顺手:缓存命中,任务并行,只有真正改到的包才会重新构建。
但 CI 环境是另一回事。
CI 的每次运行都是全新环境,没有本地那份 node_modules,也没有上次构建的缓存。如果不做额外配置,Turborepo 在 CI 里就和你单独跑 npm run build 没有什么差别——每次都全量构建。
这一篇讲两件事:如何在 CI 里用好 Turborepo,以及 monorepo 里的环境变量怎么管。
2. CI 里的远程缓存:让缓存跨越机器边界
Turborepo 的远程缓存(Remote Caching)能把构建产物上传到云端,让任何机器——包括其他开发者的本地和 CI——都能共享同一份缓存。
这意味着:一个 PR 在开发者本地构建过一次,推上去以后 CI 可以直接命中缓存,几秒内完成构建,而不是重新跑几分钟。
配置远程缓存:
Turborepo 官方提供的远程缓存服务叫 Vercel Remote Cache(已内置,免费额度足够用)。在本地登录一次:
// code.ts
npx turbo login
npx turbo link之后本地的 ~/.turbo/config.json 里会保存 token 和 team 信息。
在 CI 里,需要通过环境变量传入这两个值:
// code.ts
TURBO_TOKEN=your_token_here
TURBO_TEAM=your_team_slug或者也可以用 TURBO_REMOTE_CACHE_SIGNATURE_KEY 做产物签名验证(更安全,适合对缓存内容有安全要求的团队)。
3. GitHub Actions 完整配置示例
// code.ts
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Turborepo 需要对比 git diff,不能浅克隆
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'
- run: yarn install --frozen-lockfile
- run: turbo run build lint test几个注意点:
fetch-depth: 2:Turborepo 需要通过 git diff 判断哪些包发生了变化。如果用默认的浅克隆(只取最新一次提交),它无法对比,会退化成全量构建。至少要取 2 个提交。
--frozen-lockfile:CI 里安装依赖要锁定版本,防止因为 lockfile 不一致导致构建结果不可重复。
secrets:TURBO_TOKEN 必须存在 GitHub 仓库的 Secrets 里(Settings → Secrets and variables → Actions),不能明文写在 yaml 里。
4. 只构建受影响的包:--filter
当 monorepo 里有很多 app 时,每次 CI 跑全量构建往往没必要。Turborepo 的 --filter 标志可以限定只处理发生变化的包。
// code.ts
# 只构建相对于 main 分支有变化的包
turbo run build --filter="...[origin/main]"这个语法的意思是:找出从 origin/main 到当前提交,所有有文件变化的包,以及依赖它们的包。
在 PR 构建里,这个写法特别实用:如果你这次 PR 只改了 packages/ui,Turborepo 会自动发现 apps/web 依赖了 packages/ui,把这两个一起构建,而跳过 apps/api 等不相关的包。
// code.ts
- name: Build affected packages
run: turbo run build --filter="...[origin/${{ github.base_ref }}]"5. 环境变量在 monorepo 里的分层管理
monorepo 里有多个 app,每个 app 需要的环境变量不同,但也有一些是共享的。.env 文件的组织容易变乱。
一个清晰的分层方案:
// code.ts
my-monorepo/
├── .env ← 全仓库共享的变量(谨慎,通常为空)
├── .env.local ← 本地覆盖,加入 .gitignore
├── apps/
│ ├── web/
│ │ ├── .env ← web 应用的默认变量
│ │ └── .env.local ← web 的本地覆盖
│ └── api/
│ ├── .env ← api 服务的默认变量
│ └── .env.local ← api 的本地覆盖分层原则:
- 根目录 .env:只放真正全局的变量,比如
NODE_ENV。保持极简,避免把各 app 的变量混进来 - 各 app 的 .env:该 app 特有的配置,只对这个 app 生效
- .env.local:永远加入
.gitignore,用于存放本地开发的覆盖值和敏感 key
CI 里的环境变量:
在 CI 里不用 .env 文件,改用平台的 Secrets / Environment Variables 功能注入。这样敏感信息不会进入代码仓库,每个 app 的变量也可以分组管理。
6. NEXT_PUBLIC_* 变量和 Turborepo 缓存
这里有一个容易踩的坑,值得单独说一下。
NEXT_PUBLIC_* 变量在构建时会被打包进前端产物。如果你在 CI 里用了不同的 NEXT_PUBLIC_API_URL(比如 staging 环境和 production 环境用的是不同 URL),而 Turborepo 的 turbo.json 里没有把这个变量声明在 env 字段里,就可能出现这种情况:
staging 环境的构建缓存被 production 环境命中,导致 production 的产物里还是 staging 的 API URL。
这就是为什么上一篇要强调 env 字段的配置:
// code.ts
"build": {
"env": ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_APP_ENV"]
}凡是会影响构建产物内容的环境变量,都必须在 turbo.json 的 env 里声明,否则缓存对这些变量是盲目的。
7. 多环境部署的常见做法
对于需要区分 staging 和 production 的项目,一个常用的做法是用不同的 .env 文件:
// code.ts
# 在 CI 里根据分支选择配置
if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then
cp apps/web/.env.production apps/web/.env
else
cp apps/web/.env.staging apps/web/.env
fi
turbo run build或者更简洁地,用 Turborepo 的 --env-mode 控制环境变量的处理方式(Turborepo v2 新增的功能,可以更细粒度地控制哪些变量可以从外部注入)。
8. 总结
在 CI 里用好 monorepo,核心是两件事:
第一件:接入远程缓存。 配置 TURBO_TOKEN 和 TURBO_TEAM,让 CI 和本地共享同一份构建缓存。这件事做完,CI 的构建时间通常能降低 50%-80%。
第二件:管好环境变量。 在 monorepo 里,.env 按层级分放,敏感变量走 Secrets,以及把影响构建产物的变量都声明在 turbo.json 的 env 里,防止不同环境之间的缓存污染。
把这两件事做到位,monorepo 在 CI 里才能真正发挥出它的价值,而不只是比单仓库多了一堆目录。