一、问题背景:5人团队和18人团队是两种生物

先说结论:小团队的Git习惯,在人数翻倍后一定会反噬。

我们团队最初5个人,工作流非常“野生”:所有人直接在master上开发,git pull --rebase一下就开始写,写完push。5个人的时候冲突概率低,互相看一眼代码就合了,也没人管CI。

到18人(前端6、后端9、测试2、运维1)之后,问题集中爆发:

  • 平均每周3~4次合并冲突,最长一次解冲突花了4小时;
  • 有人把没测完的代码直接推到master,导致预发环境挂了2次;
  • 发布前需要“冻结”两天专门解冲突和联调;
  • 没有Code Review,线上事故复盘时发现一半是低级错误(空指针、漏判null、配置写错环境)。

于是我们决定正经梳理一套工作流。目标很明确:分支隔离风险、Review拦住低级错误、CI保证每次合并都可构建可测试。

二、环境与版本

先把家底交代清楚,避免配置对不上:

组件 版本/配置
Git 2.39.2
GitLab 16.5 (CE,自建)
GitLab Runner 16.5,Docker executor
Node.js 18.18.2(前端项目)
JDK 17.0.9(后端项目)
Docker 24.0.7
pre-commit 3.5.0

项目结构:前端web-app、后端api-service两个仓库,共用一套GitLab CI模板。

三、方案设计:分支策略选“简化版Git Flow”

我们对比过三种方案:

  1. GitHub Flow(只有master+feature):简单,但我们的发布有明确版本节奏(两周一个release),需要有个稳定的发布分支兜底。
  2. 标准Git Flow(master/develop/feature/release/hotfix五类分支):太啰嗦,develop和master长期并存,团队嫌merge来merge去烦。
  3. 简化版Git Flow(最终选择):保留main(生产)、release/*(发布候选)、feature/*、hotfix/*,砍掉长期存在的develop。

最终分支模型:

main ─────●────────────●───────────●─────→  (生产,只接受MR合并)
           \          /           /
release/1.4.0 ─●──●──●           /        (发布候选,只修bug)
              /                  /
feature/user-login ─●──●──●     /          (功能开发)
                              /
hotfix/1.4.1 ─────●──────────●            (紧急修复,同时合回main和release)

规则:

  • main、release/*受保护:禁止直接push,必须MR + 至少1个Approve + CI通过;
  • feature/*从main切出,命名feature/需求号-简述,如feature/PAY-231-alipay-refund;
  • 合并方式统一用 Squash Merge,保持main历史线性、一个feature一个commit;
  • release/*只允许cherry-pick bugfix,不允许新功能,发布后打tag v1.4.0。

GitLab上的分支保护配置(Settings → Repository → Protected branches):

Branch: main
  Allowed to merge: Maintainers
  Allowed to push: No one
  Allowed to force push: No
  Code owner approval required: Yes

Branch: release/*
  Allowed to merge: Maintainers
  Allowed to push: No one

四、核心实现:MR模板、CI配置、pre-commit钩子

4.1 Code Review流程与MR模板

Review流程定死三条:

  1. MR标题格式:[PAY-231] 支持支付宝退款,关联Jira需求号;
  2. 作者自查清单必须勾选(本地跑过测试、无调试代码、无敏感信息);
  3. 至少1个同模块Reviewer Approve,涉及支付/权限的改动要求2个。

.gitlab/merge_request_templates/Default.md:

## 变更说明


## 关联需求
Jira: PAY-231

## 自查清单
- [ ] 本地 `npm run test` / `mvn test` 通过
- [ ] 无 `console.log` / `System.out.println` 调试代码
- [ ] 无硬编码密钥、IP、测试账号
- [ ] 涉及DB变更已附SQL及回滚方案
- [ ] 新增接口已更新API文档

## 影响范围


## 截图/日志(可选)

Review本身我们要求24小时内响应,超过就由Tech Lead介入。实际执行下来,MR平均Review时长从最初的11小时降到3.5小时。

4.2 GitLab CI流水线配置

这是重头戏。.gitlab-ci.yml(前端web-app仓库,后端结构类似,只是构建命令不同):

# .gitlab-ci.yml  GitLab 16.5
stages:
  - install
  - lint
  - test
  - build
  - deploy

variables:
  NODE_IMAGE: node:18.18.2-alpine
  DOCKER_DRIVER: overlay2
  npm_config_cache: "$CI_PROJECT_DIR/.npm"

# 缓存 node_modules,跨 job 复用
cache:
  key:
    files:
      - package-lock.json
  paths:
    - .npm/
    - node_modules/

install:
  stage: install
  image: $NODE_IMAGE
  script:
    - npm ci --prefer-offline --no-audit
  artifacts:
    paths:
      - node_modules/
    expire_in: 2 hours

lint:
  stage: lint
  image: $NODE_IMAGE
  needs: ["install"]
  script:
    - npx eslint src --max-warnings=0
    - npx prettier --check "src/**/*.{ts,tsx,css}"
  allow_failure: false

test:
  stage: test
  image: $NODE_IMAGE
  needs: ["install"]
  script:
    - npm run test:unit -- --coverage --ci
  coverage: '/Statements\s*:\s*(\d+\.\d+)%/'
  artifacts:
    when: always
    reports:
      junit: coverage/junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

build:
  stage: build
  image: docker:24.0.7
  services:
    - docker:24.0.7-dind
  needs: ["lint", "test"]
  rules:
    - if: '$CI_COMMIT_BRANCH =~ /^release\// || $CI_COMMIT_BRANCH == "main"'
  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

# 仅 release/* 分支自动部署到预发环境
deploy:staging:
  stage: deploy
  image: bitnami/kubectl:1.28
  needs: ["build"]
  rules:
    - if: '$CI_COMMIT_BRANCH =~ /^release\//'
  environment:
    name: staging
  script:
    - kubectl set image deployment/web-app web-app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -n staging
    - kubectl rollout status deployment/web-app -n staging --timeout=180s

# main 分支打 tag 后才允许上生产,手动触发
deploy:production:
  stage: deploy
  image: bitnami/kubectl:1.28
  needs: ["build"]
  rules:
    - if: '$CI_COMMIT_BRANCH == "main" && $CI_COMMIT_TAG'
      when: manual
  environment:
    name: production
  script:
    - kubectl set image deployment/web-app web-app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -n prod

几个关键设计点:

  • needs 做DAG:lint和test并行跑,不必等彼此,流水线整体从原来的14分钟压到6分半;
  • rules 控制触发:feature分支只跑 lint+test,release分支才构建镜像并部署预发,避免浪费Runner资源;
  • 覆盖率门槛:后面用GitLab的Coverage Check(Settings → CI/CD → Coverage check)设了语句覆盖率低于70%直接MR不可合并。

4.3 pre-commit钩子:把低级错误拦在本地

CI再快也不如本地拦。我们用pre-commit 3.5.0,.pre-commit-config.yaml:

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-merge-conflict
      - id: check-added-large-files
        args: ["--maxkb=1024"]
      - id: detect-private-key

  - repo: https://github.com/pre-commit/mirrors-eslint
    rev: v8.54.0
    hooks:
      - id: eslint
        files: \.[jt]sx?$
        args: ["--fix", "--max-warnings=0"]

安装:pre-commit install,之后每次git commit自动跑。实测它拦下了团队约60%的“漏删调试代码”和“提交密钥”问题,成本是每次commit多1~2秒。

五、踩坑与优化

坑1:Squash Merge之后feature分支没删,仓库里堆了200多个僵尸分支。
解决:GitLab设置里勾选“Delete source branch when merge request is accepted”,同时写了个每月跑的清理脚本,删掉90天无提交的feature/*。

坑2:CI缓存node_modules导致偶发依赖不一致。
一开始cache key用分支名,不同分支锁文件不同却共享缓存,出现过npm ci跳过后运行报错。改成key.files: package-lock.json后,锁文件一变缓存自动失效,问题消失。

坑3:release分支上直接改代码,和main的修复产生分叉。
规定所有release上的修复必须走MR,并且合并后由CI自动往main回合并(GitLab有“Merge train”机制,我们用了简单的cherry-pick job)。这条规则执行后,发布后再回补的冲突从每周2次降到几乎为零。

坑4:docker:24.0.7-dind在共享Runner上偶发超时。
把Runner的concurrent从4调到8,并给dind加了--mtu=1450,超时率从5%降到0.3%。

六、效果数据

跑了3个月,对比前后:

指标 改造前 改造后
发布周期 14天 2天
每周合并冲突次数 3.5 0.7
平均MR Review时长 11h 3.5h
CI流水线时长 14min 6.5min
因低级错误导致的回滚 2次/月 0.3次/月
语句覆盖率 无统计 74%

七、总结

回头看,这套工作流真正起作用的不是某个工具,而是三个约束:分支保护让“直接推main”这条路物理上不存在;MR模板+Approve让Review成为流程的一部分而不是靠自觉;CI的rules+覆盖率门槛让“合并即质量”有了硬标准。

如果要给正在踩同样坑的团队一句建议:先上分支保护和CI,再谈分支模型多优雅。 工具链的强制力,远比文档里的规范有效。配置都在上面了,拿去改改就能用。