一、问题背景:从5人到18人,Git先崩了
我们是一个做SaaS后台的团队,2022年初还是5个人,用最朴素的Git Flow:master + develop + feature/* + release/*。人少的时候没问题,大家口头同步一下就行。到2023年中团队扩到18人,同时并行4-5个需求迭代,问题集中爆发:
- 合并冲突高频:
develop分支每天被合并十几次,feature分支活过一周基本必冲突,最夸张的一次一个分支解冲突花了3小时。 - 发布不可控:
release/*分支上经常混入未测试完的提交,回滚靠git revert一个个找,平均回滚耗时40分钟。 - Review形同虚设:没有强制MR,有人直接push到
develop;有MR的也经常是几百行diff,Reviewer看一眼就点Approve,线上事故3个月内出了2次,都是"review过但没人真看"的代码。 - 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里做了强制):
- feature分支存活不超过3天。超过就必须拆分或先合入(用feature flag控制开关)。这条是治冲突的根本——分支活得越短,冲突越少。
- 所有合入main的代码必须走MR,且CI全绿 + 至少2个Approve(其中1个必须是组内资深)。
- release分支只接受cherry-pick,不接受直接提交,保证main和release的差异可追溯。
- 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卡合并"这三步开始改,成本最低、见效最快。配置都在上面了,拿去改改就能用,有问题的欢迎评论区聊。
—— 一个被合并冲突折磨过的后端开发