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提倡使用功能分支来开发单个功能,并使用单独的分支进行集成和发布。下面的图片转载自Vincent Driessen的博客,Git开发团队对这个流程非常很熟悉了。
作为Git用户,我们都知道“master”分支。这是我们在首次初始化Git项目后由Git创建的默认主线或“主干”分支。在采用Gitflow之前,大部分都是提交到master分支。
要使用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会引用对应于两个不同功能分支的两个不同的二进制文件(如果有更多这样的功能分支,则引用会更多!)。这是一个非常常见的冲突。
我们会鼓励开发人员进行频繁提交和尽早提交!那么我们如何避免这种冲突呢?答案是将“feat-”分支与Maven的verify步骤(在本地构建并运行所有测试)而不是deploy步骤(这样会将快照二进制文件发送到Nexus)相关联,让GitLab CI进行构建,但不会部署到Nexus。
我们通过在项目根目录中定义一个叫作.gitlab-ci.yml的文件来配置GitLab CI,这个文件包含确切的CI/CD执行步骤。这个功能的优点在于,运行脚本随后会与提交相关联,因此可以根据提交或分支对其进行更改。
我们为GitLab CI配置了以下的作业,其中包含用于构建功能分支的正则表达式和脚本:
feature-build: stage: build script: - mvn clean verify sonar:sonar only: - /^feat-w+$/
我们鼓励团队进行频繁的提交。每个提交都会单独执行测试,确保当前的功能不会破坏任何内容,并允许将测试添加到已更改的代码中。
现在是时候讨论一下测试覆盖率了。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)。
release-build: stage: build script: - mvn versions:set -DnewVersion=${CI_COMMIT_REF_NAME}-SNAPSHOT # now commit the version to the release branch - git add . - git commit -m \u0026quot;create snapshot [ci skip]\u0026quot; - git push # Deploy the binary to Nexus: - mvn deploy only: - /^d+.d+.d+$/ except: - tags
请注意提交消息中的[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工件。
master-branch-build: stage:\tbuild script:\t# Remove the -SNAPSHOT from the POM version\t- mvn versions:set -DremoveSnapshot\t# use the Maven help plugin to determine the version. Note the grep -v at the end, to prune out unwanted log lines.\t- export FINAL_VERSION=$(mvn --non-recursive help:evaluate -Dexpression=project.version | grep -v '[.*')\t# Stage and commit the binaries (again using [ci skip] in the comment to avoid cycles)\t- git add .\t- git commit -m \u0026quot;Create release version [ci skip]\u0026quot;\t# Tag the release\t- git tag -a ${FINAL_VERSION} -m \u0026quot;Create release version\u0026quot;\t- git push \t- mvn sonar:sonar deploy artifacts:\tpaths:\t# list our binaries here for Ansible deployment in the master-branch-deploy stage \t- target/my-binaries-*.jar only:\t- master master-branch-deploy: stage:\tdeploy dependencies:\t- master-branch-build script: # \u0026quot;We would deploy artifacts (target/my-binaries-*.jar) here, using ansible only:\t- master
在测试期间,可能会发现需要修复的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