用配置造出流水线的执行模型
目标
通过八个步骤逐步构建一份 .gitlab-ci.yml,亲手实现 GitLab CI 的执行模型:阶段顺序、needs 构成的 DAG、rules 决定是否创建作业,以及 artifacts 与 cache 的区别。最后自行编写计算该模型的解析器。
为什么重要
此 Pod 中没有 GitLab 服务器,也没有 GitLab Runner,因此本实验不会假装流水线真的在运行。它只涵盖没有 Runner 也能如实学习的内容:配置语言及其执行模型。实际工作中,流水线受阻通常不是因为不知道命令,而是无法解释为何某个作业未创建、为何另一个作业仍在等待;答案都来自该文件的结构。评分也不会肉眼扫描文件,而会用 YAML 解析器读取并检查结构,因为缩进错一格就可能让作业挂到错误键下,人眼看来正常,实际却无法运行。
步骤
- 创建
/root/glci/.gitlab-ci.yml。顶层stages按顺序包含build、test、deploy;创建build-app作业,设置stage: build,并至少写一条script。 - 添加作业,填满三个阶段。
lint和unit-test使用stage: test,deploy-staging使用stage: deploy。所有作业都必须有script,所有stage值都必须位于stages列表中。 - 创建隐藏作业
.python-base,加入image和before_script,让build-app、lint、unit-test通过extends: .python-base继承。继承后的作业不要重复写image。在顶层default中设置默认image。 - 添加
needs构建 DAG。lint使用needs: [];unit-test的needs包含build-app;deploy-staging的needs包含unit-test。build-app不设置needs。 - 为
deploy-staging添加rules:当$CI_COMMIT_BRANCH == "main"时使用when: on_success,最后用无条件when: never收尾。新增deploy-prod作业,使用stage: deploy,在needs中加入deploy-staging;相同分支条件下设置when: manual和allow_failure: false,同样以无条件when: never收尾。不要使用only和except。 - 为
build-app添加artifacts,写明paths和expire_in。将unit-test的needs改为长格式,设置job: build-app、artifacts: true;将deploy-staging的needs设置为artifacts: false。 - 创建有内容的
/root/glci/requirements.txt。为build-app和unit-test添加cache,其key使用files指向requirements.txt。build-app使用policy: pull-push,unit-test使用policy: pull。cache.paths不得与artifacts.paths重叠。 - 创建
/root/glci/plan.py。读取参数指定的配置文件,将{"stages": [...], "waves": [[...], ...]}作为 JSON 输出到标准输出。存在needs时只等待该列表,否则等待前一阶段的全部作业(默认stage为test)。以点开头的键和保留键(stages、variables、default、include、workflow)不是作业。每个 wave 按名称排序。如果没有任何作业可启动,输出cycle并以非 0 退出码结束。最后将针对自身配置运行的输出保存到/root/glci/plan.json。
参考
- 养成用解析器检查的习惯,事故会减少一半:运行
python3 -c "import yaml,sys;print(list(yaml.safe_load(open(sys.argv[1]))))" /root/glci/.gitlab-ci.yml查看顶层键列表,可立刻发现作业是否消失。 needs: []与完全没有needs键含义正好相反:前者不等待任何内容,后者等待之前阶段的全部作业。rules只应用第一条匹配项并停止。无条件when: never必须放在列表末尾。- 常见错误:隐藏片段名漏掉点而变成可执行作业;用
extends继承后仍重复写image;缓存路径与产物路径相同;解析器把诊断消息写到标准输出而破坏 JSON。
阶段顺序与第一个作业
创建 /root/glci/.gitlab-ci.yml。顶层 stages 按顺序包含 build、test、deploy;创建 build-app 作业,设置 stage: build,并至少写一条 script。
创建 /root/glci/.gitlab-ci.yml,在顶层放置 stages 列表,其顺序就是默认执行顺序。在下方添加顶层键 build-app,将 stage 和 script 缩进到其中。评分使用 YAML 解析器而非 grep,因此缩进错一格就会使作业成为其他键的子项并失败。
填满三个阶段
添加作业,填满三个阶段。lint 和 unit-test 使用 stage: test,deploy-staging 使用 stage: deploy。所有作业都必须有 script,所有 stage 值都必须位于 stages 列表中。
把 lint 和 unit-test 加入 test 阶段,把 deploy-staging 加入 deploy 阶段。同一阶段的两个作业会并行运行。若使用了 stages 列表中不存在的名称作为 stage,GitLab 会拒绝整个配置,因此请检查拼写。每个作业都必须有 script。
用隐藏作业和 extends 消除重复
创建隐藏作业 .python-base,加入 image 和 before_script,让 build-app、lint、unit-test 通过 extends: .python-base 继承。继承后的作业不要重复写 image。在顶层 default 中设置默认 image。
名称以点开头的键是不会执行的配置片段。在 .python-base 中加入 image 和 before_script,让 build-app、lint、unit-test 通过 extends 继承。继承后不要在作业中重复写 image。为不使用该片段的作业,在顶层 default 中设置默认镜像。
用 needs 跨越阶段壁垒
添加 needs 构建 DAG。lint 使用 needs: [];unit-test 的 needs 包含 build-app;deploy-staging 的 needs 包含 unit-test。build-app 不设置 needs。
让 unit-test 通过 needs 只等待 build-app,让 deploy-staging 只等待 unit-test。为 lint 设置 needs: []——没有该键和空列表含义相反,只有空列表才能不等待任何前置阶段、立即启动。build-app 不设置 needs。
用 rules 设置分支条件与手动批准
为 deploy-staging 添加 rules:当 $CI_COMMIT_BRANCH == "main" 时使用 when: on_success,最后用无条件 when: never 收尾。新增 deploy-prod 作业,使用 stage: deploy,在 needs 中加入 deploy-staging;相同分支条件下设置 when: manual 和 allow_failure: false,同样以无条件 when: never 收尾。不要使用 only 和 except。
deploy-staging 在 $CI_COMMIT_BRANCH == "main" 时使用 when: on_success;deploy-prod 在相同条件下使用 when: manual。两个作业最后都必须是无条件 when: never,因为 rules 只采用第一条匹配项;无条件项放在中间会使其后规则全部失效。不要使用 only/except。deploy-prod 属于 deploy 阶段并等待 deploy-staging。
定义 artifacts 及其接收者
为 build-app 添加 artifacts,写明 paths 和 expire_in。将 unit-test 的 needs 改为长格式,设置 job: build-app、artifacts: true;将 deploy-staging 的 needs 设置为 artifacts: false。
在 build-app 的 artifacts 中用 paths 指定要传递的目录,并设置 expire_in。随后把 needs 改成长格式(- job: 이름 / artifacts: true|false):真正使用产物的 unit-test 设为 true,仅等待顺序的 deploy-staging 设为 false。让无关作业下载产物会悄悄拖慢流水线。
用锁文件哈希生成 cache 键
创建有内容的 /root/glci/requirements.txt。为 build-app 和 unit-test 添加 cache,其 key 使用 files 指向 requirements.txt。build-app 使用 policy: pull-push,unit-test 使用 policy: pull。cache.paths 不得与 artifacts.paths 重叠。
先创建 /root/glci/requirements.txt,缓存键必须有真实文件可供哈希。然后为 build-app 和 unit-test 添加 cache,key 不用固定字符串,而用 files 形式指向该文件。创建缓存的 build-app 使用 policy: pull-push,只读取的 unit-test 使用 policy: pull。cache.paths 不得与 artifacts.paths 重叠。
用流水线解析器计算执行顺序
创建 /root/glci/plan.py。读取参数指定的配置文件,将 {"stages": [...], "waves": [[...], ...]} 作为 JSON 输出到标准输出。存在 needs 时只等待该列表,否则等待前一阶段的全部作业(默认 stage 为 test)。以点开头的键和保留键(stages、variables、default、include、workflow)不是作业。每个 wave 按名称排序。如果没有任何作业可启动,输出 cycle 并以非 0 退出码结束。最后将针对自身配置运行的输出保存到 /root/glci/plan.json。
以 /root/glci/plan.py <설정파일> 调用时,标准输出必须为 {"stages": [...], "waves": [[...], ...]}。wave 规则有三项:存在 needs 时只等待该列表;否则等待前一阶段所有作业(默认 stage 为 test);以点开头的键和保留键不是作业。每个 wave 按名称排序。若陷入无人可启动状态,则输出 cycle 并以非 0 退出码结束。诊断信息写入标准错误,才能让标准输出保持 JSON。最后将针对自身配置运行的结果保存为 /root/glci/plan.json。