绿灯 CI 实际打包了整个 working tree 和第三方库,allow-list 比 exclude-list 更安全;发版是系统首次全链路为真。
我的 CI 徽章连续几周都是红色的,我已经学会无视它了。发版那天,我终于把日志从头到尾读了一遍——而红色所掩盖的真相,比红色本身更糟糕。
当天的计划是:构建、上传、打标签、收工。结果,给 HivePlane v0.1.0 打标签变成了整个项目最有价值的一次调试会话,因为发版日是所有事情第一次必须同时为真的日子。如果你曾经把 CI 失败当作背景噪音,第 3 条是给你的。如果你发布包,第 1 条适用于所有人。
以下是我发现的五件事,按尴尬程度从高到低排列。
第一个包构建"成功"了。然后我列出了它的内容:1,442 个文件,包括整个 field-test 语料库——其中有一个 35 MB 的上游仓库提交记录 JSON,外加 111 个来自 vendored 第三方 agent 检出的文件。
我完全没有配置过 sdist。默认值悄悄地把整个工作树的某个切片打进了包,而不是产品。我的第一个修复——一个排除列表——仍然有漏洞:构建器在遍历磁盘,而不是被跟踪的树,所以未跟踪的第三方文件从缝隙中溜了过去。
真正有效的修复是相反的理念:允许列表。只有 src/hiveplane 加上根级的元数据文件可以进入 sdist。其他一切默认被排除。
tip: 对于产物内容,允许列表优于拒绝列表。拒绝列表枚举你记得要防范的东西;允许列表枚举你实际发货的东西。
审计被检入的内容——不是会发货的那些,是 git 里的那些——发现了两个完整复制的他人公开仓库:一个是 GitHub 分析 agent(10 个文件),一个是天气 agent(7 个文件),被 vendored 到了我的 field-test 树里,原来的 README 还在告诉读者从上游克隆。
它们只被文档引用,没有任何东西依赖它们。它们是未跟踪的(git rm --cached),被 gitignore 掉了,并在重新克隆表中记录了文档说明,以保持 field test 的可复现性。证据目录——也就是实际的 field-test 结果和 Docker 证据——没有被触动,因为这就是这次审计的全部目的:移除借来的代码,保留凭证。
可以安心的是:横跨每个提交的全历史 secret 扫描已经返回了干净结果——零发现。这是许可和卫生问题,不是泄露凭证问题。两项发现都归入了安全审计。
每次最近的推送都显示同样的红色 ❌。我一直在看本地套件(make test:绿色,925 个通过),把 CI 当噪音处理。
失败日志讲述了真实的故事:严格的 mypy 步骤在 pytest 运行之前就失败了——每次推送都是,连续几周。这意味着 Postgres 门控的测试在 CI 中从未执行过。我的"CI 不稳定"实际上是"CI 一直在测试一个比我想象中更小、更不同的系统,而且连那个都没测试成功。"
这个阻塞本身的修复只有一行——一个类型注解。一行代码,在它背后隐藏了四个真实的测试失败,连续几周。 最糟糕的那个在下一条。
当 CI 的 pytest 最终在 Postgres 上运行时,它在一个我希望它失败的测试上失败了:篡改审计日志,验证它能检测到篡改。
测试直接在数据库中编辑了 actor 列,期望 verify() 返回 False。它返回了 True。原因是一个经典的反规范化陷阱:审计日志把每条记录存储了两次——一次在正确的列中,一次作为 JSON payload——而验证是从 payload 重建记录,用 payload 检查哈希链,同时列被篡改了却从未被读取。
一个验证对象篡改者没有触碰的副本的防篡改日志,并不是防篡改的。修复方案是从权威列读取哈希字段。这个 bug 在本地是不可见的,因为 Postgres 门控的测试在没有数据库时会跳过——我绿色的本地套件在它从未执行过一次的安全属性上是绿的。
tip: 通过篡改来测试防篡改。纯验证的测试只能证明链能验证自己;它无法说明编辑是否会被检测到。
python -m pytest 不是一回事最后一个很小,但它解释了一整类"在我机器上能跑"的问题:
Makefile 运行的是 python -m pytest,它会把当前目录加到 sys.path。本地是绿的,每天都绿。
CI 运行的是 pytest console script,它不会。两个导入顶层 examples 包的测试模块在收集阶段就失败了——而且一直在收集阶段失败,躲在 mypy 那堵墙后面,全程都是。
在修复后续的警告过滤器时,我硬学到了 pytest 的 filterwarnings 条目按 : 分割——包含冒号的消息正则会让 pytest 尝试按字面量导入一个名为 <function BaseConnection 的模块。解析失败的配置应该大声失败;这一个却是安静地失败,通过误读。
一道永远失败的关卡会掩盖一切。连续几周的红色 CI 让我养成了不再读它的习惯——而在这片噪音里,四个真实的测试失败静静地待在那里无人注意。一道永远红的关卡在操作上等同于没有关卡,甚至更糟:它养成了忽略信号的习惯。
你的本地套件对从未运行过的东西是绿的。被跳过的 Postgres 测试不是在失败;它们是沉默的——而沉默在绿色终端上看起来和成功一模一样。
在 registry 让它永久化之前,检查你的产物。发布是不可逆的。上传前那 30 秒的 tar -tzf 是我做过的最便宜的质检门。
审计被检入的内容,而不只是发货的内容。那两个第三方 agent 从来没有进入过 wheel——但仓库也是产品,它刚刚公开了。
所有五项发现都在 v0.1.0 中修复并发布了——这才是这篇文章真正的论点:发版日在几小时内发现了数周开发悄悄错过的东西,因为那是所有事情第一次必须同时为真的日子。
安全审计 (v0.1.0) — 第 1 条和第 2 条背后的扫描证据
Field test report · Docker test report — 这些发现所保护的行为
Release notes · Changelog
现在你的 CI 日志或构建产物里藏着什么你一直想看却没看的东西——你不去看的借口是什么?