放进缓存的构建产物在部署作业里不见了
目标
把流水线运行多次,通过日志看到缓存命中、未命中和失效,确认产物的排除、接收方的限制、dotenv 值的传递,然后创建产物检查脚本。
为什么重要
产物如果没有,后面的作业就应该失败才对,而缓存即使没有,所有作业也应该能运行才对。如果把两者混淆,在缓存为空的那天,部署作业就会悄悄发出一个空目录,或者没有过期时间的产物会把仓库容量填满。缓存的全部取决于用什么生成键,如果把键设为固定字符串,就会带着过时的依赖到处跑,如果让消费者也上传,污染就会蔓延。产物没有办法让人每次都确认“上传了什么”,所以最好把检查自动化。
步骤
- 把
/root/glci-transfer创建为 Git 仓库(.gitignore 中写入.gitlab-ci-local/),在.gitlab-ci.yml中放入 stages[build, test, deploy]和两个作业。package(build)创建dist/app.tgz和dist/app.js.map,用 artifacts 上传dist/,但排除dist/*.map,并设置expire_in: 1 week。ship(deploy)执行test -f dist/app.tgz && echo has-tgz、test -e dist/app.js.map && echo has-map || echo no-map、test -d public && echo has-public || echo no-public。运行后,ship 日志必须是 has-tgz、no-map、no-public。 - 添加作业
docs(build),创建public/index.html,并用 artifacts 上传public/(expire_in 2 days)。给ship设置dependencies: [package],使其只接收 package 的产物。运行后,ship 日志仍必须是 has-tgz、no-map、no-public。 - 在
requirements.txt中放入一行requests==2.32.3,并加入提交对象。作业deps(build)把 cache 设为key: files: [requirements.txt]、paths: [.deps/]、policy: pull-push,并在mkdir -p .deps之后,如果有.deps/installed就输出cache-hit,没有就输出cache-miss,并创建该文件。连续运行两次,必须是先 miss 后 hit。评分器也会在副本中修改 requirements.txt,检查是否再次出现 miss。 - 再添加两个作业。
unit(test)把与 deps 相同的键和路径的 cache 设为policy: pull,如果有.deps/installed就输出unit-hit,没有就输出unit-miss,然后创建.deps/junk。verify-cache(deploy)也以 pull 接收同一个缓存,如果有.deps/junk就输出junk-saved,没有就输出junk-not-saved。运行后,必须出现 unit-hit 和 junk-not-saved。 - 把 deps 的 cache 改成两个列表项。第一个是在
key: files: [requirements.txt]上加了prefix: py的.deps/(pull-push),第二个是key: tools-$CI_COMMIT_REF_SLUG的.tools/。在 deps 脚本中,如果有.tools/lint就输出tools-hit,没有就输出tools-miss,并加上创建它的那一行。unit 和 verify-cache 的键中也要放置同样的 prefix。运行两次,第二次运行时两个缓存都必须是 hit。 - 让作业
version(build)把一行VERSION=1.4.2-${CI_COMMIT_SHORT_SHA}写入build.env,并用artifacts: reports: dotenv: build.env上传。作业announce(deploy)以needs: [version]执行echo "version=$VERSION"。运行后,announce 日志必须是version=1.4.2-<짧은 커밋 해시>(占位符为短提交哈希)。 - 创建
/root/glci-transfer/artifact-audit.sh <저장소>(占位符为仓库)。把仓库复制成临时副本,提交所有文件并运行流水线之后,如果已上传的产物(.gitlab-ci-local/artifacts之下)中有*.map、*.env、*.pem或超过 1MiB 的文件,就输出一行BAD <잡>/<경로> …(占位符依次为作业与路径,以空格分隔、排序)并以 3 结束;没有则输出OK并以 0 结束;流水线失败则输出ERROR并以 1 结束。报告(.gitlab-ci-reports/之下)不纳入检查。不要在原仓库中留下任何东西。评分器会用这个仓库(OK)和放入了会泄漏到产物中的文件的副本来确认。
参考
- 这台 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/cache/<키>(占位符为缓存键)中。“因为 Runner 更换而没有缓存的情况”,只要删除这个目录再重新运行就行。artifacts:when: on_failure 在 gitlab-ci-local 中不会重现(实测)。 - 常见错误:把消费者作业设为 pull-push,导致每次都重新上传缓存。常见错误:想在后面作业的 rules 中使用通过 dotenv 传递的值。
- Caching in GitLab CI/CD · Job artifacts · artifacts:reports(dotenv) · CI/CD YAML syntax reference
产物只上传需要的
把 /root/glci-transfer 创建为 Git 仓库(.gitignore 中写入 .gitlab-ci-local/),在 .gitlab-ci.yml 中放入 stages [build, test, deploy] 和两个作业。package(build)创建 dist/app.tgz 和 dist/app.js.map,用 artifacts 上传 dist/,但排除 dist/*.map,并设置 expire_in: 1 week。ship(deploy)执行 test -f dist/app.tgz && echo has-tgz、test -e dist/app.js.map && echo has-map || echo no-map、test -d public && echo has-public || echo no-public。运行后,ship 日志必须是 has-tgz、no-map、no-public。
artifacts:exclude 会从用 paths 选中的文件中去掉不上传的文件。它可以防止像 source map、调试符号这样体积大、部署又不需要的文件在每条流水线中堆积。expire_in 是何时删除,实际的删除由服务器执行。
部署作业不接收用不到的文档
添加作业 docs(build),创建 public/index.html,并用 artifacts 上传 public/(expire_in 2 days)。给 ship 设置 dependencies: [package],使其只接收 package 的产物。运行后,ship 日志仍必须是 has-tgz、no-map、no-public。
既没有 needs 也没有 dependencies 的作业,会下载前面 stage 的全部产物。这是部署作业因为连测试报告和文档也要接收而变慢的常见原因。dependencies 保持 stage 顺序不变,只挑选要接收的产物。
锁文件一变,缓存也随之改变
在 requirements.txt 中放入一行 requests==2.32.3,并加入提交对象。作业 deps(build)把 cache 设为 key: files: [requirements.txt]、paths: [.deps/]、policy: pull-push,并在 mkdir -p .deps 之后,如果有 .deps/installed 就输出 cache-hit,没有就输出 cache-miss,并创建该文件。连续运行两次,必须是先 miss 后 hit。评分器也会在副本中修改 requirements.txt,检查是否再次出现 miss。
如果给缓存键指定文件列表,那些文件内容的哈希就成为键。依赖列表一变,键自然就变了,不会带着过时的缓存到处跑。写成即使没有缓存,作业也能一直运行到结束,是缓存的契约。
消费者只接收缓存
再添加两个作业。unit(test)把与 deps 相同的键和路径的 cache 设为 policy: pull,如果有 .deps/installed 就输出 unit-hit,没有就输出 unit-miss,然后创建 .deps/junk。verify-cache(deploy)也以 pull 接收同一个缓存,如果有 .deps/junk 就输出 junk-saved,没有就输出 junk-not-saved。运行后,必须出现 unit-hit 和 junk-not-saved。
pull 只在开始时接收,结束时不上传。如果只让创建缓存的一个作业使用 pull-push,消费者就省去了把同样内容再次压缩上传的时间,消费者污染缓存的事也就被堵住了。
寿命不同的缓存要分开键
把 deps 的 cache 改成两个列表项。第一个是在 key: files: [requirements.txt] 上加了 prefix: py 的 .deps/(pull-push),第二个是 key: tools-$CI_COMMIT_REF_SLUG 的 .tools/。在 deps 脚本中,如果有 .tools/lint 就输出 tools-hit,没有就输出 tools-miss,并加上创建它的那一行。unit 和 verify-cache 的键中也要放置同样的 prefix。运行两次,第二次运行时两个缓存都必须是 hit。
一个作业可以设置多个缓存。如果把像依赖那样随锁文件变化的缓存,与想按分支分别保存的工具缓存混在同一个键里,一边的变化就会让另一边也失效。prefix 可以避免与使用相同文件哈希的其他缓存名称重叠。
把前面作业计算出的值传给后面的作业
让作业 version(build)把一行 VERSION=1.4.2-${CI_COMMIT_SHORT_SHA} 写入 build.env,并用 artifacts: reports: dotenv: build.env 上传。作业 announce(deploy)以 needs: [version] 执行 echo "version=$VERSION"。运行后,announce 日志必须是 version=1.4.2-<짧은 커밋 해시>(占位符为短提交哈希)。
变量是在创建流水线时确定的,所以运行中计算出的值,通常的方法无法传给后面的作业。dotenv 报告会把作为产物上传的 KEY=VALUE 文件,作为环境变量放进后面的作业。rules 已经评估完毕,看不到这个值。
找出不该进入产物的文件
创建 /root/glci-transfer/artifact-audit.sh <저장소>(占位符为仓库)。把仓库复制成临时副本,提交所有文件并运行流水线之后,如果已上传的产物(.gitlab-ci-local/artifacts 之下)中有 *.map、*.env、*.pem 或超过 1MiB 的文件,就输出一行 BAD <잡>/<경로> …(占位符依次为作业与路径,以空格分隔、排序)并以 3 结束;没有则输出 OK 并以 0 结束;流水线失败则输出 ERROR 并以 1 结束。报告(.gitlab-ci-reports/ 之下)不纳入检查。不要在原仓库中留下任何东西。评分器会用这个仓库(OK)和放入了会泄漏到产物中的文件的副本来确认。
gitlab-ci-local 会把每个作业上传的产物放在 .gitlab-ci-local/artifacts/<잡이름>/(占位符为作业名称)之下。dotenv 报告是有意传递的值,所以 gitlab-ci-local 把它单独放在 .gitlab-ci-reports/ 之下,检查时必须排除这个位置,正常配置才不会变成 BAD。find 的 -size 可以用 k 为单位来计算。