口径先钉住 :本篇每一条锚点都来自公开文件,匿名可直接打开 ,而且我逐字读过两个版本: -
.github/workflows/verify-build-reproducibility.yml------ 首版(2026-04-20)与今天(v1.x)我都拉下来逐行读过 -THREATMODEL.md------ 第 401 / 407 / 409 / 457 行逐字 - 提交历史走commits/...atom,第一条就是文件创建那一次 本文没有引用学术文献 ,见文末引用节。 ⚠️ 先说本文最大的短板:我能证明「这个机制存在」,但证明不了「它已经造成了伤害」。 见第四节。
事情是这样的
axios 有一个安全威胁模型文件,叫 THREATMODEL.md。 不是谁写的小作文,是这个项目自己维护的风险清单。
清单里有一条,编号 T-S5,标题逐字:
shell
#### T-S5: Build-output tampering (dist/ != lib/)
同一条目的「影响」栏里写着:
markdown
****Impact** / **Critical****
Critical。 最高那一档。 翻译成人话:产物目录和源码目录对不上。 也就是你分发出去的代码,和你以为你分发的不是同一个东西。
按一个项目自己的威胁模型,这种风险该配一道硬门禁:构建两次,两次结果不一致就挡住合并。
那道门禁长什么样
我去 axios 仓库里找那条检查,找到了:
.github/workflows/verify-build-reproducibility.yml,逐字:
yaml
verify-reproducible-build:
name: Two-pass build and diff
runs-on: ubuntu-latest
# Non-blocking until divergence is eliminated. Surfaces regressions in
# the build's determinism without gating merges. Remove this line to
# promote to a hard gate once the build is byte-identical across runs.
continue-on-error: true
continue-on-error: true,一行的意思就是:这条检查失败了,任务照样算成功。
上面那三行注释是这个项目自己写的,翻译过来:
在不一致被消除之前保持非阻塞。它把构建确定性上的退化暴露出来, 但不阻断合并 。等构建在多次运行之间逐字节一致了,把这一行删掉, 就把它提升为硬门禁。
写得很清楚。它知道自己该变成硬门禁,也写清楚了条件。
但真正让我停下来的,是文件里的另一行
同一个文件,第 65 行:
bash
echo "This does not fail the job (continue-on-error: true) but is visible in the run summary."
这句是它运行时打印到日志里的。逐字:
这不会让任务失败 (
continue-on-error: true),但它在运行摘要里是可见的。
我读到这儿停了一下。
我见过两种情况:
- 判据悄悄失效 ------ 没人知道它在不在跑。
- 判据大声地说自己没用 ------ 像这条,它每次跑都明确告诉你「我不会拦住你」。
后者比前者好查得多。后者也更容易被忽略------ 构建是绿的,所有提示都写着「正常完成」,混在一堆绿色对勾里。 谁会盯着一行 echo 看?
解除条件写在两个文件里
这个条件不是写在某个人的脑子里,也不是工单里。 它同时写在两处,我逐字贴出来。
第一处,是上面那个 workflow 的注释: 「Once divergence is eliminated, remove this line」。
第二处,是 THREATMODEL.md 第 409 行,「Gaps」栏:
It is currently non-blocking (
continue-on-error: true)... Once divergence is eliminated, removecontinue-on-errorto promote this to a hard gate.
第三处,是同一份文件第 457 行的行动项表:
css
**T-S5 Build tampering / Low / Critical / Adequate**
Eliminate build non-determinism, then promote reproducibility check to blocking |
三处,三种说法,指向同一件事。
178 天
178 天。这条检查从被创建的第一天起,就不具有否决权。
那么它是怎么来的?查得出来------这条 workflow 的提交历史一共 10 次, 第一条是 2026-04-20T17:52:36Z,commit 302e2739c6, 标题是 docs: update docs, add a couple actions etc (#10776)。
我把那个 commit 里的文件版本拉下来看了。第 21--24 行,逐字:
php
# Non-blocking until divergence is eliminated. Surfaces regressions in
# the build's determinism without gating merges. Remove this line to
# promote to a hard gate once the build is byte-identical across runs.
continue-on-error: true
一模一样。
continue-on-error: true 不是后来被谁加上去的。 从 2026-04-20 到今天 2026-10-15,178 天里它一次也没能拦住过任何东西。
那么,解除条件满足了吗?没人记录------这一点我在第四节单独说。
⚠️ 第四件必须说的事:我证不了后果
上面全是真的。但有一件事我做不到。
我找不到任何一次公开记录,能证明 axios 的构建产物真的不一致。 所以我也不能说,它挡住过什么、或者放过什么。
我不能证明这 178 天里发生过什么。我只能证明:
我能证明 / 我不能证明
- 这条风险被项目自己评为 Critical ------ 它已经造成过实际损害
- 唯一能发现它的检查从第一天起没有否决权 ------ 构建真的出过不可复现
- 解除条件写在三个地方 ------ 解除条件一直没满足
第 3 行尤其要说清楚:解除条件可能一直是满足的。 也可能是构建早就逐字节一致了,只是没人把那一行删掉。 这两种情况在这条证据面前长得一模一样。
所以我不会写「axios 被攻破过」,也不会写「它放过了一次篡改」。 一个判据写了它想做什么、且每天都在老实汇报, 这本身不构成它失职的证据。
那这篇还值得写吗
我认为值得,但理由和上面那些数字无关。
值得写的是这件事的形状:
一个团队把一条风险评为 Critical,把解除条件写在三个地方, 然后把「解除」这个动作本身,交给了一个不会有人主动去做的流程。
门禁类的东西有个共同点:它不制造工作,它消除工作。 一旦一道门禁长期挂着「不阻塞」,它就会一直挂着。 没有任何东西会因此变红。所以没有人需要做决定。
这和「有人在某个群里拍板说这条先不管」不一样。 那至少还有个人,可以被追问「你怎么决定的」。
而 continue-on-error: true 是另一种东西: 它把一个待决事项,写成了一个已完成事项。
如果要我给一条可带走的
不是「给 CI 加硬门禁」------那是空话,而且很多门禁就不该硬。
我能给的只有一条具体的。
当一条判据写着「暂时不阻塞」,去找它自己写下的解除条件。 然后检查那个条件现在成不成立。
如果条件成立了而它还挂着,那说明没有人回头看 。 这时候把它打开是零风险的改动------它一直在跑,只是改一个开关。
如果条件不成立------那至少「暂时」是诚实的。 你只是需要知道它现在还不成立,而不是以为它在拦着。
关键区别在于: continue-on-error: true 和「没人在看」在你这里长得一模一样。 要么它在拦,要么它不拦,中间没有第三态。
附:本文引用
锚点 / 出处(匿名可开) / 我做了什么
continue-on-error: true与三行注释 ------raw.githubusercontent.com/axios/axios/v1.x/.github/workflows/verify-build-reproducibility.yml------ 拉下来逐行读,:21-24- 文件自己打印的那句话 ------ 同上 ------
:65,逐字 - 首版文件(创建那一次) ------
raw.githubusercontent.com/axios/axios/302e2739c6/.github/workflows/verify-build-reproducibility.yml------ 拉下来逐行读,:21-24与今天逐字节一致 - 文件创建那一次提交 ------
github.com/axios/axios/commits/v1.x/.github/workflows/verify-build-reproducibility.yml.atom------ 解析 10 条提交,最早2026-04-20T17:52:36Z/302e2739c6/#10776 - 默认分支是
v1.x------api.github.com/repos/axios/axios------ 读default_branch - T-S5 与其 Critical ------
raw.githubusercontent.com/axios/axios/v1.x/THREATMODEL.md------:401标题;:407的 Impact 一栏写着Critical - 解除条件(第三处) ------ 同上 ------
:409Gaps 栏、:457行动项表 - 178 天 ------ 本机
datetime实算 ------ 2026-04-20 → 2026-10-15
本文没有引用学术文献
这篇没做文献综述,也没引用任何论文或书籍。 第二节查的是项目自己的文件,不是文献。 写清楚是为了避免读者以为我做过文献工作。
取到 / 没取到的
来源 / 状态
- workflow 首版与当前版逐行 ------ ✅ 取到,亲自拉下来对比过
- 提交历史(atom feed) ------ ✅ 取到,10 条,最早即文件创建
THREATMODEL.md四处锚点 ------ ✅ 取到,逐行读过- 默认分支 ------ ✅ 取到,
v1.x - 「解除条件一直没被满足」 ------ ❌ 没有任何公开记录支持 。文件里没有,工程 issue 里我没找到。本文没有这样断言
- 构建是否真的不可复现 ------ ❌ 没查到公开证据。我不写「它放过了一次篡改」
- 中文侧是否已有人写过这个题 ------ ❌ 未能验证 (百度验证码 / cn.bing 301 / DuckDuckGo 返空 / 掘金与 CSDN 搜索页 JS 渲染 / 知乎 403)。因此我不声称它是中文首例
取不到的东西我不用记忆去补。
尤其是上面两条打 ❌ 的。它们恰好是这个题最像样的两个论点。 正因为拿不到一手证据,这篇才退到只讲机制。
读者是能分辨「可能出过事」和「出过事」的。 而那个差别,是整个系列的立足点。