用 shell 做一条三段流水线
本实验在真正的 VM 中运行
这台机器不是 Pod,而是 KubeVirt 启动的虚拟机。它拥有独立运行的 Linux 内核,
systemd 会真正管理服务,docker 也不是模拟品,而是真正的 Docker 引擎。
通过 docker run 启动的容器会成为实际进程,docker exec 和 docker logs
也都能按原样工作。
过去,本实验运行在 Pod 内。由于该环境移除了全部内核权限, 无法执行启动容器的步骤,因此只能学习手动解开镜像归档的变通方法。 现在不再需要绕行。
有两点需要提前了解。
- 首次启动需要一分多钟。 因为 VM 需要启动并安装 Docker, 比 Pod 实验(通常 40 秒)更慢。
- 没有浏览器预览。 进入 VM 的连接只开放评分端口。
如果启动了 Web 服务器,请在 VM 内使用
curl检查。
目标
仅使用 shell 脚本构建 build → test → package 三阶段流水线,并亲手生成以源代码哈希命名的不可变产物及镜像标签。
为什么这很重要
CI 工具每隔几年就会变化,但其原理不会改变。阶段具有固定顺序;前一阶段失败时,后一阶段不得运行;仅看产物名称,就应能知道它由什么生成。标签是可以被人移动的名称,因此可能像 tj-actions/changed-files 事件那样被整体重新指向,所以部署标识符必须使用提交 SHA 之类不可变的值。如果生产环境里只有 latest,就无法追踪部署了什么,也没有明确的回滚目标。本实验将在不依赖任何厂商 UI 的情况下,手动复现这些原理。
步骤
- 创建
/root/ci1/pipeline.sh,并使用chmod +x赋予执行权限。在脚本开头加入set -euo pipefail(评分会分别检查set -e系列、set -u和pipefail)。创建构建目标目录/root/ci1/src/,并放入至少 2 个文件。 - 流水线在各阶段开始时依次各输出一次
::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打开。 - 使用相同源代码再次运行时,应复用已有产物。此时输出
::artifact-exists,并将本次执行日志保存到/root/ci1/run2.log。/root/ci1/out中的app-*.tar.gz仍应只有 1 个。 - 创建
/root/ci1/out/build-info.json。字段为source_hash(上述 12 位哈希)、status(字符串success)、stages(数字3)、created_at(时间字符串)。 - 创建
/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,恢复原状。 - 构建包含产物的镜像,标签为
labhub/ci:<해시12>。基础镜像只能从 Pod 中预先存在的alpine:3.20、busybox:1.36、python:3.12-alpine、nginx:1.27-alpine中选择。 - 使用
docker tag为同一个镜像额外添加labhub/ci:latest。然后在build-info.json中加入tags数组,同时记录labhub/ci:<해시12>和labhub/ci:latest两个值(至少 2 个)。
参考
- 哈希必须使用与评分相同的方式计算:
H=$(cat /root/ci1/src/* | sha256sum | cut -c1-12)。glob 顺序不同会导致结果不同。 - 第 3 步之后如果修改
/root/ci1/src的内容,哈希会变化,产物将变成 2 个。如果修改了源代码,请删除旧产物后重新生成。 - 与其手动拼接 JSON 字符串,不如使用
jq -n --arg h "$H" '{source_hash:$h, status:"success", stages:3}',这样更安全。 - 常见错误:使用
|| true吞掉失败(使其不再是门禁);结束前不删除 fail-flag(导致后续评分持续失败);通过单独构建 latest 导致镜像 ID 不同。
流水线骨架与安全选项
创建 /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 数组中记录两个标签。