一、背景:为什么我们受够了“标准GitFlow”

我们团队维护一个微服务网关(Spring Cloud Gateway + Nacos),规模约8人。两年前,我们严格遵循GitFlow:develop作为集成分支,featuredevelop切出,release分支做发布前准备。

现实很快打脸:

  1. 发布流程太重:每次发布都要从developrelease/*,测试在release分支上修bug,修完再合回developmaster。一次发布涉及至少4次merge操作,且经常冲突。
  2. 主干形同虚设master分支常年“冻结”,除了release合入,没有任何直接提交。导致每次发版后,masterdevelop的差异大到无法快速热修复。
  3. Code Review流于形式:PR动辄上千行,Reviewer看不过来,最后变成“LGTM”机器。有一次一个同事改了application.yml里Nacos的命名空间ID,导致的线上故障,直到灰度流量异常我们才发现。

我们决定在2024年Q3进行一次彻底的Git工作流改造。

二、环境与版本基线

在动手前,先明确我们的工具链和版本,这决定了后续所有配置的兼容性:

  • Git:2.40.0(集成在SourceTree中,但建议命令行)
  • GitHub:企业版(GHES 3.10+),支持Merge Queue功能
  • CI/CD:GitHub Actions(ubuntu-latest runner,规格2-core CPU / 7GB RAM
  • Java:JDK 17(Temurin),构建工具Maven 3.9.x
  • 核心指标:CI全量构建(含单元测试+集成测试)耗时约22分钟,SonarQube扫描耗时约3分钟。

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

我们最终选择的策略是:main为唯一主干,所有修改(包括bugfix)必须通过短生命周期的特性分支合入main。禁止长期存在的分支(超过3天未合入则告警)。

具体分层如下:

  • main:唯一稳定分支,任何时刻都应保持可部署状态。所有合并必须通过PR + 状态检查 + 至少1个Approval。
  • feat/*:从main切出,命名规范feat/issue-id-short-desc。生命周期不超过2天。
  • fix/*:紧急修复分支,同样从main切出,但允许跳过部分繁琐的检查(如SonarQube质量门禁),但必须补写单元测试。

我们没有保留develop。因为对于8人团队,main的并发冲突概率远低于大厂百人团队。如果合并冲突频繁,我们会通过PR内的“Update branch”按钮解决,而不是引入中间层。

四、核心实现:分支保护与自动化配置

这部分是重点,直接上配置。

4.1 分支保护规则(GitHub Settings)

main分支上启用以下保护规则(需要通过GitHub API或者UI配置):

- Require a pull request before merging: ✅
- Require approvals: 1个
- Dismiss stale pull request approvals when new commits are pushed: ✅
- Require status checks to pass before merging: ✅ (下面这些是必须勾选的状态检查)
  - "build-and-test" (CI)
  - "sonarqube-quality-gate"
- Require conversation resolution: ✅
- Require linear history: ✅
- Require merge queue: ✅ (需在Repo Settings中开启,且设置最大队列长度10)

关键配置说明:勾选Require linear history意味着禁止merge commit,只允许Squash或Rebase。我们选择Squash,因为每个PR只保留一个干净的提交信息,方便git bisect

4.2 CI/CD 流水线:路径过滤与合并队列

我们的CI配置文件位于.github/workflows/ci.yml。重点在于“预检”和“合并后”两阶段。

预检阶段(PR触发),我们利用GitHub Actions的pull_request事件,但通过paths过滤,避免无关文件触发全量构建:

name: CI
on:
  pull_request:
    types: [opened, synchronize, reopened]
    paths:
      - 'src/**'
      - 'pom.xml'
      - '.github/workflows/ci.yml'
  merge_group:
    types: [checks_requested]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # 全量历史,给SonarQube用

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'
          cache: 'maven'

      - name: Maven Build & Test
        run: |
          mvn clean verify \
            -DskipITs=false \
            -Dtest.failure.ignore=true \
            --batch-mode \
            -Dstyle.color=always
        # 注意:-Dtest.failure.ignore=true 让构建不因单测失败而中断,继续执行打包
        # 但status check会捕获test结果,通过Publish Test Results插件展示

      - name: Upload Artifacts (JAR) if build success
        if: success()
        uses: actions/upload-artifact@v4
        with:
          name: gateway-jar
          path: target/*.jar
          retention-days: 5

合并后阶段(main分支推送),我们部署到测试环境。这里用到了merge_group事件,这是GitHub Merge Queue提供的。当PR被合并到队列后,GitHub会创建一个临时的合并分支,并触发此事件。

# 在同一个 ci.yml 文件中的第二个job
  deploy-to-test:
    if: github.event_name == 'merge_group'
    needs: build-and-test  # 确保预检通过
    runs-on: ubuntu-latest
    environment: test
    steps:
      - name: Deploy to K8s (Test)
        run: |
          echo "Using kubectl config from secret..."
          kubectl set image deployment/gateway gateway=${{ secrets.REGISTRY_URL }}/gateway:${{ github.sha }} -n test

这里有个坑merge_group事件触发的github.sha是临时合并分支的SHA,不是PR分支的SHA。如果直接用它做镜像tag,会导致镜像与PR状态不一致。解决方案是在预检阶段将github.sha写入环境变量文件,然后通过actions/upload-artifact传递,但更推荐的做法是使用github.event.merge_group.head_sha

五、Code Review流程的“硬约束”

光有分支保护不够,我们还得让Review有效。我们制定了以下规则,并写入PULL_REQUEST_TEMPLATE.md

  1. PR必须关联Issue,模板第一行就是Fixes #,如果不填写,CI里的一个简单shell脚本会直接fail。
  2. PR描述必须包含测试计划,包括单元测试覆盖率和手动测试步骤。
  3. Reviewer数量:强制1个Approve,但如果是涉及数据库迁移或依赖升级,需要2个。
  4. “冷板凳”机制:超过24小时未Review的PR,机器人会@assignee的直属leader。

我们写了一个简单的GitHub Action来做PR规范检查(放在.github/workflows/pr-lint.yml):

name: PR Lint
on:
  pull_request:
    types: [opened, edited, synchronize]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - name: Check PR title for Issue ID
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}
          PR_BODY: ${{ github.event.pull_request.body }}
        run: |
          # 标题必须包含类似 #123 的Issue引用
          if [[ ! "$PR_TITLE" =~ \#[0-9]+ && ! "$PR_BODY" =~ \#[0-9]+ ]]; then
            echo "::error::PR must reference an Issue (e.g., #123) in title or body."
            exit 1
          fi
          # 要求包含"测试"关键词
          if [[ ! "$PR_BODY" =~ 测试 ]]; then
            echo "::error::PR description must include '测试' section."
            exit 1
          fi

六、踩坑记录与效果数据

踩坑1:Merge Queue的“饥饿”问题
启用Merge Queue后,我们发现如果多个PR同时进入队列,GitHub会为它们排队合并。但队列中的PR如果触发了新的commit(比如rebase),会重新排队。初期我们允许队列长度为10,结果导致最后一个PR等了近40分钟。优化方案:将队列长度限制为5,且禁止在PR合并前手动rebase,统一由Merge Queue自动处理。

踩坑2:路径过滤导致漏跑集成测试
最初paths只写了src/**。结果有一次我们改了pom.xml(升级了依赖版本),PR直接合并了,但集成测试因为依赖冲突在main上才暴露。优化方案:将pom.xml.github/workflows/*加入paths过滤白名单。

踩坑3:SonarQube的“新代码”口径
SonarQube默认只看PR新增代码的覆盖率,但我们的项目历史代码覆盖率极低(约40%)。如果只看新增代码,我们很难达到80%的门禁。优化方案:将质量门禁调整为“新增代码覆盖率>=70%,且不引入新的Critical及以上问题”。

效果数据(改造后第4周统计)

  • 主干分支(main)的每日构建通过率:从68% → 96%(仅一次失败是Nacos配置问题)。
  • 平均发布周期(从代码提交到生产):从2天 → 4小时(因为我们实现了主干即发布候选)。
  • CI构建时长:通过pom.xml开启Maven并行构建(-T 1C)和增量编译,从22分钟降至14分钟
  • Code Review平均响应时间:从18小时 → 3.5小时(得益于冷板凳机制和PR模板约束)。

七、总结与工具链选择建议

这套流程不是银弹。如果你的团队超过15人,或者有多个并行版本在线上维护(比如需要同时维护v1和v2),那么纯Trunk-Based会非常痛苦,你可能需要GitLab Flow的release/*分支。

对于中小团队,我强烈建议:

  • 放弃develop。它带来的成本远大于收益。让main保持绿色,比维护一个不稳定的develop更有价值。
  • Squash合并是底线。没有线性历史,git bisect就是空中楼阁。
  • 把Review规则写进PR模板,而不是靠自觉。CI只检查格式,但格式能强制思考。
  • Merge Queue是神器。它解决了“PR显示通过,但合并后马上变红”的经典问题。

最后,Git工作流的核心不是工具,而是信任。当每个开发者都能快速在本地构建、轻松理解分支策略时,他们自然会敬畏main分支。我们的下一步计划是引入conventional commits规范,自动生成CHANGELOG,让发版更自动化。