AI 电子伴侣
创建时间: 2026-04-08最后更新: 2026-04-22

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
1
npx turbo login
2
npx turbo link

之后本地的 ~/.turbo/config.json 里会保存 token 和 team 信息。

在 CI 里,需要通过环境变量传入这两个值:

code.ts
1
TURBO_TOKEN=your_token_here
2
TURBO_TEAM=your_team_slug

或者也可以用 TURBO_REMOTE_CACHE_SIGNATURE_KEY 做产物签名验证(更安全,适合对缓存内容有安全要求的团队)。

3. GitHub Actions 完整配置示例

code.ts
01
# .github/workflows/ci.yml
02
name: CI
03
04
on:
05
push:
06
branches: [main]
07
pull_request:
08
09
jobs:
10
build:
11
runs-on: ubuntu-latest
12
13
env:
14
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
15
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
16
17
steps:
18
- uses: actions/checkout@v4
19
with:
20
fetch-depth: 2 # Turborepo 需要对比 git diff,不能浅克隆
21
22
- uses: actions/setup-node@v4
23
with:
24
node-version: 20
25
cache: 'yarn'
26
27
- run: yarn install --frozen-lockfile
28
29
- 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
1
# 只构建相对于 main 分支有变化的包
2
turbo run build --filter="...[origin/main]"

这个语法的意思是:找出从 origin/main 到当前提交,所有有文件变化的包,以及依赖它们的包。

在 PR 构建里,这个写法特别实用:如果你这次 PR 只改了 packages/ui,Turborepo 会自动发现 apps/web 依赖了 packages/ui,把这两个一起构建,而跳过 apps/api 等不相关的包。

code.ts
1
- name: Build affected packages
2
run: turbo run build --filter="...[origin/${{ github.base_ref }}]"

5. 环境变量在 monorepo 里的分层管理

monorepo 里有多个 app,每个 app 需要的环境变量不同,但也有一些是共享的。.env 文件的组织容易变乱。

一个清晰的分层方案:

code.ts
01
my-monorepo/
02
├── .env ← 全仓库共享的变量(谨慎,通常为空)
03
├── .env.local ← 本地覆盖,加入 .gitignore
04
├── apps/
05
│ ├── web/
06
│ │ ├── .env ← web 应用的默认变量
07
│ │ └── .env.local ← web 的本地覆盖
08
│ └── api/
09
│ ├── .env ← api 服务的默认变量
10
│ └── .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
1
"build": {
2
"env": ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_APP_ENV"]
3
}

凡是会影响构建产物内容的环境变量,都必须在 turbo.jsonenv 里声明,否则缓存对这些变量是盲目的。

7. 多环境部署的常见做法

对于需要区分 staging 和 production 的项目,一个常用的做法是用不同的 .env 文件:

code.ts
1
# 在 CI 里根据分支选择配置
2
if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then
3
cp apps/web/.env.production apps/web/.env
4
else
5
cp apps/web/.env.staging apps/web/.env
6
fi
7
8
turbo run build

或者更简洁地,用 Turborepo 的 --env-mode 控制环境变量的处理方式(Turborepo v2 新增的功能,可以更细粒度地控制哪些变量可以从外部注入)。

8. 总结

在 CI 里用好 monorepo,核心是两件事:

第一件:接入远程缓存。 配置 TURBO_TOKENTURBO_TEAM,让 CI 和本地共享同一份构建缓存。这件事做完,CI 的构建时间通常能降低 50%-80%。

第二件:管好环境变量。 在 monorepo 里,.env 按层级分放,敏感变量走 Secrets,以及把影响构建产物的变量都声明在 turbo.jsonenv 里,防止不同环境之间的缓存污染。

把这两件事做到位,monorepo 在 CI 里才能真正发挥出它的价值,而不只是比单仓库多了一堆目录。