真正的敏捷工作流 —— GitHub flow

7991 年,随着极限编程(Extreme programming)方法论的提出,持续集成(Continuous integration)也随之成为一项标准化的敏捷实践,被逐步应用于各类软件的开发流程中。

9102 年的今天,持续集成的概念已经在软件开发领域生根发芽,广泛应用于不同平台及设备的项目开发,极大提升了项目迭代速度,降低了维护成本。

不过,作为“敏捷”的固有属性,持续集成也并不仅限于特定的模式,不同的项目可能遵循不同的实践,形式多种多样,效果可能也参差不齐。
为了解决这些问题,一些 Workflow 的通用模式被提出,而本文的主角,就是其中的天之骄子 —— GitHub flow。

GitHub flow 是什么?

GitHub flow,顾名思义,就是 GitHub 所推崇的 Workflow。千万不要理解成 GitHub 上才能用的 Workflow。

其官网的描述为:

GitHub flow is a lightweight, branch-based workflow that supports teams and projects where deployments are made regularly.

从中我们可以得出的信息是 —— 这段描述完全就是废话 GitHub flow 具有很高的通用性。

为了更便于了解 GitHub flow 的内容,我们从流程图入手:

其中的主要流程为:

  • 新建分支(Create a branch);
  • 提交修改(Add commits);
  • 创建PR(Open a Pull Request);
  • 代码评审(Discuss and review your code);
  • 部署(Deploy);
  • 合并(Merge);

细心的同学可能很快会发现,GitHub flow 最大的亮点在于部署(Deploy)发生在 合并(Merge)之前,这就是 GitHub flow 的核心,非阻塞式集成 —— 在产生任何副作用之前得知当前修改的所有集成效果,达到真正的持续集成。

GitHub flow 有什么优势?

GitHub flow 的核心优势在于其流程带来的自动化可能性,能够做到其它流程无法实现的检查过程,并极大简化开发团队的体力劳动,真正发挥自身的价值。

主要体现在以下方面:

基于修改的检查

基于修改的检查(Change-based checking) 是相对于全局检查(Global checking)的概念,最典型的例子就是代码覆盖率。项目中一般会设立覆盖率的最低阈(yù)值,并在流水线中进行检查。

根据著名的覆盖率第一定律:

随着时间的推移,项目中的实际覆盖率必将会无限趋向于要求的覆盖率。—— 沃兹基硕德

如果项目中配置的最低要求是 90%(暂不考虑覆盖率类型),那么就不要指望实际覆盖率能够超过 95%。于是问题来了,全局覆盖率要求会导致什么样的严重后果呢?

我们考虑一个假象项目,总共有 100 行代码,覆盖率要求 90%,实际覆盖率 90%。有一天,项目组成员小明发现其中有 10 行无意义的 console.log(42),决定将其删除。

学过初等数学的我们都知道,对于 0~1 之间的分数,分子分母同时增加相同数值时,分数的值会增大;反之,分子分母同时减少相同数值*时,分数的值会减小。(这里要求结果仍然处于 0~1 之间。)

如果还不能反应过来的话,可能要考虑补充六个核桃了。删除无用代码的结果是,覆盖率不再满足要求,从而无法通过流水线。

90/100 * 100% = 90.00%
80/90  * 100% = 88.89%

之后,小明可以作出以下几种选择:

  1. 撤销之前的修改,保留无用代码;
  2. 降低全局覆盖率要求;
  3. 从其它覆盖率不足的地方补充代码覆盖;
  4. 找到之前导致覆盖率不足的人,要求其补充代码覆盖。

选项 1 固然是最简单的方案,直接当作无事发生。

选项 2 虽然也简单,但是既然当前覆盖率能够降到 90%,如果降低要求以后必然还会继续下降,同时如果被其他人发现可能遭到质疑(Challenge)。

选项 3 中一个覆盖率不足的问题可能继续分为两种子类型:案例遗漏非测试友好。前者是忽略了某种应当覆盖的情况,而后者是代码的设计本身导致无法合理测试。对于前者,如果缺乏上下文而直接把当前行为当作预期,可能会埋下错误隐患(如果未覆盖当前行为本身是未定义行为甚至错误行为);对于后者需要进行额外的重构,仍然具备前述问题(在测试覆盖不足的情况重构?),并且可能导致原问题递归(如果重构本身减少了代码量)。

选项 4 原则上是最正确的方案,但实际上可行性很低。如果小明找到小红,告诉她一年前编写的代码覆盖率不足,那么得到的回应多半是:我当年都跑得好好的,你一改动就挂了,不是你的问题是什么?

归根到底,不论考虑哪种选项,对于小明而言,学到的只有一件事:

永远不要做会减少代码的修改!

永远不要做会减少代码的修改!

永远不要做会减少代码的修改!

一旦开发团队中每个人都认识到这一点之后,之后的开发过程就会向着堆垃圾的方向发展:能不动原有代码就不动,实在不行写个 if插进去。提取公共代码?双倍的覆盖就是双倍的快乐安全,怎么能说不要就不要?

不过,一旦我们使用合并前集成(Integration before Merge)的方式,便能够得知每个改动中每个文件的覆盖率情况,从而在开发过程中主动避免覆盖率下滑,把质疑集中到问题的来源 —— 提交代码并且覆盖率不足的人身上。

非错误级反馈

非 GitHub flow* 的流水线中,永远只存在一种反馈方式 —— 报错。(为了保持简洁,这里将所有不符合 Integration before Merge 的流程统称为「非 GitHub flow」。)

这时候有人可能会说,我们可以向流水线的控制台输出里打印日志。不过我可以保证,没有人会在正常构建的情况下守着看完每一条日志,一个合理设计的流水线也不应该需要主动关注这里的内容导致不必要的效率浪费。

日志的内容往往绝大部分都是非关键信息:

即便快速浏览日志,恐怕也很难发现关键信息。

不过,项目开发中往往存在很多非关键因素,平常不会太过关心,但一旦问题严重之后又会很麻烦。一个很好的例子就是应用体积。假如开发过程中对体积毫不关心(内网可能传输很快),那么等到用户真正无法容忍加载时间而导致使用率急剧下降的时候,还得专门回过头来做体积优化*。(如果本身就是打算靠创造额外优化工作赚钱的话,可以当我没说。)

通过 GitHub flow,我们能够在合并之前得到所有相关的信息,并自行判断问题的严重性(其他 Reviewer 也有义务判断)。如果本次改动并没有添加新的依赖,但是构建后大小急剧增加,那么可能就需要检查文件引用或者构建过程存在问题。

由于是基于集成结果的信息提示,因此还可以设置出现条件,例如某文件体积变化超过 0.5%。这样能够避免被固定消息所打扰,只关心必要内容。

除了自动执行的被动检查项目外,对于需要可观成本的检查,往往设计成主动检查项目。一般通过 PR 的标签或者评论内容进行触发,类似于:

性能测试(Performance testing) 就是一个较为典型的例子,如果小明不畏艰难险阻对实现代码进行了深度重构,那么在合并前就必然选择进行性能测试来避免非预期的影响。同理,如果只是添加了测试代码,那么性能测试将完全没有必要。

同样的,Reviewer 也应当评估是否所需的主动检查项目都被执行。

无限环境

多团队协作项目中,一个常见的痛点就是需要根据自身或者外部的需求准备各种环境,然而一些环境在大部分时候都不会使用到,往往需要在不明所以的情况下突然增加或者调整环境配置。

这时就可以回归到 GitHub flow 的重中之重 —— 合并前部署。

所谓的无限环境,就是自动将当前 PR 中的最新提交*部署到一个临时环境中,并返回该环境的 URL 地址。(如果资源丰富的话也可能部署每一个提交以方便比对。)

为此,环境准备工作将变得非常简单,只需要修改相应配置文件并创建 PR,即可得到一个对应的新环境。这一切甚至不需要依赖本地开发环境,而是直接在代码平台的在线编辑器中完成。

由于可以直接预览当前修改,不会再出现不必要的疑 车 虫无据的情况,Reviewer 有任何怀疑时便可以直接在预览环境中验证,而非凭空猜疑。Reviewer 也可以放心大胆的验证自己的怀疑,不需要在本地开发环境耗时耗力地切换。

有条件时甚至可以为预览模式设定特殊构建模式,例如高亮效果用于定位修改内容:

这样可以极大提升 Review 效率,降低 Reviewer 的负担。

自动化工作流

项目开发中往往有大量的时间被浪费在等待。等待构建、等待测试、等待 Review ……而这一切,都可以依靠 GitHub flow 来进行改善。

由于 PR 中能够运行所有必要的检查,所以本地开发环境中仅仅需要关注最可能受到影响的内容(例如当前文件的测试),而把其它不在固定影响范围的检查都转交给 PR。由于 PR 的工作机制,即便存在冲突无法合并也不会导致 Push 失败,并且 Push 本地代码后便可以立刻关电脑走人,即便 PR 检查失败也不会有任何后果。

PR 中能够利用 CI 环境,不受本机执行能力限制,因此可以并行检查所有需验证项目:

这里的检查本质上仍然是 Code Review,只不过参与者不是自然人。

检查期间,开发人员可以充分利用碎片时间处理其它事务,而无需关心检查进度。如果任何项目检查失败,将立刻收到邮件通知。

如果所有检查项目均已通过并且当前 PR 并非 Draft 状态*,能够自动通知 Reviewer 进行代码 Review,并且在所有 Reviewer 同意后自动 Merge,在 Happy Path 下完全无需再次人工干预。
(Draft PR 是 GitHub 最近推出的功能,用于标记当前 PR 为未完成状态。其它平台可能将采用不同的判断方式。)

说到 Reviewer,就不得不提 Code Owners。Code Owners 是 GitHub 的内置功能*,能够配置每个文件/文件夹的所有者,在 PR 完成时根据修改文件的范围自动向添加相应文件所有者为 Reviewer,只有当各个 Group 的 Reviewer 都同意时才允许合并。(一些第三方工具也提供了类似的机制,功能可能更加强大。)

Code Owners 充分保障了项目的可维护性,每个 Code Owner 同时具备以下职责:

  • Domain export:对相关代码有深度的了解,知晓其历史背景与特殊行为,能够快速发现隐藏问题;
  • Coordinator:掌握每次修改的内容及原因,避免代码/环境冲突及已有行为被破坏;
  • Contact:如果对相关代码有疑问,能够立即确定联系对象而无需层层转发;
  • Responsible person:如果相关代码出现了什么意外,负责背锅,避免不必要的推诿。

例如将环境配置文件分配个某个/某些项目组成员,那么他们就能够充分知晓各个环境的使用情况,作出合理安排。

如何开始使用 GitHub flow?

使用 GitHub flow 的基本要求有:

  • 具备一个代码版本控制环境;
  • 具备一个持续集成环境;
  • (可选)具备 CI 环境的管理员权限;
  • 能够创建一个有权限访问 VCS 平台的机器人帐号;
  • 能够自由使用 VCS 平台的 WebHook API;
  • 能够自由使用 CI 平台的 Trigger API;
  • (可选)能够自由使用 CI 平台的状态查询 API;
  • 能够创建一个高可用的内部服务器用于机器人帐号的运行;
  • 能够决定开发团队的工作流程;
  • 能够投入成本改善基础设施;

遗憾的是,我至今没有过这种条件,如果你有能力去实践 GitHub flow,希望能够珍惜这次改善开发体验的机会,让更多人了解这种流程优化带来的巨大效率优势。

如果有任何具体的技术问题,也欢迎进一步的讨论。

写在最后

以我个人的体验,GitHub flow 是世界上唯一的真理真正能够拯救开发效率的敏捷实践,将开发人员真正从体力劳动中解放出来,从而能够专注于学习与思考。

如果你也觉得 GitHub flow 真正拯救了你的项目开发,不妨将它继续推广下去。

Share

6 Comments

  1. Qbit

    没看明白,是每个特性一次PR么,那如果两个功能同时开发,第一个完成后部署,第二个才完成,这个时候没有合并就部署岂不是导致第一个功能就没了?

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据