一、问题背景:当git push origin master变成一场赌博
2023年初,我们团队从5人扩展到20人,后端仓库日均提交从12次涨到87次。当时的工作流极其原始:
- 所有人直接在
master上开发,本地跑通就push - 没有Code Review,没有CI,测试靠人肉
- 发布时拉一个
release-20230301分支,手动cherry-pick
结果就是:每周至少3次冲突回滚,发布窗口从30分钟膨胀到4小时,生产事故月均2.3次。最离谱的一次,两个同事同时改了OrderService.java,一个把另一个的幂等逻辑覆盖了,上线后重复下单投诉47笔。
我们决定彻底改造Git工作流。目标很明确:分支策略清晰、Review强制、CI/CD自动化、可回滚。
二、环境与版本
- GitLab CE 15.11(自托管)
- Git 2.39.2
- Jenkins 2.401.1 LTS + GitLab Plugin 1.7.0
- GitLab Runner 15.11.1,Docker Executor
- Java 17 + Maven 3.9.3
- 生产环境K8s 1.26,ArgoCD 2.7
三、方案设计:Trunk-Based + Release分支 + 短生命周期Feature分支
我们对比了三种主流策略:
| 策略 | 合并频率 | 冲突概率 | 适合场景 |
|---|---|---|---|
| Git Flow | 低 | 高 | 版本制软件 |
| GitHub Flow | 高 | 中 | Web服务 |
| Trunk-Based | 极高 | 低 | 持续交付 |
最终选择改良版Trunk-Based:
main:永远可部署,受保护,禁止直接pushfeature/*:生命周期≤2天,从main切出,MR合并后删除release/*:发布冻结分支,仅接受cherry-pickhotfix/*:从release切出,修复后同时合回main和release
关键规则:
1. Feature分支超过2天必须rebase main
2. MR必须至少1个Approve + 流水线全绿
3. main分支保护:禁止force push,禁止删除
四、核心实现:分支保护 + Code Review + CI/CD
4.1 GitLab分支保护配置
通过API设置main保护规则(project_id=123):
curl --request POST \
--header "PRIVATE-TOKEN: " \
--header "Content-Type: application/json" \
--data '{
"name": "main",
"push_access_level": 0,
"merge_access_level": 30,
"allow_force_push": false,
"code_owner_approval_required": true
}' \
"https://gitlab.example.com/api/v4/projects/123/protected_branches"
参数说明:
- push_access_level=0:没人能直接push
- merge_access_level=30:Developer及以上可合并
- code_owner_approval_required=true:CODEOWNERS必须批准
CODEOWNERS文件放在仓库根目录:
# 核心订单模块必须由架构组Review
/src/main/java/com/example/order/ @arch-team
# CI配置变更需DevOps批准
/.gitlab-ci.yml @devops-team
4.2 GitLab CI配置(.gitlab-ci.yml)
stages:
- build
- test
- sonar
- deploy-staging
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
DOCKER_DRIVER: overlay2
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .m2/repository/
build:
stage: build
image: maven:3.9.3-eclipse-temurin-17
script:
- mvn clean compile -DskipTests
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
test:
stage: test
image: maven:3.9.3-eclipse-temurin-17
script:
- mvn test -Dtest.coverage=true
coverage: '/Total.*?([0-9]{1,3})%/'
artifacts:
when: always
reports:
junit: target/surefire-reports/TEST-*.xml
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
sonar:
stage: sonar
image: sonarsource/sonar-scanner-cli:5.0
script:
- sonar-scanner -Dsonar.projectKey=order-service
-Dsonar.qualitygate.wait=true
allow_failure: false
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
deploy-staging:
stage: deploy-staging
image: bitnami/kubectl:1.26
script:
- kubectl set image deployment/order-service order-service=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -n staging
environment:
name: staging
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
关键点:
- sonar.qualitygate.wait=true:质量门禁不通过直接失败,MR无法合并
- coverage正则提取覆盖率,低于80%会触发GitLab的覆盖率检查
- MR事件和main分支走不同流水线,节省Runner资源
4.3 Jenkins多分支流水线(Jenkinsfile)
生产发布走Jenkins,因为需要审批和回滚:
pipeline {
agent { label 'k8s-agent' }
options {
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
}
parameters {
choice(name: 'ENV', choices: ['staging', 'prod'], description: '部署环境')
string(name: 'RELEASE_TAG', defaultValue: '', description: 'release分支tag')
}
stages {
stage('Checkout') {
steps {
checkout scm
sh 'git describe --tags --always > version.txt'
}
}
stage('Build Image') {
steps {
script {
def image = "registry.example.com/order-service:${env.BUILD_NUMBER}"
sh "docker build -t ${image} ."
sh "docker push ${image}"
env.IMAGE = image
}
}
}
stage('Deploy Prod') {
when { expression { params.ENV == 'prod' } }
steps {
input message: "确认发布到生产?", ok: "发布"
sh """
kubectl set image deployment/order-service \
order-service=${env.IMAGE} -n prod
kubectl rollout status deployment/order-service -n prod --timeout=180s
"""
}
}
stage('Rollback') {
when { expression { currentBuild.result == 'FAILURE' } }
steps {
sh 'kubectl rollout undo deployment/order-service -n prod'
sh 'curl -X POST $DINGTALK_WEBHOOK -d "{\\"msgtype\\":\\"text\\",\\"text\\":{\\"content\\":\\"生产回滚已触发\\"}}"'
}
}
}
post {
success {
sh 'curl -X POST $DINGTALK_WEBHOOK -d "{\\"msgtype\\":\\"text\\",\\"text\\":{\\"content\\":\\"发布成功: ${env.IMAGE}\\"}}"'
}
}
}
input步骤实现了人工审批,rollout status超时180秒自动失败并触发回滚。
五、踩坑与优化
坑1:rebase导致MR流水线重复触发。 GitLab在rebase后会新建pipeline,浪费Runner。解决:在.gitlab-ci.yml加workflow: rules限制,只对merge_request_event和main跑。
坑2:Sonar质量门禁误报。 新代码覆盖率要求80%,但历史代码只有45%,导致老模块改动也失败。解决:配置sonar.newCode.referenceBranch=main,只检查增量代码。
坑3:Jenkins并发发布冲突。 两个release同时发布,kubectl互相覆盖。解决:加disableConcurrentBuilds(),并在K8s用RollingUpdate策略,maxSurge=1, maxUnavailable=0。
坑4:hotfix合回main时冲突。 因为release分支落后main太多。解决:规定hotfix修复后必须由发起人当天完成双向合并,否则CI告警。
六、效果数据
改造后运行6个月的数据对比:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 平均MR合并时间 | 4.2小时 | 18分钟 |
| 发布耗时 | 4小时 | 12分钟 |
| 生产事故/月 | 2.3次 | 0.7次 |
| 回滚耗时 | 35分钟 | 90秒 |
| 代码覆盖率 | 45% | 78% |
| 冲突回滚/周 | 3次 | 0.2次 |
最直观的感受:以前发布要拉群、@所有人、手动测试,现在点一下Jenkins的"发布"按钮,12分钟后钉钉收到成功通知。
七、总结
Git工作流不是越复杂越好,关键是规则可执行、门禁自动化、回滚可预期。我们的核心经验就三条:
- 分支生命周期要短,超过2天的feature分支就是技术债
- Review和CI必须强制,靠自觉的团队最后都会退化成直接推master
- 回滚比发布更重要,
kubectl rollout undo+ 自动告警是最后的安全网
如果你团队还在用git push origin master,建议先从分支保护+MR流水线开始,成本最低,收益最明显。