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

CI/CD 流水线

用 shell 做一条三段流水线

在 TT Lab 中继续学习

本实验在真正的 VM 中运行

这台机器不是 Pod,而是 KubeVirt 启动的虚拟机。它拥有独立运行的 Linux 内核, systemd 会真正管理服务,docker 也不是模拟品,而是真正的 Docker 引擎。 通过 docker run 启动的容器会成为实际进程,docker exec 和 docker 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 -u 和 pipefail)。创建构建目标目录 /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 package。pipeline.sh 中不要使用 || true。检查完成后删除 /root/ci1/tests/fail-flag,恢复原状。
  7. 构建包含产物的镜像,标签为 labhub/ci:<해시12>。基础镜像只能从 Pod 中预先存在的 alpine:3.20、busybox:1.36、python:3.12-alpine、nginx: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 -u 和 pipefail)。创建构建目标目录 /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 package。pipeline.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.20、busybox:1.36、python:3.12-alpine、nginx: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 数组中记录两个标签。