一、问题背景:5人团队和18人团队,是两种生物
先说结论:小团队那套"大家都在master上push"的玩法,人一多必炸。
我们团队去年从5个人扩到18个人,前端8个、后端7个、测试2个、运维1个。扩张后的第一个月,git log惨不忍睹:
- 平均每周3.2次合并冲突,最长一次解决冲突花了4个小时
- 线上回滚2次,其中一次是因为有人把半成品功能直接push到了master
- Code Review形同虚设,MR(Merge Request)平均只有0.4条评论,很多MR是作者自己点Merge
- 发布全靠手动,运维同学每次发版要执行17条命令,还漏过一次数据库迁移脚本
问题的根子不在人,在流程。master分支没有任何保护,feature分支命名五花八门(dev、test1、zhangsan-dev、new),没有release概念,hotfix直接改master再cherry-pick回开发分支——每次cherry-pick都是一场赌博。
所以我们决定重构整个Git工作流。目标很明确:分支策略要能隔离风险,Code Review要能强制卡住质量,CI/CD要能自动化兜底。
二、环境与版本
先把环境钉死,避免读者对不上:
- GitLab CE 16.9.1(自建,Docker部署)
- Git 2.40.1
- GitLab Runner 16.9.1,Docker Executor
- Docker 24.0.7 / Docker Compose v2.23.0
- Node.js 20.11.0(前端)、OpenJDK 17.0.9(后端)
- 代码规范:ESLint 8.56.0、Prettier 3.2.4、Checkstyle 10.12.5
三、方案设计:Git Flow改良版
经典Git Flow有5种分支,对我们来说develop分支是多余的负担——我们不做持续集成到develop再整体发布,而是走"feature直接合入master,发布用release分支冻结"的路线。最终定下三类长期/临时分支:
| 分支类型 | 命名规范 | 生命周期 | 保护规则 |
|---|---|---|---|
| master | master |
永久 | 禁止直接push,需2人Approve,CI全绿 |
| feature | feature/{JIRA-ID}-{简述} |
合并后删除 | 禁止push到master |
| release | release/{版本号} |
发布后保留tag | 仅Tech Lead可创建 |
| hotfix | hotfix/{版本号} |
修复后合并回master和release | 仅Tech Lead可创建 |
关键设计决策有三个:
1. feature分支从master切出,合并回master。 不绕develop,减少一次合并链路。代价是master必须时刻保持可发布状态——这靠CI和Review来保证。
2. release分支用于发布冻结。 从master切出release/2.4.0,测试期间只允许hotfix类提交进入release分支,master继续接收新feature。发布完成后打tag v2.4.0,release分支保留但不再更新。
3. hotfix双路合并。 线上问题从master切hotfix/2.4.1,修复后同时合入master和release/2.4.0(如果该release还在维护期),避免修复丢失。
分支保护的具体参数(GitLab Settings → Repository → Protected branches):
master:
Allowed to merge: Maintainers
Allowed to push: No one
Code owner approval: Required
Required approvals: 2
Reset approvals on push: true
Status checks: lint, test, build 必须通过
注意Reset approvals on push这个开关一定要开——我们踩过坑,有人拿到approval后又push了一个commit,review等于白做。
四、核心实现:MR模板 + CI流水线配置
4.1 MR模板与Code Review流程
在仓库根目录放.gitlab/merge_request_templates/default.md:
## 变更说明
## 变更类型
- [ ] 新功能 (feature)
- [ ] Bug修复 (hotfix)
- [ ] 重构 (refactor)
- [ ] 配置/文档 (chore)
## 自测清单
- [ ] 本地lint通过 (`npm run lint` / `mvn checkstyle:check`)
- [ ] 单元测试通过,新增代码覆盖率 ≥ 80%
- [ ] 涉及数据库变更的,已提供迁移脚本并回滚验证
- [ ] 涉及接口变更的,已更新API文档
## 影响范围
## 截图/录屏(UI变更必填)
Review流程我们定为硬规则:非作者本人2个Approve才能Merge,其中至少1个必须是Code Owner。Code Owner通过CODEOWNERS文件指定:
# .gitlab/CODEOWNERS
* @backend-lead @frontend-lead
/services/payment/ @payment-owner
/services/auth/ @security-owner
Review关注点我们也写进了团队Wiki,避免"LGTM"式敷衍:逻辑正确性 > 边界条件 > 错误处理 > 命名可读性 > 性能。风格问题一律交给ESLint/Prettier,review里不讨论格式。
4.2 GitLab CI流水线
.gitlab-ci.yml完整配置如下,分四个stage:lint → test → build → deploy-staging。核心是每个stage失败即阻断,且build产物用Docker镜像缓存。
# .gitlab-ci.yml
stages:
- lint
- test
- build
- deploy
variables:
DOCKER_DRIVER: overlay2
NODE_IMAGE: node:20.11.0-alpine
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .m2/repository/
- node_modules/
lint:frontend:
stage: lint
image: $NODE_IMAGE
script:
- npm ci --prefer-offline
- npm run lint
- npx prettier --check "src/**/*.{ts,tsx,vue}"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "master"'
lint:backend:
stage: lint
image: maven:3.9.6-eclipse-temurin-17
script:
- mvn -B checkstyle:check -Dcheckstyle.config.location=checkstyle.xml
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "master"'
test:backend:
stage: test
image: maven:3.9.6-eclipse-temurin-17
script:
- mvn -B test
- awk -F, 'NR>1 {inst+=$4; cov+=$5} END {printf "LINE_COVERAGE=%.2f%%\n", cov/inst*100}' target/site/jacoco/jacoco.csv
coverage: '/LINE_COVERAGE=(\d+\.\d+)%/'
artifacts:
when: always
reports:
junit: target/surefire-reports/TEST-*.xml
expire_in: 7 days
build:image:
stage: build
image: docker:24.0.7
services:
- docker:24.0.7-dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
- if: '$CI_COMMIT_BRANCH =~ /^release\/.*$/'
deploy:staging:
stage: deploy
image: bitnami/kubectl:1.29
script:
- kubectl set image deployment/app app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -n staging
- kubectl rollout status deployment/app -n staging --timeout=180s
environment:
name: staging
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
when: manual
几个关键参数说明:
coverage: '/LINE_COVERAGE=(\d+\.\d+)%/'让GitLab从日志里提取覆盖率,MR页面直接显示,低于阈值可以配合coverage_check卡住合并(我们设的阈值是75%)。rules里MR事件只跑lint和test,不跑build——build只在master和release分支触发,省Runner资源。我们统计过,这样每月Runner分钟数从4200降到1900。deploy:staging设成when: manual,避免每次合master都自动部署到预发环境造成干扰。
4.3 本地钩子兜底
CI再快也是push之后的事。我们在仓库里放了.husky/pre-commit(前端)和pre-push钩子,本地先拦一道:
#!/bin/sh
# .husky/pre-commit
npx lint-staged
lint-staged配置在package.json里只检查暂存区文件,实测一次commit检查耗时从全量lint的23秒降到2.1秒,开发者不会因为慢而--no-verify跳过。
五、踩坑与优化
坑1:release分支冻结不彻底。 第一次用release分支时,有人直接往release/2.4.0推了feature代码,导致发布内容超出预期。解决:release分支加保护规则,只允许Maintainer Merge,且MR目标分支必须是release时,CI额外跑一个check-release-scope脚本,校验diff里不包含新功能文件(通过文件路径白名单)。
坑2:CI缓存污染。 cache.key用了$CI_COMMIT_REF_SLUG,feature分支各自缓存,但master的缓存被release分支共用后出现过依赖版本错乱。改成key: "$CI_COMMIT_REF_SLUG" + 在job里加cache:key:files: [package-lock.json],依赖锁文件变了才重建缓存,问题消失。
坑3:Docker build太慢。 初期每次build 6分40秒。加了多阶段构建+--cache-from后降到1分50秒:
# Dockerfile 关键片段
FROM node:20.11.0-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --prefer-offline
COPY . .
RUN npm run build
FROM nginx:1.25.3-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
配合CI里docker build --cache-from $CI_REGISTRY_IMAGE:cache --build-arg BUILDKIT_INLINE_CACHE=1,命中缓存时build稳定在55秒左右。
坑4:MR堆积。 Review卡在2个Approve,高峰期MR平均存活时间到过38小时。优化:规定每个feature分支生命周期不超过3天,超过必须拆分;每天上午10点站会过一遍待Review队列,指定reviewer。MR平均存活时间降到9.2小时。
六、效果数据
落地三个月后的对比(数据来自GitLab Analytics和团队周报):
| 指标 | 改造前(月均) | 改造后(月均) |
|---|---|---|
| 合并冲突次数 | 12.8次 | 2.1次 |
| 线上回滚 | 2次 | 0次 |
| MR平均评论数 | 0.4条 | 3.7条 |
| Code Review覆盖率 | 约40% | 100% |
| MR平均存活时间 | 31小时 | 9.2小时 |
| 发版耗时 | 手动约40分钟 | 流水线11分钟 |
| CI流水线总时长 | — | lint 1.2min / test 4.5min / build 1.8min |
回滚归零不是运气——hotfix分支流程加上CI强制卡点,让"带病上线"在流程上就过不去。
七、总结
这套工作流没有发明任何新东西,核心就三句话:分支隔离风险、Review强制卡质量、CI自动化兜底。真正难的不是配置本身,是让18个人养成习惯——所以我们把规则全部写进了GitLab的保护分支和CI里,靠工具而不是靠自觉。
如果你们团队在10人以上还在master上直接push,建议先从两件事做起:给master加保护规则(2人Approve + CI必须绿),以及统一分支命名规范。这两步做完,一半的问题就没了。剩下的release和hotfix流程,等第一次因为发布混乱出事故之后,团队自然会推动着建起来——当然,希望你不用等到那一天。