现代源代码控制系统提供了强大的工具,可以轻松地在源代码中创建分支。但最终这些分支必须重新合并在一起,许多团队花费了过多的时间来处理它们错综复杂的分支。有几种模式可以让团队有效地使用分支,专注于集成多个开发人员的工作并组织生产版本的路径。总的主题是应该经常集成分支,并将努力集中在一个健康的主线上,该主线可以以最少的努力部署到生产中。
源代码是任何软件开发团队的重要资产,几十年来,已经开发出一套源代码管理工具来保持代码的完整性。这些工具允许跟踪更改,因此我们重新创建了该软件的先前版本,并查看它如何随着时间的推移而发展。这些工具也是协调多个程序员团队的核心,所有程序员都在共同的代码库上工作。通过记录每个开发人员所做的更改,这些系统可以同时跟踪多条工作线,并帮助开发人员找出如何将这些工作线合并在一起。
这种将开发划分为拆分和合并的工作线是软件开发团队工作流程的核心,并且已经发展了几种模式来帮助我们处理所有这些活动。像大多数软件模式一样,其中很少有是所有团队都应该遵循的黄金标准。软件开发工作流程非常依赖于上下文,特别是团队的社会结构和团队遵循的其他实践。
我在本文中的任务是讨论这些模式,我是在一篇文章的上下文中讨论这些模式,在该文章中我描述了模式,但在模式解释中穿插了叙述部分,以更好地解释上下文及其之间的相互关系。为了更容易区分它们,我用“✣”标记标识了模式部分。
基本模式
在考虑这些模式时,我发现开发两个主要类别很有用。一组研究集成,即多个开发人员如何将他们的工作组合成一个连贯的整体。另一个着眼于生产路径,使用分支来帮助管理从集成代码库到生产中运行的产品的路线。一些模式支持这两种模式,我现在将这些模式作为基本模式进行处理。这留下了一些既不是基本的,也不适合两个主要群体的模式——所以我会把它们留到最后。
✣ 源分支 ✣
创建一个副本并记录对该副本的所有更改。
如果几个人在同一个代码库上工作,很快他们要在同样的文件上修改就会变得不可能。如果我想要运行编译,而我的同事正输入表达式输了一半,那么编译将失败。我们不得不互相大喊:“我正在编译,不要改变任何东西”。即使只有两个人,这样做也很难维持,如果团队更大,这将是不可理解的。
这样的问题的简单的解决办法是每个开发人员拷贝一个代码库的副本。现在我们可以轻松地开发我们自己的功能了,但是出现了一个新问题:我们如何在完成后再次将我们的两个副本合并在一起?
源代码控制系统会使这个过程容易些。关键点是它会记录在每个分支上做的更改,记录为一个提交(commit)。这不仅可以确保没有人忘记他们对utils.java所做的微小更改,而且记录更改会使进行合并更容易,特别是当几个人更改了同一个文件时。
这引出了我将在本文中使用的对分支的定义。我将分支定义为对代码库的一个特定提交序列。分支的头部或者说是顶端是这个序列中的最新提交。
这是作为名词时,但同时也可作为动词——“分支”。作为动词时意为创建一个新分支,我们也可以将其视为将原始分支一分为二。当来自一个分支的提交应用于另一个分支时,分支会合并。
我对“分支”的定义与我观察到的大多数开发人员谈论它们的方式相对应。但是源代码控制系统倾向于以更特殊的方式使用“分支”。
我可以用现代开发团队中的一个常见情况来说明这一点,该团队将他们的源代码保存在一个共享的git存储库中。其中一位开发人员Scarlett需要进行一些更改,于是她克隆该git仓库并检出(checkout)了master分支。她做了一些改变,提交(commit)回她的master分支。与此同时,另一位开发人员,我们称她为Violet,将仓库克隆到她的桌面上并检出了master分支。Scarlett和Violet是在同一个分支上工作还是在不同的分支上工作?他们都在“master”上工作,但他们的提交是相互独立的,并且她们将更改推送回共享仓库时需要合并。如果Scarlett觉得她对她所做的更改不是很肯定,于是她给最后一次提交打上标记,然后将她的主分支重置为origin/master(她从共享仓库克隆的最后一次提交),这种情况下会怎么样?
根据我之前给出的分支定义,Scarlett和Violet是在不同的分支上工作,它们彼此独立,并且与共享仓库上的master分支分开。当Scarlett打上标签暂时搁置她的工作时,根据我的定义,它仍然是一个分支(她很可能认为它是一个分支),但在git的说法中,它是带有标记的代码行。
对于像git这样的分布式版本控制系统,这意味着每当我们进一步克隆仓库时,我们还会获得额外的分支。如果Scarlett将她的本地仓库克隆到她的笔记本电脑上,以便她回家,那么她就创建了第三个主分支。在GitHub中分叉也会出现同样的效果——每个分叉的仓库都有自己额外的一组分支。
当我们遇到不同的版本控制系统时,这种术语混淆会变得更糟,因为它们对分支的构成都有自己的定义。Mercurial中的分支与git中的分支完全不同,后者更接近Mercurial的书签。Mercurial也可以使用未命名的head进行分支,Mercurial的人通常通过克隆仓库进行分支。
所有这些术语混淆导致一些人避免使用该术语。在这里有用的一个更通用的术语是codeline。我将代码行定义为代码库版本的特定序列。它可以以标签结尾,可以是分支,也可以在git的reflog中丢失。您会注意到我对分支和codeline的定义之间存在强烈的相似性。Codeline在很多方面都是更有用的术语,我确实使用它,但它在实践中并没有被广泛使用。因此,对于本文,除非我处于git(或其他工具)术语的特定上下文中,否则我将交替使用分支和代码行。
这个定义的一个结果是,无论你使用什么版本控制系统,每个开发人员只要进行本地更改,就会在他们自己的机器上的工作副本上至少有一个个人代码行。如果我克隆了一个项目的git仓库,检出master并更新了一些文件——即使在我提交任何东西之前,这也是一个新的codeline。同样,如果我制作自己的subversion存储库主干的工作副本,即使没有涉及subversion分支,该工作副本也是它自己的codeline。
何时使用
一则古老的笑话说,如果你从高楼上掉下来,坠落不会伤害你,但着陆会。同样的,对于源代码:分支很容易,但合并难。
源代码控制系统记录每个提交上的更改确实使合并过程更容易些,但并没有完全消除困难。如果Scarlett和Violet都更改了变量的名称,更改为不同的名称,冲突就产生了,源代码管理系统在没有人工干预的情况下无法解决冲突。更尴尬的是,仅仅这种文本冲突是源代码控制系统可以发现并提醒人们查看的。更常见的冲突是文本合并没有出问题,但系统仍然无法正常工作。想象一下Scarlett更改了一个函数的名称,而Violet将一些代码添加到她的分支中,通过旧名称调用了该函数。这就出现了我所说的语义冲突。当这些类型的冲突发生时,系统可能无法构建,或者它能够构建但在运行时会失败。
Jonny LeRoy好像指出人们(包括我)绘制分支图的这种缺陷
任何使用并发或分布式计算的人都熟悉这个问题。我们有一些与开发人员并行更新的共享状态(代码库)。我们需要通过将更改序列化为一些共识更新来以某种方式组合这些。由于让系统正确执行和运行意味着该共享状态的有效性标准非常复杂,我们的任务变得更加复杂。没有办法创建确定性算法来达成共识。人类需要找到共识,而这种共识可能涉及混合不同更新的选择部分。通常只能通过原始更新来解决冲突才能达成共识。
- 我从“如果没有分支会怎么样”开始。每个人都会编辑实时代码,半生不熟的更改会使系统崩溃,人们会互相踩踏。因此,我们给人一种时间冻结的错觉,他们是唯一改变系统的人,而这些改变可以在使系统出现风险前完全加工成熟。但这是一种错觉,最终它的代价将到来。谁来支付这种代价?什么时候?多少?这就是这些模式正在讨论的内容:支付债务的替代方案。——Kent Beck
因此,在本文的其余部分,我将列出各种模式,在隔离时给你带来愉快,坠落时带来疾风吹拂你的头发,但同时最大限度地减少你与地面不可避免的接触的后果。
✣✣✣
✣ 主线 ✣
作为产品当前状态的单个共享分支
主线是一个特殊的代码线,我们把它看作是团队代码的当前状态。每当我想开始一项新工作时,我都会将代码从主线拉入我的本地仓库以便开始工作。每当我想与团队的其他成员共享我的工作时,我会把我的工作更新到主线,最好使用我将很快讨论的主线集成模式。
不同的团队为这个特殊的分支使用不同的名称,这通常受到所使用的版本控制系统的习惯的影响。git用户通常称其为“master”,subversion用户通常称其为“trunk”。
我在这里必须强调一下,主线是一个单一的、共享的codeline。当人们在git中谈论“master”时,他们可能说的是几个不同的事物,因为每个仓库的克隆副本都有自己的本地master。通常这样的团队有一个中央仓库——一个共享仓库,作为项目的单一存储点,是大多数克隆的来源。从头开始一项新工作意味着克隆该中央仓库。如果我已经有一个克隆,我首先从中央仓库中拉取master,因此它与主线保持同步。在这种情况下,主线是中央存储库中的主分支。
当我在开发我的功能时,我有我自己的个人开发分支,它可能是我的本地master,或者我可以创建一个单独的本地分支。如果我已经在这个分支上工作了一段时间,我可以通过每隔一段时间拉取主线的更改并将它们合并到我的个人开发分支来保持与主线的同步。
同样,如果我想创建一个新版本的产品以便发布,我可以从当前的主线开始。如果我需要修复错误以使产品足够稳定以供发布,我可以使用发布分支。
何时使用
我记得在2000年代初,我曾与客户的构建工程师交谈。他的工作是组装团队正在开发的产品。他会向团队的每个成员发送一封电子邮件,团队成员会把他们工作的代码库中数个已经准备好集成的文件回复给他。然后他将这些文件复制到他的集成树中并尝试编译代码库。他通常需要几周的时间来创建一个可以编译的构建,并为某种形式的测试做好准备。
相比之下,有了主线,每个人都可以从主线的头部快速启动一次产品的最新构建。此外,主线不仅可以使得查看代码库的状态更容易,它还是我随后将探讨的许多其他模式的基础。
主线的一种替代方案是Release Train。
✣✣✣
✣ 健康分支 ✣
在每次提交时,执行自动检查,通常是构建和运行测试,以确保分支上没有缺陷
由于主线具有这种共享的、已被验证的状态,将其保持在稳定状态非常重要。另一次,在2000年代初期,我记得与另一个组织的团队交谈,该团队以每日构建他们的产品而闻名。这在当时被认为是一种相当先进的做法,该组织因这样做而受到称赞。但这样的评价中没有提到的是,这些日常构建并不总是成功。事实上,好几个月没有编译他们的每日构建的团队并不罕见。
为了解决这个问题,我们可以努力保持分支健康——这意味着它可以成功构建并且软件能够几乎没有bug地运行。为了确保这一点,我发现编写自测试代码至关重要。这种开发实践意味着,在编写产品代码时,我们还编写了一套全面的自动化测试,以便我们确信如果这些测试都通过,代码就不包含任何bug。如果我们这样做,那么我们可以通过在每次提交时运行构建来保持分支健康,这个构建包括运行这个测试套件。如果系统无法编译,或者测试失败,那么我们的首要任务是在我们对该分支做任何其他事情之前修复它们。通常这意味着我们“冻结”了这个分支——除了修复以使其再次健康之外,不允许对其进行任何提交。
对于提供足够信心的健康的测试程度存在矛盾。更彻底的测试需要大量时间来运行,从而延迟了关于提交是否健康的反馈。团队通过将测试分成部署流水线上的多个阶段来解决这个问题。这些测试的第一阶段应该运行很快,通常不超过十分钟,但仍然相当全面。我将这样的套件称为提交套件(尽管它通常被称为“单元测试”,因为提交套件通常主要是单元测试)。
最好每次提交时都要运行所有测试。但是,如果测试速度很慢,例如需要让服务器几个小时高负荷运行的性能测试,那是不切实际的。如今,团队通常可以构建一个可以在每次提交时运行的提交套件,并尽可能多地运行部署流水线的后期阶段。
代码运行时没有bug并不足以说明代码很好。为了保持稳定的交付速度,我们需要保持代码很高的内部质量。一种流行的方法是使用集成前评审,尽管我们将看到,还有其他替代方法。
何时使用
每个团队都应该对他们开发工作流程中每个分支的健康状况有明确的标准。保持主线健康具有巨大的价值。如果主线是健康的,那么开发人员只需拉取当前的主线就可以开始一项新的工作,而不会被妨碍他们工作的缺陷纠缠。我们经常听到人们花费数天时间试图修复或解决他们拉取的代码中的错误,然后才能开始一项新工作。
一个健康的主线也可以让产品化的道路更顺利。一个候选产品可以随时从主线的头部构建。最好的那些团队发现他们只需要做很少的工作来稳定这样的代码库,通常能够直接从主线发布到产品。
拥有一个健康的主线的关键是自测试代码和一个在几分钟内可以运行完的提交套件。构建这样的能力是一项有重大意义的投资,一旦我们能够在几分钟内确保我的提交没有破坏任何内容,这将彻底改变我们的整个开发过程。我们可以更快地进行更改,自信地重构我们的代码以使其易于工作,并大大缩短从期望功能到能在产品中运行的代码的时间周期。
对于个人开发分支,保持它们的健康是明智的,因为这样可以启用差异调试。但是这种愿望与频繁提交检查点当前状态背道而驰。如果我要尝试不同的路径,即使编译失败,我也可能会创建一个检查点。我解决这种矛盾的方法是在我完成我的直接工作后消除任何不健康的提交。这样,只有健康的提交会保留在我的分支上超过几个小时。
如果我保持我的个人分支健康,这也使提交到主线变得更容易——我知道主线集成中出现的任何错误纯粹是由于集成问题,而不是我的代码库中的错误。这将使查找和修复它们变得更快、更容易。
✣✣✣
集成模式
分支用于管理隔离和集成之间的相互作用。让每个人一直在一个共享的代码库上工作是行不通的,因为如果您正在输入变量名,我将无法编译程序。所以至少在某种程度上,我们需要一个私人工作空间的概念,在这个私人空间里面我可以工作一段时间。现代源代码控制工具使得分支和管理对这些分支的更改变得容易。然而,在某些时候我们需要集成。分支策略实际上就是要决定我们如何以及何时集成。
✣ 主线集成 ✣
开发人员通过从主线中提取、合并和(如果健康的话)推回主线来整合他们的工作
主线清楚地定义了团队的软件当前的状态。使用主线的最大好处之一是它简化了集成。没有主线,与我上面描述的团队中的每个人进行协调是一项复杂的任务。然而,有了主线,每个开发人员都可以自己集成。
我将通过一个例子来说明这是如何工作的。一位开发人员,我将称之为Scarlett,通过将主线克隆到她自己的仓库中来开始一些工作。使用git,如果她还没有中央仓库的克隆,她会克隆它并签出master分支。如果她已经有了克隆,她会从主线拉到她本地的master上。然后她可以在本地工作,向她的本地master提交。
在她工作时,她的同事Violet将一些更改推送到主线上。当她在自己的codeline中工作时,Scarlett可以忽略这些变更,同时完成自己的任务。
在某个时候,她进行到了需要集成的地步。第一步是将主线的当前状态拉取到她本地的master分支中,这将拉入Violet的更改。当她在本地master上工作时,提交将在origin/master上显示为单独的codeline。
现在她需要将自己的变更与Violet的变化合并起来。有些团队喜欢通过"merge"来做到这一点,有些则喜欢通过"rebase"来实现。一般来说,人们在谈论将分支放在一起时都会使用“合并”这个词,无论他们实际上是使用git merge还是rebase操作。我将遵循这种用法,因此除非我实际上是在讨论合并和变基之间的差异,否则将“合并”视为可以通过两者实现的逻辑任务。
关于是否使用普通合并,使用或避免快进合并,或使用变基,还有一个完整的其他讨论。这超出了本文的范围,但如果人们寄给我足够多的Tripel Karmeliet,我可能会写一篇关于该问题的文章。毕竟,"Quid Pro Quo"如今风靡一时。
如果Scarlett幸运的话,合并Violet的代码将是一个干净的合并,否则她将有一些冲突需要处理。这些可能是文本冲突,其中大部分源代码控制系统可以自动处理。但是语义冲突更难处理,这就是自测试代码非常有用的地方。(因为冲突会产生大量的工作,并且总是会带来大量工作的风险,我用一个令人震惊的黄色块标记它们。)
此时,Scarlett需要验证合并后的代码是否满足主线的健康标准(假设主线是健康的分支)。这通常意味着构建代码并运行构成主线提交套件的任何测试。即使是干净的合并,她也需要这样做,因为即使是干净的合并也可以隐藏语义冲突。提交套件中的任何失败都应该纯粹是由于合并造成的,因为两个合并父项都应该是绿色的。知道这一点应该有助于她追踪问题,因为她可以查看差异以寻找线索。
通过这个构建和测试,她已经成功地将主线拉入了她的代码线,但是——这很重要而且经常被忽视——她还没有完成与主线的集成。要完成集成,她必须将她的更改推送到主线中。除非她这样做,团队中的其他人都将与她的更改隔离开来——本质上不是集成。集成既是拉取也是推送——只有Scarlett在推动后,她的工作才会与项目的其余部分集成。
如今,许多团队在将提交添加到主线之前需要一个代码评审步骤——我称之为集成前评审的模式,稍后将讨论。
偶尔,在Scarlett可以推送之前,其他人会与主线集成。在这种情况下,她必须再次拉取并合并。通常这只是偶尔的问题,无需任何进一步协调即可解决。我见过长时间构建的团队使用集成指挥棒,只有持有指挥棒的开发人员才能进行集成。但近年来,随着构建时间的改善,我并没有听到太多这样的事情。
何时使用
顾名思义,如果我们也在我们的产品上使用主线,我只能使用主线集成。
使用主线集成的一种替代方法是只从主线中拉取,并将这些更改合并到个人开发分支中。这么做是有效的——拉取至少可以提醒Scarlett其他人已集成的更改,并检测她的工作和主线之间的冲突。但在Scarlett推送之前,Violet将无法发现她正在做的事情与斯嘉丽的更改之间的任何冲突。
当人们使用“集成”这个词时,他们往往会忽略这一点。经常听到有人说他们他们正在将主线集成到他们的分支中,而实际上只是在拉取。我已经学会了对此保持警惕,并进一步探索检查它们是否意味着只是拉取或真正的主线集成。两者的结果是非常不同的,所以不要混淆这些术语是很重要的。
另一种选择是当Scarlett正在做一些尚未准备好与团队其他成员完全集成的工作时,但它与Violet重叠并且她想与她共享。在这种情况下,他们可以打开一个协作分支。
✣✣✣
✣ 特征分支 ✣
将一个特性的所有工作放在它自己的分支上,当特性完成时集成到主线中。
通过功能分支,开发人员在开始处理功能时新开一个分支,继续处理该功能直到完成,然后与主线集成。
例如,让我们跟随Scarlett。她会选择向他们的网站添加当地销售税的征收功能。她从产品的当前稳定版本开始,她会将主线拉入她的本地仓库,然后从当前主线的顶端创建一个新分支。她一直开发这个功能,并向该本地分支进行一系列提交。
她可能会将该分支推送到项目仓库,以便其他人可以查看她的更改。
在她工作的同时,主线上会有一些其他的提交。所以她可能会不时从主线上拉取,这样她就可以判断是否有任何更改可能会影响她的功能。
请注意,这不是我上面描述的集成,因为她没有推回主线。此时只有她在看她的作品,其他人看不到。
有些团队喜欢确保所有代码,无论是否集成,都保存在中央存储库中。在这种情况下,Scarlett会将她的功能分支推送到中央仓库中。这也允许其他团队成员看到她在做什么,即使它尚未集成到其他人的工作中。
当她完成该功能的工作后,她将执行主线集成以将该功能合并到产品中。
如果Scarlett同时处理多个功能,她将为每个功能打开一个单独的分支。
何时使用
特性分支是当今业界比较流行的一种模式,要说到什么时候用,先介绍一下它的主要替代方案——持续集成。但首先我需要谈谈集成频率所扮演的角色。
✣✣✣
集成频率
我们进行集成的频率对团队的运作方式有着非常强大的影响。来自DevOps报告的研究表明,精英开发团队的整合频率明显高于低绩效团队——这一观察结果与我的经验和许多行业同行的经验相吻合。我将通过考虑由Scarlett和Violet担任主角的两个集成频率示例来说明这是如何进行的。
低频率集成
我将从低频案例开始。在这里,两位主角通过将主线克隆到他们的分支中来开始工作,然后进行一些本地提交,但她们暂时不想推送。
在他们工作时,另一个人提交了一个提交到主线。(我不能很快想出另一个有颜色的名字——也许是grayham?)
该团队通过保持一个健康的分支并在每次提交后从主线拉取来工作。由于主线没有变化,Scarlett的前两次提交没有什么可拉的,但现在需要拉M1。
我用黄色框标记了合并。这个将提交S1..3与M1合并。很快,Violet需要做同样的事情。
在这一点上,两个开发人员都与主线保持同步,但他们还没有集成,因为他们彼此隔离。Scarlett不知道Violet在V1..3中所做的任何更改。
Scarlett进行了更多本地提交,然后准备进行主线集成。这对她来说很容易,因为她早些时候拉了M1。
但是,Violet有一个更复杂的练习。当她进行主线集成时,她现在必须将S1..5与V1..6集成。
我已经根据涉及的提交数量科学地计算了合并的大小。但即使你忽略了我脸颊上的舌状凸起,你也会意识到Violet的合并最有可能是困难的。
高频率集成
在前面的示例中,我们的两个富有色彩的开发人员在少数本地提交后集成了。让我们看看如果他们在每次本地提交后进行主线集成会发生什么。
第一个变化在Violet的第一次提交中很明显,因为她立即进行了集成。由于主线没有改变,这只是一个简单的推送。
Scarlett的第一次提交也有主线集成,但因为Violet先到那里,她需要做一个合并。但由于她只是将V1与S1合并,因此合并很小。
Scarlett的下一次集成是一个简单的推送,这意味着Violet的下一次提交也需要与Scarlett的最新两次提交合并。然而,这仍然是一个很小的合并,Violett的一个和Scalett的两个。
当主线上有外部的推送时,Scalett和Violett整合的惯常节奏被接受。
虽然它与之前发生的情况类似,但集成更小。Scarlett这次只需要把S3和M1结合起来,因为S1和S2已经在主线上了。这意味着在推送M1之前,Grayham必须整合主线(S1..2、V1..2)上已经存在的任何内容。
开发人员继续他们剩余的工作,与每个提交集成。
比较集成频率
再来看看两张整体图
低频率的
高频率的
这里有两个非常明显的区别。首先,顾名思义,高频集成有更多的集成——在这个玩具示例中是两倍。但更重要的是,这些集成比低频情况下的集成要小得多。更小的集成意味着更少的工作,因为可能致使冲突的代码更改更少。但比减少工作更重要的是,它的风险也更小。大合并的问题不在于它们所涉及的工作,而在于该工作的不确定性。大多数情况下,即使是大型合并也很顺利,但有时它们会非常、非常、非常糟糕。偶尔的疼痛最终比常规疼痛更严重。如果我将每次集成多花10分钟与有五十分之一的概率花费6小时修复一次集成进行比较——我倾向于哪个?如果我只看要付出的努力,那么五十分之一更好,因为它是6小时而不是8小时20分钟。但这种不确定性让五十分之一的情况感觉更糟,这种不确定性会导致集成恐惧。
让我们从另一个角度来看看这些频率之间的差异。如果Scarlett和Violet在第一次提交时发生冲突,会发生什么?他们什么时候检测到冲突发生了?在低频情况下,他们直到Violet最终合并时才检测到它,因为这是第一次将S1和V1放在一起。但在高频情况下,它们是在Scarlett的第一次合并时检测到的。
低频率的
高频率的
频繁的集成会增加合并的频率,但会降低它们的复杂性和风险。频繁的集成还可以更快地提醒团队注意冲突。这两件事当然是有联系的。令人讨厌的合并通常是团队工作中潜在的冲突的结果,只有在集成发生时才会出现。
也许Violet正在查看计费计算,并看到它包括评估税收,而作者假设了一种特定的税收机制。她的功能需要对税收进行不同的处理,因此直接的方法是将税收从计费计算中剔除,并在以后作为单独的功能进行处理。计费计算只在几个地方调用,所以很容易使用Move Statements to Callers——结果对于程序的未来发展更有意义。然而,Scarlett不知道Violet正在这样做,并假设计费功能负责税收,因此编写了她的功能。
自我测试代码在这里是我们的救星。如果我们有一个强大的测试套件,将它用作健康分支的一部分将发现冲突,因此错误进入生产的可能性要小得多。但即使有强大的测试套件充当主线的看门人,大型集成也会让生活变得更加艰难。我们需要集成的代码越多,就越难找到错误。我们也有更高的机会出现多个干扰性错误,这些错误特别难以理解。我们不仅可以通过较小的提交来查看更少的内容,还可以使用Diff Debugging来帮助缩小导致问题的更改范围。
很多人没有意识到源代码控制系统是一种通信工具。它让Scarlett可以看到团队中其他人在做什么。通过频繁的集成,她不仅会在出现冲突时立即收到警报,而且更了解每个人的工作以及代码库的发展情况。我们不像个人独立进行黑客攻击,而更像是一个团队合作。
增加集成频率是减小特征尺寸的重要原因,但也有其他优势。功能越小,构建的速度就越快,投入生产的速度越快,开始交付其价值的速度也就越快。此外,较小的功能减少了反馈时间,使团队能够在了解更多客户信息时做出更好的功能决策。
✣ 持续集成 ✣
开发人员一旦有一个可以共享的健康提交就做主线集成,这样的提交通常包含不到一天的工作。
一旦团队体验到高频率集成不仅效率更高,而且压力更小,自然要问的问题是“我们多久集成一次?”特征分支意味着变更集大小的下限——不能比一个内聚的特征还小。
持续集成应用不同的集成触发器——只要您在功能上取得了很大进展并且您的分支仍然健康,您就可以进行集成。不期望该功能是完整的,只是对代码库进行了足够量的更改。经验法则是“每个人每天都向主线提交”,或者更准确地说:您的本地仓库中不应该有超过一天的未集成的工作。在实践中,大多数持续集成实践者每天集成多次,乐于集成一个小时或更短的工作。
使用持续集成的开发人员需要习惯使用部分构建的功能达到频繁集成点的想法。他们需要考虑如何在不暴露正在运行的系统中部分构建的功能的情况下做到这一点。通常这很容易:如果我正在实施依赖于优惠券代码的折扣算法,并且该代码尚未在有效列表中,那么即使我的代码是生产代码,也不会被调用。同样,如果我要添加一个功能,询问保险索赔人是否吸烟,我可以构建和测试代码背后的逻辑,并通过将提出问题的UI保留到最后一天来确保它不会在生产中使用构建特征。通过最后连接Keystone接口来隐藏部分构建的功能通常是一种有效的技术。
如果没有办法轻松隐藏部分特征,我们可以使用特征标志。除了隐藏部分构建的功能外,此类标志还允许有选择地向用户子集显示该功能——通常对于新功能的缓慢推出非常方便。
集成部分构建的功能特别关注那些担心主线中有错误代码的人。因此,那些使用持续集成的人也需要自测试代码,因此有信心在主线中部分构建功能不会增加产生错误的机会。通过这种方法,开发人员在编写功能代码时为部分构建的功能编写测试,并将功能代码和测试一起提交到主线(可能使用测试驱动开发)。
就本地仓库而言,大多数使用持续集成的人不会费心在一个单独的本地分支上工作。提交到本地master并在完成后执行主线集成通常很简单。但是打开一个功能是完全没问题的分支并在那里完成工作,如果开发人员愿意,可以频繁地集成回本地master和主线。功能分支和持续集成之间的区别不在于是否有功能分支,而在于开发人员是否与主线集成。
何时使用
持续集成是功能分支的替代方案。两者之间的权衡涉及到足够多的内容,值得在本文中有自己的部分,现在是解决它的时候了。
✣✣✣
比较功能分支和持续集成
功能分支似乎是目前业界最常见的分支策略,但有一群从业者认为持续集成通常是一种更好的方法。持续集成提供的主要优势是它支持更高的集成频率,通常是高很多的集成频率。
集成频率的差异在于团队能够让他们的特性有多小。如果一个团队的每个特征(feature)都可以在一天之内完成,那么他们既能使用功能分支又能使用持续集成。但是大多数团队的特征(feature)都比这更长——特征越大,两种模式之间的差异就越大。
正如我已经指出的,更高的集成频率会带来更少的耗费大量时间的集成和更少的对集成的恐惧。这通常是一件很难沟通的事情。如果您生活在一个每隔几周或几个月进行集成的世界,那么集成可能是一项非常令人不愉快的活动。很难相信这是一天可以做很多次的事情。但集成是一种“频率降低难度”的事情。这是一个违反直觉的概念——“如果它有难度——就更频繁地做”。但集成越小,它变成令人痛苦和绝望的史诗般的合并的机会就越小。功能分支需要更小的特性(feature):几天而不是几周(当然不是几个月)。
持续集成让团队获得高频集成的好处,同时将特征长度与集成频率分离。如果一个团队更喜欢一周或两周的特征长度,持续集成允许他们这样做,同时仍然获得集成频率的所有好处。合并更小,需要处理的工作也更少。更重要的是,正如我上面解释的,更频繁地进行合并降低了产生糟糕的合并的风险,这既消除了这带来的坏意外,也减少了对合并的总体的恐惧。如果代码中出现冲突,高频集成会在它们导致糟糕集成之前很快发现它们。这些好处足够强大,以至于有些团队的功能只需要几天时间,仍然可以进行持续集成。
持续集成的明显缺点是它缺乏对激动人心的集成到主线的封闭。这不仅是一个失落的庆祝,对于一个不擅长保持健康分支的团队来说也是一个风险。保留一个功能的所有提交在一起还可以在后期决定是否在即将发布的版本中包含某个功能。虽然功能标志允许从用户的角度打开或关闭功能,但该功能的代码仍在产品中。关于这一点通常被夸大了,毕竟代码并不重要,但这确实意味着想要进行持续集成的团队必须制定强大的测试方案,以便他们可以确信即使一天进行多次集成,主线仍然保持健康。一些团队觉得这项技能难以想象,但其他人发现它既可行又让人自由。这个先决条件确实意味着功能分支更适合不强制健康分支并需要在发布前使用发布分支来保持稳定代码的团队。
虽然合并的大小和不确定性是Feature Branching最明显的问题,但它最大的问题可能是它会阻止重构。重构在定期进行时最有效且冲突较小。重构会引入冲突,如果这些冲突没有被发现并迅速解决,合并变得令人担忧。因此重构在高集成频率下效果最好,因此它作为极限编程的一部分变得流行也就不足为奇了,同时持续集成也是极限编程的基本实践之一。Feature Branching还阻止开发人员进行不被视为正在构建的功能的一部分的更改,这会破坏使用重构来稳定改进代码库的能力。
当我遇到有关软件开发实践的科学研究时,我通常不相信,因为他们的方法论存在严重问题。一个例外是Dev Ops报告,它提出了一个软件交付效率指标,它们与更广泛的组织效率度量相关,这反过来又与投资回报和盈利能力等业务指标相关。2016年,他们首次评估了持续集成,发现它有助于提高软件开发效率,此后的每次调查都重复了这一发现。
使用持续集成并没有消除保持小功能的其他优势。频繁发布小功能提供了一个快速的反馈周期,这可以为改进产品创造奇迹。许多使用持续集成的团队也努力构建产品的薄片并尽可能频繁地发布新的功能。
功能分支和开源
许多人将Feature Branching的流行归因于GitHub和起源于开源开发的pull-request模型。鉴于此,理解开源工作和许多商业软件开发之间存在的非常不同的上下文是值得的。开源项目以许多不同的结构来组织,但一个共同的结构是属于一个人或一个小组,作为维护者完成大部分编程。维护者与作为贡献者的更多程序员一起工作。维护者通常不认识贡献者,因此不了解他们贡献的代码的质量。维护者也不确定贡献者实际投入到工作中的时间,更不用说他们在完成工作时的效率了。
在这种情况下,功能分支很有意义。如果有人要添加一个功能,无论大小,我不知道它何时(或是否)完成,那么我等到它完成后再集成是有意义的。能够审查代码也更重要,以确保它通过我的代码库的任何质量标准。
但是许多商业软件团队有一个非常不同的工作环境。他们是全职的团队,所有这些人都对软件投入巨大,通常是全职的。项目的领导者很了解这些人(除了他们刚开始)并且可以对代码质量和交付能力有可靠的期望。由于他们是有薪员工,因此领导者也可以更好地控制投入项目的时间以及编码标准和团队习惯等方面。
鉴于这种非常不同的背景,应该清楚的是,此类商业团队的分支策略不必与在开源世界中运作的策略相同。持续集成几乎不可能适合偶尔为开源工作做出贡献的人,但对于商业工作来说是一个现实的替代方案。团队不应该假设适用于开源环境的东西会自动适用于他们的不同环境。
✣ 集成前评审 ✣
在接受提交之前,对主线的每个提交都经过同行评审。
代码评审长期以来一直被鼓励作为提高代码质量、提高模块化、可读性和消除缺陷的一种方式。尽管如此,商业组织经常发现代码评审难以适应软件开发工作流程。然而,开源世界广泛采用了应该在合并对项目的贡献到主线之前进行评审的想法,这种方法近年来在开发组织中广泛传播,特别是在硅谷。这样的工作流程特别适合GitHub的pull-requests机制。
当Scarlett完成她希望集成的一大堆工作时,这样的工作流程就开始了。当她进行主线集成时(假设她的团队实践了这一点),一旦她成功构建,但在她推送到主线之前,她将她的提交发送给被人评审。团队的其他成员,比如Violet,接着对提交进行代码评审。如果她在提交时遇到问题,她会发表一些评论,然后反复讨论直到Scarlett和Violet都满意为止。只有她们完成后,才会提交在主线上。
集成前评审在开源中越来越受欢迎,它们非常适合提交维护者加上偶尔贡献者这样的组织模式。它们允许维护者密切关注任何贡献。它们也与特征分支很好地融合,因为一个完成的功能标志着进行这样的代码评审的一个明确时机。如果你不确定贡献者是否会完成一个功能,为什么要评审他们的部分工作?最好等到功能完成。这种做法也广泛在较大的互联网公司使用,谷歌和Facebook都构建了特殊的工具来帮助使这项工作顺利进行。
制定纪律来确保及时的集成前评审是很重要的。如果开发人员完成了一些工作,然后继续做其他事情几天,那么当评审评论到来时,这项工作已经不再是他们的首要任务了。对于已完成的功能这是令人沮丧的,但对于部分完成的功能来说更糟糕,在评审确认之前可能会停止不前。原则上,可以通过集成前评审进行持续集成,在实践上也确实可行——Google遵循这种方法。虽然这是可能的,但它很难,而且相对较少。集成前评审和功能分支是更常见的搭配。
何时使用
尽管集成前评审在过去十年中已成为一种流行的做法,但也存在缺点和替代方案。即使做得很好,集成前评审总是会在集成过程中引入一些延迟,鼓励较低的集成频率。结对编程提供了持续的代码评审过程,反馈周期比去等待代码评审更快。(与持续集成和重构一样,它是极限编程的原始实践之一)。
许多使用集成前评审的团队没有足够快地完成评审。他们可以提供的有价值的反馈来得太晚而没有用处。此时,可以在大量返工或接受可能有效的东西之间做出尴尬的选择,但破坏了代码库的质量。
代码评审不仅限于代码集成到主线之前。许多技术领导者发现在提交后评审代码很有用,当他们看到问题时再赶上开发人员。重构文化在这里很有价值。做好这一点就会建立一个社区,其中的每个人都定期审查代码库并修复他们看到的问题。
围绕集成前评审的权衡主要取决于团队的社会结构。正如我已经提到的,开源项目通常有几个受信任的维护者和许多不受信任的贡献者的结构。商业团队通常都是全职,但可能具有类似的结构。项目负责人(如维护者)信任一小群(可能是单一的)维护者,并对团队其他成员贡献的代码保持警惕。团队成员可能被一下子分配到多个项目,让他们更像开源贡献者。如果这样的社会结构存在,那么集成前评审和特征分支就很有意义。但是一个具有更高信任度的团队通常会发现其他机制来保持高质量的代码,不会给集成过程增加摩擦。
因此,虽然集成前评审可能是一种有价值的做法,但它绝不是通往健康代码库的必要途径,特别是如果您希望发展一个不过度依赖其初始领导者的均衡团队。
✣✣✣
集成摩擦
集成前评审的问题之一是,它常常使集成变得更加麻烦。这是集成摩擦的一个例子——使集成需要时间或努力去做的活动。集成摩擦越多,越多的开发者倾向于降低集成频率。想象一下,一些(功能失调的)组织坚持所有到主线的提交都需要一个半小时才能填写好的表格。这样的制度不鼓励人们频繁集成。无论如何你对是倾向于特征分支还是持续集成,检查任何增加这种摩擦的东西是有价值的。除非它明显增加了价值,否则任何这种摩擦都应该被去除。
手动流程在这里是一个常见的摩擦源,特别是如果它涉及与不同组织的协调。这种摩擦通常可以通过使用自动化流程、改进开发人员教育(以消除需求)以及将步骤推到后续的部署流水线和产品QA的步骤。你可以在持续集成和持续交付的材料中找到更多消除这种摩擦的想法。这种摩擦也出现在生产的路径中,具有相同的困难和处理。
让人不愿意考虑持续集成的一个因素是他们是否只在高度集成摩擦的环境中工作过。如果需要一个小时来做一个集成,那么一天做几次显然是荒谬的。加入一个集成不是问题的团队,有人可以在几分钟内完成集成,感觉就像一个不同的世界。我怀疑特征分支和持续集成之间优点的许多争论是浑浊的,因为人们没有同时经历过这两个世界,就不能完全理解这两种观点。
文化因素会影响集成摩擦——尤其是团队成员之间的信任。如果我是团队leader,我不相信我的同事会把工作做好,那么我可能会阻止破坏代码库的提交。自然地,这是集成前评审的驱动因素之一。但如果我在一个我信任我的同事的判断力的团队中,我可能更愿意接受提交后评审,或者完全砍掉评审环节并依靠共同重构来解决任何问题。我在这种环境中的收获是消除了提交前评审带来的摩擦,从而鼓励了更高频率的集成。通常,团队信任是特征分支与持续集成的争论中最重要的因素。
Rouan Wilsenach的Ship/Show/Ask是一种有趣的方法,可以在需要时保留集成前评审,同时带来了减少摩擦的道路。它将更改分类为Ship(集成到主线)、Show(集成到主线,但打开拉取请求以交流和讨论更改)或Ask(打开拉取请求以进行预集成评审)。
模块化的重要性
大多数关心软件架构的人都强调模块化对于行为良好的系统的重要性。如果我面临着对模块化较差的系统进行小改动,我必须了解几乎所有内容,因为即使是小改动也会影响代码库的许多部分。但是,有了良好的模块化,我只需要了解一两个模块中的代码,再了解几个模块的接口,其余部分可以忽略。这种减少我需要付出的理解的努力的能力就是随着系统的增长而在模块化上付出如此多的努力是值得的原因。
模块化也会影响集成。如果系统具有良好的模块,那么大多数时候Scarlett和Violet将在代码库中分离良好的部分工作,并且它们的更改不会引起冲突。良好的模块化还增强了Keystone Interface和Branch By Abstraction等技术,以避免需要分支提供的隔离。团队经常被迫使用源代码分支,因为缺乏模块化使他们缺乏其他选择。
特征分支是穷人的模块化架构,它不去构建能够在运行时/部署时轻松换进换出功能的系统,而是将自己耦合到通过手动合并提供此机制的源代码控制系统。——Dan Bodart
支持是双向的。尽管进行了多次尝试,但在我们开始编程之前构建一个好的模块化架构仍然极其困难。为了实现模块化,我们需要不断观察我们的系统随着它的增长并朝着更加模块化的方向发展。重构是实现这一目标的关键,重构需要高频集成。因此,模块化和快速集成在健康的代码库中相互支持。
这就是说模块化虽然难以实现,但值得付出努力。这项工作涉及良好的开发实践、学习设计模式以及从代码库的经验中学习。混乱的合并不应该只是以忘却它们的可以理解的愿望而被关闭——而是要问为什么合并是混乱的。这些答案通常是如何改进模块化、改善代码库的健康状况,从而提高团队生产力的重要线索。
关于集成模式的个人想法
作为作家,我的目标不是说服您遵循特定的路径,而是告知您在决定要遵循的路径时应该考虑的因素,由您自己来决定遵循哪条路径。尽管如此,我还是会在此处添加我对我之前指出的模式中我更喜欢的观点的看法。
总的来说,我更喜欢在使用持续集成的团队工作。我认识到上下文是关键,在很多情况下,持续集成不是最佳选择——但我的反应是努力改变上下文。我有这个偏好,因为我希望在这样一个环境中,每个人都可以轻松地继续重构代码库,改进其模块化,保持其健康——所有这些都使我们能够快速响应不断变化的商业需求。
如今,我更像是一名作家而不是一名开发人员,但我仍然选择在Thoughtworks工作,这是一家充满喜欢这种工作方式的人的公司。这是因为我相信这种极限编程风格是我们开发软件的最有效方式之一,我想看到团队进一步发扬这种方法以提高我们专业的效率。
从主线到产品发布的路径
主线是一个活跃的分支,定期发布新的和修改过的代码。保持健康很重要,这样当人们开始新工作时,他们就会有一个稳定的基础。如果它足够健康,您还可以直接从主线发布代码到生产中。
这种保持主线始终处于可发布状态的理念是持续交付的核心原则。为此,必须具备将主线维护为健康分支的决心和技能,通常使用部署流水线来支持密集的测试需求。
以这种方式工作的团队通常可以通过在每个发布的版本上使用标签来跟踪他们的发布。但是不使用持续交付的团队需要另一种方法。
✣ 发布分支 ✣
一个只接受那些被接受来稳定准备发布的产品版本的提交。
典型的发布分支将从当前主线复制,但不允许添加任何新功能。主要开发团队继续向主线添加此类功能,这些功能将在未来版本中采用。致力于发布的开发人员只专注于消除任何阻止发布准备就绪的缺陷。对这些缺陷的任何修复都在发布分支上创建并合并到主线。一旦没有更多的故障需要处理,分支就准备好进行产品发布了。
尽管发布分支上修复的工作范围(希望)小于新功能代码,但随着时间的推移,将它们合并回主线变得越来越困难。分支不可避免地会发散,因此随着更多提交修改主线,将发布分支合并到主线变得更加困难。
以这种方式将提交应用到发布分支的一个问题是很容易忽略将它们复制到主线,特别是由于分歧而变得更加困难。由此产生的回归非常尴尬。因此,有些人喜欢在主线上创建提交,并且只有在他们在那里工作时才将它们cherry-pick到发布分支中。
cherry-pick是指将提交从一个分支复制到另一个分支,但这些分支没有合并。也就是说,只复制一次提交,而不是自分支点以来的先前提交。在这个例子中,如果我将F1合并到发布分支,那么这将包括M4和M5。但是cherry-pick只需要F1。cherry-pick可能不会完全适用于发布分支,因为它可能依赖于M4和M5中所做的更改。
在主线上提交发布修复的缺点是许多团队发现这样做更困难,并且在主线上以一种方式修复它并且在发布之前必须在发布分支上返工令人沮丧。当存在发布发布的进度压力时尤其如此。
在产品中一次只有一个版本的团队只需要一个发布分支,但有些产品在生产中会有多个发布。在客户套件上运行的软件只会在客户希望时升级。许多客户不愿意升级,除非他们有引人注目的新功能,因为升级失败曾让他们痛苦不堪。但是,这些客户仍然希望修复错误,尤其是在涉及安全问题时。在这种情况下,开发团队为仍在使用的每个版本保持发布分支打开,并根据需要对它们应用修复。
随着开发的进行,将修复应用到旧版本变得越来越困难,但这通常是开展业务的成本。只能通过鼓励客户频繁升级到最新版本来缓解这种情况。保持产品稳定对此至关重要,一旦失败,客户将不愿意再次进行不必要的升级。
(我听说过发布分支的其他术语包括:“发布准备分支”、“稳定分支”、“候选分支”和“强化分支”。但“发布分支”似乎是最常见的。)
何时使用
当团队无法将主线保持在健康状态时,发布分支是一个有价值的工具。它允许团队的一部分专注于为生产做好准备所需的必要错误修复。测试人员可以从这个分支的顶端拉出最近最稳定的候选者。每个人都可以看到为稳定产品所做的工作。
尽管发布分支很有价值,但大多数最好的团队不会将这种模式用于单生产产品,因为他们不需要。如果主线保持足够健康,则可以直接发布对主线的任何提交。在这种情况下,发行版应标记为公开可见的版本和内部版本号。
您可能已经注意到我在上一段中插入了笨拙的形容词“单一制作”。这是因为当团队需要在生产中管理多个版本时,这种模式变得必不可少。
当发布过程中存在重大摩擦时,发布分支也可能很方便——例如必须批准所有生产版本的发布委员会。正如Chris Oldwood所说,“在这些情况下,发布分支更像是一个隔离区,而公司的齿轮慢慢转动”。一般来说,这种摩擦应该尽可能地从发布过程中消除,就像我们需要消除集成摩擦一样。但是,在某些情况下,例如移动应用程序商店,这可能是不可能的。在许多情况下,大多数情况下一个标签就足够了,并且只有在源需要进行一些基本更改时才会打开分支。
发布分支也可能是环境分支,这取决于使用该模式的问题。还有一个长期存在的发布分支的变体,我将很快准备好对其进行描述。
✣✣✣
✣ 成熟度分支 ✣
一个分支,其头部标志着代码库成熟度的最新版本。
团队通常想知道源代码的最新版本是什么,这对于具有不同成熟度级别的代码库可能会变得复杂。QA工程师可能希望查看产品的最新暂存版本,调试生产故障的人员可能希望查看最新的生产版本。
成熟度分支提供了一种进行这种跟踪的方法。一旦代码库的某个版本达到一定程度的就绪程度,就会将其复制到特定分支中。
考虑一个用于生产的成熟度分支。当我们准备好生产版本时,我们会打开一个发布分支来稳定产品。一旦准备就绪,我们将其复制到一个长期运行的生产分支。我认为这是复制而不是合并,因为我们希望生产代码与在上游分支上测试的完全相同。
成熟度分支的吸引力之一是它清楚地显示了到达发布工作流程中该阶段的代码的每个版本。因此,在上面的示例中,我们只希望在生产分支上进行一次提交,该提交将提交M1-3和F1-2组合在一起。有一些源代码控制管理的小技巧(SCM-jiggery-pokery)可以实现这一点,但无论如何这都会失去与主线上细粒度提交的链接。这些提交应该记录在提交消息中,以帮助人们以后追踪它们。
成熟度分支通常以开发流程中的适当阶段命名。因此,诸如“生产分支”、“暂存分支”和“QA分支”之类的术语。偶尔我听说人们将生产成熟度分支称为“发布分支”。
何时使用
源代码控制系统支持协作和跟踪代码库的历史。使用成熟度分支允许人们通过显示发布工作流中特定阶段的版本历史记录来获取一些重要的信息。
我可以通过查看相关分支的头部来找到最新版本,例如当前运行的生产代码。如果出现我确定事先不存在的错误,我可以查看分支上的先前版本并查看生产中的特定代码库更改。
自动化可以与对特定分支的更改相关联——例如,只要对生产分支进行提交,自动化流程就可以将版本部署到产品中。
使用成熟度分支的替代方法是应用标记方案。一旦一个版本准备好进行QA,它就可以被标记为这样——通常以包含内部版本号的方式。因此,当build 762准备好进行QA时,它可以被标记为“qa-762”,当准备好进行生产时,它会被标记为“prod-762”。然后,我们可以通过在代码存储库中搜索与我们的标记方案匹配的标记来获取历史记录。自动化同样可以基于标签分配。
因此,成熟度分支可以为工作流增加一些便利,但许多组织发现标记工作得非常好。所以我认为这是一种没有很强的收益或成本的模式。然而,像这样使用源代码管理系统进行跟踪的需要通常是团队部署流水线工具不佳的标志。
变体:长寿命的发布分支
我可以将其视为发布分支模式的一种变体,它将它与发布候选的成熟度分支相结合。当我们想要发布时,我们将主线复制到这个发布分支中。与每个发布分支一样,提交仅在发布分支上进行以提高稳定性。这些修复也合并到主线中。我们在发布时标记发布,当我们想要进行另一个发布时可以再次复制主线。
提交可以像成熟度分支中更典型的那样被复制或合并。如果合并,我们必须小心确保发布分支的头部与主线的头部完全匹配。一种方法是在合并之前恢复已应用于主线的所有修复。一些团队还在合并后压缩提交,以确保每个提交都代表一个完整的候选版本。(发现这很棘手的人有充分的理由更喜欢为每个版本切割一个新分支。)
此方法仅适用于一次在生产中发布一个版本的产品。
团队喜欢这种方法的一个原因是它确保发布分支的头部始终指向下一个候选版本,而不必挖掘最新发布分支的头部。然而,至少在git中,我们通过拥有一个“发布”分支名称来实现相同的效果,当团队剪切一个新的发布分支时,该分支名称会随着hard reset而移动,在旧的发布分支上留下一个标签。
✣✣✣
✣ 环境分支 ✣
通过应用源代码提交将产品配置为在新环境中运行。
软件通常需要在不同的环境中运行,例如开发人员的工作站、生产服务器,也许还有各种测试和模拟环境。通常在这些不同的环境中运行需要一些配置更改,例如用于访问数据库的URL、消息系统的位置以及关键资源的URL。
环境分支是包含应用于源代码以重新配置产品以在不同环境中运行提交的分支。我们可能在主线上有2.4的运行版,现在希望在我们的模拟服务器上运行它。为此,我们从2.4版开始切割一个新分支,应用适当的环境更改,重新构建产品,并将其部署到模拟环境。
这些更改通常是手动应用的,但如果负责人对git感到满意,他们可能会从较早的分支中挑选更改。
环境分支模式通常与成熟度分支结合使用。长期存在的QA成熟度分支可能包括QA环境的配置调整。合并到此分支将获取配置更改。类似地,一个长期存在的发布分支可能包括这些配置更改。
何时使用
环境分支是一种吸引人的方法。它允许我们以我们需要的任何方式调整应用程序,使其为新环境做好准备。我们可以将这些更改保留在一个差异中,这些差异可以被cherry-pick到产品的未来版本中。然而,它是反模式的典型例子——当你开始时看起来很有吸引力,但很快就会导致一个充满痛苦、可怕和冠状病毒的世界。
伴随着环境中的任何转变的潜在的危险是当我们将应用程序从一个环境移动到另一个环境时应用程序的行为是否会发生变化。如果我们不能在生产中运行一个版本并在开发人员的工作站上调试它,那么修复问题就会变得更加困难。我们可以引入只出现在某些环境中的错误,最危险的是生产环境。由于这种危险,我们希望尽可能确保在生产中运行的代码与在其他任何地方运行的代码相同。
环境分支的问题在于非常灵活,使它们如此吸引人。由于我们可以更改这些差异中代码的任何方面,因此我们可以轻松引入导致不同行为和随之而来的错误的配置补丁。
因此,许多组织明智地坚持一条铁律,即一旦可执行文件被编译,在每个环境中运行的必须是同一个可执行文件。如果需要更改配置,则必须通过显式配置文件或环境变量等机制将其隔离。这样就可以将它们最小化为在执行过程中不会改变的简单常量设置,从而减少错误滋生的空间。
对于直接执行其源代码的软件(例如JavaScript、Python、Ruby),可执行文件和配置之间的简单界限很容易变得非常模糊,但同样的原则仍然适用。将任何环境更改保持在最低限度,并且不要使用源分支来应用它们。一般的经验法则是,您应该能够检查产品的任何版本并在任何环境中运行它,因此任何纯粹由于不同部署环境而发生的变化都不应该在源代码控制中。在源代码管理中存储默认参数的组合是有争议的,但应用程序的每个版本都应该能够根据环境变量等动态因素在这些不同的配置之间切换。
环境分支是使用源分支作为穷人的模块化架构的一个例子。如果应用程序需要在不同的环境中运行,在不同环境之间切换的能力需要成为其设计的首要部分。对于缺乏该设计的应用程序,环境分支可以用作操纵机制,但应该优先考虑使用可持续的替代方案进行删除。
✣✣✣
✣ 热修复分支 ✣
一个分支,用于获取那些修复紧急生产缺陷的工作。
如果产品中出现严重的错误,则需要尽快修复。解决此错误的优先级高于团队正在进行的任何其他工作,并且没有其他工作应当减慢这个热修复的工作速度。
热修复工作的完成需要源代码控制,以便团队可以正确记录和协作。 他们可以通过在最新发布的版本上打开一个分支并在该分支上应用热修复的任何更改来做到这一点。
一旦热修复应用于产品,每个人都有机会睡个好觉,然后可以将修复程序应用于主线,以确保下一个版本不会出现回归。如果有一个为下一个版本打开的发布分支,则热修复也需要应用到上面。如果发布之间的时间很长,那么修补程序很可能是在更改的代码之上进行的,因此合并起来会更加尴尬。在这种情况下,暴露错误的良好测试真的很有帮助。
如果团队正在使用发布分支,则可以在发布分支上进行热修复,并在完成后发布新版本。本质上,这将旧的发布分支变成了一个热修复分支。
与发布分支一样,可以在主线上进行热修复并将它们拣选到发布分支。但这不太常见,因为热修复通常是在强大的时间压力下完成的。
如果一个团队进行持续交付,它可以直接从主线发布修补程序。 他们可能仍然使用修补程序分支,但他们将从最新的提交而不是最后发布的提交开始。
我将新版本标记为 2.2.1,因为如果团队以这种方式工作,则 M4 和 M5 可能不会公开新功能。 如果他们这样做,那么修补程序很可能会被合并到 2.3 版本中。 当然,这说明了使用持续交付修补程序不需要回避正常的发布过程。 如果一个团队有一个足够响应的发布过程,则可以像平常一样处理修补程序——这是持续交付思维的一个重要好处。
一种适用于持续交付团队的特殊处理方式是在热修复完成之前禁止对主线进行任何提交。这符合没有人有比修复主线更重要的任务要做的口头禅——事实上,在主线上发现的任何缺陷都是如此,即使是那些尚未投入生产的缺陷。 (所以我想这并不是真正的特殊处理。)
合适使用
热修复通常是在压力相当大的时候完成的,当团队压力最大的时候,更有可能会犯错误。 在这些情况下,使用源代码控制和提交比看起来更合理的频率比平时更有价值。 将这项工作保留在一个分支上可以让每个人都知道正在做什么来处理这个问题。 唯一的例外是可以直接应用于主线的简单修复。
这里更有趣的问题是决定哪些是要热修复的错误,哪些可以留在正常的开发工作流程中。 团队发布的频率越高,它就越能按照常规的开发节奏进行生产错误修复。在大多数情况下,决策将主要取决于缺陷造成的商业影响,以及它如何与团队的发布频率相适应。
✣✣✣
✣ 发布分支 ✣
在设定的时间间隔内发布,就像火车定时发车一样。 开发人员在完成功能后选择要搭乘的列车。
使用发布序列的团队将设置定期发布节奏,例如每两周或每六个月。 按照火车时刻表的比喻,团队为每个版本削减一个发布分支的时间被设定。 人们决定他们想要一个特性捕捉哪一列火车,并将他们的工作定位为那列火车,在火车加载时将他们的提交放到适当的分支上。 一旦火车离开,那个分支就是一个发布分支并且只接受修复。
根据 2 月发布的版本,使用月度列车的团队将在 3 月开始分支。 随着月份的推移,他们将添加新功能。 在设定的日期,也许是本月的第三个星期三,火车出发——特征冻结那个分支。 他们为 4 月的火车开设了一个新分支,并为其添加了新功能。 与此同时,一些开发人员稳定了 三月列车,在它准备好时将其发布到生产环境中。适用于三月列车的任何修复都被拣选到四月火车上。
发布列车通常与特征分支一起使用。 当Scarlett感觉到她的特征什么时候完成时,她会决定要赶什么火车。 如果她认为她可以完成 3 月的发布,她将集成到3月的火车,但如果不是,她将等待下一个并在那里集成。
一些团队在火车出发(这是硬冻结)前几天使用软冻结。一旦发布列车处于软冻结状态,那么开发人员不应将工作推到该列车上,除非他们确信他们的功能稳定并准备好发布。任何出现软冻结后添加的错误的功能都将被恢复(从列车上推下),而不是在列车上修复。
如今,当人们听到“发布列车”时,他们经常会听到 SAFe 的敏捷发布火车概念。SAFe 的敏捷发布列车是一种团队组织结构,指的是一个大型的团队的团队,共享一个共同的发布列车时间表。虽然它使用发布列车模式,但它与我在这里描述的不同。
何时使用
发布列车模式的一个核心概念是发布过程的规律性。如果您提前知道发布列车应该何时出发,您可以计划要为该列车完成的功能。 如果您认为您无法为 3 月的列车完成您的功能,那么您知道您会赶上下一班火车。
当发布过程中存在着重要的摩擦时,发布列车特别有用。 一个外部测试小组需要几周的时间来验证一个发行版或一个发布委员会需要在产品的新版本出现之前达成一致。 如果是这种情况,尝试消除发布摩擦并允许更频繁的发布通常是更明智的做法。当然,在某些情况下,这几乎是不可能的,例如移动设备上的应用程序商店使用的验证过程。 调整发布列车以匹配这种发布摩擦然后可以充分利用这种情况。
发布列车机制有助于将每个人的注意力集中在什么功能应该在什么时候出现,从而有助于预测功能何时完成。
这种方法的一个明显缺点是,在这辆列车早期完成的功能将在等待出发时坐在火车上看书。如果这些功能很重要,那就意味着产品会在数周或数月内错过重要功能。
发布列车可能是改进团队发布流程的重要阶段。 如果一个团队难以发布稳定版本,那么一路跳到持续交付可能是跳得太远了。 选择一个合适的发布周期,一个艰难但合理的发布周期,可以是一个很好的第一步。 随着团队获得技能,他们可以增加列车的频率,最终随着能力的增长而放弃他们并进行持续交付。
变体:装载未来的列车
特征列车的基本特征是在上一辆列车出发的同时有一辆新列车到达站台并承载特征。但另一种方法是让多个列车同时接受特征。如果 Scarlett 认为她的功能不会在 3 月的列车上完成,她仍然可以将她大部分完成的功能推送到 4 月的列车上,并在它出发前把推送更多的提交来完成它。
每隔一段时间,我们就会从三月的列车上拉到四月的列车上。一些团队更喜欢只在三月的列车离开时这样做,所以他们只有一个合并要做,但我们这些知道小合并要容易得多的人更愿意尽快拉取每个三月的提交。
加载未来列车允许正在处理 4 月功能的开发人员进行协作,而不会干扰 3 月列车上的工作。 它的缺点是如果 4 月的人员做出与 3 月工作冲突的更改,则 3 月的工作人员不会得到反馈,从而使未来的合并更加复杂。
与主线外的常规发布相比
发布列车的主要好处之一是定期发布到生产环境。但是有多个分支进行新的开发会增加复杂性。如果我们的目标是定期发布,我们也可以使用 mainline 来实现。 决定发布时间表是什么,然后从主线顶部的任何内容中根据该时间表剪下一个发布分支。
如果有一个发布就绪的主线,就不需要发布分支。 对于像这样的常规版本,开发人员仍然可以选择通过不推送到主线来阻止下一个版本的即将完成的功能,如果它就在常规发布日期之前。 使用持续集成,如果人们希望某个功能等待下一个预定的发布,他们总是可以延迟放置基石或关闭功能标志。
✣✣✣
✣ 发布就绪主线 ✣
保持主线足够健康,主线的头部总是可以直接投入生产
当我开始本节时,我评论说,如果你让 Mainline 成为一个健康的分支,并且你的健康检查足够高,那么你可以直接在主线外发布,随时用标签记录发布。
我花了很多时间来描述可以替代这种简单机制的模式,所以我认为是时候强调这一点了,因为如果一个团队可以做到,这是一个很好的选择。
仅仅因为对主线所做的每一次提交都是可发布的,并不意味着它应该被发布。 这是持续交付和持续部署之间的细微区别。 使用持续部署的团队确实会发布主线接受的每个更改,但是在持续交付的情况下,每个更改都是可发布的,是否发布是商务决策。(因此,持续部署是持续交付的一个子集。)我们可以认为持续交付为我们提供了随时发布的选项,我们决定行使该选项取决于更广泛的问题。
何时使用
仅仅因为对主线所做的每一次提交都是可发布的,并不意味着它应该被发布。 这是持续交付和持续部署之间的细微区别。 使用持续部署的团队确实会发布主线接受的每个更改,但是在持续交付的情况下,每个更改都是可发布的,是否发布是商务决策。(因此,持续部署是持续交付的一个子集。)我们可以认为持续交付为我们提供了随时发布的选项,我们决定行使该选项取决于更广泛的问题。
然而,模式都是关于上下文的。 在一种情况下非常出色的模式在另一种情况下可能会成为陷阱。 发布就绪主线的有效性取决于团队的集成频率。 如果团队使用 Feature Branching 并且通常每个月只集成一次新功能,那么团队很可能处于糟糕的境地,坚持发布就绪主线可能会成为他们改进的障碍。 不好的地方是他们无法响应不断变化的产品需求,因为从创意到产品的周期时间太长。由于每个特征都很大,它们也可能有复杂的合并和验证,从而导致许多冲突。 这些可能会在集成时出现,或者在开发人员从主线拉入其功能分支时持续消耗。 这种阻力阻碍了重构,从而降低了模块化,从而加剧了问题。
摆脱这个陷阱的关键是提高集成频率,但在许多情况下,这在保持发布就绪主线的同时很难实现。 在这种情况下,通常最好放弃准备发布的主线,鼓励更频繁的集成,并使用发布分支来稳定生产主线。 当然,随着时间的推移,我们希望通过改进部署流水线来消除对发布分支的需求。
在高频集成的背景下,发布就绪的主线具有明显的简单优势。 没有必要为我所描述的各个分支的所有复杂性而烦恼。 甚至热修复也可以应用于主线,然后应用于生产,使它们不再特别值得一提。
此外,保持主线发布准备好鼓励有价值的纪律。 它将生产准备放在开发人员的首要考虑,确保问题不会逐渐蔓延到系统中,无论是作为错误还是作为减慢产品周期时间的流程问题。 持续交付的完整规则——开发人员每天多次集成到主线中而不破坏它——对许多人来说似乎非常困难。 然而,一旦实现并成为一种习惯,团队就会发现它可以显着减轻压力并且相对容易保持。 这就是为什么它是 Agile Fluency® 模型交付区的关键元素。