三个服务只改了一个,却全部重新构建
目标
用 parallel:matrix 按组合数量增加作业,处理例外组合和等待特定组合,然后把 monorepo 拆分为按服务划分的子流水线,以及根据仓库结构生成的动态流水线,并用 gitlab-ci-local 运行。
为什么重要
配置变大的方式,通常是复制粘贴。每增加一个 Python 版本就复制一个作业,每增加一个服务就往父文件里贴一个块,这样下去,所有团队的规则都堆在一个文件里,每一次变更都会重新构建所有服务。matrix 把组合压缩为一个块,子流水线把服务的配置送回该服务的目录,changes 和动态生成则让只有改变的部分才运行。相应地,必须了解作业名称、产物、变量是如何流动的,才能正确设置 needs 和规则。
步骤
- 把
/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]的形式生成四个。 - 给 build 加上 rules,使
OS为alpine且PY为"3.11"的组合为when: never,其余为when: on_success。提交并运行,应该只运行三个作业。 - 添加作业
package-linux(stage package),用 needs 只指向 build 中PY: "3.12"、OS: linux这一个组合(needs:parallel:matrix),脚本用ls dist。提交并运行,package-linux 日志中必须只看到py3.12-linux.txt一个文件。 - 在
/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。 - 在
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。 - 让
/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 必须运行。 - 在
/root/glci-mono/services/billing/中不创建ci.yml,只创建README.md(内容任意)并提交、push。不要修改父级.gitlab-ci.yml。运行后,动态子流水线中必须新生成并运行lint-billing。评分器也会在副本中再创建一个服务目录来确认。
参考
- 这台 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 的差异(实测):子流水线是实验功能,不遵循 trigger:strategy,所以即使子流水线失败,trigger 作业也会以成功结束。
--variable不会传给子流水线,只有 trigger 作业的 variables 会传递。 - 子流水线作业的日志也会保留在
.gitlab-ci-local/output/<잡이름>.log(占位符为作业名称)中。 - parallel:matrix(Job control) · Downstream pipelines · needs · Specify when jobs run with rules
两个 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 之下放了不是服务的目录,它也会变成作业,所以必须把这条规则写成文档留下来。