一、问题背景: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流程,等第一次因为发布混乱出事故之后,团队自然会推动着建起来——当然,希望你不用等到那一天。