GitLab CI/CD 配置实战

.gitlab-ci.yml 的官方文档列出了四十多个参数。但真正让你把 CI 配置写好的,不是记住这些参数,而是理解 stage 依赖、产物传递、配置复用和条件触发这四条主线。本文以具体场景串起关键参数,从能用的配置到好维护的配置。

一、核心概念:Pipeline → Stage → Job

GitLab CI 的层次关系:

Pipeline (一次提交触发)
  └── Stage: build
  │     ├── Job: compile-js    ─┐
  │     └── Job: compile-css   ─┤ 同 stage 内并行
  └── Stage: test
        ├── Job: unit-test     ─┐
        └── Job: lint          ─┘ 等 build 全部成功后才开始
  • Job 是最小执行单元,每个 job 至少有一个 script 指令
  • Stage 是 job 的分组,决定执行顺序
  • Pipeline 是一次完整的运行

一个 job 最少这样写就能跑:

say-hello:
  script:
    - echo "Hello, CI!"

没有指定 stage 的 job 默认属于 test 阶段。没有显式声明 stages 列表时,GitLab 使用内置的三个阶段:build、test、deploy。

一个重要细节:同一个 stage 内的所有 job 是并行执行的,但前提是 runner 有空闲的并发槽位——默认情况下 concurrent = 1,意味着即使配置了并行,单个 runner 也只能一次跑一个 job。真正的并行需要配置 runner 的并发数。

二、Job 之间的依赖:artifacts 与 dependencies

2.1 artifacts:跨 stage 传递文件

构建阶段的产物如何在测试阶段使用?答案是通过 artifacts:

stages:
  - build
  - test

build:
  stage: build
  script: npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour

test:
  stage: test
  script: npm test

test job 启动时,GitLab 自动把之前所有 job 的 artifacts 下载到工作目录。这就是为什么 test 能用到 build 产出的 dist/ —— 跨 stage 的文件传递不需要手动声明依赖。

2.2 dependencies:按需选择产物

默认行为是所有前一 stage 的 artifacts 都会下载。当 pipeline 有很多 job 时,不需要的 artifacts 会拖慢执行:

deploy-to-staging:
  stage: deploy
  script: scp -r dist/ user@staging:/app
  dependencies:
    - build     # 只下载 build job 的 artifacts,忽略 test 的

dependencies 同时做了两件事:限定了只下载哪些 job 的产物;如果依赖的 job 没有 artifacts,不会报错,只是什么都不下。

2.3 cache:跨 pipeline 的加速

cache 和 artifacts 容易混淆:

概念 传递范围 用途 典型场景
artifacts 同 pipeline 内不同 stage 传递构建结果 dist/ 给 deploy 用
cache 跨 pipeline 加速重复安装 node_modules 下次不用重装
build:
  script: npm ci
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/

key 决定了缓存如何匹配——用分支名做 key,同分支的下次 pipeline 就能命中缓存。注意 .gitlab-ci.yml 中 yarn 和 npm 的缓存默认路径不同,用错路径缓存就白配了。

三、触发条件:only/except 与 rules

3.1 only/except:限定触发分支

deploy:
  script: echo "deploying"
  only:
    - master
    - /^release-.*$/
  except:
    - schedules

only 和 except 同时出现时是「交集」关系:必须满足 only 且不满足 except 才触发。这是最常见的误解——不是 only 或 except 二选一,而是两者都生效。

特殊关键字:

关键字 含义
branches 任何分支 push
tags tag push
merge_requests MR 创建/更新
schedules 定时触发
web 手动在 UI 上点跑

3.2 changes:只对文件变更敏感

当只有特定文件变更时才需要跑对应的 job——比如 Dockerfile 变了才构建镜像:

docker-build:
  script: docker build -t app:latest .
  only:
    changes:
      - Dockerfile
      - docker/**/*

changes 判断的是本次 push 相对于上次的变更。需要注意:对于新分支的第一次 push,changes 永远为 true——GitLab 没有参考基线,无法判断哪些文件是新变更的。

3.3 rules:only/except 的替代者

GitLab 12.3 引入了 rules,并在 14.0 开始推广它替代 only/except。rules 的优先级更高、表达能力更强:

deploy:
  script: echo "deploying"
  rules:
    - if: '$CI_COMMIT_BRANCH == "master"'
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
      when: manual
    - when: never

三条规则的逻辑:master 分支自动部署 → tag 推标签时手动确认 → 其他情况不触发。rules 中的 when 除了 on_success/always/manual/never,还可以用 delayed 实现延时执行。

四、消除重复:extends 与 include 与锚点

一个项目中的几十个 job 往往共享大量配置。三种复用机制各有用处。

4.1 YAML 锚点:同文件内复用

.docker-cache: &docker-cache
  image: docker:latest
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

build-image:
  <<: *docker-cache
  script: docker build -t app .

锚点是纯 YAML 特性,GitLab 不感知。优点是不依赖 GitLab 版本;缺点是只能同文件复用,跨项目跨文件无法引用。

4.2 extends:继承模板 job

.template:
  image: node:16
  before_script:
    - npm ci
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/

lint:
  extends: .template
  script: npm run lint

test:
  extends: .template
  script: npm test

extends 实现的是合并:子 job 的字段会覆盖父模板的同名字段,但 script 是例外——如果父模板和子 job 都定义了 script,只会执行子 job 的 script,不会合并。如果需要同时执行,应该在子 job 的 before_script 中加入额外的命令。

4.3 include:从外部文件引入

当 .gitlab-ci.yml 超过 500 行,就该拆分了:

include:
  - local: '/ci/build.yml'
  - local: '/ci/test.yml'
  - local: '/ci/deploy.yml'
  - template: 'Security/SAST.gitlab-ci.yml'

include 支持四种来源:local(同仓库)、remote(HTTP URL)、template(GitLab 官方模板)、project(其他 GitLab 项目)。include 仅仅是文件内容的拼接,被引入文件中的 extends 引用和 !reference 标签都正常生效。

五、variables:变量的优先级与作用域

5.1 变量的定义来源

GitLab CI 中同一变量名可能在 6 个地方被定义,优先级从高到低:

  1. 手动触发 pipeline 时传入的变量(最高)
  2. Job 级 variables 定义
  3. Pipeline 级 variables 定义
  4. 项目 Settings → CI/CD → Variables
  5. Group 级 Variables
  6. GitLab 预定义变量(如 $CI_COMMIT_REF_NAME)(最低)

一个常见的坑:在项目 Settings 中定义 NODE_ENV=production,在 .gitlab-ci.yml 中写 variables: NODE_ENV: development,两者的预期是谁生效?答案是 yml 中的 pipeline 级定义优先。

5.2 变量在 job 间传递

默认情况,一个 job 中 export 的变量不会传递到下一个 job。需要显式声明:

build:
  script:
    - export BUILD_VERSION=$(cat version.txt)
  artifacts:
    reports:
      dotenv: build.env

artifacts:reports:dotenv 是 GitLab 13.1 引入的特性——它把指定文件中 KEY=VALUE 格式的变量注入到后续 job 的环境变量中。比在 script 中 echo "VAR=$var" >> build.env 然后让下游 job source build.env 的方式更标准化。

六、Pipeline 优化

6.1 needs:打破 stage 顺序

默认情况下,job 必须等前一 stage 全部成功才能开始。但有些 job 不需要:

stages:
  - build
  - test
  - deploy

compile-js:
  stage: build
  script: npm run build:js

compile-css:
  stage: build
  script: npm run build:css

test-js:
  stage: test
  script: npm run test:js
  needs: [compile-js]   # 不等 compile-css,编译完 js 立刻开始测试

deploy:
  stage: deploy
  script: deploy.sh
  needs:
    - test-js
    - job: compile-css
      artifacts: true   # 需要 compile-css 的 artifacts

needs 把 pipeline 从严格串行的 waterfall 变成了 DAG(有向无环图)。副作用是 needs 声明的依赖关系会覆盖默认的 stage 顺序——如果 deploy 配置了 needs: [compile-js],它会在 compile-js 完成后立即执行,不会等 test-js。

6.2 retry:自动重试

网络抖动导致的临时失败不应该让整个 pipeline 红灯:

deploy:
  script: deploy.sh
  retry:
    max: 2
    when:
      - runner_system_failure
      - stuck_or_timeout_failure

when 指定哪些类型的失败才重试——runner 宕机或超时可以重试,但代码编译错误不应该重试,避免浪费时间。

6.3 并行化

test:
  script: jest --shard=$CI_NODE_INDEX --shard-count=$CI_NODE_TOTAL
  parallel: 4

parallel: 4 会创建 4 个相同配置的 job,分别注入 $CI_NODE_INDEX(1-4)和 $CI_NODE_TOTAL(4)。测试框架利用这两个变量对测试用例分片。

七、配置的可维护性策略

7.1 什么时候该拆分

一个判断标准:当你需要滚动超过 3 屏才能看完 .gitlab-ci.yml 时,就该用 include 拆分了。典型拆分策略是按功能模块:

.gitlab-ci.yml        # 入口,包含 stages 定义和 include
ci/
  build.yml           # 构建相关 job
  test.yml            # 测试相关 job
  deploy-staging.yml
  deploy-prod.yml
  security.yml        # SAST、依赖扫描等

7.2 模板化的通用 Job

把重复的部分抽成模板 job,让每个具体 job 只写自己差异化的部分:

.node-job:
  image: node:16
  before_script:
    - npm ci --cache .npm --prefer-offline
  cache:
    key: $CI_COMMIT_REF_SLUG
    paths:
      - node_modules/
      - .npm/

lint:
  extends: .node-job
  stage: test
  script: npm run lint

unit-test:
  extends: .node-job
  stage: test
  script:
    - npm test -- --coverage
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

重点:before_script 用 --prefer-offline 避免每次都向 npm registry 请求元数据;.npm/ 缓存让 npm ci 真正从本地拷贝而非网络下载。

7.3 关键性能指标

指标 如何诊断 优化方向
安装依赖耗时高 chart 中 before_script 占用大 配置 cache、锁定 lockfile
安装依赖重复 每个 job 都有 npm ci 抽成模板 + extends
构建产物传递慢 artifacts 体积大 只上传必要的文件,设置合理的 expire_in
同 stage job 未并行 配置了但实际串行 配置 runner 的 concurrent 参数

总结

GitLab CI 的配置能力远超一个「构建-测试-部署」的线性流水线。理解阶段依赖、artifacts 的传递边界、变量优先级和配置复用机制后,.gitlab-ci.yml 可以成为一个可维护的工程文件,而不是一个越滚越大的命令脚本。