技术细节:简单说“不是高深莫测”(单调与 bors 的历程)[译]
技术细节:简单说“不是高深莫测”(单调与 bors 的历程)
我去年编写了一个程序,名字叫做 bors,现在来聊聊它以及它的一些前身的故事。这篇技术分享可能会让大多数人觉得乏味至极,但如果你的饭碗是靠编程挣的,那么我建议你不妨花一点时间看看。
大约十三年前,我在 Cygnus/RedHat 工作,与一位直言不讳的澳大利亚黑客,Ben Elliston,合作了一个项目。我们当时遇到了一个棘手的问题,需要进行跨时区的集成测试,而且如果测试过程中出现了即刻修复的 bug,我们会面临严重的后果(当时我们使用的版本控制系统是 CVS)。为了保证项目能够稳健地进行,Ben、Frank 和可能还有其他几位团队成员设计了一个复杂的系统,包括定时任务(cron jobs)、数据同步(rsync)、多个 CVS 仓库以及一个用于跟踪测试结果的小型 postgres 数据库。
这个系统承担了一个至关重要的使命:自动保证代码库始终能通过所有测试。这让我们大家都安心了不少(客户只会从这个库中拉取代码,因此永远不会遇到问题),同时也为工程师提供了便利,他们可以基于已知无误的版本开展日常工作,无需浪费时间去解决别人引入的 bug。由于这套系统是自动化的,我们可以确信,大家不会因为急于提交而犯下草率的错误。你可以提交所有你认为不够完美的代码到非自动化的仓库中;如果提交的代码没能通过任一测试,那么自动化流程就会拒绝接受它。这虽然严格,却也是公平的。当时,我意识到了一个重要的工程学原则——我相信,任何我将来可能加入的优秀团队也应该采纳这一原则。原来,如果你明确以此为目标去设计一个系统,其实并不复杂。Ben 把这个理念称作“不是什么高深莫测的科学”。我当时还不知道,这个理念实际上是多么难以实现!作为参考,我在此重申这个原则:
软件工程的简单法则:
自动保证代码库始终能通过所有测试
随着时间推移,那个系统逐渐老化并最终退出了使用。我对版本控制产生了浓厚的兴趣,尤其是对那些能够自动执行“非火箭科学”规则的系统。让人惊讶的是,只有 Aegis(由 Peter Miller,一位无废话、魅力十足的澳大利亚人所编写,遗憾的是,他现在正面临生命的最后阶段)能做到这一点。当时,Aegis 还不是一个分布式系统,但我个人在使用 Aegis 的过程中意识到,如果能将其与分布式版本控制结合起来,必定有更多可能,正如我见识到的朋友们在使用 bitkeeper 时那样。因此,我决定将这两种思想结合,开发了一个名为 monotone 的系统。
Monotone 之名,源自我追求“单调增加测试覆盖率”的理念。它的版本分支包含机制,是基于那些可以由测试机器人随意发布的证书。等等。我认为这将是未来的方向。当然,随之而来的是一系列冒险,最终诞生了 mercurial 和 git,monotone 被边缘化,版本控制世界发生了翻天覆地的变化。欢呼声中,一切都改变了。
过程中,出现了一些不可思议的现象。“持续集成”成为了行业标准,随后是“持续部署”,但几乎没有哪些平台真正遵循了“非火箭科学”规则。换句话说,无论我看到哪里的“持续集成”实践,似乎都是颠倒顺序:在进行测试之前就接受了代码(这可能会导致代码库出现问题),或者在隔离环境中进行测试,然后基于这次测试进行集成(但这并不能保证集成后的代码能够正常工作)。持续集成似乎更多的是用来迅速发现问题而非预防问题。这让我感到震惊和失望。
因此,几年后,当我们需要扩大对 Rust 的贡献时,我决定必须强制执行“非火箭科学”规则,以确保代码库保持可控。虽然我一再地解释这个概念,但似乎没有人真正买账,因此作为技术负责人,我不得不亲自动手实现。最终成果是一个名为 bors 的简洁脚本。
Bors 实施了一项被称为“非火箭科学规则”的策略,该策略结合了构建机器人测试场和 GitHub 仓库的功能:它会监控合并请求,等待审查者的批准。一旦获得批准,它会为每个审查通过的修订创建一个临时集成版本,这一版本会将所提议的更改加入到你的集成分支中。然后,它对这个临时版本进行测试,并且只有在测试通过的情况下,才会更新你的集成分支,使之指向这个新的集成版本。如果测试未通过,该集成版本会被舍弃,并且会在合并请求中留下一条评论,列出测试失败的原因。
你可以在这里查看 Rust 项目的工作队列示例,以及在这里查看 Servo 项目的示例。成功的集成案例可以在这里找到,一个在撰写本文时被拒绝的集成案例在这里。我想再次强调,这项规则并不复杂。我之所以一再提及,是因为我对如此少的工具支持这一规则感到非常惊讶。我不得不亲自动手实现它!它对快速发展且变化无常的项目具有极大的益处,确保主分支始终保持正常工作。这意味着你的集成流程的时间将由测试所需的时间来决定,有时候某些更改可能需要经过多次尝试才能成功集成(bors 提供了优先级标记来帮助你手动优化集成顺序)。
对于一些项目来说,如果每次修订都进行测试,考虑到测试时间可能过长,这种策略可能不实用,除非你定义了一个集成测试的子集。这通常是人们最初最担心的问题,这种担忧是有道理的。然而,根据我的经验,这种顾虑就像是人们最初反对进行测试或类型检查,担心这会花费太多时间。实际上,如果你推迟发现问题,那么解决这些问题所需的时间会更长。我强烈推荐任何软件行业的工作者尝试这种做法,亲身体验它带来的变化。