1. 问题背景:分支海洋与发布灾难

去年Q3,我们支付核心组经历过一次“至暗时刻”:由于长期使用git flowdevelop分支,且每个需求都拉独立feature分支,导致develop分支积压了超过300个未合并的提交。在一次紧急发布中,合并developrelease分支时爆发了47个冲突文件,修复耗时6小时,最终引发线上支付超时事故。

核心矛盾:业务迭代速度(每天30+个MR)与代码集成周期(每周1次)严重不匹配。我们需要的不是更多的分支,而是更短的代码存活时间。于是,我们决定转向Trunk-based Development

2. 环境与版本基线

  • Git版本:2.39.1(强制开启protocol.version=2
  • GitLab:15.11.3-JH(使用其Premium版的Merge Request Approvals功能)
  • CI Runner:基于Kubernetes的autoscaling runner,缓存挂载在S3
  • 代码库规模:2个核心仓库(Java微服务),约120W行代码

3. 方案设计:两条主线的舞蹈

我们最终落地的策略是 “主干开发 + 短期特性分支 + 发布分支” 三轨制。

  • 主线(main):唯一的集成主干,所有代码必须通过MR合入。永远保持可发布状态。
  • 特性分支(feat/xxx):存活期不超过2天。一旦超过,则要求git fetch后频繁git rebase origin/main
  • 发布分支(release/vX.Y.Z):仅从main拉起,只接受bugfix。合并返回main使用--no-ff保留合并记录。

禁止:禁止直接push到mainrelease/*。禁止在main上直接git commit --amend

4. 核心实现:用配置强制规矩

4.1 分支命名与保护规则(GitLab配置)

Settings -> Repository -> Protected Branches中:

  • main:设置Allowed to mergeMaintainersAllowed to pushNo one
  • release/*:设置Allowed to mergeMaintainers + Release Manager角色。

4.2 Code Review 强制Approval规则

在GitLab的Settings -> Merge Request Approvals中,我们配置了多级审批

  1. 规则A(必选):至少2名来自Core-Reviewers组的Approval。
  2. 规则B(条件):若MR涉及/src/main/java/com/payment/core/下的文件,必须额外获得安全组(Security-Team)的Approval。
  3. 规则C(非阻塞):若MR包含test关键字,需由QA Lead确认。
# .gitlab/merge_request_templates/Default.md
## Review Checklist
- [ ] 代码符合 [Google Java Style](link)
- [ ] 包含单元测试(覆盖率增量 > 5%)
- [ ] 无 `System.out.println` 或调试断点
- [ ] 数据库迁移脚本已评审

4.3 CI/CD集成:Pipeline即门禁

下面这个.gitlab-ci.yml是我们Java服务的核心配置。关键在于rules控制流水线阶段,以及合并前必须通过所有测试与构建

# .gitlab-ci.yml (基于 GitLab 15.11)
stages:
  - verify
  - test
  - build
  - deploy

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
  SONAR_TOKEN: $SONAR_TOKEN

cache:
  key: "$CI_COMMIT_REF_SLUG"
  paths:
    - .m2/repository/
    - target/

# 阶段1:静态检查与单元测试(仅MR事件触发)
verify:checkstyle:
  stage: verify
  image: maven:3.8.7-eclipse-temurin-17
  script:
    - mvn checkstyle:check -Dcheckstyle.config.location=google_checks.xml
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  allow_failure: false

# 阶段2:单元测试 + 覆盖率收集
test:unit:
  stage: test
  image: maven:3.8.7-eclipse-temurin-17
  script:
    - mvn test jacoco:report
  artifacts:
    reports:
      junit: target/surefire-reports/TEST-*.xml
      cobertura: target/site/jacoco/jacoco.xml
  coverage: '/Total.*?\(([0-9]{1,3})%/'
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_BRANCH == "main"'

# 阶段3:构建镜像(仅main分支或tag)
build:image:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  script:
    - docker build -t registry.gitlab.com/xxx/payment-core:$CI_COMMIT_SHORT_SHA .
    - docker push registry.gitlab.com/xxx/payment-core:$CI_COMMIT_SHORT_SHA
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'

关键点:在GitLab的MR设置中,开启“Pipelines must succeed”和“All threads resolved”为合并必要条件。这意味着,CI的verifytest阶段一旦失败,Merge按钮直接置灰。

4.4 本地钩子:杜绝不规范的提交信息

为了确保CHANGELOG能自动生成,我们写了一个commit-msg钩子脚本,强制Conventional Commits规范:

#!/bin/sh
# .git/hooks/commit-msg
# 检查提交信息格式: type(scope): subject
# 例如: feat(payment): add refund callback

commit_msg=$(cat "$1")
regex="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\(.+\))?: .{1,50}$"

if ! echo "$commit_msg" | grep -qE "$regex"; then
    echo "ERROR: Commit message must match Conventional Commits!" >&2
    echo "Example: feat(api): add new endpoint" >&2
    exit 1
fi

# 禁止直接push到main(通过pre-push钩子实现)

5. 踩坑与优化:血泪教训

坑1:Rebase 还是 Merge?
初期我们强制git pull --rebase,导致新手把main的历史搞乱。后来调整为:只允许在特性分支上执行rebase origin/main合并回main时一律使用--squash,保持主干线性整洁。

坑2:CI并行任务过多触发GitLab RPS限制
当Runner不足时,Pipeline排队导致MR合并等待时间超过10分钟。优化方案:
- 在GitLab Runner配置中设置concurrent = 10
- 在.gitlab-ci.yml中利用resource_group控制数据库迁移类Job只能串行执行,避免死锁。

坑3:Code Review流于形式
纯人工Review效率低下。我们引入了go vet(Go服务)和SpotBugs(Java服务)作为第一道自动检查,将人工Review精力聚焦于业务逻辑和设计模式,而非代码风格。

6. 效果数据:从数据看变化

推行该流程3个月后,我们统计了核心仓库的DORA指标:

  • 部署频率:从每周1次提升至每天4次(生产环境)。
  • 变更失败率:从12%下降至3.5%。
  • MR合并等待时间(p95):从45分钟缩短至4.5分钟
  • 主干冲突数:从平均每月15次严重冲突降为0次(非正常操作导致的除外)。

7. 总结与思考

这套工作流并非银弹,它依赖于两个关键前提:自动化测试覆盖率足够高(我们核心模块约78%)团队对主干开发纪律的认同

对于小团队(<20人),建议砍掉Release分支,直接main打Tag发布即可;对于大型团队,务必引入“特性开关”而非依赖长期分支来管理未完成功能。

最后想说:Git分支策略的本质是沟通机制。当你的规则需要靠“人”去提醒而非工具强制时,就是失败的。把规矩写进CI配置和Git Hook里,让机器当警察,让开发专注于代码本身。