一、问题背景:从5人到18人,Git先崩了

我们是一个做SaaS后台的团队,2022年初还是5个人,用最朴素的Git Flow:master + develop + feature/* + release/*。人少的时候没问题,大家口头同步一下就行。到2023年中团队扩到18人,同时并行4-5个需求迭代,问题集中爆发:

  1. 合并冲突高频:develop分支每天被合并十几次,feature分支活过一周基本必冲突,最夸张的一次一个分支解冲突花了3小时。
  2. 发布不可控:release/*分支上经常混入未测试完的提交,回滚靠git revert一个个找,平均回滚耗时40分钟。
  3. Review形同虚设:没有强制MR,有人直接push到develop;有MR的也经常是几百行diff,Reviewer看一眼就点Approve,线上事故3个月内出了2次,都是"review过但没人真看"的代码。
  4. CI是摆设:只在master上跑一次构建,feature分支的代码合进来才发现编译不过。

痛定思痛,我们花了两周重构工作流。下面是我们最终落地的方案,跑了快一年,数据在后面。

二、环境与版本

  • GitLab CE 16.9(自建,Docker部署)
  • GitLab Runner 16.9,Docker executor
  • Git 2.40+
  • 构建环境:Node.js 18.19 / JDK 17(后端Java服务)
  • 团队规模:18人,4个小组,每周约35个MR

三、方案设计:混合分支策略

我们放弃了完整Git Flow,改成 Trunk-Based 为主 + Release分支兜底 的混合模式。核心思路:日常开发全部围绕main,只有发版时拉短期release分支。

分支模型:

分支 生命周期 用途 保护级别
main 永久 主干,随时可发布 禁止直接push,需MR+2 Approve+CI通过
feature/- ≤3天 功能开发 无保护,但必须从main拉
release/ 1-2周 发版冻结、hotfix 需MR+1 Approve
hotfix/ ≤1天 线上紧急修复 从main拉,修完合回main和release

关键约束(写进了团队规范,也在GitLab里做了强制):

  1. feature分支存活不超过3天。超过就必须拆分或先合入(用feature flag控制开关)。这条是治冲突的根本——分支活得越短,冲突越少。
  2. 所有合入main的代码必须走MR,且CI全绿 + 至少2个Approve(其中1个必须是组内资深)。
  3. release分支只接受cherry-pick,不接受直接提交,保证main和release的差异可追溯。
  4. commit message规范:type(scope): subject,type限定feat/fix/refactor/docs/test/chore。用commitlint在CI里卡。

为什么不用纯Trunk-Based?因为我们是To B产品,客户环境发版节奏不一致,需要release分支做版本冻结和补丁。纯主干开发对feature flag基础设施要求高,我们暂时没到那个成熟度,混合模式是当前性价比最高的。

四、核心实现:分支保护 + MR模板 + CI配置

4.1 分支保护规则(GitLab)

在 Settings → Repository → Protected branches 配置,或用API批量设置。我们的main规则:

  • Allowed to merge: Maintainers
  • Allowed to push: No one(关键!连Maintainer也不能直接push)
  • Allowed to force push: 关闭
  • Code owner approval: 开启
  • Required approvals: 2
  • 勾选 "Status checks must succeed"(等CI流水线通过)
  • 勾选 "Require a merge request before merging"

release/* 用通配符保护,Required approvals 设为1,push权限只给Release Manager角色。

4.2 Merge Request模板

放在仓库根目录 .gitlab/merge_request_templates/default.md:

## 变更说明


## 关联Ticket
Closes #

## 变更类型
- [ ] 新功能 feat
- [ ] 缺陷修复 fix
- [ ] 重构 refactor
- [ ] 配置/构建 chore

## 自测清单(提交前必须勾选)
- [ ] 本地跑通 `npm run test`
- [ ] 新增/修改的逻辑有对应单测,覆盖率不低于80%
- [ ] 涉及DB变更的已附migration脚本并本地验证
- [ ] 涉及接口变更的已更新API文档
- [ ] 已在dev环境手工验证核心路径

## 影响面与回滚方案


## 截图/录屏(UI变更必填)

这个模板不是形式主义。我们踩过的坑:有次一个MR改了公共工具类没写影响面,合并后三个业务线编译失败。从那以后"影响面与回滚方案"成了Reviewer必看项。

4.3 CI/CD 流水线配置

.gitlab-ci.yml,分四个stage:lint → test → build → deploy。feature分支跑lint+test,main跑全量,release分支跑build+deploy到预发。完整配置:

stages:
  - lint
  - test
  - build
  - deploy

variables:
  NODE_IMAGE: node:18.19-alpine
  DOCKER_DRIVER: overlay2
  GIT_DEPTH: "20"

# 缓存node_modules,跨job复用
.node_cache: &node_cache
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - node_modules/
      - .npm/

commitlint:
  stage: lint
  image: $NODE_IMAGE
  }"
COMMIT_SHA="${2:?缺少commit sha}"

git fetch origin "$RELEASE_BRANCH"
git checkout -B "$RELEASE_BRANCH" "origin/$RELEASE_BRANCH"

if git cherry-pick -x "$COMMIT_SHA"; then
  echo "✅ cherry-pick 成功: $COMMIT_SHA -> $RELEASE_BRANCH"
  echo "下一步: git push origin $RELEASE_BRANCH 并创建MR"
else
  echo "❌ 冲突,请手动解决后 git cherry-pick --continue"
  exit 1
fi

-x参数会在commit message里追加(cherry picked from commit xxx),追溯来源非常有用。

五、踩坑与优化

坑1:commitlint把merge commit也拦了。 一开始没排除merge commit,导致合并main时CI报错。解决:在commitlint.config.js里加ignores: [(msg) => msg.startsWith('Merge')]。版本:@commitlint/cli@18.4.3。

坑2:feature分支的生命周期靠自觉,没人守。 规范写了3天,实际有人挂两周。后来加了个定时Job,每周一扫超过5天未更新的远程分支,Slack提醒作者,两周不动的自动删。用GitLab API写的,20行脚本,效果立竿见影——远程分支数从高峰87个降到常态20个左右。

坑3:CI缓存串味。 node_modules缓存key只用了package-lock.json的hash,但不同Node版本缓存会混。修复:cache key加上$CI_JOB_IMAGE变量,或者直接用files多文件hash。改完后偶发的"本地能跑CI挂"问题消失。

坑4:deploy-staging在main上manual,结果没人点。 一开始想着手动控制,实际大家都忘了点,预发环境长期落后main。后来改成main合入后自动部署staging(保留production的manual),预发环境终于跟上了主干。

坑5:覆盖率阈值设太高导致大家绕过。 初期设了85%,结果新项目根本达不到,有人直接在CI里加|| true。后来降到80%并对存量代码做baseline豁免,新增代码严格卡80%,才真正执行下去。

六、效果数据

跑了约10个月(2023.10 - 2024.08),对比改造前后:

指标 改造前 改造后
feature分支平均存活 9.2天 2.4天
每周合并冲突次数 11次 ≤2次
MR平均合并时长 72小时 6.1小时
线上回滚平均耗时 40分钟 5分钟(镜像回滚)
因Review遗漏导致的线上事故 3个月2次 10个月1次
CI平均流水线时长 无(没跑) 6分20秒(lint+test+build)

几个直观感受:分支短了,冲突自然少;MR小了,Review质量上来了;CI卡在合并前,脏代码进不了main。工作流这东西不是配完就完事,得靠规则+工具+数据持续盯,我们到现在每月还会看一次MR时长和冲突数的趋势。

七、总结

回头看,这套方案没有什么高深技术,核心就三件事:分支活得短、合并门槛硬、反馈来得快。Git Flow本身没错,错在团队规模和节奏变了还抱着老流程不放。如果你的团队也在10人以上、并行需求多,建议从"限制feature分支生命周期 + 强制MR + CI卡合并"这三步开始改,成本最低、见效最快。配置都在上面了,拿去改改就能用,有问题的欢迎评论区聊。

—— 一个被合并冲突折磨过的后端开发