一、问题背景:从混乱到秩序
2023年初,我们团队从5人扩张到20人,业务模块拆分为支付、用户、订单三个子域。最初大家直接在master上提交,配合git merge进行同步。很快问题爆发:
- 冲突地狱:同一文件(如
pom.xml、config.js)每天产生超过15次冲突,解决冲突平均耗时20分钟。 - 发布失控:每次发版需要手动挑选提交(cherry-pick),漏掉关键commit导致线上事故2次。
- Code Review形同虚设:直接push master后,review变成事后诸葛,无法拦截设计缺陷。
我们需要的不是推翻重来,而是一套有约束但不过度的流程。本文记录的就是这套方案的设计、落地与迭代过程。
二、环境与版本
- Git版本:2.39.1(支持
git switch、git restore等新语法) - GitLab版本:15.11.3(社区版)
- CI/CD:GitLab CI(内置)+ Jenkins 2.414.2(用于跨项目流水线)
- 代码托管:自建GitLab,仓库规模约2.3GB,日均提交120次
分支权限模型基于GitLab的Protected Branches功能,关键设置如下:
# .gitlab/protected_branches.yml (示例配置)
master:
push: "0" # 禁止直接push,0表示不允许任何人
merge: "maintainers" # 仅Maintainer角色可合并
code_owner: true
develop:
push: "developers" # 开发人员可push
merge: "maintainers"
allow_force_push: false
三、分支策略设计:Trunk-based + 短生命周期特性分支
我们放弃了Git Flow的develop/release/hotfix多长分支模型,原因很简单:长分支意味着长合并。最终采用:
- 主干分支
master:始终可部署,每个提交对应一个生产版本。 - 特性分支
feature/xxx:从master拉出,生命周期不超过3天,必须经过CI+Review才能合回。 - 修复分支
fix/xxx:针对线上紧急问题,允许绕过特性分支直接提MR,但需加急处理(24小时内)。
关键规则:
1. 禁止直接push master,全部通过Merge Request。
2. MR必须关联Issue,未关联的MR无法创建。
3. MR分支必须保持最新:合并前需执行git rebase master或点击GitLab的“Rebase”按钮。
为什么不用develop?因为我们需要每次合并到master都触发生产部署(通过CI判断)。如果存在develop中间层,部署点会模糊,且develop与master的同步周期会变长。
四、Code Review流程:强制双人+机器人检查
我们的MR检查分三层:
- 静态检查层:ESLint(代码规范)、SonarQube(质量门禁,阈值:覆盖率≥60%,新增代码缺陷数=0)。
- 人工Review层:必须至少2名Maintainer批准,且批准者不能是提交者本人。
- 合并策略层:启用GitLab的“Merge only when pipeline succeeds” + “All threads resolved”。
具体在GitLab中配置MR审批规则:
# .gitlab/merge_request_approvals.rb (伪代码)
approval_rules:
- name: "Maintainer double approval"
approvals_required: 2
applies_to: "all_merge_requests"
eligible_approvers:
- "maintainers"
- name: "No self-approval"
prevent_self_approval: true
踩坑记录:最初我们只要求1人批准,结果出现“熟人互批”现象——Review流于形式。后来强制2人后,Review时间虽然变长(从平均10分钟→25分钟),但代码缺陷率下降了40%。建议:如果团队小于5人,可以降为1人+强制CI,但必须禁止自批。
五、CI/CD集成:GitLab CI + Jenkins双轨
我们采用双轨制:GitLab CI负责MR验证(快,5分钟内),Jenkins负责部署(涉及跨系统操作,慢,需要10分钟)。
5.1 GitLab CI:MR验证流水线
.gitlab-ci.yml核心配置(版本:GitLab CI 15.11):
stages:
- lint
- test
- build
variables:
SONAR_HOST_URL: "http://sonar.internal:9000"
SONAR_TOKEN: ${SONAR_TOKEN} # 从CI/CD变量注入
lint-job:
stage: lint
image: node:18-alpine
script:
- npm ci --silent
- npx eslint src/ --ext .js,.jsx
only:
- merge_requests
test-job:
stage: test
image: node:18-alpine
services:
- postgres:15
variables:
POSTGRES_DB: myapp_test
POSTGRES_USER: tester
POSTGRES_PASSWORD: testpass
script:
- npm ci --silent
- npm run test:coverage -- --reporter=jest-junit
artifacts:
when: always
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
only:
- merge_requests
build-job:
stage: build
image: docker:24
script:
- docker build -t ${CI_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_SHORT_SHA} .
- docker push ${CI_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_SHORT_SHA}
only:
- master # 合并到master后构建镜像
关键点:only: merge_requests保证MR不触发部署,只跑验证;master分支才触发构建镜像。
5.2 Jenkins:生产部署流水线
Jenkinsfile(声明式,版本2.414.2)负责从GitLab拉取镜像并滚动更新K8s集群:
pipeline {
agent any
triggers {
gitlab(triggerOnPush: true, branchFilterType: 'all')
}
environment {
K8S_NAMESPACE = 'production'
DEPLOYMENT_NAME = 'myapp-backend'
}
stages {
stage('Deploy to K8s') {
when {
branch 'master'
}
steps {
script {
// 从GitLab Registry拉取镜像
def image = "registry.internal/myapp/backend:${env.GIT_COMMIT.take(8)}"
sh """
kubectl set image deployment/${DEPLOYMENT_NAME} \
myapp-container=${image} -n ${K8S_NAMESPACE}
kubectl rollout status deployment/${DEPLOYMENT_NAME} -n ${K8S_NAMESPACE} --timeout=300s
"""
}
}
}
}
post {
success {
echo "部署成功,版本: ${env.GIT_COMMIT}"
// 调用钉钉通知
sh "curl -X POST http://alert.internal/dingtalk -d 'status=success'"
}
failure {
echo "部署失败,触发回滚"
// 自动回滚到上一个版本
sh "kubectl rollout undo deployment/${DEPLOYMENT_NAME} -n ${K8S_NAMESPACE}"
}
}
}
集成方式:GitLab推送事件通过Webhook通知Jenkins(GitLab插件配置)。由于GitLab CI已经完成了测试和构建,Jenkins只做部署动作,避免重复构建。
六、踩坑与优化:真实数据与调优记录
坑1:MR合并时rebase vs merge
最初我们用git merge --no-ff策略,导致master历史出现大量分叉。后来强制使用rebase(在GitLab MR设置中勾选“Squash commits + Rebase”),历史变成线性,git bisect定位回归版本的时间从平均15分钟缩短到3分钟。
坑2:CI流水线超时
测试任务包含50个E2E用例,跑完需要8分钟。通过jest --maxWorkers=4并行化和只跑变更影响模块(利用jest --onlyChanged),时间缩短到4分30秒。
坑3:多分支并行冲突
当5个特性分支同时修改package.json,必然冲突。我们引入自动化依赖锁定:将package-lock.json设为唯一变更者,使用npm ci而非npm install,并在MR描述中必须标注“依赖变更原因”。冲突率从每周12次降为2次。
效果数据(对比实施前3个月与后3个月):
| 指标 | 实施前 | 实施后 | 变化 |
|---|---|---|---|
| 平均合并等待时间 | 45分钟 | 12分钟 | -73% |
| 线上回归缺陷率 | 4.2% | 1.6% | -62% |
| 发布频率 | 每周1次 | 每天2次 | +100% |
| 代码冲突率(每周) | 15次 | 2次 | -87% |
七、总结与建议
这套工作流并非银弹,但有三个核心原则值得借鉴:
- 短分支:让分支生命周期短于你的记忆周期(<3天),避免长期分叉。
- 自动化前置:CI必须跑在Review之前,机器能拦截的不要让人肉承担。
- 度量驱动改进:每周看合并等待时间、冲突率、缺陷率,用数据调整流程。
最后提醒:工具只是辅助,人的习惯才是关键。我们花了2周培训团队使用git switch/git restore等新语法,并强制废弃git pull(改用git fetch + git rebase)。如果你们的团队还在用git pull,建议先统一基础命令再谈策略。
未来计划:引入trunk-based的自动提交前缀校验(commitlint),并尝试git worktree优化多分支并行开发。欢迎在评论区交流你们的Git工作流实践。