一、问题背景:从15人到300人,主干开发模式崩了

2021年我们团队只有15人,所有人在master上直接提交,一天合并20次代码,发布靠手动打tag。当时觉得Git就是个存代码的工具,直到团队扩张到300人,噩梦开始了:

  • 合并地狱:一个特性分支存活超过3天,合并时冲突率高达70%,解决冲突要花半天
  • Code Review形同虚设:没有强制机制,很多MR(Merge Request)直接自我审查通过,上线前才发现逻辑漏洞
  • 发布失控:每周五发布日,10个特性同时上线,出问题不知道是谁的改动导致的

最惨的一次:一个同事在master上直接push了一个未完成的重构,导致线上支付接口500了4个小时。这就是压垮骆驼的最后一根稻草——我们决定彻底重构Git工作流。

二、环境与版本:我们用的工具链

先交代一下技术栈,方便你对号入座:

  • Git版本:2.39.1(强制要求,低于2.33会有安全漏洞)
  • GitLab:15.11.3(社区版,用Docker部署在k8s集群)
  • CI/CD:Jenkins 2.414.2 + Jenkinsfile(声明式流水线)
  • 代码质量:SonarQube 10.2(社区版,配置了Quality Gate)
  • 制品管理:Nexus 3.49(存储jar包和docker镜像)

三、方案设计:Trunk-Based + 短期特性分支

我们最终选型是Trunk-Based Development(主干开发)的变体,不是GitFlow——GitFlow太重了,300人团队用GitFlow会死得很惨(release分支和develop分支的合并成本太高)。

核心规则只有四条:

  1. master永远是可部署状态,任何时刻都可以一键发布
  2. 新功能开短期特性分支(feature/xxx),存活时间不超过2个工作日
  3. MR必须通过两级Code Review + 自动化门禁,才能合并
  4. 紧急修复走hotfix分支,直接修改master,但必须在1小时内补上回归测试

分支命名规范:

feature/PRJ-123-add-payment-timeout
bugfix/PRJ-456-fix-npe-on-order-detail
hotfix/PRJ-789-rollback-config

四、核心实现:分支保护 + MR强制规则 + CI/CD流水线

4.1 GitLab分支保护配置

我们在GitLab上对master设置了严格的推送规则(Settings → Repository → Protected Branches):

  • Allowed to merge:Maintainers + Developers(但必须满足MR审批条件)
  • Allowed to push:Maintainers 仅此而已,开发者不允许直接push代码到master

关键点:防止绕过MR直接推送,GitLab有个隐藏开关,必须开启:

# 在gitlab.rb中(Omnibus安装方式)
gitlab_rails['gitlab_shell_push_events'] = true
gitlab_rails['gitlab_shell_receive_max_size'] = 200  # 限制单次推送大小,防止误提交大文件

4.2 MR Approval Rules(强制Code Review)

这是整个流程的灵魂。在GitLab项目设置中配置Approval Rules:

  • 规则1:至少2个Maintainer批准(其中至少1个非作者本人)
  • 规则2:代码质量机器人(SonarQube)必须通过Quality Gate
  • 规则3:Jenkins流水线必须绿色通过

配置文件.gitlab/merge_request_templates/default.md

## MR描述
- 关联Issue:#{issue_id}
- 变更类型:bugfix / feature / refactor
- 测试情况:
  - [ ] 单元测试通过(覆盖率不低于80%)
  - [ ] 集成测试通过
  - [ ] 已手动验证关键路径

## 自检清单
- [ ] 无调试日志残留(如console.log、System.out.println)
- [ ] 无硬编码配置
- [ ] 数据库变更已包含迁移脚本

4.3 CI/CD集成:Jenkins声明式流水线

这是.gitlab-ci.yml的核心片段,我们用的是Jenkins + GitLab插件,但原理一样。关键是在MR阶段就跑完整的构建+测试+静态扫描,而不是合并后再跑:

# .gitlab-ci.yml (GitLab 15.11)
stages:
  - build
  - test
  - quality
  - package

variables:
  MAVEN_OPTS: "-Xmx2048m -Xms512m"
  SONAR_HOST_URL: "http://sonar.internal:9000"
  REGISTRY: "registry.internal:5000"

cache:
  paths:
    - .m2/repository/

build:jar:
  stage: build
  image: maven:3.9.2-eclipse-temurin-17
  script:
    - mvn clean compile -DskipTests -q
  only:
    - merge_requests  # 只在MR阶段触发
    - master

test:unit:
  stage: test
  image: maven:3.9.2-eclipse-temurin-17
  script:
    - mvn test -Pcoverage
  artifacts:
    reports:
      junit: target/surefire-reports/*.xml
    paths:
      - target/site/jacoco/jacoco.xml
  coverage: '/Total.*?([0-9]{1,3})%/'

quality:sonar:
  stage: quality
  image: sonarsource/sonar-scanner-cli:5.0.1
  script:
    - sonar-scanner
      -Dsonar.projectKey=${CI_PROJECT_NAME}
      -Dsonar.sources=src/main/java
      -Dsonar.java.binaries=target/classes
      -Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
  only:
    - merge_requests

package:docker:
  stage: package
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  script:
    - docker build -t ${REGISTRY}/${CI_PROJECT_NAME}:${CI_COMMIT_SHORT_SHA} .
    - docker push ${REGISTRY}/${CI_PROJECT_NAME}:${CI_COMMIT_SHORT_SHA}
  only:
    - master  # 只有master合并后才会构建镜像

关键设计思路only: merge_requests让所有检查都在MR阶段执行,一旦合并到master,流水线只做打包发布,不再重复跑测试——把质量卡在合并前,而不是发布后

五、踩坑与优化:真实教训

坑1:MR Approval Rules被绕过

最初我们只设置了"至少1个Approval",结果开发者自己approve自己的MR,形同虚设。后来改成了2个Maintainer + 非作者,并在GitLab的Settings → General → Merge request approvals里勾选了 Prevent author from approving own merge request。这个选项在UI里藏得很深,很多团队没发现。

坑2:SonarQube Quality Gate让所有MR卡死

刚开始SonarQube的规则太严格(比如禁止任何TODO注释),导致所有MR都红着,开发者直接忽略红色状态强推合并。优化方案:

  • 把Quality Gate设置为新增代码覆盖率≥80%,而不是整体覆盖率(存量代码不管)
  • 把阻塞性问题设为A级(阻断)和B级(严重),C级以下不阻塞合并
  • 在SonarQube中配置sonar.coverage.exclusions排除生成的代码(如**/generated/**

坑3:CI流水线跑30分钟,开发者等烦了

最初流水线包含全量测试、覆盖率统计、静态扫描、安全扫描,跑完要35分钟。开发者为了快速合并,直接推hotfix绕过CI。优化措施:

  • 并行化:把单元测试(10分钟)和覆盖率统计(10分钟)并行跑
  • 增量构建:用Maven的-pl参数只构建变更模块(配合git diff判断变更范围)
  • 缓存依赖.m2目录缓存,避免每次下载依赖,省了5分钟

优化后流水线平均耗时8分42秒,开发者接受度直线上升。

六、效果数据:用数字说话

这套体系运行了6个月,数据对比非常明显(内部度量工具统计):

指标 改造前 改造后 变化
特性分支平均存活时间 5.2天 1.8天 降低65%
MR合并冲突率 70% 12% 降低83%
生产环境回滚率(月均) 6次 2次 降低63%
代码评审覆盖率 40% 100%(强制) 提升150%
单次发布耗时 45分钟 12分钟 降低73%

最明显的感受:代码质量变好了,但开发速度反而变快了。因为冲突少了,返工少了,虽然每次MR要等CI跑完,但算总账是赚的。

七、总结:这套体系适合谁?不适合谁?

适合
- 团队规模20人以上,有专职的DevOps或工具链维护者
- 产品迭代节奏快,需要频繁发布(每周1次以上)
- 代码质量要求高,有自动化测试基础

不适合
- 5人以下小团队,流程会拖慢速度
- 强法规合规场景(如金融核心系统),需要更严格的审批链(建议GitFlow + 人工发布审批)
- 单仓库多团队(建议拆分为monorepo + 独立CI)

最后说一句大实话:Git工作流没有银弹,我们这套体系也是从GitFlow、GitHub Flow一路踩坑踩过来的。关键不是选哪个模型,而是让规则自动化、强制化——把人的不可靠因素降到最低。如果你正在为代码冲突和发布事故头疼,不妨从强制MR Approval和CI门禁开始,这两步投入最少、见效最快。

附:分支保护GitLab API脚本(用命令行配置,比UI批量操作快):

#!/bin/bash
# 批量设置所有项目的分支保护规则
# 依赖: GitLab API v4, 需要PRIVATE_TOKEN

PROJECT_ID=$1
TOKEN=$2

curl --request POST "https://gitlab.internal/api/v4/projects/${PROJECT_ID}/protected_branches" \
  --header "PRIVATE-TOKEN: ${TOKEN}" \
  --data "name=master" \
  --data "push_access_level=40" \
  --data "merge_access_level=40" \
  --data "allow_force_push=false" \
  --data "code_owner_approval_required=true"

这个脚本我写在团队内部的git-toolkit仓库里,配合jq可以批量处理200+项目,强烈建议你搞一个。