继承了 before_script,却少了一行
目标
用 include、extends、!reference、YAML 锚点、default、spec:inputs 消除配置中的重复,并通过运行日志和合并后的配置,确认每一种合并规则实际给作业留下了什么。
为什么重要
流水线文件变大之后,就会把公共部分拆出来复用,而每种复用装置的合并规则都不同。哈希(映射)会合并,数组会被替换,锚点只在文件内有效,default 只在没有人指定时才使用。如果不了解这些规则,“以为继承了的准备命令只在某一个作业中缺失”这样的事故,就会在没有任何错误的情况下发生。接收输入的模板可以避免按环境复制同一个作业,但如果不规定允许的范围,连拼写错误的环境名称也会原样变成作业。
步骤
- 把
/root/glci-reuse创建为 Git 仓库。在/root/glci-reuse/ci/templates.yml中放入隐藏作业.base(before_script 只有一行echo base-setup,变量LOG_LEVEL: info),.gitlab-ci.yml通过include: - local: ci/templates.yml引入该文件,然后放入 stages[build, test]和作业build(stage build、extends: .base、scriptecho "LOG=$LOG_LEVEL")。用gitlab-ci-local --shell-isolation --no-artifacts-to-source运行,看看 build 日志中是否打印出base-setup和LOG=info。 - 再添加两个作业。
test(stage test)在 extends.base的同时,把变量写成LOG_LEVEL: debug、PYTEST: "1",并放入 scriptecho "LOG=$LOG_LEVEL PYTEST=$PYTEST"。lint(stage test)在 extends.base的同时,放入自己的 before_script(echo lint-setup)和 scriptecho lint。运行后确认,test 日志中有base-setup和LOG=debug PYTEST=1,而 lint 日志中只有lint-setup,没有base-setup。 - 添加作业
package(stage build)。不使用 extends,把 before_script 写成!reference [.base, before_script]和echo package-setup两项,运行后必须在base-setup之后打印出package-setup。script 是echo package。 - 在
/root/glci-reuse/broken-anchor.yml中 include ci/templates.yml,并放入一个作业 build,它在 variables 中用<<: *base_vars使用了这个文件中未定义的锚点*base_vars。把gitlab-ci-local --file broken-anchor.yml --list的输出保存到/root/glci-reuse/anchor-error.txt。然后在.gitlab-ci.yml中,在同一个文件内定义锚点&docs_vars(DOCS_OUT: public),在作业docs(stage test)的 variables 中放入<<: *docs_vars和DOCS_FMT: html,并用 scriptecho "$DOCS_OUT/$DOCS_FMT"使其打印出public/html。 - 在
.gitlab-ci.yml顶层,用default:放入 before_scriptecho default-setup。然后让作业report(stage test、scriptecho report)通过inherit: default: false不继承默认值。运行后确认,docs 日志中有default-setup,report 日志中没有任何准备行,而 build 和 test 日志中仍然打印base-setup。 - 把
/root/glci-reuse/ci/deploy.yml创建为带有spec:inputs头部的模板。输入env只允许staging和production之一,replicas是数字,默认为 1。头部之后(---),作业deploy-$[[ inputs.env ]](stage deploy)执行echo "deploy <env> replicas=<replicas>"。.gitlab-ci.yml在 stages 中加入 deploy,并把这个文件 include 两次——staging(默认 replicas)、production(replicas 3)。 - 把
gitlab-ci-local --preview的输出保存到/root/glci-reuse/expanded.yml。这个文件中必须有 include、extends、!reference、锚点、default、inputs 全部展开后的结果。评分器会在仓库副本中运行同一个命令,检查内容是否相同以及若干个值。
参考
- 这台 VM 上没有 GitLab 服务器和 Runner,由 gitlab-ci-local 4.75.1 按与 GitLab 相同的规则解析 .gitlab-ci.yml,并用 shell 执行作业。写了
image:就会试图用 Docker 运行,所以不要使用。受保护变量、掩码、CI_JOB_TOKEN、Runner tag、合并请求流水线的生成是服务器功能,在这里不会重现。 - 运行:在仓库根目录用
gitlab-ci-local --shell-isolation --no-artifacts-to-source(每个作业使用单独的工作目录,不把产物写回仓库),作业列表:gitlab-ci-local --list-csv-all,解析后的配置:gitlab-ci-local --preview。gitlab-ci-local 只把 Git 跟踪的文件传给作业,所以创建文件后请git add。评分器会把仓库复制一份,提交所有文件后,再用同一个工具重新运行。 - 查看作业日志:运行输出中的
잡이름 > ...(占位符为作业名称)行,或者.gitlab-ci-local/output/<잡이름>.log(占位符为作业名称)。 - 常见错误:以为能从片段中获得 before_script,却又在作业中再写了 before_script,导致片段中的命令消失。
- CI/CD YAML syntax reference · include · Optimize GitLab CI/CD configuration files(锚点、extends、!reference) · gitlab-ci-local
include 模板文件,并用 extends 继承
把 /root/glci-reuse 创建为 Git 仓库。在 /root/glci-reuse/ci/templates.yml 中放入隐藏作业 .base(before_script 只有一行 echo base-setup,变量 LOG_LEVEL: info),.gitlab-ci.yml 通过 include: - local: ci/templates.yml 引入该文件,然后放入 stages [build, test] 和作业 build(stage build、extends: .base、script echo "LOG=$LOG_LEVEL")。用 gitlab-ci-local --shell-isolation --no-artifacts-to-source 运行,看看 build 日志中是否打印出 base-setup 和 LOG=info。
include 会先把多个文件合并成一份配置,然后再解析。名称以点开头的作业不会出现在列表中,只作为可继承的片段使用。gitlab-ci-local 只看 Git 跟踪的文件,所以新文件要 git add。
哈希会合并,数组会整体替换
再添加两个作业。test(stage test)在 extends .base 的同时,把变量写成 LOG_LEVEL: debug、PYTEST: "1",并放入 script echo "LOG=$LOG_LEVEL PYTEST=$PYTEST"。lint(stage test)在 extends .base 的同时,放入自己的 before_script(echo lint-setup)和 script echo lint。运行后确认,test 日志中有 base-setup 和 LOG=debug PYTEST=1,而 lint 日志中只有 lint-setup,没有 base-setup。
extends 是深度合并。像 variables 这样的哈希会按键合并,覆盖在继承来的键之上,而像 before_script 这样的数组不会合并,会被作业所写的内容整体替换。
想合并数组时用 !reference
添加作业 package(stage build)。不使用 extends,把 before_script 写成 !reference [.base, before_script] 和 echo package-setup 两项,运行后必须在 base-setup 之后打印出 package-setup。script 是 echo package。
!reference 是一个 tag,会把其他作业(包括隐藏作业)的特定键的值插入到那个位置。它也可以指向 include 进来的文件中的片段,所以弥补了 extends 会整体替换数组的局限。
锚点越不过文件边界
在 /root/glci-reuse/broken-anchor.yml 中 include ci/templates.yml,并放入一个作业 build,它在 variables 中用 <<: *base_vars 使用了这个文件中未定义的锚点 *base_vars。把 gitlab-ci-local --file broken-anchor.yml --list 的输出保存到 /root/glci-reuse/anchor-error.txt。然后在 .gitlab-ci.yml 中,在同一个文件内定义锚点 &docs_vars(DOCS_OUT: public),在作业 docs(stage test)的 variables 中放入 <<: *docs_vars 和 DOCS_FMT: html,并用 script echo "$DOCS_OUT/$DOCS_FMT" 使其打印出 public/html。
锚点和别名是 YAML 解析器在读取一个文件时处理的。include 是在那之后由 GitLab 进行合并的阶段,所以其他文件的锚点已经不存在了。跨文件的复用要用 extends 或 !reference。
default 是兜底值——会被 extends 和 inherit 挤掉
在 .gitlab-ci.yml 顶层,用 default: 放入 before_script echo default-setup。然后让作业 report(stage test、script echo report)通过 inherit: default: false 不继承默认值。运行后确认,docs 日志中有 default-setup,report 日志中没有任何准备行,而 build 和 test 日志中仍然打印 base-setup。
default 是填充给没有指定任何内容的作业的全局兜底值,所以已经通过 extends 获得 before_script 的作业不会使用它。inherit 是按作业关闭是否接收默认值的开关。
用接收输入的模板按环境生成作业
把 /root/glci-reuse/ci/deploy.yml 创建为带有 spec:inputs 头部的模板。输入 env 只允许 staging 和 production 之一,replicas 是数字,默认为 1。头部之后(---),作业 deploy-$[[ inputs.env ]](stage deploy)执行 echo "deploy <env> replicas=<replicas>"。.gitlab-ci.yml 在 stages 中加入 deploy,并把这个文件 include 两次——staging(默认 replicas)、production(replicas 3)。
spec 头部规定了 include 时可以传入的值的形式和允许范围。在模板内部用 $[[ inputs.이름 ]](占位符为输入名称)来使用值。如果传入了不在允许列表中的值,流水线在创建之前就会被拒绝。
提取出 GitLab 将会看到的合并后的配置
把 gitlab-ci-local --preview 的输出保存到 /root/glci-reuse/expanded.yml。这个文件中必须有 include、extends、!reference、锚点、default、inputs 全部展开后的结果。评分器会在仓库副本中运行同一个命令,检查内容是否相同以及若干个值。
配置越是分散在多个文件中,就越难只看文件来回答“这个作业实际执行什么”。把合并后的结果附到评审中,就不必在脑子里计算合并规则了。GitLab 界面的流水线编辑器中也有同样的功能(查看完整配置)。