启用合并队列
一个**合并队列**(也称为**提交队列**或**合并列车**)通过提供两个关键功能来改进持续集成 (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
中每个提交的正确性。但是,在一个活跃的单一存储库中,积压很快就会堆积起来,因为最终被合并的构建根本没有并行化。**乐观:**为了不那么严格,我们可以选择允许 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 或合并修复以恢复到良好状态。根据支持人员,这可能需要几个小时甚至几天的时间,在此期间,每个人的工作都会中断。在一个流量很大的单一存储库中,这些事件会变得非常昂贵。**天真地乐观:**值得一提的是,早期的系统甚至没有执行热合并。他们使用乐观策略,但使用了可能非常过时的
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 | 合并 |
不应该为 start+pr2+pr1
添加一个额外的列吗,因为这是最终出现在 main
中的内容?不,签出的文件与 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 代码,因此它们之间不可能有任何依赖关系。” 或者
- “这个文件夹只包含非可构建文件,例如文档,因此忽略那里的任何差异。”
但是,在一个繁忙的单一存储库中,包含数百或数千个项目,优化合并队列需要准确地模拟项目文件夹之间的细粒度依赖关系。为此,我们正在合作开发一种语言无关的 project-impact-graph.yaml 规范,合并队列等服务可以使用它来查询任何编程语言的任何单一存储库中的项目依赖关系。使用 Rush 插件,此 YAML 文件将由 rush update
生成并提交到 Git,这使得合并队列服务能够有效地查询任何分支的文件夹依赖关系,而无需 Git 签出。
流行的合并队列
建议在您的单一存储库中使用合并队列。以下是一些可能的选项
- GitHub 包含一个内置的 合并队列,它可以与 GitHub Actions 一起或单独使用
- Mergify 为 GitHub 提供了一种带有高级优化的附加服务。请参阅 集成:将 Mergify 与 Rush 结合使用,了解设置详细信息。
- GitLab 包含一个内置的 合并列车 功能
如果您的组织正在将合并队列与 Rush 结合使用,但它未在上面列出,请将其添加。