TT Lab
开始
学习 学习路径 课程

GitLab CI/CD

一个阶段名拼写错误,让部署作业悄无声息地消失了

在 TT Lab 中继续学习

目标

把用 GitLab 配置 schema 进行验证的工具包装成门禁,并加上团队策略,把它设置到提交前钩子、流水线的第一个作业,以及展开 include 之后对整个仓库的验证上,并为合并请求评审提取两个提交之间作业列表的变化。

为什么重要

流水线配置的错误通常是悄无声息的。stage 名称的拼写错误,或者缩进错一格,就会让作业消失或阻止流水线的创建,而这件事要到 push 之后才会暴露。应该把验证交给工具,而不是人的眼睛,但必须了解工具能抓住什么、会漏掉什么,才能用策略补上那个缺口。之所以把同样的检查放在提交前和流水线的第一个作业两个地方,是因为本地钩子可以被跳过,而在评审中,比起文本 diff,“会生成哪些作业、消失哪些作业、哪些变成自动”是更重要的信息。

步骤

  1. 把 /root/glci-gate 创建为 Git 仓库(.gitignore 中写入 .gitlab-ci-local/),并创建 /root/glci-gate/ci-validate.sh <설정파일>(占位符为配置文件)。把这一个文件作为临时 Git 仓库的 .gitlab-ci.yml 放入,用 gitlab-ci-local --list 验证,如果被拒绝,就输出 INVALID <이유>(占位符为原因,取工具输出中有意义的第一行)并以 1 结束,通过则输出 VALID 并以 0 结束。在 .gitlab-ci.yml 中放入下面 files 中的正常配置并提交。评分器会用不存在的 stage、不被允许的 when、不存在的 needs 对象、YAML 语法错误的样本来确认。
  2. 让 ci-validate.sh 对通过了工具验证的文件,再应用两条团队策略。如果一个作业中同时有 rules 和 only/except,就输出 POLICY <잡> rules-with-only-except,如果 artifacts 中有 paths 却没有 expire_in,就输出 POLICY <잡> artifacts-without-expire_in(占位符均为作业),按作业名称顺序逐行输出,并以 2 结束。如果全部遵守策略,就是 VALID 和 0。隐藏作业(以点开头)和保留键(stages、variables、default、include、workflow 等)不是作业。
  3. 创建 /root/glci-gate/hooks/pre-commit 并提交,再把同一个文件复制到 .git/hooks/pre-commit 并赋予执行权限。钩子只在有已暂存的 .gitlab-ci.yml 时,才用 ci-validate.sh 验证该已暂存的内容(git show :.gitlab-ci.yml),如果不通过,就把原因输出到标准错误并阻止提交。评分器会在副本中尝试提交错误配置和违反策略的配置,并检查是否不会阻止与配置无关的文件的提交。
  4. 在 /root/glci-gate/broken.yml 中原样放入下面 files 中的错误配置,用 ci-validate.sh 逐个修复每次暴露出来的一个问题,创建 /root/glci-gate/fixed.yml。不要改变作业名称(build、unit、deploy)和各作业的 script。fixed.yml 必须是 VALID,unit 必须等待 build,deploy 必须在 main 上以手动审批(allow_failure false)创建。两个文件都要提交(钩子只看 .gitlab-ci.yml)。
  5. 在 .gitlab-ci.yml 中添加作业 ci-lint(stage .pre,bash ci-validate.sh .gitlab-ci.yml)并提交。运行后,ci-lint 必须以 VALID 通过,其余作业必须运行。评分器会在副本中用放入了策略违规(没有过期时间的产物)的配置来运行,检查 ci-lint 是否失败、build 是否不开始。
  6. 创建 /root/glci-gate/ci-validate-repo.sh <저장소>(占位符为仓库)。把仓库复制成临时副本(不含钩子)并提交,用 gitlab-ci-local --preview 获得展开了 include 的合并后的配置,如果失败,就输出 INVALID <이유>(占位符为原因)并以 1 结束,如果成功,就把合并后的配置交给 ci-validate.sh,并原样给出其结果(VALID 0、POLICY 2)。对这个仓库运行时必须是 VALID。评分器会用在被 include 的文件一侧放入了错误和策略违规的副本来确认。
  7. 创建 /root/glci-gate/ci-diff.sh <저장소> <옛커밋> <새커밋> [브랜치](占位符依次为仓库、旧提交、新提交与分支)。把两个提交分别取出到临时工作树,并给出 --variable CI_COMMIT_BRANCH=<브랜치>(占位符为分支),使其按分支(默认 main)的流水线来计算,用 gitlab-ci-local --list-csv-all 获得作业名称和 when,按作业名称顺序输出 ADDED <잡>、REMOVED <잡>、CHANGED <잡> <옛when>-><새when>(占位符依次为作业、旧 when 与新 when)。不要动原仓库的工作树和分支,并删除临时工作树。评分器会在副本中创建添加、删除作业并改变 when 的提交来确认。

参考

让工具来验证 schema 和引用

把 /root/glci-gate 创建为 Git 仓库(.gitignore 中写入 .gitlab-ci-local/),并创建 /root/glci-gate/ci-validate.sh <설정파일>(占位符为配置文件)。把这一个文件作为临时 Git 仓库的 .gitlab-ci.yml 放入,用 gitlab-ci-local --list 验证,如果被拒绝,就输出 INVALID <이유>(占位符为原因,取工具输出中有意义的第一行)并以 1 结束,通过则输出 VALID 并以 0 结束。在 .gitlab-ci.yml 中放入下面 files 中的正常配置并提交。评分器会用不存在的 stage、不被允许的 when、不存在的 needs 对象、YAML 语法错误的样本来确认。

gitlab-ci-local 会先用 GitLab 的配置 schema 验证,还会检查 stage、needs、extends 的引用是否真的存在。有问题时退出码不是 0。输出中会混有像远程仓库提示这样的噪声,请把它们过滤掉,只留下第一条原因。

用团队策略抓住工具放行的内容

让 ci-validate.sh 对通过了工具验证的文件,再应用两条团队策略。如果一个作业中同时有 rules 和 only/except,就输出 POLICY <잡> rules-with-only-except,如果 artifacts 中有 paths 却没有 expire_in,就输出 POLICY <잡> artifacts-without-expire_in(占位符均为作业),按作业名称顺序逐行输出,并以 2 结束。如果全部遵守策略,就是 VALID 和 0。隐藏作业(以点开头)和保留键(stages、variables、default、include、workflow 等)不是作业。

这个工具会放行混用了 rules 和 only 的作业,以及没有过期时间的产物(实测)。GitLab 服务器则会以 key may not be used with rules 拒绝前者(GitLab 源码中的配置验证)。不要把一个工具的判定当作门禁的全部,了解了差异,就用策略补上相应的部分。

错误的配置,从提交时就拦截

创建 /root/glci-gate/hooks/pre-commit 并提交,再把同一个文件复制到 .git/hooks/pre-commit 并赋予执行权限。钩子只在有已暂存的 .gitlab-ci.yml 时,才用 ci-validate.sh 验证该已暂存的内容(git show :.gitlab-ci.yml),如果不通过,就把原因输出到标准错误并阻止提交。评分器会在副本中尝试提交错误配置和违反策略的配置,并检查是否不会阻止与配置无关的文件的提交。

钩子必须查看的不是工作树中的文件,而是将要提交的内容(索引)——如果修改之后没有 add 就提交,即使工作树没问题,被提交的也是旧内容。.git/hooks 不会上传到仓库,所以要与团队共享,就要把它放在被跟踪的位置,并说明安装方法。

修复四处错误的配置

在 /root/glci-gate/broken.yml 中原样放入下面 files 中的错误配置,用 ci-validate.sh 逐个修复每次暴露出来的一个问题,创建 /root/glci-gate/fixed.yml。不要改变作业名称(build、unit、deploy)和各作业的 script。fixed.yml 必须是 VALID,unit 必须等待 build,deploy 必须在 main 上以手动审批(allow_failure false)创建。两个文件都要提交(钩子只看 .gitlab-ci.yml)。

工具会在第一个错误处停止,所以每修复一处就重新运行,查看下一个错误。其中混有 stage 名称拼写错误、指向不存在作业的 needs、不被允许的 when 值、rules 与 only 的混用、没有过期时间的产物。

流水线的第一个作业验证自己的配置

在 .gitlab-ci.yml 中添加作业 ci-lint(stage .pre,bash ci-validate.sh .gitlab-ci.yml)并提交。运行后,ci-lint 必须以 VALID 通过,其余作业必须运行。评分器会在副本中用放入了策略违规(没有过期时间的产物)的配置来运行,检查 ci-lint 是否失败、build 是否不开始。

钩子在本地可以被跳过(--no-verify),所以服务器一侧也必须有同样的检查。放在 .pre stage 中,它会先于其他所有作业运行,在用错误的配置消耗 Runner 时间之前就停下来。检查脚本就在仓库里,所以可以在作业中直接调用。

连同被 include 的文件一起合并验证

创建 /root/glci-gate/ci-validate-repo.sh <저장소>(占位符为仓库)。把仓库复制成临时副本(不含钩子)并提交,用 gitlab-ci-local --preview 获得展开了 include 的合并后的配置,如果失败,就输出 INVALID <이유>(占位符为原因)并以 1 结束,如果成功,就把合并后的配置交给 ci-validate.sh,并原样给出其结果(VALID 0、POLICY 2)。对这个仓库运行时必须是 VALID。评分器会用在被 include 的文件一侧放入了错误和策略违规的副本来确认。

只验证一个文件,看不到被 include 的文件中的错误——因为那个文件不在副本中。--preview 会给出展开了 include、extends、锚点之后的结果,所以对这个结果应用策略,分散在多个文件中的配置也能一次性检查。

向评审者展示流水线会发生什么变化

创建 /root/glci-gate/ci-diff.sh <저장소> <옛커밋> <새커밋> [브랜치](占位符依次为仓库、旧提交、新提交与分支)。把两个提交分别取出到临时工作树,并给出 --variable CI_COMMIT_BRANCH=<브랜치>(占位符为分支),使其按分支(默认 main)的流水线来计算,用 gitlab-ci-local --list-csv-all 获得作业名称和 when,按作业名称顺序输出 ADDED <잡>、REMOVED <잡>、CHANGED <잡> <옛when>-><새when>(占位符依次为作业、旧 when 与新 when)。不要动原仓库的工作树和分支,并删除临时工作树。评分器会在副本中创建添加、删除作业并改变 when 的提交来确认。

当 include、extends、rules 混在一起时,配置文件的 diff 无法告诉你实际改变了什么。如果比较流水线将会生成的作业列表,“这次合并让生产部署变成了自动”这样的变化,就会用一行暴露出来。可以用 git worktree add --detach 把提交取出到另一个目录,但在那种状态下没有分支,所以依赖分支条件的 rules 都会被排除,因此要把分支作为变量传入。这个警告用 --ignore-predefined-vars 关闭。