一、问题背景:为什么"直接推master"撑不住了
我们团队做的是一个SaaS后台产品,后端Spring Boot 3.2 + JDK 17,前端Vue 3.4 + Vite 5,仓库托管在GitHub,CI最初只有一条"推上去就跑测试"的脚本。2023年下半年团队从6人扩到15人后,问题集中爆发:
- 冲突常态化。三个人同时改同一个Service,本地各自rebase,最后合并时冲突文件平均每次4.7个。
- master长期不可发布。有人把半成品直接push到master,导致主干构建成功率只有82%,发布前临时"挑commit"成了常态。
- Code Review形同虚设。没有分支保护,PR可以自己approve自己merge,代码评审覆盖率不到30%。
- 回滚靠人肉。出问题后手动revert一串commit,平均耗时40分钟。
我们的目标很明确:主干随时可发布、每次变更都可追溯、评审必须发生、回滚必须一键。下面是我们最终落地的方案。
二、环境与版本
- Git 2.43.0(服务端GitHub Enterprise,客户端统一要求 ≥ 2.40)
- GitHub Enterprise Cloud,启用Rulesets(替代旧版Branch protection)
- GitHub Actions(runner: ubuntu-22.04,自托管runner 4核8G)
- 备用流水线:GitLab CI 17.4(部分内部项目迁移中)
- 构建工具:Maven 3.9.6、Node 20.11.1、pnpm 9.1.0
- 代码质量:SonarQube 10.4、ESLint 8.57、Checkstyle 10.12
三、方案设计:分支策略怎么选
我们评估了三种主流策略:
| 策略 | 优点 | 缺点 | 是否适合我们 |
|---|---|---|---|
| Git Flow(develop/release/hotfix) | 发布节奏清晰 | 分支多、合并链路长,15人团队维护成本高 | 否 |
| Trunk-Based Development | 主干唯一、集成频繁 | 要求特性开关和强自动化测试 | 部分适合 |
| 短生命周期特性分支 + 保护主干 | 折中,PR即评审单元 | 需要严格控制分支存活时间 | 是 |
最终定稿的规则:
- main 为唯一长期分支,永远可发布,禁止直接push。
- 特性分支
feat/-,从main切出,存活时间不超过3个工作日(超过则拆分)。 - 修复分支
fix/-,hotfix同样走PR,但允许priority/hotfix标签跳过部分非关键检查(见第五节)。 - 发布用tag:
v1.8.0,语义化版本,tag由CI在main上自动打。 - 禁止develop分支。这是我们踩坑后砍掉的(见第六节)。
分支命名用正则强校验,在CI第一步执行:
# scripts/check-branch-name.sh
#!/usr/bin/env bash
set -euo pipefail
BRANCH="${GITHUB_HEAD_REF:-$(git rev-parse --abbrev-ref HEAD)}"
# 允许 main 与 release tag 触发
if [[ "$BRANCH" == "main" ]]; then
echo "branch ok: $BRANCH"; exit 0
fi
PATTERN='^(feat|fix|chore|refactor)/[A-Z]+-[0-9]+-[a-z0-9-]{3,40}$'
if [[ ! "$BRANCH" =~ $PATTERN ]]; then
echo "::error::分支名不合规: $BRANCH"
echo "期望格式: feat/PROJ-123-add-order-export"
exit 1
fi
echo "branch ok: $BRANCH"
配套的main分支保护(GitHub Rulesets JSON,通过API导入):
{
"name": "protect-main",
"target": "branch",
"enforcement": "active",
"conditions": { "ref_name": { "include": ["refs/heads/main"], "exclude": [] } },
"rules": [
{ "type": "deletion" },
{ "type": "non_fast_forward" },
{ "type": "required_linear_history" },
{ "type": "pull_request",
"parameters": {
"required_approving_review_count": 1,
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": true,
"require_last_push_approval": true
}
},
{ "type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": true,
"required_status_checks": [
{ "context": "build-and-test" },
{ "context": "sonarqube-quality-gate" },
{ "context": "branch-name-check" }
]
}
}
]
}
关键参数说明:required_approving_review_count: 1(不是2,因为15人团队2个approve会显著拖慢小改动)、require_last_push_approval: true(防止approve后再偷偷push)、strict_required_status_checks_policy: true(分支落后main时必须先更新再合并)。
四、核心实现:PR流程 + CI/CD流水线
4.1 PR模板与评审SLA
.github/pull_request_template.md:
## 变更说明
Closes PROJ-
## 变更类型
- [ ] feat 新功能
- [ ] fix 缺陷修复
- [ ] refactor 重构(无行为变更)
- [ ] chore 构建/依赖/配置
## 自测清单
- [ ] 本地 `mvn -q verify` 通过
- [ ] 新增/修改逻辑有对应单测,覆盖率不低于70%
- [ ] 涉及接口变更已更新 OpenAPI 文档
- [ ] 无敏感信息硬编码(密钥、内网地址)
## 风险与回滚
评审SLA:工作日4小时内首次响应,24小时内给出明确结论。我们用GitHub Action定时提醒超时PR(cron每2小时跑一次,超过8小时未评审的在群里@reviewer)。
4.2 GitHub Actions 主流水线
.github/workflows/ci.yml:
name: ci
on:
pull_request:
branches: [main]
push:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
MAVEN_OPTS: "-Xmx1g -XX:+UseG1GC"
NODE_VERSION: "20.11.1"
jobs:
branch-name-check:
if: github.event_name == 'pull_request'
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- run: bash scripts/check-branch-name.sh
build-and-test:
runs-on: ubuntu-22.04
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Sonar 需要完整历史做 blame
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
cache: maven
- name: Backend verify
run: mvn -B -q verify -Dspring.profiles.active=test
- name: Upload surefire reports
if: failure()
uses: actions/upload-artifact@v4
with:
name: surefire-reports
path: target/surefire-reports/
retention-days: 7
frontend:
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9.1.0
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint && pnpm test -- --run
- run: pnpm build
sonarqube-quality-gate:
needs: [build-and-test]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
cache: maven
- name: Sonar scan
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
run: |
mvn -B sonar:sonar \
-Dsonar.projectKey=saas-backend \
-Dsonar.qualitygate.wait=true \
-Dsonar.qualitygate.timeout=300 \
-Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
注意几个真实调过的参数:timeout-minutes: 25(最初默认360分钟,一次卡住的跑占用runner两小时);concurrency.cancel-in-progress: true(同一PR多次push自动取消旧任务,runner占用下降约35%);sonar.qualitygate.wait=true + timeout=300(让质量门禁真正阻塞合并,而不是只出报告)。
4.3 GitLab CI 等价配置(内部迁移项目)
.gitlab-ci.yml:
stages: [validate, test, quality, build]
variables:
MAVEN_OPTS: "-Xmx1g"
GIT_DEPTH: "50"
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
validate-branch:
stage: validate
image: alpine/git:2.43.0
script:
- apk add --no-cache bash
- bash scripts/check-branch-name.sh
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
unit-test:
stage: test
image: maven:3.9.6-eclipse-temurin-17
cache:
key: "m2-$CI_COMMIT_REF_SLUG"
paths: [.m2/repository]
script:
- mvn -B -q verify -Dspring.profiles.active=test
artifacts:
when: on_failure
paths: [target/surefire-reports/]
expire_in: 1 week
quality-gate:
stage: quality
image: maven:3.9.6-eclipse-temurin-17
needs: [unit-test]
script:
- mvn -B sonar:sonar -Dsonar.qualitygate.wait=true
allow_failure: false
合并请求的"Merge when pipeline succeeds"配合Settings → Merge requests → "Pipelines must succeed" + "All threads must be resolved",效果等同GitHub的branch protection。
五、踩坑与优化
坑1:develop分支成了"第二主干"。 初期照搬Git Flow,结果develop上的代码和main分叉越来越远,两次合并花了整整一天。结论:15人以内、按周发布的团队不要开develop。砍掉后每周集成成本从约6人时降到1人时以内。
坑2:require_last_push_approval 上线第一周引发投诉。 开发者approve后改个typo,review又失效,觉得"形式主义"。我们的处理是:保留该规则,但在PR模板里加"reviewer approve后仅允许注释/文档类改动免重新评审"的人工约定——规则管代码,约定管噪音。
坑3:hotfix绕检查导致质量门禁被架空。 一开始给priority/hotfix开了allow_failure: true,结果两周内出现4个带标签的"紧急"PR。改进方案是:hotfix不允许跳过单测与质量门禁,但允许并行运行且interruptible: false保证不被取消,同时把hotfix的SLA写进值班流程。之后"紧急"PR回落到每月1~2个。
优化点:自托管runner + Maven缓存。 把~/.m2/repository用actions/cache按pom.xml哈希缓存,后端构建从6分12秒降到2分40秒;前端pnpm store缓存后install从75秒降到18秒。
六、效果数据与总结
落地三个月后的对比(数据来自GitHub Insights + Sonar + 内部周报):
| 指标 | 改造前 | 改造后 |
|---|---|---|
| main构建成功率 | 82% | 99.1% |
| PR平均首次评审时长 | 26h | 4.5h |
| 代码评审覆盖率 | <30% | 100%(规则强制) |
| 每周合并冲突事故 | 2.3次 | 0.4次 |
| 单次CI时长(后端) | 6m12s | 2m40s |
| 发布回滚耗时 | ~40min | <3min(revert PR + 自动流水线) |
几点总结给同样规模的团队:分支策略不要迷信教科书,分支数量与团队规模成反比;保护规则要"少而硬",只保留真正阻塞合并的检查,其余降级为提醒;CI配置里最值钱的三个参数是concurrency、timeout-minutes和qualitygate.wait;最后,工作流是给人用的,规则上线前先在团队里跑一周"影子模式"(只告警不阻塞),阻力会小很多。