LabHub
学习 学习路径 课程

CI/CD 流水线

用 shell 做一条三段流水线

在 LabHub 中继续学习

本实验在真正的 VM 中运行

这台机器不是 Pod,而是 KubeVirt 启动的虚拟机。它拥有独立运行的 Linux 内核, systemd 会真正管理服务,docker 也不是模拟品,而是真正的 Docker 引擎。 通过 docker run 启动的容器会成为实际进程,docker execdocker logs 也都能按原样工作。

过去,本实验运行在 Pod 内。由于该环境移除了全部内核权限, 无法执行启动容器的步骤,因此只能学习手动解开镜像归档的变通方法。 现在不再需要绕行。

有两点需要提前了解。

目标

仅使用 shell 脚本构建 build → test → package 三阶段流水线,并亲手生成以源代码哈希命名的不可变产物及镜像标签。

为什么这很重要

CI 工具每隔几年就会变化,但其原理不会改变。阶段具有固定顺序;前一阶段失败时,后一阶段不得运行;仅看产物名称,就应能知道它由什么生成。标签是可以被人移动的名称,因此可能像 tj-actions/changed-files 事件那样被整体重新指向,所以部署标识符必须使用提交 SHA 之类不可变的值。如果生产环境里只有 latest,就无法追踪部署了什么,也没有明确的回滚目标。本实验将在不依赖任何厂商 UI 的情况下,手动复现这些原理。

步骤

  1. 创建 /root/ci1/pipeline.sh,并使用 chmod +x 赋予执行权限。在脚本开头加入 set -euo pipefail(评分会分别检查 set -e 系列、set -upipefail)。创建构建目标目录 /root/ci1/src/,并放入至少 2 个文件。
  2. 流水线在各阶段开始时依次各输出一次 ::stage build::stage test::stage package,结束时输出 SUCCESS。将一次成功执行的输出保存到 /root/ci1/run1.log
  3. package 阶段应在 /root/ci1/out/app-<해시>.tar.gz 中准确生成 1 个文件。<해시> 是通过 cat /root/ci1/src/* | sha256sum | cut -c1-12 计算得到的 12 个字符。该文件必须是真正的 gzip tar,能够用 tar tzf 打开。
  4. 使用相同源代码再次运行时,应复用已有产物。此时输出 ::artifact-exists,并将本次执行日志保存到 /root/ci1/run2.log/root/ci1/out 中的 app-*.tar.gz 仍应只有 1 个。
  5. 创建 /root/ci1/out/build-info.json。字段为 source_hash(上述 12 位哈希)、status(字符串 success)、stages(数字 3)、created_at(时间字符串)。
  6. 创建 /root/ci1/tests/fail-flag 文件,使 test 阶段失败,然后重新运行流水线。将输出保存到 /root/ci1/fail.log,退出码保存到 /root/ci1/fail-exit.txt(必须不为 0)。fail.log 中应有 ::stage test,但不应有 ::stage packagepipeline.sh 中不要使用 || true。检查完成后删除 /root/ci1/tests/fail-flag,恢复原状。
  7. 构建包含产物的镜像,标签为 labhub/ci:<해시12>。基础镜像只能从 Pod 中预先存在的 alpine:3.20busybox:1.36python:3.12-alpinenginx:1.27-alpine 中选择。
  8. 使用 docker tag 为同一个镜像额外添加 labhub/ci:latest。然后在 build-info.json 中加入 tags 数组,同时记录 labhub/ci:<해시12>labhub/ci:latest 两个值(至少 2 个)。

参考

流水线骨架与安全选项

创建 /root/ci1/pipeline.sh,并使用 chmod +x 赋予执行权限。在脚本开头加入 set -euo pipefail(评分会分别检查 set -e 系列、set -upipefail)。创建构建目标目录 /root/ci1/src/,并放入至少 2 个文件。

创建 /root/ci1/pipeline.sh,不要忘记 chmod +x。在开头加入 set -euo pipefail。评分会分别查找 -e 系列、-u 和 pipefail 三项。如果失败阶段不能让流水线停止,那就不是门禁,只是日志生成器。构建目标是在 /root/ci1/src/ 下放置至少 2 个文件。

build → test → package 顺序

流水线在各阶段开始时依次各输出一次 ::stage build::stage test::stage package,结束时输出 SUCCESS。将一次成功执行的输出保存到 /root/ci1/run1.log

每个阶段开始时只输出一次 ::stage build::stage test::stage package,最后输出 SUCCESS。将成功执行的输出保存到 /root/ci1/run1.log。重复输出标记会破坏顺序检查。

使用源代码哈希命名

package 阶段应在 /root/ci1/out/app-<해시>.tar.gz 中准确生成 1 个文件。<해시> 是通过 cat /root/ci1/src/* | sha256sum | cut -c1-12 计算得到的 12 个字符。该文件必须是真正的 gzip tar,能够用 tar tzf 打开。

哈希必须与评分使用完全相同的方式计算:cat /root/ci1/src/* | sha256sum | cut -c1-12。最终只能有一个 /root/ci1/out/app-<해시>.tar.gz,且必须能用 tar tzf 打开。名称中应体现生成它的材料,以便日后追溯。

相同输入不重复生成

使用相同源代码再次运行时,应复用已有产物。此时输出 ::artifact-exists,并将本次执行日志保存到 /root/ci1/run2.log/root/ci1/out 中的 app-*.tar.gz 仍应只有 1 个。

package 阶段发现目标文件已存在时,输出 ::artifact-exists 并跳过。第二次执行日志保存到 /root/ci1/run2.log。如果产物数量增加,说明相同输入产生了不同输出。

记录构建元数据

创建 /root/ci1/out/build-info.json。字段为 source_hash(上述 12 位哈希)、status(字符串 success)、stages(数字 3)、created_at(时间字符串)。

/root/ci1/out/build-info.json 中加入 source_hash、status、stages、created_at。status 是字符串 success,stages 是数字 3。为减少引号错误,请使用 jq -n --arg 创建。

不要吞掉失败,立即停止

创建 /root/ci1/tests/fail-flag 文件,使 test 阶段失败,然后重新运行流水线。将输出保存到 /root/ci1/fail.log,退出码保存到 /root/ci1/fail-exit.txt(必须不为 0)。fail.log 中应有 ::stage test,但不应有 ::stage packagepipeline.sh 中不要使用 || true。检查完成后删除 /root/ci1/tests/fail-flag,恢复原状。

创建 /root/ci1/tests/fail-flag 使 test 失败,将输出保存到 /root/ci1/fail.log,退出码保存到 /root/ci1/fail-exit.txt。package 阶段不得运行。pipeline.sh 中不能有 || true,检查完成后务必删除 fail-flag 恢复原状。

使用每次提交变化的标签构建镜像

构建包含产物的镜像,标签为 labhub/ci:<해시12>。基础镜像只能从 Pod 中预先存在的 alpine:3.20busybox:1.36python:3.12-alpinenginx:1.27-alpine 中选择。

使用 labhub/ci:<해시12> 构建。环境离线,无法 pull,因此基础镜像只能从已有的 alpine:3.20、busybox:1.36、python:3.12-alpine、nginx:1.27-alpine 中选择。podman 显示时会加上 localhost/ 前缀,但评分会去掉后再比较。

latest 是别名,哈希才是标识符

使用 docker tag 为同一个镜像额外添加 labhub/ci:latest。然后在 build-info.json 中加入 tags 数组,同时记录 labhub/ci:<해시12>labhub/ci:latest 两个值(至少 2 个)。

使用 docker tag 为同一个镜像再添加一个名称。如果重新构建 latest,镜像 ID 会不同,从而导致失败。请在 build-info.json 的 tags 数组中记录两个标签。