一、问题背景:当仓库从5人用到20人

我们是一个做SaaS后台的团队,2023年初只有5个后端,Git基本是“谁写完谁推main”。到了2024年Q1,团队扩到20人(8后端、5前端、3测试、2运维、2产品),仓库数从3个涨到11个。问题集中爆发:

  • main分支平均每天收到17次直接push,其中约4次会导致CI失败;
  • 发布前一周,developmain的差异提交达到230+,合并冲突要花2小时手动解;
  • 有次测试同学把.env.local提交进仓库,导致预发环境连错数据库,回滚花了40分钟。

痛定思痛,我们花了三周把工作流重新设计并落地。下面直接讲我们最终在用的方案,版本和配置都是线上真实在跑的。

二、环境与版本

  • GitLab CE 16.9.1(自建,Docker部署)
  • GitLab Runner 16.9.1,executor为docker,并发4
  • Node.js 20.11.1(前端项目)
  • husky 9.0.11 + lint-staged 15.2.2 + @commitlint/cli 19.0.3
  • 后端Java 17 + Maven 3.9.6,前端Vite 5.1.6
  • 分支模型:Git Flow简化版(去掉release长期存在,改为临时分支)

三、方案设计:五类分支 + 三道门禁

3.1 分支策略

分支 用途 生命周期 保护规则
main 生产对应代码,每次合并打tag 永久 禁止直接push,需2人approve
develop 集成测试基线 永久 禁止直接push,需1人approve
feature/* 单需求/单issue 合并后删除 无保护,可自由push
release/* 发布候选,只允许bugfix 发布后删除 需1人approve
hotfix/* 线上紧急修复 合并后删除 需2人approve

命名约定:feature/ISSUE-1234-short-desc,强制带Jira号,方便追溯。

3.2 Code Review流程

  1. 开发在feature/*完成后推远端,开Merge Request(MR)到develop
  2. MR必须填写:关联issue、变更类型(feat/fix/refactor)、自测结果、影响范围;
  3. 至少1名同模块同事approve,CI必须全绿,才能点Merge;
  4. developrelease/*main的MR,必须由Tech Lead或架构师approve;
  5. 禁止“自我approve”,GitLab配置里勾选“Prevent approval by author”。

3.3 CI/CD集成

我们有三条流水线:

  • MR流水线lintunit testbuildsonar,全部通过才允许合并;
  • develop流水线:额外跑integration test,部署到staging环境;
  • main流水线:打tag、构建镜像、推Harbor、部署prod(手动触发)。

四、核心实现:真实配置

4.1 本地提交校验(husky + commitlint)

package.json片段:

{
  "scripts": {
    "prepare": "husky"
  },
  "devDependencies": {
    "husky": "9.0.11",
    "lint-staged": "15.2.2",
    "@commitlint/cli": "19.0.3",
    "@commitlint/config-conventional": "19.0.3"
  },
  "lint-staged": {
    "*.{js,ts,vue}": ["eslint --fix", "prettier --write"],
    "*.{java}": ["mvn -q spotless:apply"]
  }
}

commitlint.config.cjs

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [2, 'always',
      ['feat','fix','docs','style','refactor','perf','test','build','ci','chore','revert']],
    'subject-case': [0],
    'header-max-length': [2, 'always', 100]
  }
};

.husky/commit-msg

npx --no -- commitlint --edit "$1"

.husky/pre-commit

npx lint-staged

这套配置上线后,我们统计了两个月:fix:feat:占比从原来的43%提升到81%,CI因为“格式/语法”失败的次数从每周9次降到1次。

4.2 GitLab CI配置(.gitlab-ci.yml

这是我们后端Java项目在用的精简版,真实跑在16.9.1上:

stages:
  - lint
  - test
  - build
  - deploy

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
  DOCKER_DRIVER: overlay2
  IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"

cache:
  key: "$CI_COMMIT_REF_SLUG"
  paths:
    - .m2/repository/

lint:
  stage: lint
  image: maven:3.9.6-eclipse-temurin-17
  script:
    - mvn -q spotless:check
    - mvn -q checkstyle:check
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

unit-test:
  stage: test
  image: maven:3.9.6-eclipse-temurin-17
  script:
    - mvn -q test
  artifacts:
    when: always
    reports:
      junit: target/surefire-reports/TEST-*.xml
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "develop"'

build-image:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  script:
    - docker login -u "$HARBOR_USER" -p "$HARBOR_PWD" harbor.internal
    - docker build -t harbor.internal/app/backend:$IMAGE_TAG .
    - docker push harbor.internal/app/backend:$IMAGE_TAG
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_COMMIT_BRANCH =~ /^release\/.*/'

deploy-staging:
  stage: deploy
  image: bitnami/kubectl:1.29
  script:
    - kubectl set image deployment/backend backend=harbor.internal/app/backend:$IMAGE_TAG -n staging
    - kubectl rollout status deployment/backend -n staging --timeout=180s
  environment:
    name: staging
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'

deploy-prod:
  stage: deploy
  image: bitnami/kubectl:1.29
  script:
    - kubectl set image deployment/backend backend=harbor.internal/app/backend:$IMAGE_TAG -n prod
    - kubectl rollout status deployment/backend -n prod --timeout=300s
  environment:
    name: prod
  when: manual
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

注意几个关键点:
- merge_request_event只在MR场景触发,避免每个push都跑全套;
- develop跑单测+部署staging,main只构建镜像+手动部署prod;
- rollout status加了超时,避免流水线挂死。

4.3 分支保护(GitLab UI配置,也可用API)

Settings → Repository → Protected branches

  • main:Allowed to merge = Maintainers,Allowed to push = No one,Required approvals = 2
  • develop:Allowed to merge = Developers + Maintainers,Allowed to push = No one,Required approvals = 1
  • 勾选:Prevent approval by authorPrevent approval by committerRequire approval from code owners(我们配了CODEOWNERS

CODEOWNERS示例:

# 后端核心模块必须由架构组review
/backend/core/        @arch-team
/backend/payment/     @arch-team @payment-team
# CI配置变更必须由运维review
/.gitlab-ci.yml       @devops-team

五、踩坑与优化

坑1:husky 9的prepare脚本在CI里报错。 husky 9改了初始化方式,husky install废弃,改为husky。但CI里npm ci会触发prepare,而CI环境没有.git(浅克隆)。解决:在CI里设HUSKY=0,或git config --global --add safe.directory /builds/xxx。我们直接在CI变量里加HUSKY: "0"

坑2:MR流水线和分支流水线重复跑。 一开始develop的push和MR都触发流水线,Runner并发4被占满,MR排队超过8分钟。解决:用rules区分merge_request_eventpush,并给MR流水线加interruptible: true,同一MR新push自动取消旧流水线。

坑3:release分支合并回develop时冲突。 我们最初只把release/*合到main,忘了回合develop,导致下个版本又把旧bug带回来。解决:在发布checklist里强制“release → main”和“release → develop”两个MR,并在GitLab里用Push rules禁止直接push release分支。

优化:mvn test加了-T 1C并行编译,单测时间从6分12秒降到3分40秒;Sonar扫描改为增量分析(sonar.scm.revision),从4分钟降到1分20秒。

六、效果数据

上线这套工作流并稳定运行3个月后(2024年4月—6月):

  • main分支直接push次数:从日均17次降到0;
  • CI因格式/语法失败:每周9次 → 1次;
  • MR平均合并时长:从26小时降到4.5小时;
  • 发布回滚次数:Q1共5次 → Q2共1次;
  • 合并冲突手动解决耗时:平均2小时/次 → 20分钟/次。

七、总结

Git工作流没有银弹,核心是约束入口、自动化校验、明确责任人。我们的五分支+三道门禁(本地commitlint、MR流水线、分支保护)在20人规模下跑得比较稳。如果你团队还在10人以内,可以先把main保护起来、加上commitlint和MR流水线,成本最低、收益最明显。等人数上来了,再逐步引入release分支和CODEOWNERS。

配置都在上面,拿去改改就能用。有问题欢迎评论区聊。