极狐GitLab头像
关注

改一行前端却跑全量 CI?用 rules:changes 做单仓多服务按需构建实战

一、本文环境

项目版本 / 说明
平台极狐GitLab(JihuLab.com 与私有化部署均适用)
CI 语法参考.gitlab-ci.yml(官方文档 ci/yaml)
仓库结构单仓多服务(monorepo):services/order、services/payment、services/gateway、packages/common
RunnerKubernetes executor,Docker 24.0
验证方式同一条合并请求分支,改造前后各跑 3 次取中位数

二、问题复现

单仓多服务最容易出现的症状:改一行前端文案,后端三个服务全量编译 + 全量测试。

我先把它复现出来。新建一个分支,只改 services/gateway/README.md 一行,然后推送:

git switch -c docs/touch-readme
echo "update deploy note" >> services/gateway/README.md
git commit -am "docs(gateway): update deploy note"
git push -u origin docs/touch-readme

流水线跑起来后看作业列表,12 个作业一个没少:

Pipeline #48213  status: running
├── build:order        running   (6 min)
├── build:payment      running   (7 min)
├── build:gateway      running   (4 min)
├── test:order         pending
├── test:payment       pending
├── test:gateway       pending
├── test:common        pending
├── lint:order         pending
├── lint:payment       pending
├── lint:gateway       pending
├── e2e:checkout       pending
└── security:scan      pending

整条流水线耗时 18 分 42 秒,其中真正与本次改动相关的作业只有 lint:gateway。其余 11 个作业全是"陪跑"。

再看看流水线里的资源占用(Runner 侧统计):

$ kubectl top pod -n gitlab-runner --sort-by=memory | head -5
NAME                                 MEMORY
runner-xxxxx-build-order-7d9k2       3.1Gi
runner-xxxxx-build-payment-2m4pz     2.8Gi
runner-xxxxx-build-gateway-9xq7c     1.9Gi
runner-xxxxx-test-order-5b8vn        2.4Gi

这不是"流水线慢",这是把算力花在了不可能出错的地方。

三、原因定位

极狐GitLab 的默认行为很简单:只要 .gitlab-ci.yml 里定义了作业,且没有规则把它排除,它就会进流水线。

很多团队的配置长这样:

# 改造前的写法
stages:
  - build
  - test

build:order:
  stage: build
  script: make build ORDER=1

test:order:
  stage: test
  script: make test ORDER=1

没有任何 rules,于是每次推送都全量触发。要让作业"按需出现",需要用 rules:changes 做路径过滤——它的作用是检查特定文件是否发生变更,再决定作业是否加入流水线。

这里有两个必须先搞清楚的语义,否则改完会发现"有的跑有的不跑,说不清为什么":

  1. 合并请求流水线中,rules:changes 把变更与目标分支比较;
  2. 分支流水线中,rules:changes 把变更与该分支上的上一个提交比较。

还有一个最容易踩的点:对于新分支流水线,或者没有 Git push 事件的流水线(标签流水线、计划流水线、手动流水线),rules:changes 始终评估为真——也就是说,刚推上去的新分支,过滤规则是不生效的,第一次必然全量跑。

四、解决步骤

第 1 步:给整条流水线装总开关 workflow:rules

先控制"要不要创建流水线",再控制"流水线里有哪些作业"。workflow:rules 与作业级 rules 类似,但作用于整条流水线,且 when 只能是 always 或 never。

workflow:
  rules:
    - if: $CI_COMMIT_TITLE =~ /-draft$/
      when: never
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

踩坑 1:同时匹配"分支流水线"和"合并请求流水线"会产生重复流水线——一个提交跑两条。要么只保留 MR 流水线,要么用 $CI_OPEN_MERGE_REQUESTS 做互斥判断。

第 2 步:给每个服务作业加 rules:changes

.order_template: &order_rule
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - services/order/**/*
        - packages/common/**/*
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      changes:
        - services/order/**/*
        - packages/common/**/*

build:order:
  <<: *order_rule
  stage: build
  script: make build SERVICE=order

test:order:
  <<: *order_rule
  stage: test
  script: make test SERVICE=order

通配符支持三种写法,注意区别:

changes:
  - services/order/*           # 只匹配该目录一层
  - services/order/**/*        # 该目录及其所有子目录
  - "**/*.{go,mod,sum}"        # 任意目录下指定扩展名

踩坑 2:rules:changes 的 glob 用 Ruby File.fnmatch 解释,路径是精确字符串比较,不是 shell 求值。写 ./services/order/**/*、services//order/* 这类相对或双斜杠路径匹配不到。另外每个 rules:changes 段最多 50 个模式,超过会报错;模式匹配检查次数上限是 50,000 次,超了以后规则一律视为匹配(即退化为全量)。

第 3 步:用 compare_to 给"没有 push 事件"的流水线兜底

计划流水线、标签流水线没有 Git push 事件,rules:changes 会恒为真。用 compare_to 显式指定比较基准:

nightly:order:
  stage: test
  script: make test SERVICE=order
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
      changes:
        paths:
          - services/order/**/*
        compare_to: 'refs/heads/main'

compare_to 支持分支名、标签名、提交 SHA,17.2 起也支持 CI/CD 变量。

踩坑 3:compare_to 在合并结果流水线下可能给出意外结果,因为比较基准是系统创建的内部提交;在 fork 项目中也有已知问题。如果你们的 MR 必须跑合并结果流水线,这里的过滤要额外验证。

第 4 步:用 include:rules 把配置也拆开

只过滤作业还不够,配置本身也可以按需加载。include 支持 rules:if、rules:exists、rules:changes:

include:
  - local: ci/services/order.yml
    rules:
      - if: $CI_PIPELINE_SOURCE == "merge_request_event"
        changes:
          - services/order/**/*
  - local: ci/services/payment.yml
    rules:
      - if: $CI_PIPELINE_SOURCE == "merge_request_event"
        changes:
          - services/payment/**/*

好处是流水线配置解析阶段就少加载文件,配置复杂时解析耗时也会下降。

第 5 步:公共库变更时,用下游流水线精确展开

packages/common 变了,理论上所有依赖它的服务都要测。与其写一堆 changes 模式,不如生成一条下游流水线:

# ci/generate-children.yml
generate:children:
  stage: build
  script:
    - |
      SERVICES=$(scripts/affected-services.sh)
      echo "affected: $SERVICES"
      for s in $SERVICES; do
        echo "  - project: $CI_PROJECT_PATH" >> children.yml
        echo "    file: ci/services/$s.yml" >> children.yml
      done
    - cat children.yml
  artifacts:
    paths:
      - children.yml

trigger:children:
  stage: test
  needs: [generate:children]
  trigger:
    include:
      - artifact: children.yml
        job: generate:children
    strategy: depend

scripts/affected-services.sh 自己实现依赖推导即可,最简版本:

#!/usr/bin/env bash
set -euo pipefail
changed=$(git diff --name-only "$CI_MERGE_REQUEST_DIFF_BASE_SHA" "$CI_COMMIT_SHA")
for s in order payment gateway; do
  if echo "$changed" | grep -qE "services/$s/|^packages/common/"; then
    echo "$s"
  fi
done

第 6 步:本地自检,别等推上去才发现规则写错

# 先做语法检查
curl -s --noproxy '*' \
  -H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "$CI_API_V4_URL/projects/$CI_PROJECT_ID/ci/lint" \
  --form "[email protected]" | jq '.status'

# 再模拟一次路径匹配
git diff --name-only origin/main...HEAD | head -20

五、验证结果

同一条 MR 分支,改造前后各执行 3 次,取中位数:

指标改造前改造后变化
作业总数123-75%
流水线时长18 分 42 秒4 分 16 秒-77%
Runner Pod 峰值内存9.2 Gi2.4 Gi-74%
每月 CI 计算时长4,180 分钟1,260 分钟-70%
开发者平均等待反馈19 分钟5 分钟-74%

需要注意:这里的比例取决于你的仓库结构。服务越多、耦合越低,rules:changes 的收益越大;如果 packages/common 天天改,收益会被大幅稀释。

踩坑 4:改造后第一次全量回归别省。路径过滤一旦写漏(比如忘了 go.mod、Dockerfile、.gitlab-ci.yml 本身),会出现"改了依赖却没跑测试"的静默漏测。建议给 .gitlab-ci.yml 与根构建文件单独加一条兜底规则:

build:fallback:
  stage: build
  script: make build-all
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - .gitlab-ci.yml
        - go.mod
        - go.sum
        - Dockerfile

六、什么时候别这么做

  • 仓库服务少于 3 个:过滤的维护成本高于收益,直接全量更简单。
  • 服务之间大量共享代码且高频改动:packages/common 一改就全量,过滤形同虚设,应该先拆依赖。
  • 发布要求"每次提交都全量回归"(如强合规行业):按需构建与这个诉求冲突,不要为了省算力牺牲审计口径。
  • 没有流水线负责人:规则写在 YAML 里,没人维护就会腐化,半年后没人敢动。

七、小结

按需构建不是"给每个作业加个 changes"就完事。真正生效的顺序是:先用 workflow:rules 决定要不要建流水线,再用 rules:changes 决定作业进不进,接着用 compare_to 覆盖计划与标签场景,最后用下游流水线处理公共库扩散,同时留一条兜底规则防漏测。

本文用到的完整配置模板与 affected-services.sh 脚本,我整理成了一份可复制的仓库骨架,放在评论区置顶和我主页的资源合集里,需要的自取。如果你在改造过程中遇到"新分支第一次总是全量""compare_to 结果和预期不一致"这类问题,也欢迎在评论区贴出你的 rules 片段,一起排查。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_44749269/article/details/166317503

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--