一、问题背景:我们为什么需要一套工作流
先说结论:没有约定的Git用法,比SVN更难管。
我们团队30人左右,后端18人、前端8人、测试4人。2022年从SVN迁到Git后,头三个月基本是“放飞自我”状态:
- 所有人直接往
master推代码,谁的本地跑通了就push; - 有人用
dev分支,有人用develop,还有人用development,三个分支并存; - 发布靠“谁记得最后一次改了什么”,上线出问题就
git reset --hard; - 最夸张的一次,两个同事同时改了同一个配置文件,合并后服务起不来,排查了2小时。
数据很直白:迁移后前3个月,平均每周2.3次合并冲突导致的事故,平均回滚耗时45分钟。这不是Git的问题,是我们没有工作流。
后来我们花了大概两周时间,参考Vincent Driessen的Git Flow,裁剪出一套适合我们规模的策略,并把它固化到GitLab和Jenkins里。这篇文章就是把我们最终跑通的配置完整写出来。
二、环境与版本
先把环境交代清楚,避免你照着配发现对不上:
- GitLab CE 16.8.2(自建,Docker部署)
- Git 2.43.0
- Jenkins 2.426.3 LTS + GitLab Plugin 1.7.16 + Pipeline 2.6
- 构建环境:Docker 24.0.7,Node 20.11.0 / JDK 17.0.9
- 部署目标:K8s 1.28集群,通过
kubectl滚动更新
分支模型最终定为5类:
| 分支 | 用途 | 生命周期 | 保护级别 |
|---|---|---|---|
| main | 生产代码,每次提交对应一个tag | 永久 | 最高 |
| develop | 集成开发分支 | 永久 | 高 |
| feature/* | 单功能开发 | 临时 | 中 |
| release/* | 发布准备 | 临时 | 高 |
| hotfix/* | 线上紧急修复 | 临时 | 最高 |
命名规范:feature/JIRA-123-user-login、release/1.8.0、hotfix/1.7.2-payment-timeout。强制带单号,方便追溯。
三、方案设计:分支怎么走
核心规则就几条,但每条都踩过坑:
- main只接受来自release和hotfix的合并,禁止直接push,禁止feature直接合入。
- develop接受feature合入,每天至少一次CI全量构建。
- feature从develop切出,完成后合回develop,生命周期不超过5天,超了就拆。
- release从develop切出,只允许bugfix提交,测试通过后同时合入main和develop。
- hotfix从main切出,修完同时合入main和develop,并打tag。
发布节奏:每两周一个release,hotfix随时。
这里有个我们改过的点:Git Flow原版要求release合入main后再合回develop,我们额外加了一步——合回develop时必须通过MR,且强制跑一遍集成测试。因为曾经有一次release分支漏合回develop,导致下个版本把已修的bug又带回来了。
四、核心实现:分支保护 + Code Review + CI/CD
4.1 GitLab分支保护配置
在GitLab项目 → Settings → Repository → Protected branches里配置。也可以通过API批量配置,我们用的是API:
# 保护main分支:只有Maintainer能合并,禁止直接push
curl --request POST --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.example.com/api/v4/projects/42/protected_branches?name=main&push_access_level=0&merge_access_level=40&allow_force_push=false"
# 保护develop分支:Developer可合并,禁止直接push
curl --request POST --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.example.com/api/v4/projects/42/protected_branches?name=develop&push_access_level=0&merge_access_level=30&allow_force_push=false"
push_access_level=0表示没人能直接push,merge_access_level=40是Maintainer,30是Developer。这一步是关键——只要允许直接push,任何工作流都是纸老虎。
同时开启Merge Request的强制检查:
- Settings → Merge requests → Merge checks:勾选“Pipelines must succeed”和“All threads must be resolved”
- Merge method选“Merge commit”,保留完整历史
- 勾选“Delete source branch when merge request is accepted”
4.2 Code Review流程
我们的MR规则:
- 至少1个Approver,核心模块(支付、权限)要求2个;
- CI必须绿;
- MR描述必须填:变更内容、影响范围、测试方式、回滚方案;
- 单次MR代码量超过400行,Reviewer有权打回要求拆分。
GitLab的.gitlab/merge_request_templates/default.md模板:
## 变更内容
## 关联单号
JIRA-XXX
## 影响范围
- [ ] 涉及数据库变更
- [ ] 涉及接口变更
- [ ] 涉及配置变更
## 测试方式
## 回滚方案
这套模板上线后,MR平均Review时长从原来的11小时降到3.5小时,因为Reviewer不用再反复问“这个改动影响啥”。
4.3 Jenkins CI/CD集成
Jenkinsfile放在项目根目录,用Multibranch Pipeline,自动发现feature/release/hotfix分支。核心逻辑:
pipeline {
agent any
environment {
REGISTRY = 'harbor.example.com/app'
IMAGE_TAG = "${env.BRANCH_NAME.replaceAll('/', '-')}-${env.GIT_COMMIT.take(8)}"
}
triggers {
// develop每天凌晨2点全量构建
cron(env.BRANCH_NAME == 'develop' ? 'H 2 * * *' : '')
}
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Build & Unit Test') {
steps {
sh 'npm ci --prefer-offline'
sh 'npm run test:unit -- --coverage'
}
post {
always { junit 'reports/**/*.xml' }
}
}
stage('SonarQube Scan') {
when { anyOf { branch 'develop'; branch 'main'; branch 'release/*' } }
steps {
withSonarQubeEnv('sonar') {
sh 'sonar-scanner -Dsonar.projectKey=app -Dsonar.qualitygate.wait=true'
}
}
}
stage('Docker Build & Push') {
when { not { branch 'feature/*' } }
steps {
sh """
docker build -t ${REGISTRY}:${IMAGE_TAG} .
docker push ${REGISTRY}:${IMAGE_TAG}
"""
}
}
stage('Deploy to Staging') {
when { branch 'develop' }
steps {
sh "kubectl set image deployment/app app=${REGISTRY}:${IMAGE_TAG} -n staging"
sh "kubectl rollout status deployment/app -n staging --timeout=180s"
}
}
stage('Deploy to Prod') {
when { branch 'main' }
steps {
// 生产部署需要人工确认
input message: "确认发布到生产?", ok: "发布"
sh "kubectl set image deployment/app app=${REGISTRY}:${IMAGE_TAG} -n prod"
sh "kubectl rollout status deployment/app -n prod --timeout=300s"
}
}
}
post {
failure {
sh 'curl -X POST -H "Content-Type: application/json" -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"构建失败: ${JOB_NAME} ${BUILD_NUMBER}\"}}" https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=$WEBHOOK_KEY'
}
}
}
几个关键参数说明:
sonar.qualitygate.wait=true:质量门禁不通过直接失败,避免技术债进develop;kubectl rollout status --timeout=180s:部署超时自动判定失败,触发回滚;- 生产部署用
input做人工卡点,只有main分支才走。
4.4 自动化版本tag
release合入main后,自动打tag。用GitLab CI的job实现(和Jenkins并存,各管一摊):
# .gitlab-ci.yml
tag-release:
stage: tag
only:
- main
script:
- VERSION=$(cat version.txt)
- git config user.email "ci@example.com"
- git config user.name "CI Bot"
- git tag -a "v${VERSION}" -m "Release v${VERSION}"
- git push origin "v${VERSION}"
五、踩坑与优化
坑1:feature分支生命周期太长。 有个feature开了3周,合回develop时冲突文件47个。后来我们定了硬规则:feature超过5天必须拆,CI里加了个job扫描分支存活时间,超时发告警。
坑2:SonarQube门禁太严。 一开始设了“0新增bug”,结果小需求都过不了。改成“新增bug≤2,覆盖率不下降”,落地顺畅多了。
坑3:Jenkins多分支流水线扫描太频繁。 默认1分钟扫一次,GitLab API被打爆。改成Scan Multibranch Pipeline Triggers设为5分钟,并禁用自动触发,只靠GitLab webhook。
坑4:hotfix合回develop被遗忘。 我们加了个每周五的定时任务,检查所有已合并到main但没合回develop的commit,自动发提醒。
坑5:合并冲突处理。 我们要求所有MR合并前必须本地git rebase develop(feature分支),保持线性历史。这条写进了CONTRIBUTING.md,新人不遵守直接在MR里打回。
六、效果数据
跑了大概半年,数据对比:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 合并冲突事故 | 每周2.3次 | 每月0.6次 |
| 平均回滚耗时 | 45分钟 | 4.5分钟 |
| MR平均Review时长 | 11小时 | 3.5小时 |
| 发布频率 | 每月1次 | 每两周1次,hotfix随时 |
| 生产事故数 | 每季度5次 | 每季度1次 |
| CI平均构建时长 | — | 6分20秒 |
回滚快主要是因为每次部署都有明确tag,kubectl rollout undo一条命令搞定。事故少主要是因为Code Review + SonarQube把大部分低级问题拦在了合并前。
七、总结
一句话:工作流不是写给别人看的,是写进工具里强制执行才有用。
我们这套方案没有多高级,核心就是三件事:分支保护堵死直接push、MR模板和Review规则让协作有章法、CI/CD把质量门禁和部署自动化。任何一条只停留在文档上,都撑不过一个月。
如果你团队规模在10-50人、正在被Git搞得很痛苦,建议先从“保护main和develop”开始,跑两周再加CI门禁,循序渐进。全套配置我都放在上面的代码块里了,可以直接抄。
最后提醒一句:工具是次要的,团队对规则的认同才是关键。我们当时开了两次全员会专门讲这套流程,新人有onboarding文档,这才真正跑起来。