RFC-0148:CI 指南

RFC-0148:CI 准则
状态已接受
领域
  • 开发者
  • 治理
说明

为 Fuchsia 生态系统中的项目和基础架构负责人打造可持续的 CI(持续集成)体验的指南。

Gerrit 更改
  • 613084
作者
审核人
提交日期(年-月-日)2021-12-02
审核日期(年-月-日)2022-01-18

总结

面向 Fuchsia 生态系统中的项目和基础架构负责人创建可持续的 CI(持续集成)体验的指南。

设计初衷

在 2021 年年中之前,我们将大部分源代码和预构建文件集中放置在一棵“紫红色树”中。因此,基础架构及其所有者主要致力于支持这棵树。

每当有新的树外项目(例如 RFC-0095)提出时,树内贡献者可能最近就会成为树外贡献者。树外 CI 系统应提供与树内体验相当或更好的体验,并且体验应该足够熟悉,以便顺畅地在项目之间切换。否则,在树外工作意味着工作效率损失,可能会阻碍平台的发展。

同时,基础架构团队的规模将无法相对于树外项目的数量线性扩缩。我们需要将 CI 功能从“主要针对 Fuchsia 项目定制”到“可供 Fuchsia 生态系统中的许多项目使用”。否则,每个项目都需要自定义基础架构和自己的专门维护人员。

在过去几年中,从构建和维护 Fuchsia 的 CI 中学到的经验可以为我们今后在项目基础架构集成方面采取的操作、继续和/或避免奠定基础。归根结底,我们的 CI 系统旨在让我们的项目易于更改、难以破坏且可高效交付:此 RFC 为项目和基础架构所有者提供了简要建议,以便上述系统能够最有效地实现这些目标。

利益相关方

教员

  • Hunter Freyer (hjfreyer@google.com)

审核者

  • Aidan Wolter (awolter@google.com) - 产品组装
  • Chase Latta (chaselatta@google.com) - 产品开发套件
  • David Gilhooley (dgilhooley@google.com) - 驱动程序
  • Jiaming Li (lijiaming@google.com) - 产品开发套件,工作站 OOT
  • Marc-Antoine Ruel (maruel@google.com) - 工程生产力
  • Nicolas Sylvain (nsylvain@google.com) - 工程生产力
  • Renato Mangini Dias (mangini@google.com) - Bazel

咨询人员

  • Anirudh Mathukumilli (rudyMathu@google.com) - 基础基础架构
  • Nathan Mulcahey (nmulcahey@google.com) - 基础基础架构
  • Oliver Newman (olivernewman@google.com) - 平台基础架构
  • Petr Hosek (phosek@google.com) - 工具链
  • Sébastien Marchand (sebmarchand@google.com) - 第一方基础架构

社交

此设计最初通过 Fuchsia 工程生产力邮件列表进行社交,在 Google 文档中进行迭代,并与利益相关方分享,以确定上一部分中列出的审核人员。然后,它遵循 RFC 模板转换为 Markdown,并进入 RFC“迭代”阶段。

设计

下面的“避免”小节列出了会对项目的 CI、项目贡献者和/或基础架构所有者产生负面影响的常见误区。相反,“必选”和“考虑”小节则是有助于解决上述陷阱的指南等。它们并未构成详尽列表:不包括性能跟踪、稳定性检测等方面的注意事项,这些注意事项也可能会改善长期项目运行状况,但对于最低可行的 CI 实现而言,并不是必需的。

避免:基础架构依赖于项目内部

如果基础架构依赖于项目内部,则双方都会更难以更改。在 Fuchsia 工作期间,在进行看似良性的更改时遇到基础架构尖锐的问题一直是困扰,而这也是贡献者对工程流程抱怨的较大原因之一。

例如,过去的基础架构知道 Fuchsia 构建系统的许多(并且仍然知道部分)内部细节,这在开发中创造了清晰的优势,也就是说,如果 Fuchsia 版本违反了基础架构的任何预期,不能随意进行更改。基础架构代码不与 Fuchsia 代码一起使用,因此其预期很难找到:它们通常只有在出现故障时,在提交前或提交后运行时才会知道。其他有害示例包括检出分支中的基础架构硬编码路径、测试名称等。这类引用往往会自然而然地累积,随着时间的推移,这种引用会产生越来越多的阻碍。

为了确保基础架构与项目兼容,涉及的分支越多且/或它们存留的时间越长,就越困难。在项目历史记录中,基础架构的版本编号或者当前版本的基础架构必须与项目的所有活跃分支保持兼容。

此外,当基础架构编码了大量特定于项目的知识时,每个项目都有自己的一套定制 CI 脚本,这些脚本具有线性扩缩的实现和维护费用。

避免:大规模重现基础架构行为

如果贡献者无法重现基础架构正在执行的操作,那么基础架构的结果的可操作性会大大降低。

如需调试无法重现的测试失败情况,需要反复向基础架构提交补丁,直到测试通过。与本地调试相比,这通常更慢,也更耗费资源。这也表明,本地测试毫无意义,因为与基础架构运行测试的相关性较低。

难以重现或无法在本地重现的 build 也是如此。基础架构的配置 build 不应以不明显的方式与开发者工作流存在很大差异。例如,在撰写本文时,Fuchsia SDK 仍然难以在本地构建。基础架构维护自己的逻辑,该逻辑与仅供内部使用的 fx 脚本明显不同,并且没有自动化程序来检查它们是否生成相同的输出。

在退化情况下,不可重现的基础架构行为可能会强制“暂时”停用失败的构建或测试,以解除提交并恢复 CI。在这种状态下,它们可能会因堆叠破坏而进一步降级,由于修复不切实际,它们实际上会被永久停用。

避免:浮动依赖项

项目应避免使用浮动依赖项,例如“实时获取最新版本的 Bazel”。悬浮依赖项包括机器的预安装软件。

任何浮动依赖项都可以流入构建和测试,将其呈现为非封闭状态。使用浮动依赖项时,基础架构的结果不能完全归因于受测的确切 CL 或提交,因为它们不是唯一可能的更改来源。请注意,基础架构本身的某些部分通常可以有效是浮动依赖项。网络不稳定是导致测试结果出现不可预测性的常见原因之一。

预期的 build 越稳定,浮动依赖项会相应带来更大的麻烦。例如,版本分支通常只接受修补程序,以尽可能降低引入新 bug 的风险,但浮动依赖项总是存在此类风险。

它们还促成了神秘的“它在本地运行,但在基础架构中无法运行”现象,反之亦然。

必备条件:可重现的结账流程

项目的签出必须在“干净”的工作区中执行一系列简单步骤即可完全重现。该工作区可以是开发者的机器,也可以是基础架构机器。在任何时间点,在 commit-ish 下的现有检出的“更新”必须始终产生相同的结果,就像从该 commit-ish 新创建的检出一样。这意味着,所有提取的依赖项都必须固定。固定(非浮动)依赖项最好是加密和确定性的依赖项,例如内容哈希。您也可以使用不可变引用,例如采用 Git 标记的语义版本,但首选前者。

可重现的结账流程不仅可以为开发者提供良好的项目入门体验,还可以降低基础架构对项目的看法与开发者的观点不一致的可能性。

不可再现也可能因源代码或二进制文件在任何时候被删除和/或无法访问。托管位置必须经过 Fuchsia 基础架构所有者的批准才能集成到项目的签出中。

必备条件:在结账、构建和测试之间明确分离

项目必须明确划分其结账阶段、构建阶段和测试阶段。 这是基础架构强制执行安全边界以及优化检出、构建、测试运行时以及资源使用的必要条件。此外,明确分隔的阶段还有助于更好地归因故障,尤其是基础架构故障与用户错误。例如,失败的构建应归因于代码问题,而不是提取远程依赖项时超时等。

结账阶段会获取源代码和所有依赖项。在结账阶段之后,必须具备构建所需的一切。这意味着构建阶段是封闭的,也就是说,无法实时提取任何依赖项。

build 必须能够在无法访问互联网的情况下运行。在实践中,当使用远程分布式编译器时,它仍然可以访问互联网,但只是为了优化性能(不应改变构建结果)。此要求也有利于离线工作或互联网访问受限的用户,例如在飞机上飞的用户。

项目不得假设构建和测试阶段在基础架构中的同一台计算机上运行。例如,Fuchsia 构建在测试编排器和执行器不同的机器(具有更多核心)上运行。这样,基础架构就可以更高效地分配机器资源并加快构建速度。

同样,测试应该是封闭的,即其输入已显式映射。如需了解详情,请参阅测试范围。测试不应假定运行它们的机器上存在完整检出或 build,也不应该依赖于同一机器上运行的其他测试。基础架构可能会将测试分片到单独的机器上,仅传递明确映射的输入。

至于 linter,它们可以在检出后或构建后运行,以在代码分析和/或代码审核的上下文中提供非二进制的通过/失败提示。在检出阶段运行的 linter 可视为检出阶段的一部分;同样,针对构建输出运行的 linter 可视为构建阶段的一部分。可以假定它们与其关联阶段在同一机器上运行。

考虑:可重现的 build

如果有相同的检出和依赖项,任何两个 build 在理想情况下都应产生逐位相同的输出,无论是在开发者的计算机上还是在基础架构机器上。如果逐位不完全相同,则 build 至少应在功能上等效。可重现的构建(例如可重现的检出)有助于跨用户、跨时间创建一致的项目视图。

build 可再现性包括不依赖于系统预配的工具或服务,例如不依赖于系统的 curl、ping、ip 等。build 应仅依赖于签出,后者负责提供所有 build 依赖项。类似地,项目应谨慎使用任何无法轻松跨平台移植的技术。理想情况下,项目应该可以在 Debian/Ubuntu Linux、MacOS 或 Windows 的原生版本上运行。

请注意,实际引导结账流程所需的最小依赖项集绝不应超出结账流程。例如,如果 bash 需要执行结账,并且 build 也需要 bash,则检出应拉取 vendored bash。然后,build 应使用 vendor 中的 bash,而不是用于引导检出的 bash。

为了在提交前加快构建速度,基础架构可能会在结账阶段从缓存中生成构建目录。如果增量构建并非总是正确处理,此策略可能会导致不确定的行为。在提交前,偶尔出现的增量构建问题通常值得为之牺牲构建速度。不过,这种优化不应该用于提交前之外的其他用途,并且绝对不要用于正确性和安全性无法降低的官方 build。

考虑:确保项目和基础架构分层

基础架构负责大规模地自动执行构建和测试。注重“大规模自动化”:项目应支持在本地执行这些任务,这些任务大部分或完全独立于基础架构。

这意味着基础架构用于构建和测试任何特定项目的逻辑很少。这些功能应由项目本身提供,并由基础架构调用,除了常见的入口点、输出和配置之外。一种实用的思路是,将基础架构视为新贡献者,并浏览项目的“使用入门”指南中关于构建和测试的内容。

例如,fint 是对 Fuchsia 构建系统的抽象,从基础架构的视野中遮盖其内部构件。使用 fint 时,基础架构甚至不知道或不关心 Fuchsia 是否使用 GN。这减少了 Fuchsia 贡献者在修改 build 时可能会遇到的锐利边缘数量。

此外,基础架构不应保留用于提取任何项目依赖项(例如 Bazel、Python3、其他工具链等)的配置。依赖项应由项目本身声明。默认情况下,不应假定基础架构机器包含任何依赖项,除了引导检出所需的最少工具集之外。项目所有者应该预料到,可用的预安装工具集未来会减少。

在有些情况下,项目需要了解基础架构预期。由基础架构进行后处理的某些特殊类型输出应遵循基础架构定义的合同。例如,要在 Gerrit 中显示的二进制文件大小报告或代码覆盖率报告应符合预期的格式。这样,基础架构就不需要对使用特定基础架构功能的每个项目进行自定义处理。

注意事项:优先采用 CI 配置,而非代码

为了扩大受支持项目的数量,基础架构应采用新配置而不是新代码。例如,用于构建类似项目的类的 CI 代码大部分应该在脚本级别或库级别共享。配置可以考虑到项目之间的所有必要差异,例如代码库网址、服务帐号、结账策略、build 入口点、工件上传目标位置等。

我们支持两种结账工具:Jiri 或 Git(带有或不带子模块)。项目应使用这些选项之一。预构建的依赖项应由 Git-on-BorgCIPD 托管。如果按照上一部分的说明对构建每个项目的逻辑进行了很好的抽象化,也应该主要共享用于构建的基础架构代码。

偏向配置,新 CI 的实现费用应低于从头开始编写新 CI 代码,这对需要快速启动的项目有益。他们还可以从共享基础架构代码库和服务的持续支持和维护工作中受益。

考虑:构建输出抽象

为方便使用 build 工件,build 应该针对其输出 surface 区域提供明确记录的协定。基础架构可能是此 surface 的使用者,以便执行各种构建后操作,例如将数据上传到 BigQuery、分片和运行测试,或运行二进制文件大小检查。这与“中级”构建输出相反,中级构建输出应被视为内部构建输出,且不直接受下游消费者依赖。

项目定义的工具也可以作为构建输出的使用方。例如,artifactory 工具可读取 Fuchsia 的构建输出,以定位并整理 Cloud Storage 中的构建工件。基础架构仅负责使用特定于基础架构的参数(即存储分区名称和唯一 build 标识符)调用该工具。

构建合同可能遵循一些常见的基础架构 API。这有助于保持集成的稳健性,例如与基础架构的代码覆盖率服务的集成。对生成代码覆盖率指标的 build 内部构件进行更改时,不应需要在基础架构端更改代码。

应测试 build 协定,例如架构更改不会导致下游消费者进行硬转换。

考虑:Main 优先开发

项目应旨在确保 build 随时保持良好的运行状况。这样,所有贡献者都能使用最新版本的代码,而无需派生分支,也无需处理旧版源代码树以规避 bug。这有助于减少合并冲突,并防止贡献者在任意给定时间获得截然不同的项目视图。

默认情况下,基础架构的提交前测试将尝试基于最新提交内容对 CL 进行 rebase 操作(因为这是测试干净提交内容的代理),因此实践者的工作流会尽可能接近此行为。就像开发者对代码库拥有类似的视图一样,基础架构也应如此。

基础架构的提交后测试通过不断测试新 CL 不断测试树尖端,有助于使 build 随时保持良好的运行状况。如果 build 最先变为红色,基础架构应快速报告这种情况,并由开发者采取措施。

沙盒分支可用于不应提交的代码。请注意,它们的使用通常是一种例外,并且不是由基础架构支持的一级流程。

考虑:快投和发布节奏

每个项目都应尝试快速部署其依赖项。基础架构应通过自动化滚动依赖项的过程来协助实现这一点,并且项目所有者应以高优先级解决失败的滚动尝试问题。理想情况下,依赖项会在发布后的 O(小时)内滚动。依赖项越旧,向前前滚和/或应用择优挑选的难度就越大。这对于时间敏感的安全补丁尤为重要。

秉承同样的民主精神,每个项目都应该尝试加快发布节奏。基础架构应通过在代码完全集成到 Mainline(通常称为“持续部署”)后自动执行发布流程来实现此目的。项目所有者应该在编写自动化测试方面投入大量资金,以便能够遵循“主优先”开发模型,将“临近树尖端”的版本可靠地集成到下游。

基础架构还应提供项目的依赖关系图的可见性,其中项目构成“节点”,滚动和发布形成“边缘”。项目所有者应能够跟踪流经图表的 CL,并发现 CL 已到达或卡住的位置,等等。

实现

此 RFC 简要介绍了项目应如何与基础架构进行交互,但有意简化了实现细节。每个项目都可以在许多方面遵循相关准则,我们不想通过规定具体细节来人为地施加约束条件。目前,新的树外项目仍在开发中,随着项目的演变,我们在此处绘制的任何内容都可能会过时。

安全注意事项

虽然我们鼓励项目拥有自己的构建和测试逻辑,但基础架构仍必须自己拥有安全边界。每个项目的源代码和/或工件都必须能够安全地流入下一个项目,以便多项目生态系统最终交付到产品上。

CI 任务的输入必须可信:必须从 Fuchsia 基础架构所有者批准的托管位置提取所有源代码和二进制文件。结账阶段完成后,不能再有输入了,而这应该由基础架构强制执行,例如,在构建阶段尝试提取依赖项应该会导致错误。

任务的任何输出都应提供出处,即 artifact 是从 revision:Xproject 构建的。上传工件后,基础架构应强制要求在适当范围内将工件上传到存储空间。例如,必须阻止依赖内部源代码的项目将工件上传到公共存储分区。

测试

本 RFC 中提及的 CI 系统支持采用与目前 Fuchsia 项目类似的方式大规模构建和测试新项目。这样可以减少项目贡献者在办公桌前所需的手动测试和调试工作,从而将工作分流到基础架构机器。

在基础架构方面,Fuchsia 的 CI 已经过广泛开发,实现了对其自身代码的大规模自动测试:换句话说,CI 能够测试对自身的更改。虽然可能需要进行一些泛化,但在构建新的 CI 时,我们会在很大程度上继承这些功能。

文档

此 RFC 可作为新项目和现有项目的参考。

在基础架构方面,当我们对新功能进行泛化后,我们将编写有关新的 CI 配置的文档,使该过程基本上是自助式的。我们还将对现有文档进行泛化,以解释新的树外项目,而不是仅应用于树内基础架构。

缺点、替代方案和未知情况

与许多软件开发最佳实践一样,对于项目贡献者来说,遵循这些最佳实践可能要预先完成一些工作。例如,在无需滚动工具的情况下,跟踪浮动依赖项就是一种在前沿快速迭代的常用快捷方式。可以认为,它们在短期内是一种有用的黑客行为,但应将其视为技术债务,以及此 RFC 中其他不鼓励的做法。

为每个新项目找到技术债务的最佳平衡点仍未知,就像在 Fuchsia 开发期间一样。我们会持续偿还构建、测试和基础架构的技术债务,这些债务通常是为了实现项目目标而付出的。本 RFC 并不旨在预防技术债务,而是让此类权衡更明智、更有针对性。