CI绿灯可能根本没检查任何文件
文章分析了 CI 在零文件、零测试或零规则被执行时仍返回成功的空跑问题,强调退出码为零不能证明验证有效。内容聚焦 GitHub Actions 和 Terraform 工作流,并讨论让空跑触发失败的修复方式。
AI 深度解读
CI 全绿,可能连一个文件都没检查。作者把这种情况叫作「空通过」(vacuous pass):测试没有收集到用例、扫描任务被跳过、策略检查拿到空产物,流程却照样返回成功。
这篇文章的价值,是提醒我们给检查补上「确实干过活」的证据。不过,作者给出的部分 Shell 和 Terraform 解释不够严谨,修复方案也有适用前提。值得带走的是审计方法,示例不能全部照搬。
退出码为 0,为什么还不能放心
退出码回答的是「这个进程有没有报告失败」,未必回答「预期检查是否完成」。
作者用 go test ./... 举例:输出中可以同时出现 pkg/billing [no test files]、pkg/auth [no test files],以及 ok pkg/util 0.004s。命令返回 0,但真正运行测试的只有部分包。目录改名、build tag 配错,都可能让原本应该运行的测试消失。
原文拿 pytest 作对比:没有收集到测试时,pytest 返回退出码 5。对于必须执行测试的任务,这种默认行为能更早暴露异常。
这里要分清两个目标:证明「至少运行了一些测试」,以及证明「应该运行的测试都运行了」。作者建议统计 ^ok 行,前者可以靠数量大于 0 初步判断;后者还需要与预期包列表核对。只剩一个工具包跑成功,也不代表整个业务测试集完整。
检查成功,需要同时证明执行成功和覆盖范围符合预期。
同样的问题藏在 Shell 循环里。原文用 find terraform -name '*.tf' -newer .last-scan 生成列表,再逐个调用 tflint "$f"。路径写错、时间戳异常,或者确实没有匹配文件,循环都会执行零次。最后一句 echo "lint complete" 仍然可以成功。
作者提出先检查 [ -n "$files" ],再进入循环。这个方向成立:让空输入进入明确分支。但「没有变化」是否应该失败,需要根据任务目的判断。全量扫描突然没有输入,和增量检查没有相关改动,不该共用一个结论。

Shell 怎样把失败变成最后一行成功
第二类问题发生在命令之间:工具已经失败,外层脚本却没有把失败传出去。
原文用下面这条命令判断 Terraform 是否没有变更:
terraform plan -no-color | grep -q "0 to add, 0 to change, 0 to destroy"
它试图从输出中识别干净计划,但要重点看 |:默认情况下,管道状态通常来自最后一个命令,不能直接证明 terraform plan 成功完成。
作者接着描述:即使 grep 返回 1,只要脚本继续执行 echo "plan is clean",最终步骤仍可能返回 0。这里其实有两个独立问题:上游失败是否被管道保留,以及失败后脚本是否继续执行。
set -o pipefail 处理前一个问题,让管道能够反映上游失败;它单独使用,并不会保证后面的 echo 停止运行。原文第二个演示启用 pipefail 后,恰好也删掉了后续 echo,因此不能把最终结果变化全归功于 pipefail。
作者建议使用 set -euo 配合 pipefail。实际接入时,还要弄清 -e 在条件判断、管道等上下文中的行为,关键步骤也可以显式检查状态,不能只在文件开头加一行就结束审查。
原文还说 GitHub Actions 默认 Bash 已设置 pipefail,这个表述过于宽泛。工作流是否显式指定 shell: bash、是否使用自定义 shell,会影响执行方式。Jenkins 的 sh、GitLab job 和本地脚本,也不该凭平台名称推断选项。
真正应该确认的是:当前这段脚本由谁解释,失败状态沿哪条路径传到 CI。
跳过扫描和空计划,应该怎样进入合并门禁
作者列出的另两类问题,都发生在任务交接处。
第一类是 GitHub Actions 路径过滤。示例使用 dorny/paths-filter@v3,只匹配 terraform/**;security-scan 通过 needs.detect.outputs.tf == 'true' 决定是否运行。
当条件不满足,扫描 job 被跳过。作者指出,被跳过的 job 可以满足 required check 的要求。于是,只把条件扫描任务设为合并门禁,就无法表达「扫描必须真正执行成功」。
作者的修复是增加 scan-gate:依赖 detect 和 security-scan,使用 if: always(),再检查 needs.security-scan.result 是否等于 success,最后让分支保护要求这个汇总任务通过。
这套写法明确,但也改变了路径过滤的效果:只要扫描被跳过,就拒绝合并。如果团队原本允许无关改动免扫,汇总任务还应区分「按规则无需扫描」与「本该扫描却没有执行」。原文没有展开这层判断。
第二类是 Terraform 策略检查。作者先用 terraform show -json tfplan > plan.json 转换计划,再让 jq 从 .resource_changes[] 的 .change.actions[] 中寻找 contains("delete"),以匹配数量 length == 0 判断没有删除操作。
在运行策略前,原文增加了文件检查:
[ -s plan.json ] || { echo "FAIL: plan.json missing or empty"; exit 1; }
它解决文件缺失或零字节的问题。要看的是 -s:它只能证明文件存在且非空,不能证明 JSON 有效、计划生成成功,或者产物属于本次提交。
原文还要求 .resource_changes | length > 0。这可以阻止空资源列表通过,却也可能拒绝合法的无变更计划。要不要采用,取决于任务是否明确要求存在资源变更。
另外,原文把「空文件」和「没有资源变更的 JSON」混在了一起。两者对 jq -e 的行为并不相同;无输入不会自然产生一次可信的「零删除」判断。后续 echo 掩盖前面失败,是需要单独追查的问题。

我的判断
作者最有用的贡献,是把流水线审计压缩成三个问题:工具是否运行、是否检查了预期对象、发现违规后能否让最终门禁失败。
这比只看绿色徽章更接近真实质量。尤其是安全扫描、迁移测试、备份验证和 Terraform 策略检查,输入、产物与任务状态都应该留下可核对的证据。
但「零输入一律失败」太绝对。增量检查、无变更计划都有合理的空结果。更稳妥的规则是:没有解释的空结果应该失败,允许的空结果应该有明确依据。
相比给所有 job 强加非零数量断言,结合预期范围的汇总门禁更灵活;代价是需要维护「什么时候必须运行」的规则。数量断言实现简单,适合范围固定、输入必然存在的检查。
文章把示例称为真实经历,但没有提供完整运行环境、故障频率或修复前后的对照数据。「花 20 分钟通常就能发现问题」属于作者的经验判断。它适合作为审计起点,不能当作经过验证的收益承诺。
原文关于空目录下 terraform validate 和 terraform plan 都会失败的说法,也不宜直接作为保障。配置、状态和执行上下文需要分别考虑,外围脚本仍要自己验证输入与产物。
能拿来做什么
- 给前端或 Node 项目的测试任务记录收集数量,并与预期目录或项目范围核对。零用例应进入明确分支,测试数量异常缩水也应能被发现。
- 检查日常 AI 编程产生的 CI 脚本,重点看文件匹配、空循环、管道和末尾
echo。让 AI 解释每条失败路径怎样抵达最终退出码,再用一次可控失败验证。 - 对使用路径过滤的仓库增加汇总门禁,明确记录必须扫描、允许跳过和异常未执行三种情况。合并条件应依据任务结果及跳过理由。
- 优先审计部署、迁移和基础设施检查,分别验证产物存在、内容可解析及违规能够阻断流程。拿一份确定违规的输入走完整条链,比再看一次全绿更有说服力。