1. 问题背景:从“能跑”到“跑不动”的Git之痛

2021年,我们团队还维持着“单分支+直接push”的野路子。当时20人,日均提交80次,冲突靠人肉协调,发版靠手动打tag。但2022年团队扩张到200人,业务线拆成6个,事故开始频发:

  • 合并地狱:多人同时改UserService.java,每天光解决冲突就要花掉1.5小时。
  • 代码审查形同虚设:直接在master上提交,reviewer只能事后看diff,发现问题时代码已上线。
  • 发布不可控:没有 staging 环境,master 即生产,一次错误提交直接导致线上P0事故。

我们意识到,Git工作流不是“选一个模式”,而是“设计一套约束系统”——包括分支命名、合并策略、审查门槛、自动化检查,缺一不可。

2. 环境与版本:工具链选型

  • Git:2.39.1(使用git worktree支持并行开发)
  • GitLab:15.11.3(Community Edition,开启Merge Request功能)
  • CI/CD:Jenkins 2.414.2 + GitLab Plugin,流水线即代码(Jenkinsfile
  • 代码质量:SonarQube 9.9(社区版,强制Quality Gate)
  • 制品库:Nexus 3.49(存储jar包与docker镜像)

为了兼容老习惯,我们保留了master分支,但将其设为“只读保护分支”,所有变更必须通过Merge Request进入。

3. 方案设计:分支策略与角色权限

我们最终采用了“Trunk-based + 短期特性分支”的折中方案,抛弃了纯GitFlow的长期分支(太笨重)。

分支体系
- master(生产分支,受保护,禁止直接push)
- develop(集成分支,所有特性分支合并到这里,跑完整CI)
- feature/xxx(从master拉出,生命周期不超过3天)
- hotfix/xxx(从master拉出,必须附带JIRA工单号)

关键规则
1. feature分支必须每日同步mastergit rebase),防止长期偏离。
2. 合并到develop采用--squash压缩为一次提交,保持历史线性。
3. develop通过CI与自动化测试后,由Release Manager合并到master禁用--no-ff,强制线性历史)。

权限矩阵
- 开发者:对feature/*有push权限,对develop仅能发起MR。
- 技术Leader:有develop合并权,但必须通过2人Code Review。
- Release Manager:唯一有master合并权限的人,且必须通过全部流水线。

4. 核心实现:Code Review流水线与CI/CD配置

4.1 强制Code Review:GitLab机器人 + 规则

我们通过GitLab的Code Owner机制,强制核心模块必须有指定人审查:

# .gitlab/CODEOWNERS
# 每个文件匹配规则,至少需要1个Owner批准才能合并
src/main/java/com/company/payment/** @alice @bob
src/main/java/com/company/risk/** @carol

同时,通过GitLab的Merge Request Approval规则(Settings → Merge Requests)设置:
- 最低批准人数:2(非作者)
- 禁止作者批准自己的MR
- 过期后自动重新请求审查(当有新push时)

4.2 CI/CD集成:.gitlab-ci.yaml(片段)

这是我们的精简版流水线,包含静态检查→单元测试→集成测试→构建镜像四个阶段:

# .gitlab-ci.yml
stages:
  - lint
  - test
  - build

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
  DOCKER_REGISTRY: "registry.example.com:5000"

cache:
  paths:
    - .m2/repository/

lint-job:
  stage: lint
  image: maven:3.8.6-openjdk-11
  script:
    - mvn checkstyle:check  # 自定义规则,禁止System.out.println
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'  # 只在MR时运行

unit-test-job:
  stage: test
  image: maven:3.8.6-openjdk-11
  services:
    - mysql:8.0
  script:
    - mvn test -Dtest=UnitTestSuite
  artifacts:
    when: always
    reports:
      junit: target/surefire-reports/TEST-*.xml

build-docker:
  stage: build
  image: docker:20.10.16
  services:
    - docker:20.10.16-dind
  script:
    - docker build -t $DOCKER_REGISTRY/myapp:$CI_COMMIT_SHORT_SHA .
    - docker push $DOCKER_REGISTRY/myapp:$CI_COMMIT_SHORT_SHA
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'  # 只有develop分支才构建镜像
4.3 Jenkins流水线(用于复杂发布流程)

GitLab CI负责MR内的快速反馈(2分钟内跑完),而发布到生产用Jenkins,因为我们需要手动审批+蓝绿部署:

// Jenkinsfile
pipeline {
    agent any
    parameters {
        string(name: 'RELEASE_VERSION', defaultValue: '1.2.0', description: '版本号')
    }
    stages {
        stage('Checkout') {
            steps {
                git branch: 'master',
                    credentialsId: 'gitlab-ssh-key',
                    url: 'git@gitlab:company/app.git'
            }
        }
        stage('Quality Gate') {
            steps {
                // 调用SonarQube,质量阈值:覆盖率>80%,bug=0
                sh 'mvn sonar:sonar -Dsonar.qualitygate.wait=true'
            }
        }
        stage('Deploy to Staging') {
            steps {
                sh "docker-compose -f docker-compose.staging.yml up -d"
                // 运行冒烟测试脚本(检查健康端点)
                sh 'bash scripts/smoke_test.sh http://staging.example.com/health'
            }
        }
        stage('Manual Approval') {
            steps {
                input message: '确认部署到生产?', ok: 'Deploy'
            }
        }
        stage('Deploy to Production') {
            steps {
                sh "kubectl set image deployment/myapp myapp=$DOCKER_REGISTRY/myapp:$RELEASE_VERSION -n production"
                sh "kubectl rollout status deployment/myapp -n production --timeout=5m"
            }
        }
    }
}

5. 踩坑与优化:三个真实教训

坑1:Rebase vs Merge的拉锯战
- 最开始我们用git merge保持“真实历史”,但develop分支出现了大量无意义的Merge branch 'master' into feature/x提交,图形一团乱麻。
- 解决方案强制使用git pull --rebase,并在GitLab设置Merge method: Merge commit的同时,要求开发者本地先rebase。配合git config --global pull.rebase true,从源头杜绝。

坑2:CI在MR上跑得太慢
- 单元测试+集成测试全量跑需要8分钟,开发者等到花儿都谢了。
- 优化:使用rules条件只跑变更相关的测试;集成测试拆分成独立Stage,仅在develop分支执行。最终MR内反馈时间缩短到1分40秒。

坑3:Code Owner匹配失效
- 我们最初在CODEOWNERS里写了*.java @team,但Java文件有500+,导致每个MR都要等5个人批准,效率极低。
- 修正:改为按模块目录精确匹配(如src/main/java/com/company/payment/),并把Owner人数限制为2人。现在平均MR等待审批时间为4小时(可接受)。

6. 效果数据:从混乱到有序

推行这套工作流4个月后,我们收集了关键指标:

  • 合并冲突:从日均17次降至日均2次(主要是同时改配置文件)。
  • 发布频率:从每周1次升至每周3-4次(通过自动化,减少了人工检查)。
  • 线上事故:由每月3起降至每季度1起(因为Code Review+CI质量门禁拦截了问题)。
  • 开发者满意度:内部调查显示,73%的人认为“提交-审查-合并”流程清晰,不再害怕提交代码。

7. 总结:工作流是“活”的,不是“抄”的

这套体系并非万能——它牺牲了部分灵活性(比如禁止长期分支),但换来了可预测性与安全感。如果你要落地,请记住三点:

  1. 机器人优先:所有能自动化检查的(格式、单元测试、依赖漏洞),绝不用人工。
  2. 分支生命周期最短:超过3天的特性分支是坏味道,及时拆解。
  3. Code Review不是过场:用CODEOWNERS强制核心模块的专家审查,否则形同虚设。

最后,工具只是载体,真正改变的是团队对“变更”的敬畏。如果你们也在扩张,不妨从这里开始。