.gitlab-ci.yml 配置文件基础结构归纳总结

说实话,刚开始接触 GitLab CI 的时候,我也被那个 YAML 文件搞晕过。什么 stage、job、script,看着像模板又像脚本,写错一个缩进整个流水线就罢工。但用习惯了你会发现,.gitlab-ci.yml 其实就那几套东西来回组合。我帮你把骨架抽出来,以后你照着填空就行。

.gitlab-ci.yml 配置文件基础结构 .gitlab-ci.yml 全局配置 stages(阶段列表) Job(作业定义) 全局配置常见项 default / variables cache / tags workflow / include - build - test - deploy Job 内部结构 stage: test script: 命令列表 image: node:18 artifacts / needs only / except / rules 三条核心规则 1. 所有 Job 必须属于某个 stage 2. 同一 stage 并行执行 3. 失败默认中断后续 stage

整体骨架:三个核心部分

.gitlab-ci.yml 说白了就是三块——全局配置、阶段列表、作业定义。你可以把它们想象成一个生产车间:全局配置是工厂规章,阶段列表是流水线工序,作业是每个工位上的具体操作。

直接看个最小例子:

stages:
  - build
  - test
  - deploy

build_app:
  stage: build
  script:
    - echo "Building..."
    - mkdir -p output

test_app:
  stage: test
  script:
    - echo "Running tests..."

deploy_app:
  stage: deploy
  script:
    - echo "Deploying..."

这个文件定义了三个阶段,三个 Job。每个 Job 的 stage 告诉 GitLab Runner 该在哪个阶段跑。顺序从先到后,谁也别插队。

一个小习惯:我通常把 stages 放在文件最顶部,哪怕后面再加全局配置也放 stages 下面。这样一眼就能看清整个流水线有几步,心理踏实。

全局配置:定基调的东西

除了 stages,文件顶部还能放一堆共享配置。最常用的有这么几个:

关键词用处我踩过的坑
default给所有 Job 设定默认值,比如 image、tags曾经把默认 image 设成 ubuntu:20.04,结果后来一个 Job 需要 node 环境,默认镜像里没有 node,折腾了半天才想起来加 image 覆盖
variables定义全局环境变量,所有 Job 都能用变量名里不小心用了短横线,在脚本里引用时 Bash 报错,改成下划线就好了
cache缓存目录或文件,加速并行 JobNextflow 的 work/ 目录千万不要全局缓存,体积太大,还容易导致并发冲突
workflow控制整个流水线何时运行,比如只在 tag 上触发——
include引用外部 YAML 文件,实现复用路径写相对路径时容易搞错,建议用 project/file/ref 的完整写法

举个例子,我们团队做 Nextflow 流程部署,通常会这样写全局配置:

default:
  image: nextflow/nextflow:23.10.0
  tags:
    - docker

variables:
  NXF_ANSI_LOG: 'false'
  NXF_WORK: /cache/work
  GIT_STRATEGY: clone

cache:
  key: "$CI_COMMIT_REF_SLUG"
  paths:
    - .nextflow/
    - work/
  policy: pull
注意:缓存 policy: pull 意思是只读取不写入,适合多个 Job 共享同一个缓存。如果每个 Job 都要往缓存里写,就改成 pull-push。我曾经因为忘记改 policy,导致后续 Job 拿不到最新缓存,白等了好几分钟。

阶段 stages:定义流水线步骤

stages 就是一个字符串列表。顺序就是你期望的执行顺序。默认情况下,所有 Job 会按照 stages 的顺序串行执行——前一个 stage 全部成功后,后一个 stage 才开始。

同一 stage 内的多个 Job 是并行跑的。这一点非常关键,尤其在 Nextflow 部署场景里:

stages:
  - prepare
  - test
  - deploy

prepare_env:
  stage: prepare
  script:
    - conda env create -f environment.yml

validate_config:
  stage: prepare
  script:
    - nextflow config my_pipeline.nf

dry_run:
  stage: test
  script:
    - nextflow run my_pipeline.nf -stub-run

unit_test:
  stage: test
  script:
    - pytest tests/

deploy_prod:
  stage: deploy
  script:
    - nextflow run my_pipeline.nf
  only:
    - tags

这里面 prepare_env 和 validate_config 都在 prepare 阶段,它们同时启动。等两个都通过了,才进入 test 阶段,两并行跑。最后 deploy 阶段只有一个 Job,并且只在 tag 推送时触发。

关键点:stages 不写全也没事,GitLab CI 会自动推断——但那样容易乱。我建议所有 stage 都在文件顶部显式列出来,哪怕某个 Job 用不到也行。方便以后扩展,也方便他人阅读。

Job 定义:真正干活的单元

每个 Job 必须有一个唯一的名字,比如 build_app、test_api。名字不能包含空格和特殊符号,但可以用下划线。Job 体里最核心的就三样:

  • stage —— 归属哪个阶段
  • script —— 要执行的命令列表(数组形式)
  • 其他修饰属性 —— image、tags、only/except/rules、artifacts、needs、allow_failure 等等

看一个实际 Nextflow 部署的 Job:

deploy_staging:
  stage: deploy
  image: nextflow/nextflow:23.10.0
  script:
    - nextflow pull my-org/my-pipeline
    - nextflow run my-org/my-pipeline -profile staging
  artifacts:
    paths:
      - results/
    expire_in: 1 week
  only:
    - master
  when: manual

这里我加了 when: manual,意思是部署到 staging 需要手动点一下按钮。为啥?因为我在生产环境吃过亏——自动部署一旦出问题,整个流水线就卡住了。手动触发至少给人留个确认的窗口。

rules vs. only/except:条件控制的进化史

GitLab 早期只有 only 和 except,后来推出了 rules,功能更强,也更灵活。我个人建议新项目直接上 rules,除非你要兼容老版本 Runner。

对比一下:

方式优点缺点
only/except简洁,一眼看懂不能做「或」「与」逻辑,很难组合条件
rules支持 if/else、when、exists、allow_failure 任意组合写多了容易啰嗦,需要仔细管理顺序

举个例子,我想让「只有 master 分支且是定时调度」时才跑部署:

deploy_prod:
  stage: deploy
  script:
    - nextflow run my-pipeline -profile prod
  rules:
    - if: '$CI_COMMIT_BRANCH == "master" && $CI_PIPELINE_SOURCE == "schedule"'
      when: always
    - when: never

rules 的最后一个 when: never 是兜底——没有匹配到任何规则就不跑这个 Job。这个习惯我是从一次事故中学到的:有一次忘写兜底,结果所有分支上的合并请求都触发了部署 Job,差点把生产环境覆盖了。

调试 rules 的技巧:在 Job 里加一行 script: - env,把环境变量全打印出来,然后在 CI logs 里查 $CI_COMMIT_BRANCH、$CI_PIPELINE_SOURCE 的值,很快就能定位为什么 rules 不生效。

Job 间的依赖与数据传递

在实际项目中,Job 之间常常需要相互传递文件或产物。GitLab CI 提供了两种方式:

  • artifacts —— 显式声明要保留的文件/目录,后面的 Job 可以下载
  • needs —— 声明当前 Job 依赖前面的哪个 Job,可以跳过 stage 顺序直接拉取 artifacts

还是用 Nextflow 举例:

prepare_inputs:
  stage: prepare
  script:
    - nextflow run fetch_data.nf
    - cp data/input.csv artifacts/
  artifacts:
    paths:
      - artifacts/

run_pipeline:
  stage: build
  needs: [prepare_inputs]
  script:
    - nextflow run main.nf --input artifacts/input.csv
  artifacts:
    paths:
      - results/

注意 needs 让我跳过了 stage 顺序吗?没错,run_pipeline 直接依赖 prepare_inputs,只要 prepare_inputs 完成就立刻启动,不用等 prepare 阶段的其他 Job 跑完。这能明显缩短流水线总耗时。

小心循环依赖:Job A needs B,B needs A,GitLab CI 会报错退出。我见过有人一不小心写反了,排查了半天才发现是自己搞混了依赖方向。

缩进与格式:说三遍也不为过

YAML 的缩进错误是新手最容易踩的坑。GitLab 的 Runner 会报 Invalid yaml 或 jobs:xxx config should implement a script: or a trigger:,但不会帮你定位具体哪一行。我的血泪教训:

  • 统一用 2 个空格缩进,不要用 Tab
  • 列表项前的短横线后面要空格
  • 键值对冒号后面也记得空格
  • 多行字符串用 | 或 > 时注意换行行为

一个典型的错误写法:

build_job:
stage: build
  script:
    - echo hello

要是写成这样,GitLab 会告诉你 validation failed,但不会指出 stage 缩进错误。怎么办?我推荐在本地写完后用 gitlab-ci-local 工具验证,或者在 CI 里加一个 lint 阶段:

lint_ci:
  stage: prepare
  image: alpine:latest
  script:
    - apk add --no-cache yq
    - yq eval .gitlab-ci.yml
  only:
    - merge_requests

yq 会直接打印解析结果,如果有语法错误也会报出来。这招我用了两年,再也没因为 YAML 格式问题浪费过时间。

小总结:一份靠谱的 .gitlab-ci.yml 长什么样

说一千道一万,我建议你写完一个配置文件后,对着下面这张清单检查一遍:

  1. stages 是否显式列出? 哪怕只有一个 build 阶段也别偷懒
  2. 每个 Job 都有 stage 吗? 不写 stage 默认算 test 阶段,可能不是你想要的
  3. script 是数组形式吗? 多命令时每行一个短横线,不要写在一行用 && 连起来——可读性差,调试也麻烦
  4. 全局变量是否冲突? 尤其是 Nextflow 的 NXF_* 系列变量,别让默认值覆盖了你的自定义变量
  5. 缓存策略对不对? 多 Job 并行写入同一缓存路径会出 race condition,考虑用 policy: pull 或为每个 Job 配不同 key
  6. rules 的兜底写了吗? 最后一个 when: never 防止意外触发
  7. 缩进检查一遍 —— 可以用 yq eval 或 GitLab 的 CI Lint 工具

嗯,差不多就这些。其实 .gitlab-ci.yml 的结构很简单,难的是根据实际业务选对配置组合。后面几章我们会围绕 Nextflow 的具体场景,把缓存、产物、环境变量、矩阵并行一个个拆开讲。记住今天讲的骨架,后面都是在上面添砖加瓦。