1. 问题背景:我们曾经在master上“裸奔”

2023年初,我们团队5个人,项目是Spring Boot微服务,代码直接推master。听起来很爽,对吧?直到某天:

  • 同事A改了UserService.java,同事B同时改了同一文件的不同方法,Git自动合并成功,但逻辑互相覆盖——线上订单数据错乱,紧急回滚。
  • 产品经理说“这个功能下周上线”,但代码已经在master里,没法单独剔除——只能连夜手撕git revert,然后祈祷冲突别爆炸。
  • 每次发布,大家围在电脑前,手动合并feature分支到release分支,平均耗时2小时,还经常漏掉某个commit。

我们不是不懂Git,而是没有流程。痛定思痛,我们花了2周时间,重构了整个Git工作流。现在,我把完整方案写出来,含真实配置,希望能帮你少踩坑。

2. 环境与版本:我们用的具体工具链

  • Git: 2.39.1 (支持git switchgit restore,老版本请升级)
  • GitLab: 15.11.0 (社区版,支持受保护分支、Merge Request审批)
  • Jenkins: 2.414.2 (LTS版本,使用Pipeline插件)
  • SonarQube: 9.9.0 (社区版,用于静态代码检查)
  • 构建工具: Maven 3.8.8 + JDK 17

3. 方案设计:Git Flow变体 + 三层保护

我们不搞纯Git Flow,太繁琐;也不搞GitHub Flow,太简单。采用Git Flow变体

main (生产环境,受保护,禁止直接push)
  ├── release/ (预发布分支,从main拉出,测试通过后合回main并打tag)
  ├── develop (集成分支,受保护,所有feature合入这里)
  │     └── feature/xxx (新功能,从develop拉出)
  │     └── bugfix/xxx (缺陷修复,从develop拉出)
  └── hotfix/xxx (紧急生产修复,从main拉出,修复后直接合入main和develop)

关键规则
1. 受保护分支maindevelop禁止直接push,必须通过Merge Request (MR) 合入。
2. MR审批:至少1个非作者审批人,且审批人必须是了解该模块的人。
3. CI强制检查:MR未通过Jenkins流水线,无法合并。

4. 核心实现:受保护分支 + Jenkins流水线配置

4.1 GitLab受保护分支配置

在GitLab项目设置中,找到“Repository > Protected Branches”,配置如下:

分支: main
允许合并: Maintainers
允许push: 无 (禁止直接push)
允许强制push: 否

分支: develop
允许合并: Developers + Maintainers
允许push: 无
允许强制push: 否

同时,在“Merge Request > Approval rules”里,设置:

审批人数: 1
审批人类型: 非作者 (Code Owner优先)
4.2 Jenkins Pipeline完整配置 (Jenkinsfile)

我们在仓库根目录放Jenkinsfile,实现MR触发流水线:编译 → 单元测试 → 静态扫描 → 构建镜像。

pipeline {
    agent any
    tools {
        maven 'Maven-3.8.8'  // 在Jenkins全局工具里配置的maven
        jdk 'JDK-17'
    }
    environment {
        SONAR_HOST_URL = 'http://sonarqube.example.com:9000'
        SONAR_TOKEN = credentials('sonar-token')  // Jenkins凭据
        REGISTRY = 'registry.example.com'
        IMAGE_NAME = "${REGISTRY}/myapp:${env.BRANCH_NAME}-${env.BUILD_NUMBER}"
    }
    stages {
        stage('Checkout') {
            steps {
                // 关键:检出MR源分支,而非目标分支
                checkout scm
            }
        }
        stage('Compile') {
            steps {
                sh 'mvn clean compile -DskipTests'
            }
        }
        stage('Unit Test') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit '**/target/surefire-reports/*.xml'
                }
            }
        }
        stage('SonarQube Analysis') {
            steps {
                // 注意:需在SonarQube配置GitLab的MR插件
                sh """
                    mvn sonar:sonar \
                        -Dsonar.projectKey=myapp \
                        -Dsonar.host.url=${SONAR_HOST_URL} \
                        -Dsonar.token=${SONAR_TOKEN} \
                        -Dsonar.gitlab.commit_sha=${env.CI_COMMIT_SHA} \
                        -Dsonar.gitlab.ref_name=${env.CI_COMMIT_REF_NAME} \
                        -Dsonar.gitlab.merge_request_iid=${env.CI_MERGE_REQUEST_IID}
                """
            }
        }
        stage('Build Image') {
            when { branch 'develop' }  // 仅develop分支构建镜像
            steps {
                sh """
                    docker build -t ${IMAGE_NAME} .
                    docker push ${IMAGE_NAME}
                """
            }
        }
    }
    post {
        failure {
            // 通知团队
            emailext (
                subject: "构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
                body: "请检查Jenkins构建日志: ${env.BUILD_URL}",
                to: 'dev-team@example.com'
            )
        }
    }
}
4.3 MR的CI触发配置

在GitLab项目 .gitlab-ci.yml 中,我们只做一个动作:触发Jenkins。

stages:
  - ci

ci:
  stage: ci
  script:
    - curl -X POST http://jenkins.example.com:8080/job/myapp-mr-pipeline/buildWithParameters?token=mysecrettoken
  only:
    - merge_requests

这样,每次创建/更新MR,GitLab自动调Jenkins。Jenkins里把Build when a change is pushed to GitLab勾选,并设置MR源分支作为构建分支。

5. 踩坑与优化:3个真实案例

坑1:SonarQube扫描在MR里不显示门禁状态

我们配置了SonarQube,但MR里看不到“质量门禁通过/失败”的显示。排查半天,发现是sonar-gitlab-plugin版本不对。SonarQube 9.9需要的是sonar-gitlab-plugin-4.2.0,且必须使用sonar.gitlab.commit_shasonar.gitlab.merge_request_iid这两个参数,缺一不可。同时,GitLab的Admin里要设置Allow local requests,否则SonarQube无法回调GitLab API。

坑2:Jenkinsfile里checkout scm导致MR不触发

一开始,我们在Jenkinsfile里写git checkout origin/${env.MR_SOURCE_BRANCH_NAME},结果发现MR合并时构建的是目标分支代码,而不是源分支。正确做法是使用checkout scm,它会根据GitLab插件的上下文自动检出正确的源分支。不要手写git命令

坑3:develop分支构建镜像,但release分支忘了构建

我们只在develop分支构建开发环境镜像,但release分支也需要构建预发布镜像。后来加了when { branch 'release/*' },并给IMAGE_NAME加了-release后缀,避免与dev镜像混淆。同时,release分支的构建要加--no-cache,因为之前用缓存导致依赖没更新。

6. 效果数据:用数字说话

改造前(2023年1月-2月):
- 平均每周代码冲突:15次
- 发布平均耗时:2小时15分钟
- 线上事故(因合并导致):2次

改造后(2023年3月-4月):
- 平均每周代码冲突:1.5次(下降90%)
- 发布平均耗时:8分钟(Jenkins自动构建+推送,手动点击发布按钮)
- 线上事故:0次
- MR平均从创建到合并:4.5小时(以前是1.5天)

7. 总结与思考

这套Git工作流不是什么银弹,但它让我们从“手工操作”变成“流程驱动”。几点心得:

  1. 受保护分支不是限制,是保护。刚开始团队成员抱怨“不能直接push很不爽”,两周后没人再提,因为再也不用处理莫名其妙的冲突了。
  2. CI/CD不是可选项,是必需品。没有自动流水线,Code Review就是走形式,因为合并前根本不知道代码能不能跑。
  3. 规则要硬编码,不要靠自觉。GitLab的“禁止push”+“强制审批”+“CI检查”三个开关全开,哪怕有人想绕过也没办法。

最后,留个问题:你的团队还在master上裸奔吗? 如果是,建议把这篇文章打印出来贴在工位上。别问我为什么知道——我们都是这么过来的。


附录:关键配置速查

工具 版本 关键配置
Git 2.39.1 git config --global merge.ff false (保留合并记录)
GitLab 15.11.0 受保护分支 + MR审批1人
Jenkins 2.414.2 Pipeline + GitLab插件 + 凭据管理
SonarQube 9.9.0 质量门禁: 覆盖率≥60%, 漏洞数=0, 重复率≤3%