AI 参与说明(Agent:Codex):本文由 Codex 根据作者提供的主题与公开资料辅助整理。实现示例适用于以 JavaScript/TypeScript 为主、使用 package manager workspace 与 GitHub Actions 的 Monorepo;多语言部分只讨论选型边界,落地前还需验证对应 Plugin 与 Project Graph。受监管发布流程和自建 CI 需要重新评估。资料核验于 2026-08-26,核验范围包括 Turborepo 2.10.x(
turbo query自 2.9 起成为 Stable 接口)与 Nx v23 官方文档。
一个 Monorepo 同时包含多个可独立部署的应用时,常见需求是:某次提交只修改了一个应用,就只部署这个应用;修改了共享包,则部署所有真正受到影响的应用。
当前推荐:默认使用 Turborepo,满足条件时直接选择 Nx
本文的默认前提是:纯 JavaScript/TypeScript Monorepo 已使用 pnpm/npm/Yarn workspace,存在共享 package,并且确实需要 Task Graph、缓存与 Affected 传播。此时当前推荐方案是 Turborepo + 薄适配器 + GitHub Actions 动态 matrix:以最近一次整批成功部署的 SHA 为 base,查询受影响任务,经过白名单校验后按应用部署。
Nx 可以替换 Turborepo 的 Monorepo 任务编排层,而且按能力广度衡量,Nx 更强。但它不能替换 Git、package manager 或 GitHub Actions,也不是把 turbo 命令机械改名为 nx 的 drop-in replacement。
如果仓库已经稳定使用 Turborepo,仅为了“Nx 功能更多”而迁移通常没有足够收益。若从零开始且已明确需要成熟的多语言 Project Graph、插件推断、代码生成、架构约束或 Nx Agents,则直接选择 Nx。具体命令见第 2 节,完整选型顺序见第 8 节。
| 当前仓库状况 | 当前推荐 |
|---|---|
| 尚未选型的纯 JavaScript/TypeScript workspace,核心需求是 Affected CI、Task Graph 与缓存 | 选择 Turborepo |
| 已稳定使用 Turborepo 或 Nx,且暂时没有明确的能力缺口 | 保持现状,不迁移 |
| 从零开始,且已确认需要成熟的多语言建模、Plugins、Generators、架构治理或 Nx Agents | 直接选择 Nx |
因此,本文后续的主实现采用 Turborepo;Nx 是满足明确升级条件时的替代方案,不是无条件的默认答案。
这不是 Git、GitHub Actions 或 Turborepo 单独完成的。完整链路通常由三层协作:
| 层次 | 提供的信息 | 不负责什么 |
|---|---|---|
| Git | 两个版本之间改了哪些文件 | 不理解应用、包和部署单元 |
| Monorepo 任务图工具 | 文件属于哪个包、哪些包依赖它、哪些任务受影响 | 不管理云端权限和部署审批 |
| CI 平台 | 提供事件、Runner、Job、Secret、Environment 和部署状态 | 默认不理解工作区依赖图 |
一句话概括:Git 提供变更事实,Turborepo 一类工具计算影响范围,GitHub Actions 一类 CI 平台执行部署。
1. “Changed”与“Affected”不是一回事
假设仓库包含三个 Workspace:
apps/
checkout-web/
admin-web/
packages/
shared-ui/
其中 checkout-web 依赖 shared-ui,admin-web 不依赖它。
- 修改
apps/checkout-web/**:直接变化的包和受影响的应用都是checkout-web。 - 修改
packages/shared-ui/**:直接变化的包是shared-ui,受影响的应用还包括checkout-web。 admin-web没有依赖这两个包,不应被部署。
如果只运行:
git diff --name-only "$BASE_SHA" "$HEAD_SHA"
Git 可以列出变化文件,但不会回答“哪个应用依赖 shared-ui”。这部分必须由声明过的 Workspace 依赖图或人工映射补上。Git git diff 文档
2. Turborepo 如何计算受影响任务
Turborepo 同时使用:
- Git 历史:找出基线之后发生变化的文件。
- Package Graph:根据 Workspace 的
package.json依赖关系连接各个包。 - Task Graph:根据
turbo.json中的任务和dependsOn推导任务关系。
当前更适合 CI 选择阶段的机器接口是:
pnpm exec turbo query affected \
--tasks deploy \
--base="$BASE_SHA" \
--head="$HEAD_SHA"
turbo query 从 Turborepo 2.9 起成为 Stable 接口;旧版本应先评估升级,或继续使用后文的 turbo run ... --dry=json。Turborepo 2.9
各部分含义如下:
--tasks deploy:只返回名为deploy的受影响任务。--base与--head:显式固定 Git 比较范围,避免 CI 环境中的默认分支引用产生歧义。- JSON 中的
affectedTasks.items与length:分别提供任务列表与数量;每个结果还包含reason,便于区分文件变化、依赖变化和保守回退。
turbo query affected 默认按 task inputs 做任务级判断:任务自身输入变化,或其上游任务受影响时,该任务才进入结果。Package Graph、Task Graph 与 inputs 必须正确声明,否则再精确的查询命令也无法修复错误的依赖模型。Turborepo query 参考
例如,让应用的 deploy 依赖自身 build,再让 build 依赖上游 Package 的 build,共享包变化才会沿 Task Graph 传播到应用的 deploy。部署具有外部副作用,因此显式关闭缓存:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"deploy": {
"dependsOn": ["build"],
"cache": false
}
}
}
每个可部署应用还需要在自己的 package.json 中声明 deploy script。outputs 应替换为实际构建工具的输出目录;这里的 dist/** 只是通用示例。
如果选择阶段还需要完整的 Task Graph、任务依赖、输入输出与缓存 hash,则继续使用执行命令的 dry run:
TURBO_SCM_BASE="$BASE_SHA" \
TURBO_SCM_HEAD="$HEAD_SHA" \
pnpm exec turbo run deploy --affected --dry=json
两者的分工是:turbo query affected 回答“哪些任务受影响以及为什么”,turbo run ... --dry=json 回答“如果运行,这份完整任务计划是什么”。
Turborepo 还提供更简洁的写法:
pnpm exec turbo run build lint test --affected
官方文档说明,--affected 默认等价于基于 main...HEAD 的 affected 过滤,并包含依赖变化包的下游包。它默认按 package 判断;Turborepo 2.10.x 只有启用 futureFlags.affectedUsingTaskInputs 后,才改用 task inputs。它只适合基线确实是 main 的 PR;目标分支不是 main 时,应显式设置 TURBO_SCM_BASE 与 TURBO_SCM_HEAD,或者使用支持 --base 与 --head 参数、默认任务级判断的 turbo query affected。默认分支 push 的持续部署也应显式提供可靠范围。Turborepo run 参考 Turborepo CI 指南
为什么还会有一层自定义脚本
Turborepo 可以直接执行受影响任务,但真实部署通常还需要一层很薄的适配器:
Git event
-> resolve base/head
-> Turborepo affected query
-> retain deployable applications
-> validate an allowlist
-> emit CI output or matrix
-> deploy exact applications
这层脚本不是重新实现 Git diff 或依赖图,而是负责产品无关的编排策略,例如:
- 什么样的 Package 才是可部署应用。
- 没有受影响应用时是否跳过 Deploy Job。
- 生产环境无法计算 diff 时应停止还是全量部署。
- 如何把包列表转换为 CI Job output 或 matrix。
- 如何保证每个部署子进程只得到自己的配置。
一个简化的解析示例:
const result = JSON.parse(turboOutput);
const applications = [
...new Set(
result.data.affectedTasks.items
.map((task) => task.package.name)
.filter((name) => deployableApplications.has(name))
),
];
生产实现还应验证 JSON Schema、拒绝未知包名,并对空列表和异常路径编写测试。
当 Git ref 不存在或 source control 查询失败时,Turborepo 会保守地把全部视为 affected。任务级查询只负责生成部署集合;选择脚本若还要区分“真实全量变化”与“保守回退”,应另外执行 Package 级诊断:
pnpm exec turbo query affected \
--packages \
--base="$BASE_SHA" \
--head="$HEAD_SHA"
然后检查 data.affectedPackages.items[].reason.__typename 中是否存在 GitRefNotFound、ScmError 等 repository-wide reasons,再执行预先声明的“全量部署”或“fail closed”策略,而不是把诊断失败当作普通变更或空列表。
3. GitHub Actions 在其中负责什么
GitHub 的 push 事件可以提供 push 之前的 SHA,它适合只比较本次 push;生产持续部署更稳健的基线仍是最近一次成功部署的 SHA。GitHub Actions 还可以把一个 Job 产生的 JSON output 转成后续 Job 的动态 matrix。GitHub PushEvent GitHub Actions 动态 matrix
下面给出 select 与 deploy 两个核心 Job 的结构。示例假设根 package.json 的 packageManager 字段已经锁定 pnpm 10 的精确版本,并使用 Node.js 24;如果仓库版本不同,应同步替换 Runner 配置。最终推进成功 SHA 的 Job 取决于状态存储后端,因此不在这个片段中伪造通用实现。
jobs:
select:
runs-on: ubuntu-latest
outputs:
applications: ${{ steps.plan.outputs.applications }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- id: plan
env:
# 示例占位:生产环境应从可靠的部署状态存储读取并在部署成功后更新
BASE_SHA: ${{ vars.LAST_SUCCESSFUL_DEPLOY_SHA }}
HEAD_SHA: ${{ github.sha }}
run: node scripts/select-affected-apps.mjs
deploy:
needs: select
if: needs.select.outputs.applications != '[]'
runs-on: ubuntu-latest
strategy:
matrix:
application: ${{ fromJSON(needs.select.outputs.applications) }}
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm exec turbo run deploy --filter="${{ matrix.application }}"
需要注意:
pnpm/action-setup从packageManager读取 pnpm 版本;应提交 lockfile,并在两个 Job 中使用--frozen-lockfile,避免选择阶段与部署阶段得到不同依赖图。- 还需要一个依赖
select与deploy的record-successJob:select必须成功,deploy则必须成功,或因应用列表为空而按预期跳过;满足条件后再把本次HEAD_SHA原子写入可靠的状态存储。否则LAST_SUCCESSFUL_DEPLOY_SHA会一直停留在旧值。 - Deploy Job 继续通过
turbo run deploy执行,才能兑现前文的deploy -> build -> ^buildTask Graph 并复用构建缓存。若改用pnpm --filter ... run deploy,则deployscript 必须自行完成构建,且不会经过 Turborepo 的任务编排与缓存。 fetch-depth: 0很重要。Turborepo 官方说明,Git 历史过浅时无法正确比较,可能把所有包视为已变化。- 选择阶段通常不需要生产 Secret。先计算白名单,再让真正的 Deploy Job 绑定 Environment 和最小权限。
- 动态 matrix 适合每个应用需要独立失败、重试和权限的场景;如果多个应用必须作为一个批次串行发布,也可以输出逗号列表交给单个 Job。
4. 最容易忽略的基线问题
“与哪个 SHA 比较”比具体命令更重要。
4.1 PR 检查
PR 通常关心“当前分支相对目标分支新增了什么”,适合使用 merge base、--affected,或者显式的 base/head。
4.2 默认分支持续部署
直接使用 push 事件的 previous SHA,只能覆盖本次 push:
previous push SHA -> current SHA
它有一个经典漏洞:
- 提交 A 修改应用甲,但部署失败。
- 提交 B 只修改应用乙。
- B 若只与 A 比较,就只会部署应用乙。
- 应用甲仍未上线,却不会自动重试。
更稳健的生产基线是:
last successful deployment SHA -> current SHA
Nx 的 CI 文档也建议把 base 设置为默认分支最近一次成功 CI 的提交,这样失败运行中的变化不会从后续 affected 集合中丢失。Nx Affected
本文默认采用一个“整批成功 SHA”:只有选择 Job 和全部 deploy matrix job 都成功后,最终 Job 才推进这个标记。这样不会漏掉失败应用,但下一次运行可能重复部署上次已经成功的应用,因此部署操作应保持幂等。若每个应用必须独立推进版本,则应保存 per-application SHA 或未完成应用集合;这会换取更高的状态管理复杂度。
如果暂时无法可靠保存“最近成功 SHA”,需要明确选择一种失败策略:
- 保守部署:无法计算时全量部署,成本更高但不漏应用。
- Fail closed:生产环境停止并要求人工重试,适合部署带有高风险副作用的系统。
- 显式重放:失败 Job 保存应用列表,下一次运行先合并未完成集合。
不要静默返回空列表;它会把“计算失败”伪装成“没有变化”。
5. 方案选项与适用场景
5.1 GitHub Actions paths
on:
push:
paths:
- "apps/checkout-web/**"
- "packages/shared-ui/**"
优点是无额外工具、容易理解。缺点是每个应用通常需要一套 Workflow 或重复的路径映射;共享包与下游应用的关系也要人工维护。
GitHub 会用 changed files 判断是否触发整个 Workflow,但这不是 Workspace 依赖图。官方还记录了特殊边界:超大 push、diff 超时和 changed-files 数量上限会改变过滤行为;必需检查被路径过滤跳过时还可能保持 Pending。GitHub Actions 路径过滤
适用场景:应用很少、彼此独立、共享目录关系稳定。
5.2 原生 Git + 自定义映射
git diff --name-only -z "$BASE_SHA" "$HEAD_SHA"
然后将 apps/foo/** 映射到 foo,将 packages/ui/** 映射到所有 UI 消费者。
优点是跨语言、完全可控、没有 Monorepo 工具依赖。缺点是依赖关系需要自己维护;重命名、锁文件、根配置和共享脚本的影响规则也需要自己测试。处理文件名时应使用 -z,避免空格和特殊字符破坏 shell 解析。
适用场景:异构仓库、包数量少,或者已有权威的服务目录和依赖清单。
5.3 pnpm Workspace Filter
pnpm 自身也支持 Git ref 和依赖方向:
pnpm --filter "...[origin/main]" run test
[origin/main] 选择变化的 Workspace,前缀 ... 加入其 dependents。它比手写路径映射更懂 Workspace 依赖,又比完整任务图工具轻。pnpm Filtering
适用场景:纯 JavaScript/TypeScript Monorepo,只需要按包执行命令,对跨任务缓存和复杂 Task Graph 没有需求。
5.4 Turborepo
优势包括 Package Graph、Task Graph、并行执行、本地缓存、Remote Cache、Git filter,以及两类机器接口:turbo query affected 输出受影响任务与原因,turbo run ... --dry=json 输出完整任务计划。Turborepo 的 Remote Cache 既有托管服务,也公开了 API 供自建实现。Turborepo Remote Caching
Turborepo 也提供 turbo generate,并正在实验 turbo boundaries。因此“更聚焦”不等于“完全没有代码生成或边界检查”;与 Nx 的差异主要在能力覆盖、集成深度和成熟度,而不是简单的有无。Turborepo generate Turborepo boundaries
Turborepo 2.10.x 还提供 Experimental 的原生 Cargo/Rust 与 uv/Python workspace 支持,因此不能简单称它为“只支持 JavaScript/TypeScript”。不过,当仓库要求成熟、覆盖面更广的 polyglot integration,尤其是 first-party Maven、Gradle 与 .NET Plugins 时,Nx 仍有更明确的优势。Turborepo Rust 指南 Turborepo Python 指南 Nx Multi-Language Support
主要约束是依赖必须正确声明。应用实际导入共享包、却没有在 package.json 中声明 Workspace 依赖时,任何基于 Package Graph 的 affected 计算都可能漏掉下游应用。
适用场景:JavaScript/TypeScript Monorepo,需要统一运行、缓存和筛选测试、构建及部署任务,但暂时不需要完整的代码生成、架构约束与跨 Runner 自动分发体系。这也是本文默认前提下的当前推荐。
5.5 Nx
Nx 同样组合 Git 历史与 Project Graph,并提供:
pnpm exec nx affected -t build --base="$BASE_SHA" --head="$HEAD_SHA"
Nx 提供 Project Graph、Task Graph、本地缓存、Affected、Plugins、Generators 和交互式图等能力。Remote Cache 可以通过 Nx Cloud 等后端共享;Nx Agents 跨机器任务分发则要求 workspace 连接 Nx Cloud。Nx Agents 会根据任务依赖和历史耗时在多台机器间分配任务,不能把“本地使用 Nx”与“已经拥有分布式 CI”混为一谈。Nx Affected Nx Remote Caching Nx Agents
适用场景:Project Graph 复杂,需要插件推断、代码生成、Migrations、module boundary rules、Release Management、多语言建模,或者确实需要 Nx Cloud 分布式执行的 Monorepo。
5.6 Bazel
Bazel 把源文件、规则和 Target 建模为更细粒度的构建图。其 Query Language 可以通过 rdeps(universe, changed_targets) 查询反向依赖闭包,再结合可复现构建和远程缓存执行受影响 Target。Bazel Query Reference
它适合大型、多语言、对可复现性和增量构建要求极高的仓库,但 BUILD 规则、工具链和迁移成本明显高于 Turborepo 或 Nx。
6. Nx 能否替换 Turborepo
可以替换,但不是 drop-in replacement。两者在本文架构中处于同一层:都能负责任务编排、Affected 计算和缓存。切换到 Nx 后,Git 仍负责变更事实,pnpm/npm/Yarn 仍负责 workspace 与依赖安装,GitHub Actions 仍负责 Job、Secret、Environment 与部署。
Nx v23 的官方迁移文档说明,npx nx@latest init 可以识别现有 turbo.json、生成 nx.json,并让两个工具在迁移期间并存;但按 Package 覆盖的任务、persistent、环境变量、Inputs 与其他配置语义仍需要逐项核对。只有 Nx CI 验证通过后,才应删除 Turborepo 配置和依赖。Nx:Migrating from Turborepo to Nx
| 能力轴 | Turborepo | Nx | 判断 |
|---|---|---|---|
| 核心任务编排 | Package Graph + Task Graph | Project Graph + Task Graph | 两者都能满足常规 JS/TS CI |
| Affected 自动化 | turbo query affected 可按 task inputs 查询并输出原因;dry run 可输出完整计划 | nx affected 结合 Git 与 Project Graph,并可用 nx graph --affected 可视化 | 精度取决于依赖和 inputs 建模,不应只按品牌判断 |
| 项目建模 | 主要依赖 package manager 建立的 workspace 关系 | 可通过 Plugins、项目配置和工具配置扩展 Project Graph | Nx 的建模面更广 |
| 本地与远程缓存 | 本地缓存;托管或自建 Remote Cache | 本地缓存;可连接 Nx Cloud 等 Remote Cache 后端 | 两者都具备,部署副作用都不应缓存 |
| 跨 Runner 任务分发 | 通常由 CI 自行分片和维护 | Nx Cloud 的 Nx Agents 可按任务图和历史耗时分发 | Nx 更强,但依赖 Nx Cloud |
| 工程治理 | 有代码生成与实验性 Boundaries,整体仍聚焦任务运行与缓存 | Plugins、Generators、Migrations、module boundary rules、Release Management 等集成更深 | Nx 的能力面更广 |
| 引入与心智负担 | 对现有 JS/TS workspace 更聚焦 | 可渐进引入,但可选能力和升级面更多 | 只需任务运行与缓存时,Turborepo 更直接 |
所以,“哪个更强”必须先定义强的含义:
- 按产品能力广度、项目治理和分布式 CI 上限衡量,Nx 更强。
- 按纯 JavaScript/TypeScript 仓库完成 Affected 构建与部署的最小必要复杂度衡量,Turborepo 通常更合适。
- 对已经稳定运行的 Turborepo 仓库,只有在 Plugins、架构约束、多语言 Project Graph 或 Nx Agents 等能力能解决实际问题时,迁移到 Nx 才有明确价值。
替换工具也不会自动解决错误的 base/head、浅克隆、漏声明依赖、失败后未重放或部署步骤被错误缓存等生产问题。两者都需要处理这些风险,但依赖发现方式与 inputs、lockfile 的失效规则并不完全相同,迁移时必须重新验证。
7. 方案比较
| 方案 | Git 变更 | 依赖图 | 任务图/缓存 | 维护成本 | 主要选择条件 |
|---|---|---|---|---|---|
GitHub paths | 内置 | 无 | 无 | 低到中,应用越多重复越多 | 少量独立应用 |
| Git + 自定义映射 | 原生 | 自己维护 | 自己维护 | 中到高 | 小型异构仓库 |
| pnpm Filter | 内置 | Workspace Graph | 较弱 | 低 | 只需按变化 Package 执行命令 |
| Turborepo | 内置 | Package Graph | 有 | 中 | JS/TS,聚焦运行、缓存与 Affected |
| Nx | 内置 | Project Graph | 有,治理能力更强 | 中到高 | 需要插件治理、多语言图或分布式 CI |
| Bazel | 需接入变更到 Target 映射 | Target Graph | 很强 | 高 | 大型、多语言仓库 |
8. 推荐决策
本文给出的选择顺序不是工具清单,而是一组明确的默认值与升级条件:
- 符合本文默认前提:选择 Turborepo,以
pnpm exec turbo query affected --tasks deploy --base="$BASE_SHA" --head="$HEAD_SHA"加薄适配器作为选择阶段,再交给 GitHub Actions 动态 matrix 部署。 - 已经稳定使用 Turborepo:保留现状;不要只因为 Nx 功能更多而迁移。
- 从零开始且已明确需要多语言 Project Graph、Generators、module boundary rules、Release Management 或跨 Runner 分发,并已确认对应 Nx Plugin 能正确建模:直接选择 Nx,不必先用 Turborepo 过渡。
- 只需按变化 workspace 执行命令、不需要 Task Graph 与缓存:选择 pnpm Filter。
- 只有两三个互不依赖的应用:选择 GitHub
paths,接受手工路径映射。 - 多语言巨型仓库且要求更细粒度 Target、可复现构建与远程执行:再评估 Bazel。
无论选择哪种工具,生产级实现至少应满足:
- 明确 base/head 的语义,并优先记录最近一次成功部署 SHA。
- CI Checkout 提供足够的 Git 历史。
- 依赖关系进入版本控制,不靠隐藏约定。
- 选择结果经过可部署应用白名单校验。
- “无变化”“计算失败”“首次运行”是三个不同状态。
- 选择阶段不接触部署 Secret。
- 为单应用变化、共享包变化、锁文件变化、失败后重试和全量 fallback 编写测试。
参考资料
- Turborepo:
turbo query affected - Turborepo 2.9:
turbo queryStable - Turborepo:
turbo runCLI - Turborepo 2.10
- Turborepo:Package and Task Graphs
- Turborepo:Remote Caching
- Turborepo:
turbo generate - Turborepo:
turbo boundaries - Turborepo:Rust(Experimental)
- Turborepo:Python(Experimental)
- Turborepo:Constructing CI
- Git:
git diff - GitHub Actions:路径过滤与 Git diff 比较
- GitHub Actions:动态 matrix
- pnpm:Filtering
- Nx:Run Only Tasks Affected by a PR
- Nx:Multi-Language Support
- Nx:Remote Caching
- Nx:Distribute Task Execution
- Nx:Migrating from Turborepo to Nx
- Nx:Nx vs. Turborepo
- Bazel:Query Language