完整 GitHub Actions workflow
# .github/workflows/ci.yml name: CI on: push: branches: [main] pull_request: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_SIGNATURE_KEY }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # 必须!--filter=[origin/main] 需要历史 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm - run: pnpm install --frozen-lockfile - name: Build / Test / Lint run: pnpm turbo run build test lint --filter="...[origin/main]"
关键配置点
fetch-depth: 0
checkout 默认只拉一层——changed-since filter 对比不了。0 = 全量历史。
TURBO_TOKEN / TURBO_TEAM
远程缓存认证。TEAM 是 Vercel team slug,放 vars(非 secret)里方便查看。
concurrency + cancel-in-progress
新 push 取消旧 run——避免老 PR 的缓存污染,也省 CI 额度。
pnpm install --frozen-lockfile
CI 要求 lockfile 严格匹配——防止意外升级依赖导致 hash 不对。
PR 分支只读缓存
env: TURBO_REMOTE_CACHE_READ_ONLY: ${{ github.event_name == 'pull_request' && 'true' || 'false' }}
PR 只读,main push 才写——防止 PR 的中间构建污染主缓存。
按任务拆 job
jobs: build: steps: - run: pnpm turbo run build --filter="...[origin/main]" test: steps: - run: pnpm turbo run test --filter="...[origin/main]" lint: steps: - run: pnpm turbo run lint --filter="...[origin/main]"
三个 job 并行跑——每个 job 独立用远程缓存,失败互不影响。比单 job 跑 build test lint 快。
独立 job 的缓存协作
第一个 job 跑 build,产物上传远程缓存;第二个 job 跑 test(依赖 build)——直接从远程缓存拉产物,不用自己再 build。Turbo 协议让多 job 天然共享。
第一个 job 跑 build,产物上传远程缓存;第二个 job 跑 test(依赖 build)——直接从远程缓存拉产物,不用自己再 build。Turbo 协议让多 job 天然共享。
turbo-ignore:Vercel 部署跳过
// vercel.json { "ignoreCommand": "npx turbo-ignore" }
# Vercel 构建前会运行这个命令 # 非 0 退出 → 跳过部署 # 0 退出 → 继续部署 npx turbo-ignore # 1. 读当前项目的 package.json name # 2. 跑 turbo run build --dry-run --filter=...[HEAD^1] # 3. 当前 project 没在任务列表 → 跳过
多 app 仓库的 Vercel 配置
apps/ ├── web/ ← Vercel Project A ├── docs/ ← Vercel Project B └── admin/ ← Vercel Project C 每个 Vercel Project: - Root Directory: apps/web(或 docs / admin) - Build Command: cd ../.. && turbo run build --filter=@myorg/web - Install Command: cd ../.. && pnpm install --frozen-lockfile - Ignored Build Step: npx turbo-ignore
push 一次,三个 project 各自决定要不要部署:
改 packages/ui → web + docs 受影响 → 这两个部署;admin 不部署 改 apps/admin/src → 只 admin 部署 改 README.md → 三个都不部署(inputs 排除 .md)
Changesets + Turbo 发版
# .github/workflows/release.yml name: Release on: push: branches: [main] jobs: release: runs-on: ubuntu-latest permissions: contents: write pull-requests: write id-token: write steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm - run: pnpm install --frozen-lockfile - uses: changesets/action@v1 with: publish: pnpm release # 跑 package.json 的 release script env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: true TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }}
// 根 package.json { "scripts": { "release": "turbo run build --filter=./packages/* && changeset publish" } }
流程:
- PR 附带 changeset 文件(
pnpm changeset交互生成) - merge 到 main → release workflow 触发
- changesets/action 先开一个 "Version Packages" PR,累积版本号
- merge 那个 PR → changesets publish 发到 npm
- 发版前 turbo run build(可能命中 PR 留下的缓存,几秒完成)
Nightly 构建 warm 缓存
on: schedule: - cron: '0 3 * * *' # 每天 3AM jobs: warm-cache: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: pnpm install - run: pnpm turbo run build # 全 build,填远程缓存
有规律的 nightly:第二天开发者 clone/checkout 新分支,直接命中 → 本地 0 build 时间。大团队常见。
CI 时长监控
- name: Build with summary run: pnpm turbo run build --summarize - name: Upload summary uses: actions/upload-artifact@v4 with: name: turbo-summary path: .turbo/runs/
// 分析脚本 const fs = require('fs'); const files = fs.readdirSync('.turbo/runs'); const latest = JSON.parse(fs.readFileSync(`.turbo/runs/${files[0]}`)); const hitRate = latest.executionSummary.cached / latest.executionSummary.attempted; console.log(`Cache hit rate: ${(hitRate * 100).toFixed(1)}%`);
把 hit 率发到 DataDog / Grafana,低于 80% 触发告警——可能有人在乱改 globalDependencies。
self-hosted runner
runs-on: [self-hosted, linux, x64]
自建 runner:.turbo/cache 持久化在 runner 上——本地缓存都不用再跑。加上远程缓存双保险,命中率拉满。
Vercel 上游部署
GitHub push
├── GitHub Actions: 跑 ci.yml(test/lint/build)
└── Vercel: 自动拉代码部署
→ ignoreCommand 判断要不要部署
→ 需要部署时,自己也跑 turbo run build
→ 和 GH Actions 共享 remote cache
→ 通常直接命中 → 部署 < 30s
Vercel 和 GH Actions 的缓存互通
关联同一 Vercel team slug,GH Actions 构建产物上传后,Vercel 部署拉同一缓存——你的 CI 跑完 build,Vercel 秒部署。省双倍构建时间。
关联同一 Vercel team slug,GH Actions 构建产物上传后,Vercel 部署拉同一缓存——你的 CI 跑完 build,Vercel 秒部署。省双倍构建时间。
不同 env 的构建
jobs: build-staging: env: NEXT_PUBLIC_API_URL: https://staging-api.example.com steps: - run: pnpm turbo run build --filter=@myorg/web build-production: env: NEXT_PUBLIC_API_URL: https://api.example.com steps: - run: pnpm turbo run build --filter=@myorg/web
env 不同 → hash 不同 → 两个产物各自缓存。staging 部署命中 staging 缓存,production 命中 production 缓存——互不干扰。
实战时间对比
| 阶段 | 无 Turbo | Turbo + Remote Cache |
|---|---|---|
| CI 冷跑(全 miss) | 6 分钟 | 6 分钟(首次构建缓存) |
| CI PR 改 1 文件 | 6 分钟 | 40 秒 |
| CI rerun 同一 commit | 6 分钟 | 30 秒 |
| Vercel 部署 | 5 分钟 | 30 秒(复用 CI) |
| 开发者 git pull + build | 2 分钟 | 10 秒 |
Turborepo 配置 checklist
✓ fetch-depth: 0
changed-since filter 的前提
✓ TURBO_TOKEN + TURBO_TEAM
远程缓存认证
✓ TURBO_REMOTE_CACHE_SIGNATURE_KEY
缓存投毒保护
✓ frozen-lockfile
pnpm install 严格模式
✓ concurrency group
取消旧 run
✓ --filter="...[origin/main]"
只跑受影响的
✓ turbo-ignore in vercel.json
跳过无关部署
本章小结
- GH Actions:pnpm + Turbo + remote cache +
...[origin/main]filter,PR 时间降一个量级 concurrency+cancel-in-progress省额度,fetch-depth: 0让 filter 能工作turbo-ignore让多 app 仓库里无关改动跳过 Vercel 部署- Changesets + Turbo 组合发版,
releasescript 里先 build 后 publish - CI 和 Vercel 共享远程缓存——push 一次,GH Actions 构建,Vercel 秒部署