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

GitLab CI/CD

三个服务只改了一个,却全部重新构建

在 TT Lab 中继续学习

目标

用 parallel:matrix 按组合数量增加作业,处理例外组合和等待特定组合,然后把 monorepo 拆分为按服务划分的子流水线,以及根据仓库结构生成的动态流水线,并用 gitlab-ci-local 运行。

为什么重要

配置变大的方式,通常是复制粘贴。每增加一个 Python 版本就复制一个作业,每增加一个服务就往父文件里贴一个块,这样下去,所有团队的规则都堆在一个文件里,每一次变更都会重新构建所有服务。matrix 把组合压缩为一个块,子流水线把服务的配置送回该服务的目录,changes 和动态生成则让只有改变的部分才运行。相应地,必须了解作业名称、产物、变量是如何流动的,才能正确设置 needs 和规则。

步骤

  1. 把 /root/glci-mono 创建为 Git 仓库(.gitignore 中写入 .gitlab-ci-local/),在 .gitlab-ci.yml 中放入 stages [build, package, trigger, generate, dynamic] 和作业 build(stage build)。用 parallel: matrix 创建 PY 为 "3.11"、"3.12",OS 为 linux、alpine 的组合,脚本把 py=<PY> os=<OS> 写入 dist/py$PY-$OS.txt,并用 artifacts 上传 dist/。提交并运行。作业名称必须以 build: [3.11,linux] 的形式生成四个。
  2. 给 build 加上 rules,使 OS 为 alpine 且 PY 为 "3.11" 的组合为 when: never,其余为 when: on_success。提交并运行,应该只运行三个作业。
  3. 添加作业 package-linux(stage package),用 needs 只指向 build 中 PY: "3.12"、OS: linux 这一个组合(needs:parallel:matrix),脚本用 ls dist。提交并运行,package-linux 日志中必须只看到 py3.12-linux.txt 一个文件。
  4. 在 /root/glci-mono/services/api/ci.yml 中放入作业 api-unit(echo "api unit svc=$SVC"),并在父文件中添加作业 trigger-api(stage trigger),放入变量 SVC: api 和 trigger: include: services/api/ci.yml。提交并运行,子流水线中 api-unit 的日志必须是 api unit svc=api。
  5. 在 services/web/ci.yml 中放入作业 web-unit(echo "web unit svc=$SVC"),并在父文件中添加 trigger-web(SVC web)。两个 trigger 作业分别用 rules: changes: 设置 services/api/**/* 和 services/web/**/*。在 /root/glci-mono-origin.git 中创建 bare 仓库并注册为 origin,把提交好的 main push 之后,运行 git remote set-head origin main。评分器会在副本中创建只修改了 web 的分支,检查列表中是否只有 trigger-web。
  6. 让 /root/glci-mono/scripts/generate.sh 把为 services/ 之下的每个目录包含作业 lint-<이름>(echo "lint <이름>",占位符为服务名称)的 YAML 输出到标准输出。在父文件中,作业 generate(stage generate)把该输出保存为 generated.yml 并用 artifacts 上传,作业 run-generated(stage dynamic、needs: [generate])通过 trigger: include: - artifact: generated.yml, job: generate 把它作为子流水线运行。提交并 push 后运行,lint-api 和 lint-web 必须运行。
  7. 在 /root/glci-mono/services/billing/ 中不创建 ci.yml,只创建 README.md(内容任意)并提交、push。不要修改父级 .gitlab-ci.yml。运行后,动态子流水线中必须新生成并运行 lint-billing。评分器也会在副本中再创建一个服务目录来确认。

参考

两个 Python、两个 OS,把四个作业放进一个块

把 /root/glci-mono 创建为 Git 仓库(.gitignore 中写入 .gitlab-ci-local/),在 .gitlab-ci.yml 中放入 stages [build, package, trigger, generate, dynamic] 和作业 build(stage build)。用 parallel: matrix 创建 PY 为 "3.11"、"3.12",OS 为 linux、alpine 的组合,脚本把 py=<PY> os=<OS> 写入 dist/py$PY-$OS.txt,并用 artifacts 上传 dist/。提交并运行。作业名称必须以 build: [3.11,linux] 的形式生成四个。

在 matrix 的一个条目中写入多个变量,所有组合都会成为作业。版本值必须用引号括起来,才不会出现 3.10 变成 3.1 的事。列表用 gitlab-ci-local --list-csv-all 查看。

只去掉一个不支持的组合

给 build 加上 rules,使 OS 为 alpine 且 PY 为 "3.11" 的组合为 when: never,其余为 when: on_success。提交并运行,应该只运行三个作业。

matrix 变量在 rules:if 中通常可以像 CI/CD 变量一样使用。为了去掉一个组合而把 matrix 拆成两个条目,每次增加时都要重新计算列表,而如果用规则去掉,只要写出例外即可。

只等待并获取一个组合的产物

添加作业 package-linux(stage package),用 needs 只指向 build 中 PY: "3.12"、OS: linux 这一个组合(needs:parallel:matrix),脚本用 ls dist。提交并运行,package-linux 日志中必须只看到 py3.12-linux.txt 一个文件。

不要在 needs 中把由 matrix 生成的作业原样写成名称(build: [3.12,linux]),而是用 parallel:matrix 写出变量值来指向它。只会获取所指向的组合的产物。

服务配置放在服务目录中

在 /root/glci-mono/services/api/ci.yml 中放入作业 api-unit(echo "api unit svc=$SVC"),并在父文件中添加作业 trigger-api(stage trigger),放入变量 SVC: api 和 trigger: include: services/api/ci.yml。提交并运行,子流水线中 api-unit 的日志必须是 api unit svc=api。

trigger 作业不是执行脚本,而是创建另一条流水线。用 include 指向的文件成为子流水线的完整配置,trigger 作业的 variables 会传给子流水线。这种结构让服务团队只需修改自己目录中的文件。

只启动有改动的服务的流水线

在 services/web/ci.yml 中放入作业 web-unit(echo "web unit svc=$SVC"),并在父文件中添加 trigger-web(SVC web)。两个 trigger 作业分别用 rules: changes: 设置 services/api/**/* 和 services/web/**/*。在 /root/glci-mono-origin.git 中创建 bare 仓库并注册为 origin,把提交好的 main push 之后,运行 git remote set-head origin main。评分器会在副本中创建只修改了 web 的分支,检查列表中是否只有 trigger-web。

在 monorepo 中,如果每次都构建所有服务,流水线时间会按服务数量增加。changes 看的是与远程默认分支相比的改动(以 gitlab-ci-local 为准),所以必须有远程。

读取仓库结构来创建子流水线

让 /root/glci-mono/scripts/generate.sh 把为 services/ 之下的每个目录包含作业 lint-<이름>(echo "lint <이름>",占位符为服务名称)的 YAML 输出到标准输出。在父文件中,作业 generate(stage generate)把该输出保存为 generated.yml 并用 artifacts 上传,作业 run-generated(stage dynamic、needs: [generate])通过 trigger: include: - artifact: generated.yml, job: generate 把它作为子流水线运行。提交并 push 后运行,lint-api 和 lint-web 必须运行。

作业可以在运行中生成子流水线的配置。与其每增加一个服务就修改父级配置,不如把规则(目录 = 作业)放在脚本中。官方文档中有一条限制:生成的配置内部的 include 中不能使用 CI/CD 变量。

再增加一个服务,父级配置也保持不变

在 /root/glci-mono/services/billing/ 中不创建 ci.yml,只创建 README.md(内容任意)并提交、push。不要修改父级 .gitlab-ci.yml。运行后,动态子流水线中必须新生成并运行 lint-billing。评分器也会在副本中再创建一个服务目录来确认。

由于动态流水线的规则在仓库结构中,所以仅仅是出现目录,就会生成作业。反过来,如果在 services 之下放了不是服务的目录,它也会变成作业,所以必须把这条规则写成文档留下来。