一、为什么我们不得不重构Git工作流

2023年Q2,我们的微服务架构扩展到47个服务,研发人员从80人激增到300人。混乱是肉眼可见的:develop分支上同时存在超过50个活跃功能分支,合并冲突平均需要47分钟解决,CI流水线排队时间长达2小时17分,更严重的是,一次错误的force push直接导致后端核心模块回滚了3天的代码。

问题的本质不是工具,而是缺乏约束的协作流程。Git本身是分布式的,但团队协作需要中心化的纪律。我们对比了GitFlow、GitHub Flow和Trunk-Based Development,最终选择了基于GitLab Flow的变体——因为它既保留了环境隔离(staging),又不至于像GitFlow那样笨重(release分支管理成本太高)。

二、环境清单与核心决策

  • Git版本:2.39.1(强制要求,利用git switchgit restore替代旧命令)
  • 代码托管:GitLab 15.7(EE版,开启Push Rules)
  • CI/CD:Jenkins 2.387.1 + Jenkins Kubernetes Plugin 3732.v5ea5c1e2c5b2
  • 分支模型master(生产)+ staging(预发布)+ feature/*(功能)+ hotfix/*(紧急修复)

关键决策是砍掉develop分支。理由很简单:staging环境必须始终与代码库中的某个分支对应,而develop的语义与staging重叠。我们让staging分支直接对应预发布环境,master对应生产,feature分支基于staging切出,合并回staging,由Release Manager定期将staging合并至master

三、分支策略的硬性规则

在GitLab项目设置中,我们配置了以下分支保护规则(Branch Protection Rules):

分支: master
  允许合并: Maintainers + Release Manager
  允许推送: 无(禁止直接push)
  代码所有者批准: 2人
  强制状态检查: pipeline/maven-build, pipeline/sonarqube

分支: staging  
  允许合并: Developers + Maintainers
  允许推送: 无
  代码所有者批准: 1人
  强制状态检查: pipeline/maven-build

Feature分支命名规范feature/[JIRA-1234]-简短描述Hotfix分支hotfix/[JIRA-5678]-描述,仅允许从master切出。

这里有一个关键参数:合并策略采用Squash Commit。每个MR只保留一个清晰的提交信息,保证master历史是一条直线。代价是丢失了开发过程的中间提交,但对于300人团队,可读性远比细粒度历史重要。

四、Code Review流程:不是走形式

我们的强制Code Review流程如下:

  1. MR创建:开发者基于staging切出feature分支,完成开发后创建Merge Request,目标分支为staging
  2. 静态检查:Pipeline的第一个job会运行git diff --check(检查空白错误)、ESLint(前端)或Checkstyle(Java)。
  3. 人审:至少1名(staging)或2名(master)代码所有者批准。我们使用GitLab的/approve命令,并配置了Approval Rules:当MR涉及pom.xmlDockerfile时,必须额外经过架构组审批。
  4. 自动化验证:Pipeline运行单元测试(JaCoCo覆盖率阈值80%)、SonarQube质量门禁(Bug数0,Code Smell数<10%)、以及集成测试(仅在staging环境)。

下面是我们的.gitlab-ci.yml核心配置片段:

# .gitlab-ci.yml (基于GitLab 15.7)
stages:
  - validate
  - test
  - build
  - deploy

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=/cache/.m2"
  SONAR_HOST_URL: "http://sonarqube.internal:9000"

cache:
  paths:
    - /cache/.m2

before_script:
  - export MAVEN_OPTS="-Dmaven.repo.local=/cache/.m2"

validate:whitespace:
  stage: validate
  script:
    - git diff --check --cached || git diff --check
  except:
    - master
    - staging

test:maven:
  stage: test
  script:
    - mvn -B -pl $(git diff --name-only --diff-filter=ACM ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}...HEAD | grep -E '^[a-z-]+/' | cut -d'/' -f1 | sort -u | tr '\n' ',') test
  artifacts:
    reports:
      junit: '**/target/surefire-reports/TEST-*.xml'
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

build:docker:
  stage: build
  script:
    - docker build -t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} .
    - docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
  only:
    - staging
    - master

test:maven这个job的关键在于只测试变更的模块。我们通过git diff --name-only找出变更文件所属的Maven模块,然后构建一个模块列表传给-pl参数。这让我们在高峰期将平均构建时间从13分钟降到4分20秒。

五、CI/CD集成:Jenkins流水线细节

虽然GitLab自带CI,但我们仍选择Jenkins作为主流水线工具,原因是现有的Kubernetes部署脚本和权限体系已经深度绑定Jenkins。我们在Jenkins中配置了Multibranch Pipeline,自动发现GitLab的feature/staging/master分支。

关键配置是Jenkinsfile中的when条件input步骤

// Jenkinsfile (Declarative Pipeline)
pipeline {
    agent { label 'k8s-build-agent' }

    triggers {
        gitlab(triggerOnPush: true, triggerOnMergeRequest: true)
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        stage('Tests') {
            when {
                branch 'master', 'staging'
                changeset "**/*.java", "**/pom.xml"
            }
            steps {
                sh 'mvn verify -DskipITs=false'
            }
        }
        stage('Security Scan') {
            when {
                branch 'master'
            }
            steps {
                sh 'trivy image --severity HIGH,CRITICAL --exit-code 1 ${IMAGE_NAME}'
            }
        }
        stage('Deploy to Staging') {
            when {
                branch 'staging'
            }
            steps {
                sh './deploy.sh --env staging --namespace staging'
            }
        }
        stage('Approve Production') {
            when {
                branch 'master'
            }
            input {
                message "确认部署到生产环境?"
                ok "确认部署"
                submitter "release-manager"
                parameters {
                    string(name: 'DEPLOY_TAG', defaultValue: "${env.GIT_COMMIT}", description: '镜像Tag')
                }
            }
            steps {
                sh './deploy.sh --env production --namespace production --tag ${DEPLOY_TAG}'
            }
        }
    }

    post {
        failure {
            // 发送钉钉/企业微信通知
            dingtalk(robotUrl: 'https://oapi.dingtalk.com/robot/send?access_token=xxx', 
                     type: 'text', 
                     text: "流水线失败:${env.JOB_NAME} - ${env.BUILD_URL}")
        }
    }
}

两个值得注意的坑:

  1. changeset条件失效:Jenkins 2.387.1中,changeset在GitLab MR事件中偶尔失效。我们改用git diff --name-only ${GIT_PREVIOUS_SUCCESSFUL_COMMIT}...${GIT_COMMIT}来判断是否有Java文件变更,准确性更高。
  2. Kubernetes Agent资源限制:构建Agent的内存限制设为4Gi,但Maven编译大型项目时OOM频繁。最终通过JVM参数-Xmx2g和Maven的-T 1C(单核并行)解决,代价是构建时间略微上升。

六、踩坑与优化:那些文档里没写的

坑1:Squash合并导致MR的diff与合并后内容不一致

当多个commit被squash后,GitLab的MR页面的Changes标签页仍显示原始diff。对于包含大量重构的MR,Reviewer必须二次核对合并结果。我们的解决方案:在MR描述模板中强制要求开发者勾选“已运行git diff master...feature确认无意外变更”。

坑2:Git LFS在大团队中的带宽问题

我们仓库中有部分设计稿PNG(每个约5MB),使用Git LFS后,clone时间从1分20秒涨到4分钟。优化方案:将设计稿移出Git仓库,迁移到独立的Artifactory,并在CI中通过脚本拉取。现在clone时间稳定在55秒以内。

坑3:staging环境被多个feature分支污染

由于feature分支都合并到staging,导致某个feature的代码提前暴露给另一个正在测试的feature。我们引入了环境锁:在staging流水线中增加一个check-env-lock步骤,如果当前有feature的集成测试进行中(通过Redis记录锁),则暂停当前MR的合并直到测试完成。

性能数据

  • 平均MR合并时间:从4小时缩短至1.5小时(含等待审批时间)
  • 构建时间:p50从13分钟降至4分20秒,p95从25分钟降至8分钟
  • 线上事故:由每月约5次降至每月1次(且均为配置变更导致,无代码逻辑错误)

七、总结与工具推荐

这套工作流运行了6个月,核心收益不是“流程更规范”,而是降低了认知负担。开发者不需要知道“现在该往哪合并”,只需要遵守feature → staging → master这条单向路径。对于20人以下的小团队,我不推荐这套方案——过于繁琐。但如果你正在从百人向数百人规模扩张,这套分支策略和CI/CD配置可以作为参考基线。

工具链推荐:Git 2.39+(必须,旧版本对git switch支持不友好)、GitLab 15+(Push Rules和Merge Request Approvals是核心)、Jenkins 2.387+(Pipeline需使用Declarative语法)。最后,规则是死的,人是活的——每季度收集一次团队反馈,调整保护规则和审批人数,才是长期维护这套系统的最佳方式。