工作流

WorkFlow 的字面意思,工作流,即工作流程。在分支篇里,有说过这样的话:因为有分支的存在,才构成了多工作流的特色。事实的确如此,因为项目开发中,多人协作,分支很多,虽然各自在分支上互不干扰,但是我们总归需要把分支合并到一起,而且真实项目中涉及到很多问题,例如版本迭代,版本发布,bug 修复等,为了更好的管理代码,需要制定一个工作流程,这就是我们说的工作流,也有人叫它分支管理策略。

工作流不涉及任何命令,因为它就是一个规则,完全由开发者自定义,并且自遵守,正所谓无规矩不成方圆,就是这个道理。

Git Flow

这个工作流,是 Vincent Driessen 2010 年发布出来的他自己的分支管理模型,到现在为止,使用度非常高.

Git Flow 的分支结构很特别,按功能来说,可以分支为5种分支,从5 种分支的生命时间上,又可以分别归类为长期分支和暂时分支,或者更贴切描述为,主要分支和协助分支。

主要分支

在采用 Git Flow 工作流的项目中,代码的中央仓库会一直存在以下两个长期分支:

  • master
  • develop

其中 origin/master 分支上的最新代码永远是版本发布状态。origin/develop 分支则是最新的开发进度。

当 develop 上的代码达到一个稳定的状态,可以发布版本的时候,develop上这些修改会以某种特别方式被合并到 master 分支上,然后标记上对应的版本标签。

协助分支

除了主要分支,Git Flow 的开发模式还需要一系列的协助分支,来帮助更好的功能的并行开发,简化功能开发和问题修复。是的,就是下面的三类分支。这类分支是暂时分支非常无私奉献,在需要它们的时候,迫切地创建,用完它们的时候,又挥挥衣袖地彻底消失。

协助分支分为以下几类:

  • Feature Branch
  • Release Branch
  • Hotfix Branch

Feature 分支用来做分模块功能开发,命名看开发者喜好,不要和其他类型的分支命名弄混淆就好,举个坏例子,命名为 master 就是一个非常不妥当的举动。模块完成之后,会合并到 develop 分支,然后删除自己。

Release 分支用来做版本发布的预发布分支,建议命名为 release-xxx。例如在软件 1.0.0 版本的功能全部开发完成,提交测试之后,从 develop 检出release-1.0.0 ,测试中出现的小问题,在 release 分支进行修改提交,测试完毕准备发布的时候,代码会合并到 master 和 develop,master 分支合并后会打上对应版本标签 v1.0.0, 合并后删除自己,这样做的好处是,在测试的时候,不影响下一个版本功能并行开发。

Hotfix 分支是用来做线上的紧急 bug 修复的分支,建议命名为 hotfix-xxx。当线上某个版本出现了问题,将检出对应版本的代码,创建 Hotfix 分支,问题修复后,合并回 master 和 develop ,然后删除自己。这里注意,合并到 master 的时候,也要打上修复后的版本标签。

Feature Branches

优点:

  • 同时开发多个功能分支不会影响主干和线上代码
  • 在分支上开发新功能时不用担心对其他在开发的功能的影响
  • 现有很多持续集成系统支持分支的构建、测试、部署等

缺点:

  • 分支分出去时间越长往往代码合并难度越大
  • 在一个分支中修改了函数名字可能会引入大量编译错误。这点被称为语义冲突(semantic conflict)
  • 为了减少语义冲突,会尽量少做重构。而重构是持续改进代码质量的手段。如果在开发的过程中持续不断的存在功能分支,就会阻碍代码质量的改进。
  • 一旦代码库中存在了分支,也就不再是真正的持续集成了。当然你可以给每个分支建立一个对应的CI,但它只能测试当前分支的正确性。如果在一个分支中修改了函数功能,但是在另一个分支还是按照原来的假设在使用,在合并的时候会引入bug,需要大量的时间来修复这些bug。

Merge 加上 no-ff 参数

需要说明的是,Git Flow 的作者 Vincent Driessen 非常建议,合并分支的时候,加上 no-ff 参数,这个参数的意思是不要选择 Fast-Forward 合并方式,而是策略合并,策略合并会让我们多一个合并提交。这样做的好处是保证一个非常清晰的提交历史,可以看到被合并分支的存在。

下面是对比图,左侧是加上参数的,后者是普通的提交:

Git Flow 示意图

图中画了 Git Flow 的五种分支,master,develop,feature branchs ,release branchs , hoxfixes,其中 master 和 develop 字体被加粗代表主要分支。master 分支每合并一个分支,无论是 hotfix 还是 release ,都会打一个版本标签。通过箭头可以清楚的看到分支的开始和结束走向,例如 feature 分支从 develop 开始,最终合并回 develop ,hoxfixes 从 master 检出创建,最后合并回 develop 和 master,master 也打上了标签。

缺陷

在一个团队成员流动相对较小,大家对 Gitflow 都比较熟悉的情况下,实施过程倒是没有遇到任何问题。而且可以感觉得到,这样的分支模型下的发布非常有计划性,Dev 之间的开发冲突也比较少(得益于 Feature 划分合理)。但在时间长了以后,问题还是逐一暴露出来,主要有以下几个:

  • 重复测试,一个功能从开发到上线至少要经历三次内容重合度很高的测试:本地,Develop 分支合并,Master 分支合并;如果有 Fix bug,Merge 回 Master 还要多测一次,每一次都可能有意外的结果,而且 Develop 分支的测试和 Master 分支的测试内容几乎是一模一样的。
  • Release Master 的存在,在一个目标为持续发布的敏捷团队里 Release Master 的存在是不合理的,Release Master 需要在上线前的一段时间一直盯着 Pipeline(持续交付流水线),这不但意味着一个劳动力的缺失,并且一个人要想掌握一次发布的左右更改细节和影响也是几乎不可能的,所以到后来每次上线前 Release Master 都要组织一次 Release Meeting,所有开发在一起讨论这次 Release 的 Feature,非常浪费时间。
  • 并没有做到持续交付,在 Gitflow 得分支模型下,发布是非常有计划的,一个 Feature 必须要经过以上这么多步骤才能到达生产环境,在时间上平均一个 Feature 都要等待 两周时间才能上线,这样的等待并非是需求上的“按计划发布”,而是从技术上就造成了发布瓶颈,显然难以达到持续交付的要求的。
  • 与持续集成相悖,你会发现,在坚持持续集成实践的情况下,feature 分支是一件非常矛盾的事情。持续集成鼓励更加频繁的代码集成和交互,让冲突越早解决越好。feature 分支的代码隔离策略却在尽可能推迟代码的集成。

GitHub Flow

GitHub Flow 是 GitHub 制定并使用的工作流模型,由 scott chacon 在 2011 年 8月 31 号正式发布。

所有 Story 直接提交到 Feature 分支,再从 Feature 分支发 Pull-Request 到主分支(Master 或 Develop),Pull-Request 是为了方便 Code Review,相比于 Gitflow,这种方式因为省去了一些分支而降低了复杂度,同时也更复合持续集成的思想,以一张故事卡为集成的最小单位,相对来说集成的周期短,反馈的速度也快,能够及早的遇到问题,从而及早的解决问题。

Github flow 的另一个好处在于,可以处理跨团队协作问题。当时的项目是一个多团队共享的基础设施代码库,大部分团队需要同样的功能,就从主库 Fork 一份代码,一旦产品团队产生定制化的需求,就可以在自己的代码库里更改,并向主库发一个 Pull-Request,如果主库的维护团队认为这是一个有通用价值的更改,则会接受合并到主库中。这种方式就既保证了分布式团队拥有代码和主库的同步,又让各团队都可以向主库贡献代码,非常适合多个独立团队工作在一个代码库的情形。

GitHub Flow 示意图

GitHub Flow 推荐做法是只有一个主分支 master,团队成员们的分支代码通过 pull Request 来合并到 master 上。

模型说明:

  1. 只有一个长期分支 master ,而且 master 分支上的代码,永远是可发布状态,一般 master 会设置 protected 分支保护,只有有权限的人才能推送代码到 master 分支。
  2. 如果有新功能开发,可以从 master 分支上检出新分支。
  3. 在本地分支提交代码,并且保证按时向远程仓库推送。
  4. 当你需要反馈或者帮助,或者你想合并分支时,可以发起一个 pull request。
  5. 当 review 或者讨论通过后,代码会合并到目标分支。
  6. 一旦合并到 master 分支,应该立即发布。

Pull Request

在我看来,GitHub Flow 最大的特色就是 Pull Request 的提出它的用处并不仅仅是合并分支,还有以下功能:

  • 可以很好控制分支合并权限。分支不是你想合并就合并,需要对方同意呐

  • 问题讨论或者寻求其他人的帮助。

  • 代码Review.pull request 提供了评论功能支持

issue tracking

日常开发中,会用到很多第三方库,然后使用过程中,出现了问题,是不是第一个反应是去这个第三方库的 GitHub 仓库去搜索一下 issue ,看没有人遇到过,项目维护者修复了没有,一般未解决的 issue 是 open 状态,已解决的会被标记为 closed。这就是 issue tracking。

如果你是一个项目维护者,除了标记 issue 的开启和关闭,还可以给它标记上不同的标签,来优化项目。当提交的时候,如果提交信息中有 fix #1 等字段,可以自动关闭对应编号的 issue。

Trunk-based development

顺着持续集成的思想,如果我们把上一种分支模型做得再极致一点,我们不要 Feature 分支,或者把 Feature 分支只留在本地;不需要使用 Pull-Request 而是直接 Push 到远程 Master 分支,我们就做到了 Trunk based Development。

单主干的分支实践(Trunk-based development,TBD)在SVN中比较流行。Google和Facebook都使用这种方式。trunk是SVN中主干分支的名称,对应Git中则是master分支。

TBD的特点是所有团队成员都在单个主干分支上进行开发。当需要发布时,先考虑使用标签(tag),即tag某个commit来作为发布的版本。仅依靠tag不能满足要求,则从主干分支创建发布分支。

  • 同一个产品开发的所有人员共享一个Repository,有一个trunk,单一Developer或是Developer团队可以有自己的private branch,所有修改最后都会回到主干
  • 只有在Release时才会有官方的分支,一般Developer不能对Release Branch作动作,只有Release Engineer可以更动Release Branch,当Release Branch完成它的任务,就会被砍掉
  • Bug先在trunk修好,之后利用cherry-pick把Commit合并到Release Branch,而不是在Release Branch修好再整合到trunk,這樣可以把修改Release Branch的人限制在最小程度。

由于所有开发人员都在同一个分支工作,团队需要合理的分工和充分沟通来保证不同开发人员的代码尽可能少的发生冲突。因此持续集成和自动化是必要的,用来及时发现主干分支中的bug。因为主干分支是所有开发人员公用的,一个开发人员引入的bug可能对所有人造成影响。

不过好处是由于分支所带来的额外开销非常小。开发人员不用频繁在不同的分支之间切换。

使用主干开发后,我们的代码库原则上就只能有一个 Master 分支了,所有新功能的提交也都提交到 Master 分支上,没有了分支的代码隔离,测试和解决冲突都变得简单,持续集成也变得稳定了许多,问题也接踵而至,主要有以下三个:

  • 如何避免发布的时候引入未完成的 Feature
  • 如何进行线上 Bug Fix
  • 如何重构

如何避免发布引入未完成 Feature

答案是: Feature Toggle。

既然代码要随时保持可发布,而我们又需要只有一份代码来支持持续集成,在代码库里加一个特性开关来随时打开和关闭新特性是最容易想到的也是最容易被质疑的解决方案。

Feature Flag允许关闭未完成的功能,你可以在主干上进行迭代开发,新功能即便未开发完成也不会影响发布,因为它对用户是关闭的。当功能开发完成之后,修改配置便可以让功能发布。这种操作甚至可以在线上进行,例如代码已经发布但功能不可见,你可以修改配置让功能对特定的用户(线上测试、小流量或者全量发布等)可见。如果发现新功能存在问题,那么可以通过配置文件来迅速回滚,而必须重新分支上线。Feature Flag原理示意图如下:

优点:

  • 避免了分支合并代码冲突的问题,因为是基于主干的开发
  • 每次提交都在主干,迭代速度明显有优势
  • 新功能的整个过程都持续集成

缺点:

  • 未完成的功能可能会部署到线上,如果配置有误可能将未完成的功能开启。当然可以将界面层最后开发避免过早暴露。
  • 主干上担心提交代码影响其他功能。

最佳实践

一般Feature Flag可以分为两类,见下所示:

发布开关:

  • 在发布代码时关掉未完成的功能
  • 生存期短
  • 功能稳定就马上删除
  • 在整个开发过程中有预定义的值

业务开关:

  • 实现A/B测试
  • 针对特定人群发布功能尽早获得反馈
  • 针对特定条件开启或者关闭功能。例如可以设置在指定时间点开启,这样新功能将按照设定自动上线下线,无需手动上线,适合专题等情况
  • 能线上开启或者关闭,实现快速回滚

发布开关主要是为了隐藏未开发完成的功能,而业务开关则可以帮助我们快速满足某些需求。例如A/B测试,Feature Flag可以轻松控制展现哪个功能,提升A/B测试的可维护性。我们也可以通过配置里面的逻辑让新功能针对小部分人群甚至是特定地域的人群发布,尽早获取功能的反馈。甚至是可以在线上开启调试,只让新功能对调试人员可见。而这些都只需要配置文件和简单的标记来实现。

除了主干开发,什么情况下选择使用Feature Flag呢?下面是使用Feature Flag的一些典型场景:

  • 在 UI 中隐藏或禁用新功能
  • 在应用程序中隐藏或禁用新组件
  • 对接口进行版本控制
  • 扩展接口
  • 支持组件的多个版本
  • 将新功能添加到现有应用程序
  • 增强现有应用程序中的现有功能

可以看到,由于Feature Flag本身是对业务功能的控制,所以不适于功能大范围的改动等情况。另外使用过程中需要注意一些问题:

  • 只在需要的地方创建开关。美酒虽豪,不可贪杯。滥用任何技术都会出现问题。
  • 控制开关的数量。同上,开关应按需使用并及时清除。
  • 开关之间代码保持独立。如果代码存在依赖就没法删除,最终维护性反而变差
  • 清除发布开关和废弃代码。发布开关应当在功能稳定后删除,旧代码也是。
  • 界面层最后暴露。

Feature Toggle 是有成本的,不管是在加 Toggle 的时候的代码设计,还是在移除 Toggle 时的人力成本和风险,都是需要和它带来的价值进行衡量的。事实上,在我们做一个前端的大特性变更的时候,我们确实没有因为没办法 Toggle 而采用了一个独立的 Feature 分支,我们认为即使为了这个分支单独做一套 Pipeline,也比在前端的各种样式间添加移除 Toggle 来得简单。但同时,团队商议决定在每次提交前都要先将 Master 分支 Merge 到 Feature 分支,以此避免分支隔离久以后合并时的痛苦。

如何进行线上 Bug Fix

在发布时打上 Release Tag,一旦发现这个版本有问题,如果这个时候Master分支上没有其他提交,可以直接在 Master 分支上 Hot Fix,如果 Master 分支已经有了提交就要做以下三件事:

  • 从 Release Tag 创建发布分支。
  • 在 Master 上做 Fix Bug 提交。
  • 将 Fix Bug 提交 Cherry Pick 到 Release 分支。
  • 在Release 分支再做一次发布。

线上 Fix 通常都比较紧急。看完这个略显繁琐 Bug Fix 流程,你可能会问为什么不在 Release 分支直接 Fix,再合并到 Master 分支?

这样做确实比较符合直觉,但事实是,如果在 Release 分支做 Fix,很可能会忘了 Merge 回 Master,试想深夜两点你做完 Bug Fix 眼看终于上线成功,这时的第一反应就是“终于可以下班了。什么,Merge 回 Master? 明天再来吧“ 等到第二天你早已把这个事忘得一干二净。而问题要等到下一次上线才会被暴露出来,一旦发现,而这个时候上一次 Release 的人又不在,无疑增加了很多工作量。

如何重构

这里指的是比较大规模的重构,无法在一次提交完成,TBD 要求每一次提交都是一个可上线的版本,所以这同时还意味着这个重构无法再一个上线周期内完成。

这种情况,需要在代码设计中增加一个抽象层,保证在重构过程中先不动原来的代码,也不破坏既有功能,类似于蓝绿部署中的负载均衡器的作用,这样的流程就是:

  • 在将要被重构的代码逻辑附近引入抽象层然后提交,对所有人可见。如果有需要可以是多个提交,这些提交都不能破坏 build,然后依次 push 到共享代码库。
  • 为将要被引入的代码写抽象层的第二次实现,然后提交。但在主干上由于关闭状态所以其他开发人员暂时不依赖于它。如果需要的话,这可能像上面那样需要多次提交。第一步的抽象层也可能偶然被调整,但必须遵循同样的原则:不能破坏build。
  • 切换使用重构后的代码,然后 Push。
  • 删除原有的旧实现(被重构代码)
  • 删除抽象层

这个流程和汽车换轮胎有那么点类似,新旧轮胎代表重构前后代码,抽象层就好比千斤顶。

GitLab Flow

背景

当 Git Flow 出现后,它解决了之前项目管理的很让人头疼的分支管理,但是实际使用过程中,也暴露了很多问题:

  • 默认工作分支是 develop,但是大部分版本管理工具默认分支都是 master,开始的时候总是需要切换很麻烦。
  • Hotfix 和 Release 分支在需要版本快速迭代的项目中,几乎用不到,因为刚开发完就直接合并到 master 发版,出现问题 develop 就直接修复发布下个版本了。
  • Hotfix 和 Release 分支,一个从 master 创建,一个从 develop 创建,使用完毕,需要合并回 develop 和 master。而且在实际项目管理中,很多开发者会忘记合并回 develop 或者 master。

GitHub Flow 的出现,非常大程度上简化了 Git Flow ,因为只有一个长期分支 master,并且提供 GUI 操作工具,一定程度上避免了上述的几个问题,然而在一些实际问题面前,仅仅使用 master 分支显然有点力不从心,例如:

  • 版本的延迟发布(例如 iOS 应用审核到通过中间,可能也要在 master 上推送代码)
  • 不同环境的部署 (例如:测试环境,预发环境,正式环境)
  • 不同版本发布与修复 (是的,只有一个 master 分支真的不够用)

为了解决上面那些问题,GitLab Flow 给出了以下的解决方法。

Prodution Branch

master 分支不够,于是添加了一个 prodution 分支,专门用来发布版本。

Environment Branches & Upstream First

每个环境,都对应一个分支,例如下图中的 pre-production 和 prodution 分支都对应不同的环境,我觉得这个工作流模型比较适用服务端,测试环境,预发环境,正式环境,一个环境建一个分支。

这里要注意,代码合并的顺序,要按环境依次推送,确保代码被充分测试过,才会从上游分支合并到下游分支。除非是很紧急的情况,才允许跳过上游分支,直接合并到下游分支。这个被定义为一个规则,名字叫 “upstream first”,翻译过来是 “上游优先”。

Release Branches & Upstream First

只有当对外发布软件的时候,才需要创建 release 分支。作为一个移动端开发来说,对外发布版本的记录是非常重要的,如果线上出现了一个问题,需要拿到问题出现对应版本的代码,才能准确定位问题。

在 Git Flow ,版本记录是通过 master 上的 tag 来记录。发现问题,创建 hotfix 分支,完成之后合并到 master 和 develop。

在 GitLab Flow ,建议的做法是每一个稳定版本,都要从master分支拉出一个分支,比如2-3-stable、2-4-stable等等。发现问题,就从对应版本分支创建修复分支,完成之后,先合并到 master,才能再合并到 release 分支,遵循 “上游优先” 原则。

参考

https://drprincess.github.io/2017/12/26/Git%E4%B8%89%E5%A4%A7%E7%89%B9%E8%89%B2%E4%B9%8BWorkFlow(%E5%B7%A5%E4%BD%9C%E6%B5%81)/

https://blog.csdn.net/xn_sung/article/details/51427075

https://cn.trunkbaseddevelopment.com/

https://www.duyidong.com/2017/10/29/trunk-base-development/

Feature Flag 功能发布控制