3. 流水线配置解析与语法陷阱
很多流水线故障并不是 Runner 执行失败,而是配置在“被 GitLab 解析”的阶段就已经走偏:关键字拼错、include 路径找不到、extends 覆盖顺序反了、锚点展开后变量没生效。要稳定排障,必须把 .gitlab-ci.yml 当成一份有明确解析阶段和合并规则的声明式文档来理解,而不是当成一段顺序执行的脚本。
一、解析的三个阶段:先展开、再合并、后求值
GitLab 处理一份流水线配置,可以粗略分成三个先后阶段,顺序错了就会得出错误结论:
- 结构展开阶段:读取主文件,处理
include引入的外部文件,把!reference标签替换为被引用节点的内容,把 YAML 锚点&name与别名*name展开。这一阶段只做“文本级/结构级”的拼接与替换,不解析变量。 - 合并阶段:对每个 job 应用
extends、default、inherit等继承规则,按优先级把父级配置合并进子 job,形成每个 job 的最终配置树。 - 变量求值阶段:在合并后的配置上展开
$VAR、${VAR},解析rules、only/except、needs等依赖与条件,最终生成可执行的流水线 DAG。
关键结论:锚点和 !reference 在变量求值之前就已完成展开,所以它们不能“动态”引用变量;而 extends 的合并发生在变量求值之前,因此父模板里写的 $CI_COMMIT_BRANCH 会在子 job 的上下文中求值,而不是在父模板定义处求值。
二、include:路径、时机与作用域
include 支持 local、project、remote、template 四种来源。最容易踩的坑是路径基准:local 的路径相对于仓库根目录,而不是相对于当前 YAML 文件所在目录。如果模板放在 ci/templates/build.yml,主文件在根目录,应写 include: 'ci/templates/build.yml';若在 ci/main.yml 中再 include 同级文件,仍要写完整根路径 ci/templates/build.yml,写成 ./build.yml 会失败。
另一个高频问题是 include 的求值时机:它在结构展开阶段完成,因此被 include 的文件里如果引用了主文件中定义的变量,只要变量在变量求值阶段能解析到,通常仍可用;但被 include 文件中的 include 嵌套层级和循环引用会直接报错。排障时优先看流水线创建失败的报错信息,GitLab 会明确指出是哪个 include 路径无法解析。
三、extends 与 !reference:两种复用方式的差异
extends 是深合并:父模板的 script 数组会被子 job 的 script 整体替换(数组不合并),而 variables、cache 等映射会按键合并。子 job 中显式写的键覆盖父模板同名键。若 extends 写成列表,后面的元素优先级更高,例如 extends: [.base, .override] 中 .override 覆盖 .base。
!reference 则是精确取值:它把某个节点(如 .setup.script)的内容原样插入当前位置,不做合并。它适合“只借用一段 script 或 rules”,而不想继承整个模板。注意 !reference 只能引用同一文件或已 include 文件中的节点,且被引用节点必须以 . 开头(隐藏 job)或为顶层键。
.base:
image: alpine:3.19
before_script:
- echo "prepare"
build:
extends: .base
script:
- echo "build"
variables:
MODE: release
test:
script:
- !reference [.base, before_script]
- echo "test"
上例中 build 继承 image 与 before_script,并新增 MODE;test 不继承 image,只把 .base 的 before_script 内容插入自己的 script 开头。两者语义完全不同,混用会导致“以为继承了却没继承”或“以为只借一段却带进整个模板”。
四、锚点与变量展开顺序的实战影响
YAML 锚点由 YAML 解析器处理,发生在 GitLab 变量求值之前。因此下面这种写法是无效的:
.common: &common
script:
- echo "$CI_COMMIT_BRANCH"
job_a:
<<: *common
锚点展开后 job_a.script 就是字面量 echo "$CI_COMMIT_BRANCH",变量在 Runner 执行时才由 shell 展开,这通常没问题;但若把变量用在 rules:if 或 include 路径中,就必须依赖 GitLab 的变量求值阶段,锚点无法提前“算”出结果。典型错误是试图用锚点拼接 include 路径:include: *path 中 *path 若含 $VAR,GitLab 不会在 include 阶段展开它,导致路径解析失败。
五、一份可复用的配置自检清单
| 检查项 | 正确做法 | 典型错误 |
|---|---|---|
| include 路径 | 相对仓库根目录写全路径 | 写成相对当前文件的 ./x.yml |
| extends 顺序 | 高优先级模板放列表后面 | 顺序颠倒导致覆盖失效 |
| 数组字段 | script 会被整体替换 | 以为 script 会追加合并 |
| !reference 目标 | 引用已定义的隐藏 job 节点 | 引用不存在的键或未 include 的文件 |
| 变量位置 | rules/if 中用变量需在求值阶段可解析 | 在锚点或 include 路径中写 $VAR |
| YAML 语法 | 用 CI Lint 校验 | 缩进、冒号后缺空格、重复键 |
实操建议:每次修改配置后,先在 GitLab 的 CI/CD → Editor → Lint 中校验,它会同时报告 YAML 语法错误和 include 解析错误;再在 Pipelines → 新建流水线 页面查看“流水线创建失败”的具体原因。把这两步固定成习惯,绝大多数配置类故障都能在提交前拦截。