AI 重构 CI 流程(GitHub Actions→Argo Workflows)后暴露的 6 个隐性 bug。关键教训:代码层面 lint 通过不等于运行时正确,环境切换的差异容易被忽视。
最初发布于 wostal.eu。
简而言之:我把一次 CI 平台迁移——GitHub Actions → Argo Workflows、GitHub → Codeberg——交给了一个 AI Agent。重写后的内容看起来没问题,也通过了 lint,但它漏掉了一件事:运行时环境已经彻底改变。旧 runner 是一台外部虚拟机;新 runner 则是集群内的 pod,而 Tailscale IP 在其中无法路由。没有人真正端到端运行过这条流水线,因此所有差异都成了隐藏的地雷——总共有六个。最糟糕的并不是 AI 犯了错,而是我花了几个小时归咎于网络,真正的罪魁祸首却只是 SSH key 少了一个换行符。
这是另一起事故的姊妹篇。当时我正在把 homelab 的 k3s control plane 从 SQLite 迁移到 etcd,结果发现负责执行迁移的流水线本身存在各种问题,而我从这些问题中学到的东西,甚至比迁移本身还多。
本文将介绍:
整个 CI 刚刚从 GitHub Actions 迁移到 Argo Workflows,代码托管也从 GitHub 迁移到了 Codeberg——而且这次迁移被交给了一个 AI Agent 🙂。
这次平台迁移大致按照 1:1 的方式重写了原有逻辑,却没有意识到运行时环境已经发生了根本性变化。旧 runner(GitHub Actions / ARC)有其原本的运行方式;新 runner 则是集群内的一个 pod——从这里无法路由到 Tailscale IP,却可以访问私有 IP。没有人——无论人类还是 AI——端到端运行过这条流水线,因此所有这些差异都变成了隐藏的地雷。
这就是为什么会存在六个潜在 bug,以及我接下来要讲的卡死问题。它们并非随机故障,而是一次 GHA→Argo 转换留下的痕迹:这次转换从未在新环境中得到验证。
迁移完成后,我们才发现 deploy pipeline(运行在集群中的 CI)从未端到端成功执行过——ansible-configure 一直卡在 gather_facts。我在这上面花了几个小时,而且一次又一次地判断错误:
所有这些测试都把矛头指向了网络,但它们全都在骗我。
每项测试都受到同一个 bug 的干扰:在测试 pod 中,我使用 cp 从 secret 挂载 SSH key,却没有在末尾添加换行符。这会导致 error in libcrypto → key 根本无法加载 → ssh 表现得像是随机出错(有时卡死,有时提示 "permission denied")。我一直在追逐幽灵,因为坏掉的是测试装置,而不是网络。
真正的突破出现在我终于采用正确方式之后:在节点上使用 tcpdump + conntrack,并且严格按照 CI 的写入方式生成 key(printf '%s\n')。这时,整个情况变得清晰且一致:从 pod 访问私有 IP(10.0.1.x)可以正常工作(ssh + sftp + full ansible -m setup),而访问 Tailscale IP(100.x)则会卡住——因为 pod 没有通往 tailnet 的路由。解决方案是让 CI 使用私有 IP(反正 firewall 也不会过滤私有网络)。改完后立即就能正常工作。
教训 #1:在归咎于网络、CNI 或 MTU 之前,先证明你的测试装置本身没有问题。key 中少一个换行符,让我浪费了数小时去排查一个根本不存在的 CNI 问题。测试时必须与生产环境保持完全一致(这里指严格复现 CI 写入 key 的方式);遇到“数据包在中途消失”这类问题时,应使用 tcpdump/conntrack,而不是靠猜。“在我的笔记本上能用,在 pod 中不能用”并不总是网络问题——有时只是测试装置制造的假象。
这里还有第二个陷阱。deploy pipeline 从未运行到 apply+ansible,所以后续的每个 bug,都只能在前一个 bug 被修复、对应流程开始工作之后才会暴露。总共有六个:
opensshpython3(ansible 要求 control node 安装它)duration:0 的 approve gate 实际上没有起到任何阻断作用教训 #2:一条在早期失败的流水线,会掩盖所有下游 bug。你修复一个问题,重新运行,然后下一个此前根本无法执行到的问题终于开始执行,并随即失败。第一次端到端全绿,才是所有隐藏问题一次性浮出水面的时刻。要为这次完整运行做好准备;不要想当然地认为“它通过了 lint,所以它就能工作”。
教训 #3:CI 平台迁移(GHA→Argo、GitHub→Codeberg)必须进行一次端到端运行——无论迁移工作是由人还是 AI 完成。一次按“1:1”重写的迁移,看起来可能完全正确,也能通过 lint,但它会遗漏运行时环境之间的差异:外部 runner 与集群内 pod、GitHub API 与 Forgejo。这些差异不会在任何静态审查中显现出来——只有完整跑一次才会暴露。如果你把这样的迁移委托出去(比如交给 AI Agent 🙂),就应该立即安排一次覆盖整条路径的 smoke-run,否则你可能会在几个月后的凌晨 3 点才发现这些差异。
这并不是在反对使用 AI 进行迁移。重写后的内容大部分是正确的,也确实节省了时间。真正的问题在于,人应该在哪个环节继续留在闭环之中:不是亲手重新输入 YAML,而是验证迁移后的东西能否在新环境中真正运行。AI 完成了语言层面的转换;它无法完成环境层面的转换,因为它从未在那里实际执行过。而我也没有——直到它开始出故障。
修复这些问题后,流水线终于能够按照预期方式运行:merge to main → sensor → deploy pipeline → plan → [approve: suspend] → apply → ansible → release,并在中间设置一道人工 gate(argo resume = "after approval")。我通过一次真实的 merge 对它进行了验证——流水线顺利发布了 v1.4.1。control plane 运行在 etcd 上,具备 DR(本地 + S3),配置通过 GitOps 管理,中间还有人工 gate。
一个周末,两起事故。第一起让我明白,当实验室逐渐走向成熟时,默认配置会变成隐藏的地雷。这一起则让我明白:迁移后的系统只有在新环境中真正端到端运行过,迁移才算完成——无论编写它的那一方表现得多么自信。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。