前述

“单repo开发模式”是基于微服务技术架构,将一个系统的各个前后端的工程代码放到同一个代码仓库中,采用“环境分支”+“特性分支”+“发布标签”的代码管理模式,再通过辅助脚本完成与DevOps系统集成,兼顾多环境部署的一种团队协作编码模式。

  • 存在的问题

    在一个产品从0-1的诞生过程中,为了提高团队的协作编码效率,可以采用单分支的代码管理模式,但初始版本上线后,要不断的进行迭代升级,此时再使用单分支的代码管理模式则会带来一些问题:

    • 在单分支开发模式中,若每个产品线期望有不同的上线时间时[1],因为在同一个分支中开发,一些同时涉及到的领域服务、前端工程、管理系统等会混杂B产品线未完成开发或测试的代码,只能等待B产品线完成开发后,共同上线,从而拖延了A产品线相关需求的整体效能。
    • 基于微服务技术架构的独立工程很多,随着业务的增长、业务中台的沉淀,服务层的工程会不断增加和重构,若使用分支方式进行代码管理,一个需求通常会涉及到多个工程分支,从而让分支的管理变得复杂且混乱。
    • 因为工程数量多,一个需求会修改多个工程,不易于开展代码审查工作,也不利于建立代码审查机制。
    • 在单分支的代码管理模式下,如果有一个紧急的线上异常需要修复,因为与需求混在同一个分支中进行开发,若要独立上线,则需要繁琐的操作[2],且对环境分支、发布分支的合并带来影响[3],增加了开发人员的负担,还容易出现操作失误。
    • 在开发过程中,一个需求可能会被其他需求提交的“不成熟的代码”所影响,导致出现无法启动、代码错误等异常。
  • 解决的问题

    • 提升了代码对开发人员的透明度,便于开发人员站在全局了解系统,并可对各层代码提出改进(需要通过人工代码审查)。

    • 提升了对需求独立或组合发布的支撑,提升紧急的需求或线上异常的发布及时性,通过特性分支与其他需求隔离,可灵活的进行特性的发布、合并、废除等操作。

    • 降低了工程管理的复杂度,从需要管理多个仓库变成只需要管理一个仓库,且仓库内的结构一目了然。

    • 降低了分支管理的复杂度,一个需求只需要在一个特性分支中进行开发,而无需在多个代码仓库中分别管理各个分支。

    • 简化了工程版本的管理,仅需要在一个工程的主分支上创建一个Tag(多代码库时需要分别管理版本),即可完成版本的定义,并自动触发DevOps流程,完成发布。

    • 同时兼顾了单分支和多分支的开发场景,满足将一个迭代中的需求拆分为若干个需求(特性分支)进行开发,同时也满足0-1、大迭代、高耦合时,期望在一个分支中开发多个业务线功能的需要。

    • 利于简化并统筹管理开发人员的权限,通过在GitLab代码仓库中设置Max role、Protected branches、Protected tags,可将开发人员分为“仅可以对某个分支有提交权限的”、“可以合并分支到各环境分支权限的(进行代码审查)”、“可以创建Tag权限的(进行代码发布)”。

    • 有利于代码质量的提升,可以更方便的建立基于需求的代码审查机制,审查人员可以基于独立分支,方便的完成一个完整需求的代码审查及需求发布工作。

    • 有利于测试环节的节奏把控,测试团队通过一键手动触发所有变更代码的构建及发布,灵活的决定测试环境的更新时机,降低自动构建对正在测试人员的影响。

    • 便于开展大规模重构,在同一个代码库统筹的、一致的开展全局或局部重构,如:基于目录结构快速调整工程的框架结构,全局或缩小范围查找需要统一修改某段代码或配置,全局或局部进行代码格式化等。[4]

    • ……(待发现)

  • 带来的问题

    一个新模式在团队中落地,会解决已经存在的问题,带来一些改进和新的可能性的同时,也会引入一些新的问题:

    • 因为GitLab的代码管理权限没有细到文件夹,所以理论上前端可以修改后端的代码,反之一样,有小概率可能会出现误修改的情况(例如重构某个变量名),需要团队进行约定[5]的同时,在代码审查时也要注意是否存在与需求不相关的代码。

    • 会因为开发人员对Git操作能力、对此模式的理解能力和熟悉程度的差异,而产生意外操作,尤其是在模式刚落地的初期。可通过工具来降低复杂度(如:IDE中的Git工具、GitLab中的分支管理)。

    • 需要对需求的颗粒度、耦合度进行良好的管理[6],会促进此模式的良好运转。

    • 与单分支开发相比,特性分支的合并操作会多很多[7],若需求颗粒度较大,发生合并冲突的可能性也会越大,会带来额外的工作量。

    • 没有对应的持续集成工具可以满足单repo、多模块的自动化构建、发布流程,需要自己写脚本,再结合持续集成工具完成DevOps流程。

    • ……(待发现)

概览

本节侧重于从使用者的角度,呈现本文涉及到的技术点、概念、关键点等,让受众对模式有一个整体的概念,了解自己的岗位角色在整个模式中的位置。

对于DevOps工具链的搭建及配置,详见《CI&CD方案及操作指南》

边界图

整体结构

工具链

该模式涉及到的主要工具和主要作用。

  • 钉钉机器人:连接DevOps工具和即时通讯的中间件。将后端、Web构建结果、服务器端部署结果、APP安装包构建结果(含分发地址)等信息,通过调用钉钉开放接口发送到对应环境的钉钉群,通知到相关人员。
  • 蒲公英:应用分发工具,用于存储各环境的内部测试包。开发环境一般仅在开发人员的机器中编译运行,无需借助分发工具。通过蒲公英的渠道功能,为各个环境建立独立的安装域名,将各环境的打包结果传入对应的渠道,供相关人员安装测试。
  • GitLab:提供Git代码库、协作看板等。存储单repo结构的工程代码,通过特性分支管理方式支撑需求在各个环境中的流转。各角色通过看板对齐目标,各自完成所需任务后进行卡片的维护,协作推进需求流转。
  • Jenkins:持续集成的总调度工具,集成各个开发工具,通过脚本,完成整体流程的串联和调度。测试环境由测试团队自主控制构建时机(仅在Jenkins中点一下即可),是为了避免自动化构建打乱测试节奏。生产环境的构建由GitLab中Tag的创建触发,会自动部署预生产环境,但生产环境的部署由运维团队自主控制时机(仅在Jenkins中点一下即可),是为了保障上线前的一些数据、配置准备工作到位,避免出现因非代码因素导致的线上问题。
  • Docker:将Web、Java、脚本等代码的构建结果,打包为Docker镜像后,在各个环境中分发部署。Dockerfile在各模块的根目录中,开发环境和测试环境的镜像版本号均使用latest,预产和生产则是通过Tag来定义镜像版本号。
  • Swarm:仅用于开发环境和测试环境的治理,满足开发人员的功能联调和测试人员的功能测试即可。通过在单repo的根目录定义一个docker-stack.yaml脚本,聚合工程内各个模块的部署脚本,因为开发环境和测试环境的镜像版本均使用latest,每次触发部署时仅会检查更新过的镜像进行部署。
  • Kubernetes:仅用于预生产环境和生产环境的治理。部署脚本在独立的工程中由运维开发进行维护,避免敏感信息的泄露,开发和部署的职责分离,各个角色更聚焦。
  • Shell:因为Web工程、APP工程、脚本工程、服务端工程都作为了“单repo工程”的子模块存在于代码库中,需要通过Shell程序来控制、调度各个子模块的构建时机,定义构建执行脚本,但由Jenkins触发脚本的执行,可以理解为将单个工程的构建脚本集中到一起进行管理。
  • Python[8]因为预生产环境与生产环境的镜像版本是不对等的,当完成预生产集成测试后,仅需要将变更过的镜像部署至生产环境,且预生产的版本号会存在覆盖的情况(如:集成测试发现问题,重新构建了新的版本),所以通过脚本程序来记录每次预生产的部署,当进行一键发布生产环境时,控制发布到生产的镜像范围和版本。

背景

多环境

目前团队使用4个环境,后续根据研发团队需要,可能会增加新的环境(如:性能测试环境等)

4个环境分别为:

  • 开发环境(dev)
    • 说明:仅保障技术架构完整的最小化部署环境。
    • 对应分支:dev
    • 用途:主要用于支撑开发人员在需求开发或缺陷修复时,进行自测、联调等场景。
    • 网关验签:关闭
    • 构建部署的触发时机:特性分支合并至dev分支时,自动触发构建及部署,仅构建并部署变更模块。
  • 测试环境(test)
    • 说明:仅保障功能完整的最小化部署环境。
    • 对应分支:test
    • 用途:主要面相测试团队,对提测的需求或缺陷开展功能测试,接口测试等。在迭代测试收尾阶段,也可面相产品或业务团队提前开展验收工作。
    • 网关验签:开启(在进行性能测试、接口自动化测试时,可手动关闭)
    • 构建部署的触发时机:测试团队手动触发test分支的构建及部署,仅构建并部署上次手动触发后的变更模块。
  • 预生产环境(pre)
    • 说明:与生产环境的部署架构、技术中间件、数据、配置等基本一致的最小化部署环境。
    • 对应分支:master
    • 用途:主要面向测试、运维及验收团队,进行版本(多特性)集成测试、需求验收、部署前的数据及配置的变更验证等。
    • 网关验签:开启
    • 构建部署的触发时机:在代码库创建Tag后,自动触发Tag代码的构建及部署,仅构建并部署上次创建Tag后的变更模块。
  • 生产环境(prod)
    • 对应分支:master
    • 用途:提供给真实用户使用。
    • 网关验签:开启
    • 构建部署的触发时机:运维或开发团队手动触发部署,除APP需要手动构建外,其它模块不会进行新的构建,使用预生产环境的镜像进行生产环境的部署,仅部署与预生产环境存在版本差异的模块。

Tips:

  • 各环境的APP安装包均需要手动触发构建,可以根据需要选择构建参数(是否加固、所属环境、证书),生产测试包和生产包的加固为必选项。
  • 各环境的APP安装包的分发地址会附在构建通知中,格式:https://域名/[系统名]-[ios/android]-[环境代号],另外,生产环境有生产测试包和生产包2个分发地址。
  • 开发人员不要通过本地开发机将服务注册至测试环境,容易干扰测试执行,也容易污染测试环境数据。有必要时,会控制注册中心的IP白名单来杜绝。

单repo工程结构规划

单repo工程:除了基础技术组件(如:网关、三方技术中间件)、基础公共模块(如:公共模块、插件等)、部署脚本的代码[9],其它业务类的前端和后端工程,均通过一个代码仓库进行管理,该仓库内部通过子模块(文件夹)的方式,将各前后端模块进行分隔。

示例如下:

单repo工程结构规划

说明

  • backend或services目录中集中存储后端工程,划分二级目录按服务大领域进行分类。
  • frontend目录中集中存储了各类前端工程,包括App、Web等技术栈不同的工程。
  • ci.sh是对子模块进行编译调度的入口脚本,与Jenkins集成使用,控制子模块在各个环境中仅对变更模块进行编译构建。
  • ci-app.sh[10]控制App移动端工程编译构建的入口脚本,与Jenkins集成使用。
  • 每个子模块均有自己的ci-build.sh脚本,用来定义当前模块具体的编译、打包脚本。
  • cd.py是控制预生产环境与生产环境个子模块版本的差异化发布脚本,与Jenkins集成使用,完成预生产与生产的一键部署。
  • 容器化的工程模块中采用jib插件定义镜像的构建,部署脚本统一放到根目录的docker-stack.yaml中,供docker swarm部署至开发环境和测试环境。

研发人员操作步骤指引

  1. 通过git仓库地址,将代码仓库clone至本地(用git命令或IDEA或任何带有git插件的开发工具均可)
  2. 可以使用IDEA这类满足各类技术栈工程的集成开发工具,直接加载“单repo工程”的根目录,对其子模块进行代码编写;也可根据需要或喜好,选择适用于指定工程类型的垂直技术栈开发工具,打开某个子模块的目录即可;
  3. 无论是通过哪种开发工具,打开根目录还是打开内部子模块目录,均可正常编写代码、进行git操作,不受目录层级影响;

工程中涉及到的持续集成脚本及关键点,详见附录中的”关于DevOps“章节

分支策略

约定分支管理策略的目的,是让多人团队在多任务的环境下,可以在同一个代码库中更流畅、更灵活的进行协作,并且模式具备一定的扩展性,在团队已有认知的基础上进行优化,避免不必要的认知开销。需要综合考虑产品架构、技术架构、组织架构、发布模式等因素。

分支管理有一些成熟的策略:

在“单repo开发模式”中的分支管理策略与AoneFlow、GitLab Flow近似但又略有不同,是一种固定了环境分支,通过从master拉取特性分支,随着特性在研发流程中的推进,将特性分支逐步向上游环境合并,最终通过标签确定发布生产代码的策略。

更适用于当前的产品架构、发布模式和组织架构,对代码在各个系统环境、各个研发环节的流转具有一定约束的同时也有较高的灵活性。

在该分支策略中,有“环境分支”、“特性分支”、“发布标签”这三类“分支”:

环境分支

均为保护分支,各角色均无法直接向此分支提交代码[11],但允许发起合并请求,将代码合并至环境分支。

  • dev:开发分支,Developer与Maintainer均可Merge。在CI中,dev分支对应开发环境的构建流程,与开发环境部署的代码始终保持一致。
  • test:测试分支,只有Maintainer可以Marge。在CI中,test分支对应测试环境的构建流程,与测试环境部署的代码保持一致或领先。
  • master/main:生产分支,只有Maintainer可以Marge。在CI中,master分支对应生产环境的构建流程,与预生产和生产环境部署的代码保持一致或领先。

特性分支

均为普通分支[12],各角色可根据需要随时创建或删除。特性分支均从master分支创建出来,特性分支可以向环境分支合并,环境分支只有在特殊情况可以向特性分支合并[13]。特性分支可以与其它特性分支进行合并,以便应对需求合并或代码关联的情况。每个特性分支均应与看板中至少一个卡片相关,通过分支名称建立与卡片的关系,详见后文“规范&约定”。

  • feature-*:需求分支,用于承载需求开发过程中产生的代码,通常,一个feature分支至少对应一个“需求”类型的卡片。
  • hotfix-*:修复分支,仅用于承载线上Bug修复过程中产生的代码,通常,一个hotfix分支仅对应一个“线上异常”类型的卡片。

命名规范[14]

  • 特性分支

    小写feature开头 + - + 领域缩写(可选,自行约定[15]) + #需求卡片编号(对应多个需求卡片时不用特殊符号分隔)

    例如:feature-oms#100feature-oms#192#200feature-#32

  • 修复分支

    小写hotfix开头 + - + 领域缩写(可选,自行约定[15]) + #线上异常卡片编号

    例如:hotfix-oms#99hotfix-oms#1#3feature-#66

发布标签

均从master分支创建,且只有Maintainer角色可以创建,通常由指定开发负责人完成标签创建,并推进版本发布后续事宜。

  • v*:版本标签,用于标记要在生产环境中部署的代码。

命名规范[16]

小写v开头 + yyyyMMdd(Tag的创建时间) + 2位的序列号。

例如:v2020030901

示意图

分支策略

从开发人员的角度,一个需求从开发到上线的整个过程大致为:

阶段:实现

  1. Developer在看板中被分配或领取了卡片(#1)。

  2. Developer根据命名规范,从master分支创建一个特性分支(feature-#1)。

  3. 完成代码的开发,并提交至特性分支(feature-#1)。

  4. 在本地对所开发的代码进行自测,将修改的代码提交至特性分支(feature-#1)。

    阶段:联调

  5. Developer将特性分支(feature-#1)和并至开发分支(dev),此时会自动触发开发分支的差异化构建,等待构建通知。

  6. 收到开发环境部署完成的通知后,继续进行自测,并根据需要,与其他关联模块、上下游业务、前后端系统进行联调。

  7. 将修改的代码提交至特性分支(feature-#1),并合并至开发分支(dev),持续自测、联调,直至达到可以提测的程度。

    阶段:测试、验收

  8. Developer进行“提测”,Maintainer对提测的代码完成Review后,将特性分支(feature-#1)合并至测试分支(test),并在看板中移动对应卡片。

  9. 测试团队按自己的规律,查看看板中是否有新提测的卡片或新修复的Bug,并择机手动触发测试环境的差异化构建。

  10. 测试或验收过程中,对产生的“内部测试Bug”进行修复,将修改的代码提交至Bug所属的特性分支(feature-#1),Developer自测无问题后,由Maintainer[17]走查后,将修复代码合并至测试分支(test)。

  11. 重复“9到10”,直至测试、验收通过。

    阶段:上线

  12. Maintainer将特性分支(feature-#1)合并至生产分支(master)。

  13. Maintainer将待发布版本的所有特性分支合并完成后,在生产分支(master)上创建标签(Tag),等待构建通知。

  14. 运维团队根据研发团队提供的部署变更说明,对预生产环境进行数据及配置的变更。

  15. 收到预生产环境部署完成的通知后,测试团队进行集成测试。若集成测试中发现Bug,则重复“9至13”,直至集成测试通过。

  16. 运维团队根据研发团队提供的部署变更说明,对生产环境进行数据及配置的变更。然后在Jenkins中触发一键部署,完成生产环境(后端服务及Web)的部署。

  17. 若有移动端发布,则测试团队需要进行生产环境验证(仅版本升级验证即可),测试通过后,运维将APP上传至应用市场,等待审核。若版本不兼容,则视APP的上架情况,择机开启强制升级。

操作细节在另一篇文章《单repo开发模式指南 - 详细操作》

附录

规范&约定

  • 服务层之间的引用约定

    1. 应用领域服务层:面向前台产品的服务
    2. 管理领域服务层:面相后台产品的服务
    3. 基础领域服务层:通用服务,逐步沉淀为业务中台

    约定3个服务层中的服务间接口调用的依赖如下:

    • 各层的内部服务之间,可以相互调用接口

    • 应用领域服务层管理领域服务层之间,不可跨层调用接口

    • 应用领域服务层管理领域服务层中的服务可以调用基础领域服务层中服务的接口,但反之则不可以。

  • 代码审查约定

    审查时机:

    1. 当特性分支(feature、hotfix)合并至测试分支(test)时,进行代码审查
    2. 当特性分支(feature、hotfix)合并至生产分支(master)时,进行代码走查

    审查依据:

    1. 是否符合对该特性的软件设计
    2. 代码是否符合团队总结的技术规范和最佳实践
    3. 代码是否具备良好的可维护性和可扩展性

    可根据各自团队的组织架构,明确代码审查的方式、工具、人员等[18]

  • 钉钉/邮件通知

    在整个流程中,有些动作会触发CICD构建任务,构建结果会通过即时通讯工具发送至对应的“环境构建通知群”中,通知内容包括是否构建成功、构建的分支、版本、变更内容等关键信息,便于让团队中的每个人及时了解、同步所关心环境的变更情况。

    除此之外,通知还可以在下列事件发生时触发,个人可以根据需要,申请创建通知,以便及时开展相关工作:

    • 对于需要特别关注的需求(紧急的、特别重要的),开发人员提交了Merge Request时通知。可以驱动代码审查人员可以开始处理审查合并;
    • 对于需要特别关注的需求(紧急的、特别重要的),开发人员在特性分支中提交了代码时通知。可以让团队中的每个人和进度的推进者,更快的知道大家在解决的问题;
    • ……

灵活运用

关于工程结构的灵活运用

在基于领域服务、前后端分离的技术架构风格下,工程结构的划分与“组织架构”、“团队规模”、“产品(业务)架构”、“需求管理(粒度、所属产品)”、“业务及系统的成熟度”、“前端技术栈”等方面均有相互影响的关系。

随着上述各因素的逐步成熟、稳定和规范化,单repo可向下述方向发展:

  • 当“业务中台”或“数据中台”概念的成熟度达到中等水平,可将相关服务抽离为一个新的单repo工程,由专门的团队负责该领域的演进;
  • 当产品策划成熟度达到较高水平,可以从前台系统和后台系统的维度,将代码库切分为两套,分别由不同团队负责各自领域的演进;

可能的发展节奏:

  1. 初期:单体。
  2. 中期:随着非功能需求重要性的提升,技术架构往微服务风格演进,此阶段,单repo工程结构中包含后台产品、前台产品、若干中台产品。
  3. 远期:随着业务稳定,并往精细化发展,团队规模也随之提升,此阶段的单repo工程结构,一方面会按照前后台系统领域甚至业务子领域进行分离,另一方面也会逐步沉淀出独立的中台代码库。

###关于分支策略的灵活运用

  • 通过对特性分支命名的约定,可以在单repo代码库隔离不同领域的产品(feature-产品领域-xxx),不同的团队(feature-团队名-xxx),以满足多产品、多团队在同一个代码库中协作(不推荐)。团队越多,在单repo中工作的适应程度也会不一样,如果用木桶效应做比喻,分支管理方式会向木桶的“短板”靠拢,此时团队需要适应“短板”团队的工作方式,这可能会弱化或丧失该模式可能带来的好处。
  • 特性分支的粒度最小可以是一个需求,最大可以是一个里程碑,这取决于实际需求、产品发展阶段、组织架构等多种因素,仍然可以通过对分支命名来定义。分支所承载需求的颗粒度越大时,越会弱化该模式可能带来的好处,团队需要在不同价值之间进行权衡。

其它

  • 开发人员结合看板,利用好GitLab右上角的To-Do List、Marge requests等工具,可以统筹管理个人相关的开发任务,详细参考GitLab相关文档。
  • 发布清单:每个环境在构建的时候,都会产生构建清单,标明了有哪些工程模块产生了变更。当要进行生产环境的发布时,这个清单可以作为要发布内容的依据。除移动端工程,其它工程在开发、测试、预生产这三个环境中会自动发布变更。

Q&A

如果代码提错了分支怎么办?

有A、B两个分支,本来应该提交到A分支的代码,提交(commit)到了B分支。

当错误提交仅在本地仓库时:

  • 方式1:在本地将最新A分支Checkout,将B分支中提错代码的提交点Cherry-Pick到A分支,pushA分支,删除本地B分支。
  • 方式2:代码不多时,在A分支中重写代码,然后删除本地B分支,说不定重写的过程中还能发现更优解。

当错误提交推送到了远程仓库时:

在本地将最新A分支Checkout,将B分支中提错代码的提交点Cherry-Pick到A分支,push A分支;在本地将最新B分支Checkout,对B分支中提错代码的提交点进行Revert Commit操作,push B分支。

分支合并错了怎么办?

除开发环境外都开启了分支保护,只有拥有Maintainer权限的成员可以操作分支的Merge Request。另外,还有检查工具对创建的合并请求进行检查,当有错误的分支合并关系时会给出提示,基本确保了分支合并操作的合规性。但仍然有可能出现误操作:

有A、B、C,三个分支,A分支需要合并到C分支,但误操作合并到了B分支时:

  • 方式1(建议):若使用GitLab创建Merge Request进行分支操作时,则在具体的合并请求中详情中,通过按钮可以操作本次合并的还原,再重新合并至正确的目标分支。
  • 方式2:在B分支中,通过Git的revert操作,将错误的提交点进行恢复(与代码提错了分支的处理方式类似,但可能包含了多个提交点),再重新合并至正确的目标分支。

开发过程中发现与其它需求分支有耦合(分支之间存在代码依赖),怎么办?

场景:有A、B两个需求,分别创建了feature-xxx#a、feature-xxx#b两个分支,开发过程中,发现B需求中的一个接口需要依赖A需求中新增的接口,或者发现B需求中的页面需要A需求中的页面作为前置页面等,因需求变更或需求分析未到位产生的需求耦合情况。

创建一个新的分支(feature-xxx#a#b),将feature-xxx#a、feature-xxx#b分支分别合并到新的分支中,并删除feature-xxx#a、feature-xxx#b分支。

最后,最好在A、B需求的卡片上表明需求的依赖,“提测”、“发布”等动作时,A、B两个需求需要同步进行。

如果需要将某个环境恢复至特定版本的代码,怎么办?

场景:测试环境(test分支)需要对某个特性分支,基于生产环境的代码,单独进行测试

  1. 创建test分支的备份分支test-bak-yyyyMMddxx

  2. 删除test分支,并基于最新master分支创建新的test分支

  3. 合并需要测试的特性分支

  4. 触发测试环境的构建

    此时会对之前test分支的代码和新创建test分支的代码的差异模块进行构建,并部署至测试环境

  5. 完成测试后,将新创建的test分支删除

  6. 基于第一步备份的分支,创建test分支

  7. 删除test分支的备份分支

  8. 再次触发测试环境的构建

其它环境操作类似,根据需要回退的目的不同,在选择回退点的方式上略有区分。另外,生产环境的代码不建议进行此操作,应进行紧急修复。

如果只想回退部分模块的代码到某个版本,怎么办?

在IDEA或其他Git管理工具中,根据路径(Paths)过滤提交点,选中需要回退的提交点,执行Revert Commit操作。

新增一个工程模块时,需要做什么?

  1. 在工程中的适合位置,增加一个文件夹。
  2. 在新增加的文件夹中,初始化该模块的框架代码。
  3. 根据新增模块的编译方式,在该模块文件夹的根目录下,编写该模块的编译脚本(名称固定为:ci-build.sh)。
  4. 修改“单Repo工程”根目录的编译脚本(ci.sh),增加新增模块的相对路径。
  5. 修改“单Repo工程”根目录的部署脚本(docker-stack.yaml),定义新模块在开发环境和测试环境通过docker swarm的部署。
  6. 创建预生产环境和生产环境的部署任务(Jenkins)和k8s部署脚本(目前单独抽离了xxx-deploy工程管理k8s部署脚本)
  7. 修改“单Repo工程”根目录的脚本(cd.py),增加新增模块的相对路径和Jenkins部署任务的对照关系

若该模块有新增的环境变量,需在Jenkins对应的任务中添加。

该步骤同样适用于现有模块的名称变更。

后续会将ci.sh和cd.py的能力合并到一个脚本中,k8s部署脚本也会放回各个模块中,且无需为模块单独建立部署任务。

对特性分支的测试通过了,但特性分支合并到环境分支后,出现了问题,怎么办?

首先排除是否是运行环境存在可用性异常、脏数据或数据缺失、终端兼容性而导致的问题,否则考虑下列可能:

  • 是否存在该需求(分支)依赖的其它需求(分支),且未合并至当前环境分支。
  • 是否在合并代码时存在冲突,且未按预期解决代码的冲突(代码合并过程中出错)。

特性分支合并到环境分支时,如果出现代码冲突,怎么办?

找到冲突代码对应的两个开发人员,让让两个开发人员进行充分沟通后完成冲突代码的合并。过程中应保障不会产生新的代码逻辑或导致代码缺失。

当无法通过CI工具进行编译时怎么办?

若时间允许,则首选等待ci工具的修复,但碰到卡时间点的上线,且ci服务器挂了的时候,则需要在开发人员中找到一台具备对所需发布模块进行编译、构建镜像的开发机,将指定分支/Tag的代码签出,在本地,手动触发ci脚本的执行。

需要注意对应环境的环境变量,生产环境因为涉密,环境变量不暴露。在开发机中进行CICD,一方面需要填对,另一方面也需要确保不要泄露敏感信息。

当我需要对全部子工程进行编译时怎么办?

  • 方式1:联系具有Jenkins维护权限的人(运维工程师),将对应环境的Jenkins编译任务中的workspece文件夹清空后,再次自动或手动触发编译任务时,即会进行全部子工程的编译(上次构建的代码库快照被清除,对比结果为全部代码变更了)。
  • 方式2:创建一个“特殊”的feature分支,并对需要进行构建的模块,修改其模块根目录中的任意文件(如在README.md中加一个空格),提交并合并到环境分支中,即可触发变动模块的构建。注意:为了保障各个环境代码(提交点)的一致性,减少代码冲突发生的可能性,此feature分之应合并到每个环境分支中。

如何减少合并冲突?

推迟特性分支创建时机。从主分支(master)创建新的特性分支时,在需求或线上异常卡片流转到需要编写代码的阶段时再创建,此时创建的特性分支可以最大化的接近生产环境的代码。过早创建可能会增加落后于主分支的代码范围,从而增加合并时产生冲突的概率。

保持特性分支与主分支的相似性。若特性分支有大量落后于主分支(master)的提交,则可以将主分支向特性分支进行合并,或从主分支重建一个特性分支,并Cherry-Pick原特性分支的提交点到重建的特性分支中。

对通过验收清单中的部分需求进行发布时,是否需要重新测试?

场景:A、B、C、D是4个测试完成,并通过产品验收的需求,团队决定先将B、D这2个需求发布上线。此时,将B、D对应的特性分支合并到master,但测试环境是集成了A、B、C、D四个特性分支的代码,是否需要对B、D重新测试?

需要重新测试。在发布到生产环境前的预生产环境集成测试是重要的上线前测试,除了对上线前需要准备的数据、配置等进行验证、确保依赖的需求可以同步上线外,其目的也包括对不同需求组合的集成,进行验证,避免因为合并代码冲突时、不同功能组合所导致的问题,但测试的范围可以是对主流程的验证,在没有自动化测试的支撑时,无需完成全部测试用例的执行。

当然,最好是团队对版本范围有良好的把控,尽可能确保产品计划的稳定。

如果有紧急的线上异常需要修复,此时测试环境正在进行其他需求的测试,且在测试初期,存在功能不通的情况时,怎么办?

测试人员与开发人员分析异常问题的修复代码是否与正在测试的需求有关联,若无关联,可以在测试环境单独验证线上异常并完成后续环境的验证。若有关联,或上游功能不通时,可以跳过测试环境,将线上异常分支单独合并至master,在预生产进行测试。


  1. 1.A、B两个产品线并行开发,且无依赖,A产品线已完成开发,并通过测试,可以上线,但B产品线未完成开发。
  2. 2.需要通过Git的Cherry-pick命令,将修复线上异常时涉及到的一个或多个Commit“挑选”到各环境分支或release分支,最后到master分支,才能完成上线。
  3. 3.Cherry-pick命令会在目标分支创建新的Commit,会导致环境分支间的合并需要解决不必要的代码冲突(或对环境分支进行rebase变基)。
  4. 4.需要注意的是,重构若与业务开发同步进行时,重构面积越大,越容易出现分支合并时的冲突,需要考虑重构时机,降低合并冲突带来的额外开销。
  5. 5.开发人员可以根据需要,只在开发工具中打开某一个前端工程目录作为workspace,或只将services目录作为workspace,又或某个具体的服务目录作为workspace。
  6. 6.需求颗粒度的大小会影响代码修改范围,从而增加合并时发生代码冲突的可能性。需求的耦合度影响不同特性分支进行开发、联调、测试、发布时的便利性,特性分支应尽可能的独立。
  7. 7.30人左右的团队,在一个常规迭代同时进行的分支大概会有20至60个。
  8. 8.可将Python重构为Shell,或将Shell重构为Python,统一技术栈。
  9. 9.这部分工程也可以放到单repo结构中管理,但抽离的部分属于在多系统之间共用的(由大系统边界划分多个单repo工程),代码库的稳定性高,更新频率很低,通常由公线开发组负责维护。
  10. 10.因为移动端工程打包采用手动触发,且在macOS系统环境中编译,因此与自动触发脚本分离。
  11. 11.若GitLab的“合并冲突工具”不适用于团队,当特性分支分支合并至环境分支存在代码冲突时,可以在本地开发机,将特性分支合并到环境分支,解决冲突后,将环境分支进行提交。此时需要对环境分支向合并者(Maintainer)开启提交权限。
  12. 12.当出现特性分支代码已完成开发并测试通过后,但需要等待较长时间发布,为避免分支被污染(合并错误、错误提交代码等),可将特性分支临时设置为保护分支。
  13. 13.除非存在持续周期很长的特性分支,为了保障当前特性分支中的代码在将来合并至生产分支的有效性,则可以将master分支合并至特性分支,从而同步生产环境已发布的代码。
  14. 14.原则上以卡片编号对应,若特殊情况下无卡片是,例如备份类的分支,则可以自定义命名规则,但不要与当前约定产生冲突,保持良好的命名结构即可。
  15. 15.领域缩写不要太长,可以是:产品、团队、模块、业务领域等,目的是为了将分支进行分类管理,便于日常检索。
  16. 16.当日递增,确保一天内发多个版本不冲突,可根据团队的发版规模,制定序列号的位数。
  17. 17.通常,拥有代码合并权限的人员(Maintainer)同时也是开发人员,也会负责需求的开发,当Maintainer提交的代码需要合并时,虽然自己有权限,但应找其他Maintainer进行Code Review后,由其它Maintainer完成合并。
  18. 18.例如,对于前后端技术栈分离且产品架构分前后台两类产品的团队,人员分为前端一组、后端一组、后台全栈的一组,需求根据实现需要,拆分任务卡片到各组,各组成员提交的代码,可以让该组组长作为负责代码审查的人员,组长提交的代码则由副组长进行审查。