Gitflow 是一种协作分支模型,利用了 Git 分支的强大功能、速度和简单性。但有关如何在部署管道中使用 Gitflow 的文档不是很完善。在构建、测试、部署快照版本和部署发布版本时,我们应该使用哪些众所周知的分支名称——master、develop、feature 等分支?本文提供了一种可以在 CI/CD 环境中使用的 Gitflow 方案。
关键要点
Gitflow 是一种协作分支模型,利用了 Git 分支的强大功能、速度和简单性。在本文所描述的情况下,这项技术运行良好,但也有人表示在使用 Gitflow 时也会面临一些挑战。
有关如何在部署管道中使用 Gitflow 的文档不是很完善。
功能被隔离在分支内,可以单独管理自己的功能变更。这种方法与基于主干的开发不一样,在基于主干的开发中,每个开发人员至少每 24 小时会向主分支提交一次变更。
使用隔离分支进行功能隔离可让你决定在每个版本中需要包含哪些功能,挑战性可能在于合并。
2019 年 2 月 13 日更新*:本文的最初版本引起了很大的反响,大多数是正面的,有些则不是。争论的焦点在于我们在包含手动组件的环境中使用了“持续交付”这个术语。如果你所在的团队每天需要部署数百个版本,那么我们的框架可能不适合你。但是,如果你身一个像我们这样的受到严格监管的行业,例如财务行业,在这里版本控制更加严格,并且你希望充分利用功能分支、自动化集成、自动化部署和版本控制,那么这个解决方案可能对你同样有效。*
很久以前,我参加了一个技术大会,在那里我发现了一个叫作“Git”的新奇小玩意儿。据说它是下一代源代码控制工具,我最初的反应是——我们需要它吗,毕竟我们已经有 SVN 了?今天,开发团队正在成群结队地转向 Git,并且围绕中间件和插件形成了一个庞大的生态系统。
Gitflow 是一种协作分支模型,利用了 Git 分支的强大功能、速度和简单性。正如之前 InfoQ 网站有篇文章所写的那样,这种方法确实带来了一系列挑战,特别是在持续集成方面,但这正是我们要解决的问题。2010 年,Vincent Driessen 在博文“A Successful Git Branching Model”中介绍了 Gitflow,Gitflow 允许开发团队将新的开发工作与各个分支中已完成的工作隔离开来,可以选择发布哪些新功能,同时仍然可以频繁提交和进行自动化测试,以此来减轻开发协作的痛点。我们还发现,在合并期间定期进行代码评审,甚至是自我代码评审,从而生成更干净的代码,让 bug 暴露出来,并进行重构和优化。
但是,要在自动部署管道中实现 Gitflow,需要涉及到特定于开发环境的一些细节,并且存在无限可能性,因此这方面的文档很少。我们已知这些分支名称——master、develop、feature 等,但我们构建的是哪些分支,测试的是哪些分支,哪些分支部署为快照,哪些分支作为版本发布,以及如何自动部署到 Dev、UAT、Prod 等环境?
这些是我们在会议上提出的常见问题,在本文中,我们将分享我们在一家大型金融技术公司的工作中开发出来的解决方案。
本文描述的项目使用了 Java 和 Maven,但我们相信也适用于其他任何环境。我们使用 GitLab CI 和自定义运行脚本,但也可以使用 Jenkins 或 GitHub CI 插件。我们使用 Jira 进行问题跟踪,使用 IntelliJ IDEA 作为我们的 IDE,使用 Nexus 作为依赖存储库,使用 Ansible 进行自动部署,但也可以使用其他类似的工具来替代它们。
首先,让我们看看我们是如何做到这一点的。
演化
在以前,开发人员需要花费数周或数月的时间开发应用程序功能,然后将“已完成”的工作交给“集成人”——一个善意且专注的人,他将所有功能集成在一起,解决冲突,并准备发布。集成过程令人生畏且容易出错,进度和结果都是不可预测的,于是就有了“集成地狱”的说法。然后,在世纪之交,Kent Beck 发布了他的开创性著作“Extreme Programming Explained”,这本书主张“持续集成”的概念。开发人员开发代码,并将代码集成到主分支中,并通过自动化的方式运行测试,每隔几个小时,当然不少于一天。不久之后,Martin Fowler 的 Thoughtworks 开源了 Cruise Control,这是历史上出现的第一 CI 自动化工具。
Gitflow
正如我们将要看到的,Gitflow 提倡使用功能分支来开发单个功能,并使用单独的分支进行集成和发布。下面的图片转载自 Vincent Driessen 的博客,Git 开发团队对这个流程非常很熟悉了。
作为 Git 用户,我们都知道“master”分支。这是我们在首次初始化 Git 项目后由 Git 创建的默认主线或“主干”分支。在采用 Gitflow 之前,大部分都是提交到 master 分支。
Gitflow 入门
要使用 Gitflow 开始一个项目,需要先完成一个一次性的初始化步骤,在 master 之外创建一个叫作“develop”的分支。然后,develop 分支就成为一个开发分支,所有的代码都保存在这个分支上并进行测试,并成为主要的“集成”分支。
作为开发人员,你永远不会直接提交到 develop 分支,也永远不会直接提交到 master。master 被称为“稳定”的分支——只包含生产就绪的代码,要么是已经发布的,要么准备好要发布的。master 中的代码要么是过去已发布的产品版本,要么是将来要发布的产品版本。
develop 分支被称为“不稳定”的分支,这或许有点用词不当——它其实是稳定的,因为它包含最终要发布的代码,只是需要经过编译和通过测试,而且可能包含已完成或未完成的工作,所以是“不稳定”的。
那么我们应该在哪里进行开发?请看图片的其余部分。
你需要解决一个新的 Jira 问题。你立即创建了一个功能分支,通常是从 develop 分支创建(如果 develop 分支处于稳定状态),或者从 master 创建。
我们一致同意功能分支的名称以“feat-”作为开头,后面跟上 Jira 问题编号。(如果有多个 Jira 问题,只需使用 Epic 或 Parent 任务,或其中的一个主要问题编号,然后是功能的简短描述。)例如“feat-SDLC-123-add-name-field”。“feat-”前缀提供了一种模式,CI 服务器可以标识出它是一个功能分支。我们将在后面说明为什么这个很重要。在这个示例中,SDLC-123 是我们的 Jira 问题编号,提供了指向问题的可视化链接,剩下的是对功能的简短描述。
现在,开发可以并行进行,每个人同时在他们各自的功能分支上开发,一些团队在同一分支上开发功能,其他团队则负责开发其他功能。我们发现,通过频繁地向 develop 分支合并,团队减少了在“合并地狱”上所花费的时间。
发布、快照和共享存储库
让我们用几句话来澄清这一点。在大多数企业中,一般只有一个像 Sonatype Nexus 这样的依赖项存储库。这个存储库包含两种二进制文件。“SNAPSHOT”二进制文件通常使用semver(用点号分隔的三个部分)版本号,后面跟上“-SNAPSHOT”,例如 1.2.0-SNAPSHOT。发布二进制文件使用相同的名称,但没有“-SNAPSHOT”后缀,例如 1.2.0。快照的构建是唯一的,因为只要你使用快照版本构建二进制文件,它就会替换以前具有相同名称的二进制文件。发布版本则不一样,一旦构建了一个发布版本,就可以把它放到存储库中,Nexus 中与该版本相关的二进制文件永远不会发生变化。
现在,假设你正在开发功能 X,而你的伙伴团队正在开发功能 Y。你们同时基于 develop 创建了新的分支,因此你们 POM 文件中具有相同的基础版本,例如 1.2.0-SNAPSHOT。现在假设你运行构建,并将功能分支部署到 Nexus。不久之后,伙伴团队运行他们的构建,也将构建结果部署到 Nexus 上。在这种情况下,你永远不会知道 Nexus 中哪个二进制文件是你的,因为 1.2.0-SNAPSHOT 会引用对应于两个不同功能分支的两个不同的二进制文件(如果有更多这样的功能分支,则引用会更多!)。这是一个非常常见的冲突。
GitLab CI
我们会鼓励开发人员进行频繁提交和尽早提交!那么我们如何避免这种冲突呢?答案是将“feat-”分支与 Maven 的 verify 步骤(在本地构建并运行所有测试)而不是 deploy 步骤(这样会将快照二进制文件发送到 Nexus)相关联,让 GitLab CI 进行构建,但不会部署到 Nexus。
我们通过在项目根目录中定义一个叫作.gitlab-ci.yml 的文件来配置 GitLab CI,这个文件包含确切的 CI/CD 执行步骤。这个功能的优点在于,运行脚本随后会与提交相关联,因此可以根据提交或分支对其进行更改。
我们为 GitLab CI 配置了以下的作业,其中包含用于构建功能分支的正则表达式和脚本:
我们鼓励团队进行频繁的提交。每个提交都会单独执行测试,确保当前的功能不会破坏任何内容,并允许将测试添加到已更改的代码中。
覆盖率驱动开发
现在是时候讨论一下测试覆盖率了。IntelliJ idea 提供了“coverage”运行模式,可以运行带有覆盖率的测试代码(在 debug 或 run 模式下),并根据代码是否被覆盖到将页边空白涂成绿色或粉红色。你可以(也应该)向 Maven 中添加覆盖率插件(例如 Jacoco),这样就可以在集成构建过程中得到覆盖率报告。如果你使用 IDE 没有页边空白着色功能,那么可以从这些报告中查找未覆盖到的代码。
【可惜的是,仍然有许多专业的开发团队,他们虽然在自动化和开发方面提出了一些正统观点,但是由于这样或那样的原因,他们在扩大测试覆盖率方面一直疏忽大意。现在,我们也无法让这些团队回头为未覆盖到的代码添加测试,但作为优秀的开发人员,为我们新增或修改的代码引入测试是我们的职责所在。通过查看新引入代码的测试页边空白颜色,我们可以快速识别需要在哪里引入新的测试。】
执行测试是 Maven 构建的一部分。Maven 的 test 阶段会执行单元测试(以 Test-开头或以 Test.java、Tests.java 或 TestCase.java 结尾的文件)。Maven 的 verify 阶段(需要 Maven Failsafe 插件)也会执行集成测试。对 mvn verify 的调用也会触发构建,然后执行生命周期的其他阶段,包括 test 和 verify。我们还建议安装 SonarQube 和 Maven SonarQube插件,以便在测试阶段进行静态代码分析。在我们的模型中,每个分支提交或合并都会执行这些测试。
集成我们的工作
让我们回到 Gitflow。我们现在已经对我们的功能做了更多的工作,并提交到我们的功能分支上,但本着“集成”的精神,我们要确保它与所有其他团队的功能提交能够很好地协作。因此,根据之前定下的策略,我们同意所有开发团队每天至少合并一次开发分支。
我们还有一个在 GitLab 内部强制执行的策略,如果没有经过代码评审,就不能以合并请求的形式合并到 develop:
根据你的 SDLC 策略,你可以强制开发人员与其他人一起进行代码评审,方法是为合并提供一个评审者清单。或者,你也可以允许开发人员在查看自己的合并请求后执行自己的代码评审,以此来实现一种更宽松的策略。这种策略很有效,因为它鼓励开发人员对自己的代码进行评审,但与任何系统一样,它也存在一些明星的风险。请注意,由于二进制文件永远不会部署到 Nexus 或以其他方式共享,因此开发分支的 POM 文件中所指定的版本是无关紧要的。你可以将其叫作 0.0.0-SNAPSHOT,或者保留原始 POM 版本不变。
经过一段时间之后,这个功能完成了,然后被完全合并到 develop 分支中,并被声明为“稳定”的,并且有很多这样的功能已经为发布做好准备了。请记住,到了这个时候,我们已经在每次提交时运行了验证测试,但我们还没有将 SNAPSHOT 版本部署到 Nexus 中。这是我们下一步要做的事情。
在这个时候,我们从 develop 分支创建了一个发布分支。但与传统的 Gitflow 略有不同,我们并没有把它叫作 release,相反,我们根据发布版本号来命名分支。在我们的示例中,我们使用了三部分语义版本号,如果它是一个主要版本(增加新功能或重大变更),就增加主要编号(第一个数字),如果是次要版本,就增加次要编号(第二个数字),如果是补丁,就增加第三个数字。因此,如果之前的版本是 1.2.0,那么即将发布的版本可能是 1.2.1,快照 POM 版本将是 1.2.1-SNAPSHOT。因此,我们的分支叫作 1.2.1。
配置管道
我们已经配置了GitLab CI管道用于识别已创建的发布分支(发布分支三部分语义版本号进行标识,对应正则表达式为\d+.\d+.\d+)。将 CI/CD 执行器配置为从分支名称中提取发布名称,并使用版本插件更改 POM 中的版本号,以便包含与该分支名称对应的快照版本(在我们的示例中为 1.2.1-SNAPSHOT)。
请注意提交消息中的[ciskip]。这是防止出现死循环的关键,因为每次提交都会触发新的运行和新的提交!
在 CI 执行器修改了 POM 之后,执行器将提交并推送更新过的 pom.xml(现在包含与分支名称匹配的版本)。现在,远程发布分支中的 POM 包含了该分支的正确 SNAPSHOT 版本。
GitLab CI 仍然通过语义版本模式(/^\d+.\d+.\d+$/,例如 1.2.1)来识别版本分支,它识别出分支上发生的推送事件。GitLab 执行器执行 mvn deploy,生成 SNAPSHOT 构建并部署到 Nexus。Ansible 将其部署到开发服务器上,可以在那里可以进行测试。所有到发布分支的推送都会执行这个步骤。开发人员对发布候选版本进行的小调整会触发 SNAPSHOT 构建,向 Nexus 发布 SNAPSHOT,并将该 SNAPSHOT 工件部署到开发服务器。
我们省略了 Ansible 部署脚本,因为对于不同的部署模型来说都不一样。这些脚本执行部署工件所需的所有操作,包括在安装新工件之后重启服务、更新 cron 计划以及更改应用程序配置文件。你需要专门为你的特定需求定义 Ansible 部署。
最后我们合并到 master,触发 Git 使用源发布分支的 semver 版本号对发布版本进行标记,将整个 wad 部署到 Nexus,然后运行 sonar 测试。
请注意,在 GitLab CI 中,你希望在下一个作业步骤中拥有的任何东西,都需要将其指定为工件。在这种情况下,我们将使用 Ansible 部署 jar 包,因此我们将其指定为 GitLab CI 工件。
修复 bug
在测试期间,可能会发现需要修复的 bug。这些都可有在发布分支上机械能,然后合并回开发分支(开发分支始终包含已发布或将要发布的内容)。
最后,发布分支被批准合并到 master 中。master 有一个强制性的 GitLab 策略,即只接受来自发布分支的合并。GitLab 执行器将合并后的代码检出到 master,后者仍然保留发布分支 SNAPSHOT 版本。GitLab 执行器再次使用 Maven 版本插件来执行版本:使用removeSnapshot参数集设置 goal。这个 goal 将从 POM 的版本中删除“-SNAPSHOT”,然后 GitLab 执行器将这个变更推送到远程的 master 上,对发布进行标记,将 POM 中的版本设置为下一个 SNAPSHOT 版本,并将其部署到 Nexus。然后部署到 UAT 环境中进行 QA 和 UAT 测试。一旦工件被批准发布到生产环境中,生产服务团队将获取工件,并将其部署到生产环境中(这个步骤也可以通过 Ansible 自动执行,具体取决于公司的策略)。
补丁和热修复
我们必须提到另外一个工作流程,那就是补丁或热修复。当在生产环境中或在测试发布工件期间发现问题(例如 bug 或性能问题)时,就会触发补丁或热修复。热修复类似于发布分支,以发布版本命名,就像发布分支一样。唯一的区别是它们不是来自开发分支,而是来自 master。
在完成热修复工作后,就像发布分支一样,热修复会触发 Nexus SNAPSHOT 部署,并部署到 UAT。一旦通过认证,就会被合并回到开发分支,然后将其合并到 master,并准备发布。master 将触发发布版本构建,并将要发布的二进制文件部署到 Nexus。
总结
我们可以通过下图来总结本文的内容:
这就是我们的 Gitflow。我们鼓励任何规模的开发团队探索和尝试这种策略。我们相信它具有以下这些优点:
功能是孤立的。因为有了功能分支,可以很容易单独管理自己的功能变更,但它有可能在发活跃的功能时让团队集成变得更具挑战性,或者不会经常对提交进行合并。
功能隔离,可以让你选择要包含在发行版中的功能。另一种方法是持续发布与隐藏在功能标志背后的功能相关的代码。
集成和合并过程促使我们的团队执行更严格的代码评审,这有助于获得干净的代码。
自动化测试,部署和发布到所有满足团队需求和首选工作方式的环境。
我们的做法可能偏离了这个领域的一些公认的规范,因此在社交媒体上产生了一些争论。实际上,本文的初始版本引发了 Steve Smith 对该方法的分析和讨论。我们的目的是分享我们对工作方式的见解,而且本文所描述的流程并不一定适合所有的团队或各种工作方式。
更多信息
有关使用 Atlassian Bamboo 和 BitBucket 实现更传统的 Gitflow,请看这里。
还有一个很棒的Gitflow Maven插件,由 Alex Mashchenko 负责维护,其工作方式与 Gitflow 的 Maven 发布插件非常相似,可以用于我们提出的 Gitflow 实现中。
关于作者
Victor Grazi 在野村证券从事企业基础设施应用开发工作。作为 Oracle Java Champion,Victor 还担任 InfoQ Java 主题的主编,还是 Java Community Process Execute Committee 的成员。
Bryan Gardner 最近毕业于史蒂文斯技术学院,获得计算机科学学士和硕士学位。Bryan 目前在野村证券工作,担任基础设施开发团队的软件工程师。他主要致力于 Spring Boot 后端服务开发或使用 Apache Spark 处理大数据管道。
查看英文原文:https://www.infoq.com/articles/gitflow-java-project
评论 1 条评论