启用合并队列
合并队列(merge queue),也称为提交队列(commit queue)或合并列车(merge train),通过提供两个关键功能,改善了持续集成(CI)系统:
- 增加安全性,通过在合并 Git 分支之前而非之后对其进行验证,避免构建中断
- 提高吞吐量,通过智能地结合工作或并行化任务
合并队列可以是流行的 CI 系统(如 GitHub 或 GitLab)的内置功能, 或者可能是一个附加服务。
动机示例
假设拉取请求 1 和 2 正等待合并到您的main
分支,它们的分支分别命名为pr1
和pr2
。传统上有几种基本的验证方法:
缓慢但安全:我们用
start
来指代main
分支的最新提交。 CI 系统创建一个临时分支start+pr1
(将start
与pr1
合并)。 我们构建这个“热合并”,如果成功,现在我们可以将 PR 1 合并到main
。 如果 PR 2 有正在进行的构建,它应该被中止,因为main
已经改变。它的热合并需要 用start+pr1+pr2
重做,因为这是 PR 2 合并后将在main
中的内容。 这种方法确保了main
中每个提交的正确性。然而,在一个活跃的 monorepo 中, 很快就会积累很多临时分支,因为最终合并的构建根本没有被并行化。乐观:不那么严格,我们可以选择允许 PR 2 仅在
start+pr2
构建成功的情况下合并, 即使最终提交将是start+pr1+pr2
。实际上,我们希望如果start+pr1
和start+pr2
构建成功,那么start+pr1+pr2
也会成功。这通常是正确的, 但例如,如果 PR 1 重命名了一个 API,而 PR 2 引入了对该 API 的新调用,那么它们的 组合将失败,尽管它们单独成功。乐观方法明显更快,因为 PR 1 和 PR 2 可以并行构建 并且以任何顺序合并。然而,每当
main
分支被破坏时,都是一个不幸的事件, 需要撤销 PR 或合并修复以恢复到良好状态。根据支持人员的不同, 这可能需要几小时甚至几天,在此期间每个人的工作都被打断。 在一个繁忙的 monorpeo 中,这些故障的代价会十分高昂盲目乐观:值得一提的是,早期系统甚至没有执行热合并。 它们使用了乐观策略,但基于一个可能非常过时的
main
基础。 可能会使用策略来限制基础可以有多老,以小时或 Git 提交为度量。
合并队列如何帮助
我们首先做出一个决定,安全是毋庸置疑的:在 PR 1 合并到main
之后,
我们不会接受基于start+pr2
成功构建的 PR 2。为了安全起见,我们坚持要求
start+pr1+pr2
的成功构建。
合并队列的一个重要策略是start+pr1+pr2
可以更早开始。
这里是一个
假设的时间表:
时间 | PR 1 | PR 2 | start+pr1 构建 | start+pr2 构建 | start+pr1+pr2 构建 |
---|---|---|---|---|---|
1:00 | 创建 | ||||
1:01 | . | 开始 | |||
2:00 | . | 创建 | . | ||
2:01 | . | . | . | 开始 | 开始 |
4:00 | . | . | . | . | . |
5:00 | . | . | 成功 | . | . |
5:01 | 合并 | . | . | . | |
5:02 | . | 取消 | . | ||
6:00 | . | . | |||
7:00 | . | 成功 | |||
7:01 | 合并 |
为什么我们要构建start+pr2
,只是为了稍后取消它吗?如果是 PR 2 的构建提前完成,那么实际的工作流程
可能看起来是这样的:
时间 | PR 1 | PR 2 | start+pr1 构建 | start+pr2 构建 | start+pr1+pr2 构建 |
---|---|---|---|---|---|
1:00 | 创建 | ||||
1:01 | . | 开始 | |||
2:00 | . | 创建 | . | ||
2:01 | . | . | . | 开始 | 开始 |
4:00 | . | . | . | . | . |
5:00 | . | . | . | 成功 | . |
5:01 | . | 合并 | . | . | |
5:02 | . | 取消 | . | ||
6:00 | . | . | |||
7:00 | . | 成功 | |||
7:01 | 合并 |
既然最终会合并到 main
分支,那么是否应该有一个额外的列用于 start+pr2+pr1
呢?不,检出的文件与 start+pr1+pr2
是相同的。构建验证只关心源文件内容,而不关心其 Git 历史。
注意,随着活跃 PR 的数量增加,分支组合的数量也会呈指数级增长。
例如,如果我们有三个同时进行的 PR,可能需要六个任务来处理
start+pr1
、start+pr2
、start+pr3
、start+pr1+pr2
、start+pr2+pr3
以及 start+pr1+pr2+pr3
。
构建所有组合可能会迅速耗尽我们的机器资源。
为了避免资源成本激增,我们可以跳过那些看起来相对不太可能的组合,并且平均而言仍然可以从并行性中受益。极端的例子是,如果我们对 PR 1、PR 2 和 PR 3 成功有高度信心,可能我们只需要一个任务 start+pr1+pr2+pr3
;其他组合只有在它失败时才尝试。显然,这种精细化实现的队列有更多的机会可以使合并效率显著高于基础的合并队列。
利用 Rush 工作区依赖
🚧 即将推出:此功能尚未准备好。
继续上面的例子,假设 PR 1 是对 project-a
的修复,而 PR 2 是对 project-b
的修复;
也就是说,每个 PR 的 Git 差异只影响一个项目文件夹下的文件路径。假设在 Rush 工作区内,没有其他项目依赖于 project-a
或 project-b
。这意味着:
- 通过
rush build --from project-a
构建的源代码,对于分支start+pr1
和start+pr1+pr2
是相同的。 - 通过
rush build --from project-b
构建的源代码,对于分支start+pr2
和start+pr1+pr2
是相同的。
这些假设保证了 PR 1 和 PR 2 是完全独立的。我们可以独立地构建它们,并安全地以任何顺序合并它们的分支。合并队列根本不需要构建 start+pr1+pr2
。
接下来,假设 project-b
的 package.json 文件指定了对 project-a
的依赖。
在这种情况下,PR 就不再独立:在 PR 1 合并后,PR 2 只有先验证start+pr1+pr2
后才能安全合并。
这种分析依赖于对文件夹之间依赖关系的了解,这在不同的编程语言和构建系统之间差异很大。即使在 JavaScript 的生态系统内,对 package.json 文件的解释也需要对 PNPM、Rush+PNPM、Yarn 等进行特别考虑。
合并队列通常提供了一种基本设施来描述文件夹依赖关系,可能使用一个 glob 模式来描述如下的静态关系:
- "这个文件夹包含 JavaScript 代码,而那个文件夹包含 Golang 代码, 所以它们之间不可能有任何依赖。" 或
- "这个文件夹只包含非可构建文件,例如文档,因此忽略那里的任何差异。"
然而,在一个繁忙的 monorepo 中,有成百上千个项目,优化合并队列需要
准确地模拟项目文件夹之间的细粒度依赖关系。为此,我们正在协作
一种与语言无关的
project-impact-graph.yaml 规范,
对于合并队列这样的服务而言,可以用来查询任何 monorepo 中任何编程语言的项目依赖。
使用 Rush 插件,这个 YAML 文件将通过 rush update
生成并提交到 Git,这使得
合并队列服务能够高效地查询任何分支的文件夹依赖关系,而无需进行 Git 检出。
流行的合并队列
建议在您的 monorepo 中使用合并队列。以下是一些可能的选项:
- GitHub 包括一个内置的 合并队列 可以与 GitHub Actions 一起使用或单独使用
- Mergify 为 GitHub 提供了一个附加服务,具有高级优化功能。 有关设置详情,请参阅 集成:将 Mergify 与 Rush 一起使用。
- GitLab 包括一个内置的 合并列车 功能
如果您的组织使用的 Rush 相关合并队列未在上面列出,请添加它。