RFC-0148:CI 指南

RFC-0148:持续集成准则
状态已接受
区域
  • 开发者
  • 治理
说明

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

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

摘要

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

设计初衷

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

随着新的外部项目(例如 RFC-0095)的提出,树内贡献者可能会新成为外部贡献者。外部 CI 系统应提供与树内 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。例如,在撰写本文时,Fuchsia SDK 仍然很难在本地构建。该基础架构会维护自己的逻辑,该逻辑与仅限内部使用的 fx 脚本有很大不同,并且没有任何自动化操作来检查它们是否会生成相同的输出。

在极端情况下,不可重现的基础架构行为可能会强制“暂时”停用失败的 build 或测试,以取消屏蔽提交操作并恢复 CI。在这种状态下,由于无法修复,它们可能会因堆叠故障而进一步降级,从而实际上被永久停用。

避免:浮动依赖项

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

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

预期 build 越稳定,浮动依赖项带来的问题就越大。例如,发布分支通常仅接受热修补丁,以最大限度地降低引入新 bug 的风险,但浮动依赖项始终存在此类风险。

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

必备:可重现的结账

必须能够在“干净”工作区中通过一系列简单的步骤完全重现项目的检出。该工作区可以是开发者的机器或基础架构机器。在某个提交版本上对现有检出进行“更新”时,其结果始终与在任何时间点根据该提交版本全新创建检出时产生的结果相同。这意味着,所有提取的依赖项都必须固定。理想情况下,固定(非浮动)依赖项应是加密且确定性的,例如内容哈希。不可变引用也可能适用,例如将语义版本用作 Git 标记,但前者更为推荐。

可重现的检出不仅为刚开始使用项目的开发者提供了绝佳体验,还降低了基础架构对项目的视图与开发者视图之间出现差异的可能性。

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

必备:明确区分签出、构建和测试

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

检出阶段会提取源代码和所有依赖项。在签出阶段之后,必须拥有构建所需的一切。这意味着构建阶段是密封的,即无法动态提取任何依赖项。

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

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

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

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

考虑:可重现的 build

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

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

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

为了加快提交前的构建速度,基础架构可能会在签出阶段从缓存中为 build 目录提供种子。如果增量 build 并非始终得到正确处理,此策略可能会导致非确定性行为。在提交前,偶尔出现增量 build 问题通常值得以牺牲构建速度为代价。不过,此优化不应在提交前以外的任何阶段使用,绝对不能用于正确性和安全性不能受到损害的正式 build。

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

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

这意味着,基础架构中包含的用于构建和测试任何特定项目的逻辑非常少。这些功能应由项目本身提供,并由基础架构调用,而无需了解除众所周知的入口点、输出和配置之外的任何信息。一个有用的思维模型是,将基础架构视为正在查看项目构建和测试“入门”指南的新贡献者。

例如,fint 是对 Fuchsia 构建系统的抽象,可隐藏其内部结构,使其不为基础架构所见。使用 fint 时,基础架构甚至不知道或不关心 Fuchsia 使用 GN。这可以减少 Fuchsia 贡献者在修改 build 时遇到的尖锐边缘数量。

基础架构也不应保留用于提取任何项目依赖项(例如 Bazel、Python3、各种工具链等)的配置。这些依赖项应由项目本身声明。默认情况下,不应假定基础架构计算机除了引导结账所需的一组最少工具之外,还包含任何依赖项。项目所有者应预计,未来可用的预安装工具套件将会减少。

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

建议:优先使用 CI 配置,而不是代码

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

我们支持两种签出工具:Jiri 或 Git(无论是否包含子模块)。项目应使用以下选项之一。预构建依赖项应由 Git-on-BorgCIPD 托管。如果每个项目的构建逻辑按照上一部分中所述进行了良好的抽象化,那么用于构建的基础架构代码也应在大多数情况下共享。

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

考虑:构建输出抽象

为了便于使用 build 工件,build 应针对其输出 Surface 区域提供完善的文档化协定。基础架构可能会成为此接口的使用方,以便执行各种构建后操作,例如将数据上传到 BigQuery、分片和运行测试,或运行二进制文件大小检查。这与“中间”构建输出形成鲜明对比,后者应被视为内部内容,不应由下游使用方直接依赖。

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

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

应测试 build 协定,例如架构更改不会导致下游使用方发生硬转换。

考虑:以主屏幕为先的开发

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

默认情况下,基础架构的提交前处理流程会尝试将 CL 重新基于树顶(因为这是测试干净提交内容的代理),因此贡献者的工作流程应尽可能接近此行为。正如开发者对代码库有类似的视图一样,基础架构也应如此。

基础架构的提交后流程会随着新 CL 的提交不断测试树顶,从而帮助保持树顶 build 的健康状态。如果 build 在树顶变为红色,基础架构应快速报告此问题,并由开发者采取行动。

沙盒分支可用于不打算提交的代码。请注意,使用这些方法通常是违反常规的做法,而不是由基础架构支持的一流流程。

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

每个项目都应尝试以较快的节奏发布其依赖项。基础架构应通过自动化依赖项滚动流程来促进这一点,并且项目所有者应优先修复失败的滚动尝试。理想情况下,依赖项应在发布后的 O(hours) 内进行滚动。依赖项越稳定,向前滚动和/或应用精选内容就越难。对于具有时效性的安全补丁,这一点尤为重要。

同样,每个项目都应尝试以较快的节奏发布。基础架构应在代码顺利集成到主线后自动执行发布流程(通常称为“持续部署”),从而为此提供便利。项目所有者应投入大量精力编写自动化测试,以便按照主开发模型,将接近树顶的版本可靠地集成到下游。

该基础架构还应提供项目的依赖项图,其中项目构成“节点”,而发布和版本构成“边”。项目所有者应该能够跟踪流经图表的 CL,并了解 CL 已到达何处或已卡住等信息。

实现

此 RFC 概要介绍了项目应如何与基础架构交互,但刻意不详细介绍实现细节。每个项目都可以通过多种方式遵循这些准则,我们不希望通过规定具体要求来制造人为的限制。目前,新的非树项目仍处于起步阶段,随着项目的演变,我们在此处绘制的任何图表都可能会过时。

安全注意事项

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

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

任务的任何输出都应提供来源,即工件是从项目修订版:X构建的。上传工件时,基础架构应强制将工件上传到具有适当范围的存储空间。例如,必须禁止依赖于内部源代码的项目将工件上传到公共存储分区。

测试

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

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

文档

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

在基础架构方面,我们将在通用化这些功能后,撰写有关新 CI 配置的文档,以便该流程大部分可自行完成。我们还将对现有文档进行泛化,以涵盖新的外部项目,而不是仅适用于树内基础架构。

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

与许多软件开发最佳实践一样,项目贡献者在遵循这些最佳实践时可能需要付出更多前期努力。例如,跟踪浮动依赖项是一种常用的快捷方式,可让您在尖端技术上快速迭代,而无需使用滚轮。有人可能会认为,这些方法在短期内很有用,但它们应该被视为技术债务,与本 RFC 中不鼓励的其他做法一样。

我们无法确定如何为每个新项目找到最佳的技术债务平衡点,就像在 Fuchsia 开发期间一样。我们会不断偿还 build、测试和基础架构技术债务,这些债务通常是为了实现项目目标而产生的。此 RFC 旨在让此类权衡更加明智和有意为之,而不是防止产生技术债务。