.gitlab-ci.yml 配置文件基础结构归纳总结
说实话,刚开始接触 GitLab CI 的时候,我也被那个 YAML 文件搞晕过。什么 stage、job、script,看着像模板又像脚本,写错一个缩进整个流水线就罢工。但用习惯了你会发现,.gitlab-ci.yml 其实就那几套东西来回组合。我帮你把骨架抽出来,以后你照着填空就行。
整体骨架:三个核心部分
.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,文件顶部还能放一堆共享配置。最常用的有这么几个:
| 关键词 | 用处 | 我踩过的坑 |
|---|---|---|
default | 给所有 Job 设定默认值,比如 image、tags | 曾经把默认 image 设成 ubuntu:20.04,结果后来一个 Job 需要 node 环境,默认镜像里没有 node,折腾了半天才想起来加 image 覆盖 |
variables | 定义全局环境变量,所有 Job 都能用 | 变量名里不小心用了短横线,在脚本里引用时 Bash 报错,改成下划线就好了 |
cache | 缓存目录或文件,加速并行 Job | Nextflow 的 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 推送时触发。
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,差点把生产环境覆盖了。
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 跑完。这能明显缩短流水线总耗时。
缩进与格式:说三遍也不为过
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 长什么样
说一千道一万,我建议你写完一个配置文件后,对着下面这张清单检查一遍:
- stages 是否显式列出? 哪怕只有一个 build 阶段也别偷懒
- 每个 Job 都有 stage 吗? 不写 stage 默认算 test 阶段,可能不是你想要的
- script 是数组形式吗? 多命令时每行一个短横线,不要写在一行用
&&连起来——可读性差,调试也麻烦 - 全局变量是否冲突? 尤其是 Nextflow 的
NXF_*系列变量,别让默认值覆盖了你的自定义变量 - 缓存策略对不对? 多 Job 并行写入同一缓存路径会出 race condition,考虑用
policy: pull或为每个 Job 配不同 key - rules 的兜底写了吗? 最后一个
when: never防止意外触发 - 缩进检查一遍 —— 可以用
yq eval或 GitLab 的 CI Lint 工具
嗯,差不多就这些。其实 .gitlab-ci.yml 的结构很简单,难的是根据实际业务选对配置组合。后面几章我们会围绕 Nextflow 的具体场景,把缓存、产物、环境变量、矩阵并行一个个拆开讲。记住今天讲的骨架,后面都是在上面添砖加瓦。