RFC-0148:CI 指南

RFC-0148:CI 指南
状态已接受
区域
  • 开发者
  • 治理
说明

面向 Fuchsia 生态系统中的项目和基础架构所有者的指南,旨在帮助他们打造可持续的 CI(持续集成)体验。

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

摘要

为 Fuchsia 生态系统中的项目和基础架构所有者提供指南,以创建可持续的 CI(持续集成)体验。

设计初衷

在 2021 年中之前,我们一直将大部分源代码和预构建文件集中在一个“Fuchsia 树”中。因此,基础设施及其所有者大多致力于支持该树。

随着新的树外项目(例如 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) - Toolchain
  • Sébastien Marchand (sebmarchand@google.com) - 第一方基础设施

共同化

此设计最初通过 Fuchsia 工程生产力邮件列表进行了宣传,在 Google 文档中进行了迭代,并与相关利益相关者分享,以确定上述部分中列出的审核者。然后,按照 RFC 模板将其转换为 Markdown,并移至 RFC 的“迭代”阶段。

设计

下方的“避免”子部分列举了会给项目的 CI、项目贡献者和/或基础架构所有者带来负面影响的常见陷阱。相反,“必备”和“考虑”这两个子部分是旨在帮助您避开上述陷阱及其他问题的指南。这些只是部分建议,并非详尽无遗:它们不包括性能跟踪、flake 检测等方面的考虑因素,这些因素也可能有助于改善项目的长期健康状况,但并非实现最低可行 CI 所必需的。

避免:基础设施依赖于项目内部结构

当基础设施依赖于项目内部结构时,双方都难以做出更改。在 Fuchsia 中工作时,即使做出看似无害的更改,也会遇到基础设施的尖锐边缘,这长期以来一直是一个痛点,也是贡献者对工程流程的最大抱怨之一。

例如,该基础设施过去了解(现在仍然了解)Fuchsia 构建系统的许多内部细节,这在开发中造成了尖锐的边缘,也就是说,如果 Fuchsia 构建违反了任何基础设施的预期,就无法自由更改。基础架构代码与 Fuchsia 代码并不共存,因此很难发现其预期:通常只有在预提交或提交后运行时出现故障时,才会发现这些预期。其他有害的示例包括结账时硬编码路径的基础设施、测试名称等。此类引用往往会自然累积,随着时间的推移,逐渐造成越来越多的摩擦。

随着所涉及的分支数量增加和/或分支的存续时间延长,保持基础设施与项目的兼容性会变得越来越困难。基础设施要么在项目的历史记录中进行版本控制,要么基础设施的有效版本必须与项目的所有有效分支保持兼容。

此外,当基础架构编码了大量项目专用知识时,每个项目可能都有一套量身定制的 CI 脚本,这些脚本的实现和维护成本会线性增加。

避免:对基础设施行为进行非微不足道的重现

如果贡献者无法重现基础架构的运行情况,那么基础架构的结果就会变得不太实用。

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

难以重现或无法在本地重现的 build 也是如此。基础架构不应以非显而易见的方式配置与开发者工作流程严重不同的 build。例如,截至撰写本文时,Fuchsia SDK 仍然难以在本地构建。该基础设施维护着自己的逻辑,这与仅限内部使用的 fx 脚本有很大不同,并且没有自动化功能来检查它们是否产生相同的输出。

在退化情况下,无法重现的基础设施行为可能会迫使我们“暂时”停用失败的 build 或测试,以解除提交阻塞并恢复 CI。在此状态下,它们可能会因堆叠损坏而进一步降级,由于修复不切实际,实际上会永久停用。

避免:浮动依赖项

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

任何浮动依赖项都可以流入 build 和测试,从而使它们不密封。使用浮动依赖项时,基础架构的结果无法完全归因于正在测试的确切 CL 或提交,因为它们不是唯一可能的更改来源。请注意,基础设施本身的部分内容通常可以有效地作为浮动依赖项。网络不稳定性是导致测试结果不可预测的常见原因之一。

浮动依赖项会带来相应的问题,并且 build 的预期稳定性越高,问题就越严重。例如,发布分支通常只接受紧急修复,以最大限度地降低引入新 bug 的风险,但浮动依赖项始终存在这种风险。

它们还会导致神秘的“本地运行正常,但在基础架构中运行不正常”现象,反之亦然。

必须具备:可重现的结账流程

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

可重现的结账不仅能为刚开始使用项目的开发者提供出色的体验,还能减少基础设施对项目的视图与开发者视图之间的差异。

不可重现性也可能源于源代码或二进制文件在任何时间点被删除和/或无法访问。托管位置必须先获得 Fuchsia 基础架构所有者的批准,然后才能集成到项目的结账流程中。

必须具备:结账、构建和测试之间有明确的分隔

项目必须明确区分其检出、构建和测试阶段。这是必需的,因为基础设施需要强制执行安全边界,并优化结账、构建和测试运行时以及资源使用情况。清晰分离的阶段还有助于更好地归因故障,尤其是基础架构故障与用户错误之间的区别。例如,失败的 build 应归因于代码问题,而不是在提取远程依赖项时发生的超时。

检出阶段会提取源代码和所有依赖项。在结账阶段之后,必须具备构建所需的一切。这意味着 build 阶段是密封的,即无法临时提取任何依赖项。

build 必须能够在没有互联网连接的情况下运行。实际上,在使用远程分布式编译器时,它可能仍会访问互联网,但仅作为性能优化(不应更改 build 的结果)。此要求还有助于离线工作或互联网访问受限的用户(例如机载用户)。

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

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

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

考虑:可重现的 build

在理想情况下,任何两个 build 在给定相同的签出和依赖项的情况下,无论是在开发者的机器上还是在基础架构机器上,都应产生完全相同的输出。如果 build 不是完全相同,至少应在功能上等效。与可重现的签出类似,可重现的 build 有助于在不同用户之间和不同时间点创建一致的项目视图。

构建可重现性包括不依赖于系统提供的工具或服务,例如不依赖于系统中的 curl、ping、ip 等。build 应仅依赖于 checkout,因此 checkout 负责提供所有 build 依赖项。同样,项目应谨慎使用任何无法轻松跨平台移植的技术。理想情况下,项目应可在 Debian/Ubuntu Linux、macOS 或 Windows 的纯净安装上运行。

请注意,实际引导结账所需的最小依赖项集绝不应超出结账范围。例如,如果结账需要 bash,并且 build 也需要 bash,则结账应拉取供应商提供的 bash。然后,build 应使用该供应商提供的 bash,而不是用于引导结账的 bash。

为了在预提交中加快构建速度,基础架构可能会在检出阶段从缓存中为构建目录提供初始数据。如果增量 build 未得到正确处理,此策略可能会导致不确定的行为。在预提交中,偶尔出现的增量构建问题通常值得牺牲构建速度来换取。不过,此优化不应在预提交之外使用,并且绝不能用于正确性和安全性至关重要的正式 build。

考虑:清晰的项目和基础架构分层

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

这意味着,该基础架构几乎不包含任何用于构建和测试特定项目的逻辑。这些功能应由项目本身提供,并由基础设施调用,而无需了解除已知入口点、输出和配置之外的其他信息。一个有用的心理模型是将基础设施视为正在按照项目的“入门”指南构建和测试的新贡献者。

例如,fint 是对 Fuchsia 构建系统的抽象,可从基础设施的视图中隐藏其内部结构。借助 fint,基础设施甚至不知道或不在意 Fuchsia 使用 GN。这样可以减少 Fuchsia 贡献者在修改 build 时可能遇到的尖锐边缘。

基础设施也不应保留用于提取任何项目依赖项(例如 Bazel、Python3、各种工具链等)的配置。依赖项应由项目本身声明。除了启动结账所需的最低工具集之外,不应默认假设基础架构机器包含任何依赖项。项目所有者应预计,未来可用的预安装工具集将会减少。

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

考虑:优先选择 CI 配置而非代码

为了扩大支持的项目数量,基础架构应优先考虑新配置,而不是新代码。例如,用于构建一类类似项目的 CI 代码应主要在脚本或库级别共享。配置可以考虑项目之间任何必要的差异,例如代码库网址、服务账号、检出策略、构建入口点、制品上传目的地等。

我们支持两种签出工具:Jiri 或 Git(带或不带子模块)。 项目应使用以下选项之一。预构建依赖项应由 Git-on-BorgCIPD 托管。如果每个项目的构建逻辑都按照上述部分进行了很好的抽象,那么用于构建的基础设施代码也应大部分共享。

通过优先考虑配置,新 CI 的实现成本应低于从头开始编写新 CI 代码,这有利于需要快速启动的项目。此外,他们还可以受益于共享基础架构代码库和服务的持续支持和维护。

考虑:构建输出抽象

为了方便使用 build 制品,build 应该具有明确记录的输出表面积合约。该基础架构很可能会成为此界面区域的消费者,以便执行各种 build 后操作,例如将数据上传到 BigQuery、分片和运行测试,或运行二进制大小检查。这与“中间”构建输出形成对比,后者应被视为内部内容,下游使用方不应直接依赖于它们。

项目定义的工具也可以是 build 输出的使用方。例如,artifactory 工具会读取 Fuchsia 的 build 输出,以在云存储中查找和整理 build 制品。基础架构仅负责使用特定于基础架构的实参(即存储分区名称和唯一 build 标识符)调用该工具。

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

应测试 build 合约,例如,架构更改不会导致下游消费者出现硬过渡。

考虑:以 main 为先的开发

项目应力求使 build 在树的尖端保持健康状态。这样一来,所有贡献者都可以使用最新版本的代码,而无需创建分支或使用旧版本的树来规避 bug。这有助于减少合并冲突,并防止贡献者在任何给定时间对项目有明显不同的看法。

默认情况下,基础架构的预提交会尝试将 CL 重新基于树的尖端(因为这是测试干净提交的代理),因此贡献者的工作流程尽可能接近此行为是切实可行的。正如开发者对代码库有相似的看法一样,基础设施也应如此。

基础架构的 postsubmit 通过在新的 CL 落地时不断测试 tip-of-tree,有助于保持 tip-of-tree 的 build 处于健康状态。如果 build 在树状结构的顶端变为红色,基础架构应快速报告此问题,并由开发者采取相应措施。

沙盒分支可用于不打算提交的代码。请注意,这些流程通常是例外情况,而不是由基础架构支持的一流流程。

考虑:快速发布和发布节奏

每个项目都应尝试以快速的节奏滚动其依赖项。基础架构应通过自动化滚动依赖项的流程来促进这一点,并且项目所有者应优先修复失败的滚动尝试。理想情况下,依赖项会在发布后的 O(小时) 内完成滚动更新。依赖项越旧,就越难向前滚动和/或应用 cherry-pick。对于对时间要求严格的安全补丁,这一点尤为重要。

同样,每个项目都应尝试以快速的节奏发布。基础架构应通过在代码顺利集成到主线后自动执行发布流程(通常称为“持续部署”)来促进这一点。项目所有者应投入大量精力来编写自动化测试,以便按照“主分支优先”的开发模式,可靠地将近乎树尖的发布版本集成到下游。

该基础架构还应提供项目依赖关系图的可见性,其中项目构成“节点”,而发布和版本构成“边”。项目所有者应能够跟踪流经图表的 CL,并发现 CL 的最终位置或卡住的位置等。

实现

此 RFC 提供了有关项目应如何与基础设施交互的高级指南,但有意省略了实现细节。每个项目都可以通过多种方式遵循这些准则,我们不想通过规定具体细节来人为地设置限制。新的树外项目目前仍处于起步阶段,我们在此处规划的任何内容都可能会随着项目的发展而过时。

安全注意事项

虽然鼓励项目拥有自己的 build 和测试逻辑,但基础设施仍必须拥有安全边界。每个项目的源代码和/或制品必须能够安全地流入下一个项目,这样才能最终将多项目生态系统中的内容交付到产品中。

CI 任务的输入必须是可信的:所有源代码和二进制文件都必须从经过 Fuchsia 基础架构所有者批准的托管位置获取。结账阶段完成后,不得再有任何输入,并且基础架构应强制执行此操作,例如,尝试在 build 阶段获取依赖项应会导致错误。

任务的任何输出都应提供来源信息,即制品是在项目修订版本 X 中构建的。上传制品时,基础架构应强制执行以下操作:确保制品上传到具有适当范围的存储空间。例如,依赖于内部源代码的项目必须禁止将制品上传到公共存储分区。

测试

此 RFC 中提及的 CI 系统将能够以与当前 Fuchsia 项目类似的方式大规模构建和测试新项目。这样一来,项目贡献者在办公桌上需要进行的手动测试和调试工作量就会减少,从而可以将工作分流到基础架构机器上。

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

文档

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

在基础架构方面,我们将在这些功能通用化后编写有关新 CI 配置的文档,以便该流程在很大程度上实现自助服务。我们还将对现有文档进行泛化,以涵盖新的树外项目,而不仅仅是应用于树内基础架构。

缺点、替代方案和未知因素

与许多软件开发最佳实践一样,遵循这些最佳实践可能会让项目贡献者付出更多前期工作。例如,跟踪浮动依赖项是一种常用快捷方式,可用于快速迭代最新版本,而无需使用滚动更新。从短期来看,这些做法可能是一种有用的技巧,但从长远来看,它们应被视为技术债务,与此 RFC 中其他不建议的做法一样。

与 Fuchsia 开发期间一样,我们尚不清楚每个新项目的技术债务的最佳平衡点。我们会随着时间的推移继续偿还 build、测试和基础设施方面的技术债务,这些债务通常是为了实现项目目标而产生的。此 RFC 并非旨在防止技术债务,而是希望让此类权衡更加明智和有意识。