一、问题背景:从混乱到秩序

2023年初,我们团队从5人扩张到20人,业务模块拆分为支付、用户、订单三个子域。最初大家直接在master上提交,配合git merge进行同步。很快问题爆发:

  • 冲突地狱:同一文件(如pom.xmlconfig.js)每天产生超过15次冲突,解决冲突平均耗时20分钟。
  • 发布失控:每次发版需要手动挑选提交(cherry-pick),漏掉关键commit导致线上事故2次。
  • Code Review形同虚设:直接push master后,review变成事后诸葛,无法拦截设计缺陷。

我们需要的不是推翻重来,而是一套有约束但不过度的流程。本文记录的就是这套方案的设计、落地与迭代过程。

二、环境与版本

  • Git版本:2.39.1(支持git switchgit restore等新语法)
  • GitLab版本:15.11.3(社区版)
  • CI/CD:GitLab CI(内置)+ Jenkins 2.414.2(用于跨项目流水线)
  • 代码托管:自建GitLab,仓库规模约2.3GB,日均提交120次

分支权限模型基于GitLab的Protected Branches功能,关键设置如下:

# .gitlab/protected_branches.yml (示例配置)
master:
  push: "0"          # 禁止直接push,0表示不允许任何人
  merge: "maintainers"  # 仅Maintainer角色可合并
  code_owner: true
develop:
  push: "developers"   # 开发人员可push
  merge: "maintainers"
  allow_force_push: false

三、分支策略设计:Trunk-based + 短生命周期特性分支

我们放弃了Git Flow的develop/release/hotfix多长分支模型,原因很简单:长分支意味着长合并。最终采用:

  • 主干分支 master:始终可部署,每个提交对应一个生产版本。
  • 特性分支 feature/xxx:从master拉出,生命周期不超过3天,必须经过CI+Review才能合回。
  • 修复分支 fix/xxx:针对线上紧急问题,允许绕过特性分支直接提MR,但需加急处理(24小时内)。

关键规则:
1. 禁止直接push master,全部通过Merge Request。
2. MR必须关联Issue,未关联的MR无法创建。
3. MR分支必须保持最新:合并前需执行git rebase master或点击GitLab的“Rebase”按钮。

为什么不用develop?因为我们需要每次合并到master都触发生产部署(通过CI判断)。如果存在develop中间层,部署点会模糊,且develop与master的同步周期会变长。

四、Code Review流程:强制双人+机器人检查

我们的MR检查分三层:

  1. 静态检查层:ESLint(代码规范)、SonarQube(质量门禁,阈值:覆盖率≥60%,新增代码缺陷数=0)。
  2. 人工Review层:必须至少2名Maintainer批准,且批准者不能是提交者本人
  3. 合并策略层:启用GitLab的“Merge only when pipeline succeeds” + “All threads resolved”。

具体在GitLab中配置MR审批规则:

# .gitlab/merge_request_approvals.rb (伪代码)
approval_rules:
  - name: "Maintainer double approval"
    approvals_required: 2
    applies_to: "all_merge_requests"
    eligible_approvers:
      - "maintainers"
  - name: "No self-approval"
    prevent_self_approval: true

踩坑记录:最初我们只要求1人批准,结果出现“熟人互批”现象——Review流于形式。后来强制2人后,Review时间虽然变长(从平均10分钟→25分钟),但代码缺陷率下降了40%。建议:如果团队小于5人,可以降为1人+强制CI,但必须禁止自批。

五、CI/CD集成:GitLab CI + Jenkins双轨

我们采用双轨制:GitLab CI负责MR验证(快,5分钟内),Jenkins负责部署(涉及跨系统操作,慢,需要10分钟)。

5.1 GitLab CI:MR验证流水线

.gitlab-ci.yml核心配置(版本:GitLab CI 15.11):

stages:
  - lint
  - test
  - build

variables:
  SONAR_HOST_URL: "http://sonar.internal:9000"
  SONAR_TOKEN: ${SONAR_TOKEN}  # 从CI/CD变量注入

lint-job:
  stage: lint
  image: node:18-alpine
  script:
    - npm ci --silent
    - npx eslint src/ --ext .js,.jsx
  only:
    - merge_requests

test-job:
  stage: test
  image: node:18-alpine
  services:
    - postgres:15
  variables:
    POSTGRES_DB: myapp_test
    POSTGRES_USER: tester
    POSTGRES_PASSWORD: testpass
  script:
    - npm ci --silent
    - npm run test:coverage -- --reporter=jest-junit
  artifacts:
    when: always
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
  only:
    - merge_requests

build-job:
  stage: build
  image: docker:24
  script:
    - docker build -t ${CI_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_SHORT_SHA} .
    - docker push ${CI_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_SHORT_SHA}
  only:
    - master  # 合并到master后构建镜像

关键点only: merge_requests保证MR不触发部署,只跑验证;master分支才触发构建镜像。

5.2 Jenkins:生产部署流水线

Jenkinsfile(声明式,版本2.414.2)负责从GitLab拉取镜像并滚动更新K8s集群:

pipeline {
    agent any
    triggers {
        gitlab(triggerOnPush: true, branchFilterType: 'all')
    }
    environment {
        K8S_NAMESPACE = 'production'
        DEPLOYMENT_NAME = 'myapp-backend'
    }
    stages {
        stage('Deploy to K8s') {
            when {
                branch 'master'
            }
            steps {
                script {
                    // 从GitLab Registry拉取镜像
                    def image = "registry.internal/myapp/backend:${env.GIT_COMMIT.take(8)}"
                    sh """
                        kubectl set image deployment/${DEPLOYMENT_NAME} \
                        myapp-container=${image} -n ${K8S_NAMESPACE}
                        kubectl rollout status deployment/${DEPLOYMENT_NAME} -n ${K8S_NAMESPACE} --timeout=300s
                    """
                }
            }
        }
    }
    post {
        success {
            echo "部署成功,版本: ${env.GIT_COMMIT}"
            // 调用钉钉通知
            sh "curl -X POST http://alert.internal/dingtalk -d 'status=success'"
        }
        failure {
            echo "部署失败,触发回滚"
            // 自动回滚到上一个版本
            sh "kubectl rollout undo deployment/${DEPLOYMENT_NAME} -n ${K8S_NAMESPACE}"
        }
    }
}

集成方式:GitLab推送事件通过Webhook通知Jenkins(GitLab插件配置)。由于GitLab CI已经完成了测试和构建,Jenkins只做部署动作,避免重复构建。

六、踩坑与优化:真实数据与调优记录

坑1:MR合并时rebase vs merge
最初我们用git merge --no-ff策略,导致master历史出现大量分叉。后来强制使用rebase(在GitLab MR设置中勾选“Squash commits + Rebase”),历史变成线性,git bisect定位回归版本的时间从平均15分钟缩短到3分钟。

坑2:CI流水线超时
测试任务包含50个E2E用例,跑完需要8分钟。通过jest --maxWorkers=4并行化和只跑变更影响模块(利用jest --onlyChanged),时间缩短到4分30秒。

坑3:多分支并行冲突
当5个特性分支同时修改package.json,必然冲突。我们引入自动化依赖锁定:将package-lock.json设为唯一变更者,使用npm ci而非npm install,并在MR描述中必须标注“依赖变更原因”。冲突率从每周12次降为2次。

效果数据(对比实施前3个月与后3个月):

指标 实施前 实施后 变化
平均合并等待时间 45分钟 12分钟 -73%
线上回归缺陷率 4.2% 1.6% -62%
发布频率 每周1次 每天2次 +100%
代码冲突率(每周) 15次 2次 -87%

七、总结与建议

这套工作流并非银弹,但有三个核心原则值得借鉴:

  1. 短分支:让分支生命周期短于你的记忆周期(<3天),避免长期分叉。
  2. 自动化前置:CI必须跑在Review之前,机器能拦截的不要让人肉承担。
  3. 度量驱动改进:每周看合并等待时间、冲突率、缺陷率,用数据调整流程。

最后提醒:工具只是辅助,人的习惯才是关键。我们花了2周培训团队使用git switch/git restore等新语法,并强制废弃git pull(改用git fetch + git rebase)。如果你们的团队还在用git pull,建议先统一基础命令再谈策略。

未来计划:引入trunk-based的自动提交前缀校验(commitlint),并尝试git worktree优化多分支并行开发。欢迎在评论区交流你们的Git工作流实践。