一、背景:为什么我们受够了“标准GitFlow”
我们团队维护一个微服务网关(Spring Cloud Gateway + Nacos),规模约8人。两年前,我们严格遵循GitFlow:develop作为集成分支,feature从develop切出,release分支做发布前准备。
现实很快打脸:
- 发布流程太重:每次发布都要从
develop建release/*,测试在release分支上修bug,修完再合回develop和master。一次发布涉及至少4次merge操作,且经常冲突。 - 主干形同虚设:
master分支常年“冻结”,除了release合入,没有任何直接提交。导致每次发版后,master和develop的差异大到无法快速热修复。 - 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-latestrunner,规格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:
- PR必须关联Issue,模板第一行就是
Fixes #,如果不填写,CI里的一个简单shell脚本会直接fail。 - PR描述必须包含测试计划,包括单元测试覆盖率和手动测试步骤。
- Reviewer数量:强制1个Approve,但如果是涉及数据库迁移或依赖升级,需要2个。
- “冷板凳”机制:超过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,让发版更自动化。