一、问题背景:当仓库从5人用到20人
我们是一个做SaaS后台的团队,2023年初只有5个后端,Git基本是“谁写完谁推main”。到了2024年Q1,团队扩到20人(8后端、5前端、3测试、2运维、2产品),仓库数从3个涨到11个。问题集中爆发:
main分支平均每天收到17次直接push,其中约4次会导致CI失败;- 发布前一周,
develop和main的差异提交达到230+,合并冲突要花2小时手动解; - 有次测试同学把
.env.local提交进仓库,导致预发环境连错数据库,回滚花了40分钟。
痛定思痛,我们花了三周把工作流重新设计并落地。下面直接讲我们最终在用的方案,版本和配置都是线上真实在跑的。
二、环境与版本
- GitLab CE 16.9.1(自建,Docker部署)
- GitLab Runner 16.9.1,executor为docker,并发4
- Node.js 20.11.1(前端项目)
- husky 9.0.11 + lint-staged 15.2.2 + @commitlint/cli 19.0.3
- 后端Java 17 + Maven 3.9.6,前端Vite 5.1.6
- 分支模型:Git Flow简化版(去掉
release长期存在,改为临时分支)
三、方案设计:五类分支 + 三道门禁
3.1 分支策略
| 分支 | 用途 | 生命周期 | 保护规则 |
|---|---|---|---|
main |
生产对应代码,每次合并打tag | 永久 | 禁止直接push,需2人approve |
develop |
集成测试基线 | 永久 | 禁止直接push,需1人approve |
feature/* |
单需求/单issue | 合并后删除 | 无保护,可自由push |
release/* |
发布候选,只允许bugfix | 发布后删除 | 需1人approve |
hotfix/* |
线上紧急修复 | 合并后删除 | 需2人approve |
命名约定:feature/ISSUE-1234-short-desc,强制带Jira号,方便追溯。
3.2 Code Review流程
- 开发在
feature/*完成后推远端,开Merge Request(MR)到develop; - MR必须填写:关联issue、变更类型(feat/fix/refactor)、自测结果、影响范围;
- 至少1名同模块同事approve,CI必须全绿,才能点Merge;
develop→release/*→main的MR,必须由Tech Lead或架构师approve;- 禁止“自我approve”,GitLab配置里勾选“Prevent approval by author”。
3.3 CI/CD集成
我们有三条流水线:
- MR流水线:
lint→unit test→build→sonar,全部通过才允许合并; - develop流水线:额外跑
integration test,部署到staging环境; - main流水线:打tag、构建镜像、推Harbor、部署
prod(手动触发)。
四、核心实现:真实配置
4.1 本地提交校验(husky + commitlint)
package.json片段:
{
"scripts": {
"prepare": "husky"
},
"devDependencies": {
"husky": "9.0.11",
"lint-staged": "15.2.2",
"@commitlint/cli": "19.0.3",
"@commitlint/config-conventional": "19.0.3"
},
"lint-staged": {
"*.{js,ts,vue}": ["eslint --fix", "prettier --write"],
"*.{java}": ["mvn -q spotless:apply"]
}
}
commitlint.config.cjs:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always',
['feat','fix','docs','style','refactor','perf','test','build','ci','chore','revert']],
'subject-case': [0],
'header-max-length': [2, 'always', 100]
}
};
.husky/commit-msg:
npx --no -- commitlint --edit "$1"
.husky/pre-commit:
npx lint-staged
这套配置上线后,我们统计了两个月:fix:和feat:占比从原来的43%提升到81%,CI因为“格式/语法”失败的次数从每周9次降到1次。
4.2 GitLab CI配置(.gitlab-ci.yml)
这是我们后端Java项目在用的精简版,真实跑在16.9.1上:
stages:
- lint
- test
- build
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
DOCKER_DRIVER: overlay2
IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .m2/repository/
lint:
stage: lint
image: maven:3.9.6-eclipse-temurin-17
script:
- mvn -q spotless:check
- mvn -q checkstyle:check
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
unit-test:
stage: test
image: maven:3.9.6-eclipse-temurin-17
script:
- mvn -q test
artifacts:
when: always
reports:
junit: target/surefire-reports/TEST-*.xml
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "develop"'
build-image:
stage: build
image: docker:24.0.5
services:
- docker:24.0.5-dind
script:
- docker login -u "$HARBOR_USER" -p "$HARBOR_PWD" harbor.internal
- docker build -t harbor.internal/app/backend:$IMAGE_TAG .
- docker push harbor.internal/app/backend:$IMAGE_TAG
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_BRANCH =~ /^release\/.*/'
deploy-staging:
stage: deploy
image: bitnami/kubectl:1.29
script:
- kubectl set image deployment/backend backend=harbor.internal/app/backend:$IMAGE_TAG -n staging
- kubectl rollout status deployment/backend -n staging --timeout=180s
environment:
name: staging
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
deploy-prod:
stage: deploy
image: bitnami/kubectl:1.29
script:
- kubectl set image deployment/backend backend=harbor.internal/app/backend:$IMAGE_TAG -n prod
- kubectl rollout status deployment/backend -n prod --timeout=300s
environment:
name: prod
when: manual
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
注意几个关键点:
- merge_request_event只在MR场景触发,避免每个push都跑全套;
- develop跑单测+部署staging,main只构建镜像+手动部署prod;
- rollout status加了超时,避免流水线挂死。
4.3 分支保护(GitLab UI配置,也可用API)
在Settings → Repository → Protected branches:
main:Allowed to merge =Maintainers,Allowed to push =No one,Required approvals = 2develop:Allowed to merge =Developers + Maintainers,Allowed to push =No one,Required approvals = 1- 勾选:
Prevent approval by author、Prevent approval by committer、Require approval from code owners(我们配了CODEOWNERS)
CODEOWNERS示例:
# 后端核心模块必须由架构组review
/backend/core/ @arch-team
/backend/payment/ @arch-team @payment-team
# CI配置变更必须由运维review
/.gitlab-ci.yml @devops-team
五、踩坑与优化
坑1:husky 9的prepare脚本在CI里报错。 husky 9改了初始化方式,husky install废弃,改为husky。但CI里npm ci会触发prepare,而CI环境没有.git(浅克隆)。解决:在CI里设HUSKY=0,或git config --global --add safe.directory /builds/xxx。我们直接在CI变量里加HUSKY: "0"。
坑2:MR流水线和分支流水线重复跑。 一开始develop的push和MR都触发流水线,Runner并发4被占满,MR排队超过8分钟。解决:用rules区分merge_request_event和push,并给MR流水线加interruptible: true,同一MR新push自动取消旧流水线。
坑3:release分支合并回develop时冲突。 我们最初只把release/*合到main,忘了回合develop,导致下个版本又把旧bug带回来。解决:在发布checklist里强制“release → main”和“release → develop”两个MR,并在GitLab里用Push rules禁止直接push release分支。
优化: 给mvn test加了-T 1C并行编译,单测时间从6分12秒降到3分40秒;Sonar扫描改为增量分析(sonar.scm.revision),从4分钟降到1分20秒。
六、效果数据
上线这套工作流并稳定运行3个月后(2024年4月—6月):
main分支直接push次数:从日均17次降到0;- CI因格式/语法失败:每周9次 → 1次;
- MR平均合并时长:从26小时降到4.5小时;
- 发布回滚次数:Q1共5次 → Q2共1次;
- 合并冲突手动解决耗时:平均2小时/次 → 20分钟/次。
七、总结
Git工作流没有银弹,核心是约束入口、自动化校验、明确责任人。我们的五分支+三道门禁(本地commitlint、MR流水线、分支保护)在20人规模下跑得比较稳。如果你团队还在10人以内,可以先把main保护起来、加上commitlint和MR流水线,成本最低、收益最明显。等人数上来了,再逐步引入release分支和CODEOWNERS。
配置都在上面,拿去改改就能用。有问题欢迎评论区聊。