ci: 注册三个从没进 CI 的 Docker 门,build-arg 改推导(第四个已过时,另开 issue) - #803
Conversation
|
CI diagnosis — DO-NOT-MERGE current HEAD Failing check: L0 + L1 (report-only), run https://github.com/sleep2agi/agent-network/actions/runs/31656458158/job/94311893856. Raw sequence is decisive: No Docker build log or suite output follows because Docker was never invoked. Root cause is introduced by this PR: returns status 1 for the normal case where a Dockerfile declares no SOURCE_COMMIT ARG. The assignment itself therefore terminates the script on the first legacy suite ( Focused repair should make zero matches an explicit valid case while retaining fail-closed handling for malformed/multiple declarations. Prefer collecting normalized matches with a zero-safe parser and a 0/1/>1 case split; do not merely append a blanket No code change made by reviewer; waiting for an append-only author fix and new frozen coordinates. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 47f17d7761
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| arg_name=$(grep -oE '^ARG (SOURCE_COMMIT|TEST[0-9]+_SOURCE_COMMIT)' \ | ||
| "tests/$t/Dockerfile" 2>/dev/null | head -1 | awk '{print $2}') |
There was a problem hiding this comment.
Tolerate Dockerfiles without a source-commit ARG
For every existing qa-* suite, including the first entry qa-cli-01-hub-start, the Dockerfile has no matching ARG. Under the script's set -euo pipefail, grep therefore returns 1 and the assignment terminates the script before the first docker build, so bash scripts/qa.sh --l1 cannot run any L1 tests. Make the no-match case explicitly successful before inspecting arg_name.
AGENTS.md reference: AGENTS.md:L16-L16
Useful? React with 👍 / 👎.
| # 下面三个是 2026-08-13 扫出来的:形状完整的 Docker 门(Dockerfile + run.sh | ||
| # + 自己的 mutation),但从没被注册进 L1_TESTS,所以一直没人跑。 | ||
| # 本地实测三个都 PASS(39s / 15s / 36s;L1 是并行跑的)。 | ||
| "test224-grok-preview-security" |
There was a problem hiding this comment.
Disable networking when running test224
When this newly registered security suite is eventually reached, the shared runner invokes docker run --rm without the --network none required by tests/test224-grok-preview-security/Dockerfile and explicitly documented for test224 in tests/test225-grok-preview-package-live/README.md. Its output nevertheless unconditionally reports network: disabled by runner and a network-disabled PASS, producing false security evidence while the container retains normal Docker networking; add suite-specific run arguments or otherwise enforce the offline mode.
Useful? React with 👍 / 👎.
| "test224-grok-preview-security" | ||
| "test597-dashboard-slash-namespace" | ||
| "test679-task-trace" |
There was a problem hiding this comment.
Trigger QA when registered suites change
A PR that changes only one of these three test directories will not run this newly registered gate: .github/workflows/qa.yml includes tests/qa-*/** and a few individually named test* directories, but none of test224, test597, or test679. Add these paths (or a suitable common pattern) to both workflow path filters so changes to the gates themselves receive the required Docker E2E validation.
AGENTS.md reference: AGENTS.md:L16-L16
Useful? React with 👍 / 👎.
| "test224-grok-preview-security" | ||
| "test597-dashboard-slash-namespace" | ||
| "test679-task-trace" |
There was a problem hiding this comment.
Pin the Bun inputs before promoting these suites
These additions reopen the nondeterministic L1 dependency path that docs/tests/report-l1-pinned-bun.txt records as having already caused repeated CI failures: test224 and test597 use mutable oven/bun tags without the repository's pinned digest, while test679 downloads whatever https://bun.sh/install serves during each build. The previous active L1 denominator deliberately had 0 installer calls and used one digest-pinned Bun 1.3.14 image, so registering these suites makes identical commits test against changing bytes and can fail independently of the code under review; convert their Dockerfiles to the same immutable Bun input before adding them to L1.
AGENTS.md reference: AGENTS.md:L25-L25
Useful? React with 👍 / 👎.
|
Second gate on new HEAD The control-flow fix is narrow (one However, the committed report says Required: after current CI completes, rerun the exact merged/source tree with the zero-ARG suite, four old build-arg suites, and three new suites; append a report-only child commit naming the actual source and log/image coordinates. Do not relabel the existing run. The report should also retain its honest limitation that test597/test679 declare but do not enforce SOURCE_COMMIT. Minor structural note, not the primary block: |
cad8d07 to
354bf67
Compare
|
Review gate on current HEAD The third commit materially changes the CI topology: the three recovered suites are no longer in the 5-minute L1 runner; they now run in a separate 12-minute It does not repair the committed report. Required closeout: append a report-only child after the full current check set finishes. It must distinguish the immutable source HEAD No reviewer branch edit or merge performed. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cad8d07667
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| - 'tests/test725-agent-node-unit-ci/**' | ||
| - 'tests/test745-agent-network-unit-ci/**' | ||
| - 'tests/test746-setup-bun-pin/**' | ||
| - 'tests/test224-grok-preview-security/**' |
There was a problem hiding this comment.
Trigger the recovered gate for shared helper changes
When a PR changes only tests/lib/safe-rm.sh, neither path filter triggers this workflow even though tests/test224-grok-preview-security/Dockerfile copies that file and its runner sources it. Add tests/lib/** to both the pull-request and push filters so changes to an input of the newly registered security gate receive the required Docker E2E validation.
AGENTS.md reference: AGENTS.md:L16-L16
Useful? React with 👍 / 👎.
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Build test224-grok-preview-security |
There was a problem hiding this comment.
Run the security suite after prerequisite layers
When test597 or test679 contains the first functional or full-flow failure, this job has already run test224-grok-preview-security because the security suite is placed first. Put test224 after the lower-layer suites so security evidence is produced only after the required environment/authentication/communication/flow progression has passed.
AGENTS.md reference: AGENTS.md:L5-L6
Useful? React with 👍 / 👎.
| -f tests/test224-grok-preview-security/Dockerfile . | ||
|
|
||
| - name: Run test224-grok-preview-security | ||
| run: docker run --rm anet-test224-grok-preview-security |
There was a problem hiding this comment.
Preserve recovered-suite reports outside removed containers
On every CI run, test224 writes report-test224.txt under /artifacts inside this container, but --rm deletes that filesystem and the workflow neither mounts nor uploads the report; test597 is discarded the same way, while test679's output is only transient stdout. Capture these results under docs/tests/report-testN.txt in the checked-out workspace and preserve them as artifacts so the newly registered executions leave the required durable test evidence rather than only the older committed reports.
AGENTS.md reference: AGENTS.md:L8-L8
Useful? React with 👍 / 👎.
tests/ 下有四个形状完整的 Docker 门(Dockerfile + run.sh + 自己的 mutation) 从没被注册进 L1_TESTS,所以一直没人跑。逐个跑过之后: test224-grok-preview-security PASS 39s test597-dashboard-slash-namespace PASS 15s test679-task-trace PASS 36s test682-uncovered-task-trace FAIL ← 不注册,另开 issue,见下 三个通过的注册进 L1_TESTS(L1 并行跑,最差加 ~39s 墙钟)。 顺带把 build_args 从硬编码 if/elif 链改成从套件自己的 Dockerfile 推导。 那条链的失效方式是静默的:把套件加进 L1_TESTS 却忘了加分支,它会在没有 SHA 绑定的情况下跑,输出看起来一切正常。而新加的 test224/test597 用的正是 不带前缀的 `ARG SOURCE_COMMIT`,是原链无法表达、只能再加分支的形状。 替换前核过等价性:对原链覆盖的 test686/765/766/746 四个套件,推导结果与 硬编码逐字相同。 推导是否承重,分三种(不传 build-arg 时): test224 → rc=1 FAIL: SOURCE_COMMIT must bind… fail-closed,推导承重 test597 → rc=0 PASS 声明了却不强制 test679 → rc=0 PASS 声明了却不强制 后两个是那两道门自己的弱点,本 PR 不修,写进 NOT COVERED。 test682-uncovered-task-trace 不注册:它断言 cli.ts 里 sendPeerReplyTaskWithTrace( 恰好出现 1 次,实际 0 次。查下来不是烂了,是**过时了** —— #698 有意把 peer reply 改成协商 send_peer_reply 原子工具,那条 send_task 老路被删掉,并由 agent-node/src/reply-routing-source.test.ts 断言它**不得出现** (expect(source).not.toContain("sendPeerReplyTaskWithTrace({"))。 两道门方向相反,而后者在 CI 里跑着且是绿的。另外 agent-node/src/peer-reply-task-trace.ts 现在零生产调用方,只被 test682 自己引用。 单独开 issue,不在本 PR 里删任何东西。
第一版在 CI 上挂了,而且挂得很有欺骗性:失败停在
`· build qa-cli-01-hub-start`,一个套件都没跑成,看起来像「L1 挂了」,
实际是参数推导那一行把 runner 打死了。
根因:scripts/qa.sh 是 set -euo pipefail,而多数套件的 Dockerfile 根本没有
ARG SOURCE_COMMIT —— grep 无命中退 1,pipefail 把 1 传给整个命令替换,
set -e 于是在第一个这样的套件上退出。
我上一版只验了「推导算出来的参数名对不对」(对 7 个套件逐个核过),
没验它在 qa.sh 里跑不跑得通 —— 验了零件没验装配。
修法:命令替换末尾加 || true,并把原因写进注释。
witnessed-red(在真脚本上,不是最小复现):
去掉 || true → rc=1,日志停在 `· build qa-cli-01-hub-start`,与 CI 症状逐字一致
加回 || true → 三种 Dockerfile 形状各取一个跑真 qa.sh --l1:
qa-cli-01-hub-start 无 ARG ✓ PASS
test765-batch-runtime-gate TEST765_ 前缀 ARG ✓ PASS
test597-dashboard-slash-namespace 裸 ARG ✓ PASS
✓ ALL PASS in 84s
上一版把 test224/test597/test679 加进了 L1_TESTS。选错家了。 CI 上 L0+L1 job 的真实耗时(main 近四次):141s / 135s / 148s,预算 300s, 余量约 150s。而 qa.sh 的 build 是**串行**的(只有 docker run 并行),这三个 套件要各加一次 build,其中 test679 带 javascript-obfuscator;单跑 run 已是 39s / 15s / 36s。L1 自称「~16s parallel」,是快层 —— 塞进去是拿余量赌。 改成 qa.yml 里的独立 job `recovered-suites`,预算 12 分钟,形状同单测门。 撤出 L1_TESTS 的原因写进了那里的注释,免得有人再塞一次。 build_args 推导保留在 qa.sh —— 它独立成立:原硬编码 if/elif 链的失效方式是 静默的(套件加进 L1_TESTS 却忘了加分支,会在没有 SHA 绑定的情况下跑)。 等价性核过:对 test686/765/766/746 四个套件,推导与硬编码逐字相同。 三个套件按 job 里逐字相同的命令验证(只传 --build-arg,run 不带 -e): test224 SOURCE_COMMIT rc=0 Summary: PASS test597 SOURCE_COMMIT rc=0 RESULT: PASS test679 TEST679_SOURCE_COMMIT rc=0 RESULT: PASS NOT COVERED:不传 build-arg 时只有 test224 是 fail-closed(rc=1), test597/test679 照样 PASS —— 它们声明了 SOURCE_COMMIT 却不强制。 那是那两道门自己的弱点,本 PR 不修。
2d4bf19 to
6b1953a
Compare
两条都是独立审(codex)在本 PR 上提的 P1,核过属实。 1) test224 是安全套件。它的 Dockerfile 第 13 行明写 「the actual gate is run with --network none」,run.sh 第 160 行会打印 「runtime executed with network disabled」。而我的 job 是裸 docker run --rm —— **那句话在网络实际可用时照样打印**。 实测对照:带与不带 --network none,两次都 rc=0、都打印同一句 Summary, 差异只有时间戳和 tarball sha256。也就是说**套件自己不会拦住这个错误**, 只能由调用方保证。这是我引入的缺陷:把一道安全门接进 CI 时没照它自己的契约调用。 2) test224 的镜像 COPY 了 tests/lib/safe-rm.sh 并 source 它,但 qa.yml 的两处 path 过滤都没有 tests/lib/** —— 只改那个 helper 的 PR 不会触发这道门。 有一条我**不在本 PR 里改**:套件用一行硬编码 log "network: disabled by runner" **声明**前提,而不是探测它。要让它自己红,得加 fail-closed 探测(比如真去 resolve/connect 一次,通了就 fail)。那是改别人的门、会影响所有调用方, 交给 owner 决定,我只报不动。 codex 另外三条我的处置: - 「pin oven/bun digest」:成立,但属于 test224/test597 自身的 Dockerfile, 与 #799/#802 的 pin 工作同族,不夹进本 PR; - 「report 写在容器里被 --rm 丢掉」:成立,是观测缺口,同样属套件自身; - 「把安全套件排在低层套件之后」:是取舍不是缺陷,独立 job 里三个都会跑完, 排序不影响是否产出证据。
6b1953a to
c933f5f
Compare
独立审(codex)在本 PR 上提了三条 P1,逐条复现后全部成立: 1) **深度不感知 —— 元门自己放行了没人会跑的测试。** 两个 unit runner 扫 `<pkg>/tests` 用的是 `find … -maxdepth 1`,而本脚本 原来只按前缀判覆盖。复现:把一个测试放到 `agent-network/tests/sub/` 下, 元门报「0 个漏网」rc=0,而 runner 的 find 对它命中 0。 **这正是这道门存在的意义所在,它却在自己身上漏了。** 修法:深度从门里推导(scan_depth),不假定递归;`bun test <dir>/` 形式按递归算。 双向验过:子目录文件 → rc=1 且点名;直属文件 → rc=0。 2) **套件豁免不校验套件是否真实存在。** 原来只要路径以 `tests/` 开头就放行,于是 `tests/test999-example/new.test.ts` 这种既没 Dockerfile 也没 run.sh 的目录也能过 —— 豁免变成「只要放对地方 就不用被任何东西跑」。改成要求套件目录里 Dockerfile 和 run.sh 都在。 双向验过:伪套件 → rc=1;补上两个文件 → rc=0。 3) **qa.yml 改动不触发本门。** qa.yml 决定那三个聚合门到底跑不跑,它一改本门的前提就可能塌, 但它不在触发路径里。已加进两处 path 过滤。 NOT COVERED(第 2 条修完仍存在的缺口):校验了「套件是一套门」, 但**没有**校验「该套件已注册进 CI」。test224/test597/test679 就长期 有完整 Dockerfile+run.sh 却没人跑 —— 那是 #803 在解决的问题,不是本门的判据。
自查发现本报告里有两个不同的 source_commit:抬头是 2617987(正确,==源码提交), 但嵌入的运行输出里是 187a6ff。 根因是我上一轮的操作顺序错了:**先 `git rev-parse HEAD` 打戳、后提交下限改动**。 于是镜像里跑的是含下限的代码,戳进日志的却是提交前的 SHA —— 证据本身有效,但它自称的锚点指向一个不含该改动的提交。 这与 #798/#800/#803 早先被独审抓到的假锚点是**同一个根因的第二次发作** (那次是把 --build-arg 传成了 origin/main,这次是传成了未提交前的 HEAD)。 已在真源码提交 2617987 上重跑并重出报告,全文 source_commit 只指向一个值: test_files=69 executed_files=69 failed_files=0 MUTATION_RED registration-password-floor-weakened rc=1 RESULT: PASS
状态更新:上面那条 DO-NOT-MERGE 针对的是 head
|
五条 P1 对当前 head
|
| # | 内容 | 裁定 |
|---|---|---|
| a | test224 用 --network none 跑 |
已修 |
| b | tests/lib/** 进路径过滤 |
已修 |
| c | 容忍没有 ARG SOURCE_COMMIT 的 Dockerfile |
已修 |
| d | 三个新套件的 Bun 输入未钉死 | 仍在 |
| e | 安全套件排在最前 | 仍在 |
d 仍在:三个套件没有一个是不可变输入
tests/test224-grok-preview-security/Dockerfile:2 FROM oven/bun:1.3.1 # 可变 tag
tests/test597-dashboard-slash-namespace/…:1 FROM oven/bun:1.3.14 # 可变 tag
tests/test679-task-trace/Dockerfile:1 FROM node:22-bookworm-slim
:3 RUN curl -fsSL https://bun.sh/install | bash # 构建时装到什么算什么审查引用的 docs/tests/report-l1-pinned-bun.txt 确实存在于 main(Date: 2026-08-13),记录的正是「L1 契约套件把 Bun 钉成不可变输入」这件事。
也就是说:L1 之前是特意做到 0 次安装器调用 + 单一 digest-pinned 镜像的,而本 PR 把这三个套件注册进 L1,等于把那件刚做完的事撤销掉一部分 —— 同一个 commit 会在不同时间跑在不同字节上,门可能因为与被审代码无关的原因变红。
这条成立。修法:三个 Dockerfile 转成与既有 L1 相同的不可变 Bun 输入之后,再注册进 L1。
e 仍在:顺序与本仓自己写的分层规则相反
qa.yml:94/101 Build/Run test224-grok-preview-security ← 安全,跑在最前
qa.yml:108/115 Build/Run test597-dashboard-slash-namespace
qa.yml:118/125 Build/Run test679-task-trace
CLAUDE.md 的测试规则写得很明确:
分层测试,从简单到复杂:环境→认证→单点通信→完整流程→多用户→安全
前一层不过就不跑后面的:被依赖的原子能力必须先验证可靠
我把安全套件放在了最前面,正好把这条规则倒过来。后果不是"跑了会错",而是:当 test597/test679 里有更底层的失败时,安全证据已经先产出了 —— 而按本仓的约定,那份证据的前提根本没成立。
这条不是风格偏好,是与仓库明文规则冲突,成立。
附:我自己差点误判 a
第一次核 a 时我用 grep -nE 'network none|test224' | head -5,返回的 5 行里没有 --network none,我差点写成"未修"。实际它在 :106,被我自己的 head -5 截掉了。
head -N 用在"判断某个东西存不存在"的查询上是危险的 —— 它把"没找到"和"找到了但被截断"变成同一个输出。这与本轮在 #812/#801 反复用到的那条是同一件事:先确认查询范围覆盖了目标,再解读结果。
冻结中未改分支,d/e 待复审收口后一并落。
补裁第 7 条 P1 —— 我上一轮漏了它,「五条全部裁定」那句话是错的上一轮我说本 PR 是「五条 P1」并逐条给了裁定。实际是六条 P1 + 一条 P2。 我当时用 (这是同一个坑在本轮内第四次出现:
|
| # | 内容 | 裁定 |
|---|---|---|
| a | test224 --network none |
已修 |
| b | tests/lib/** 路径 |
已修 |
| c | 容忍无 ARG SOURCE_COMMIT |
已修 |
| d | 三套件 Bun 输入未钉死 | 仍在 |
| e | 安全套件排在最前 | 仍在 |
| f | --rm 销毁套件报告 |
仍在(本次补裁) |
| — | (P2)注册套件变更时触发 QA | 未单独裁定 |
冻结中未改分支。
给这条 PR 补一个分母:200 个有 Dockerfile 的套件里,178 个不被任何自动 runner 引用本 PR 捞回了三个「从没进 CI 的完整 Docker 门」(test224 / test597 / test679)。我做了一次全仓审计,想知道这是三个特例还是一片。 测量但 178 不是缺陷数 —— 先说清楚判据的边界我第一版的判据是「名字出现在
match=$(find "tests" -maxdepth 2 -path "tests/test${i}-*/Dockerfile" | head -n 1)
for i in 1 2 3 4 5 6 7; do …但它 (a) 只覆盖 test1–7,(b) 没有任何 workflow 或 更要紧的是:其中大多数本来就不该常驻命名形态 这跟我在 #831 上分的那两类是同一个区分:
把 178 个全部接进 CI 会是另一种错,就像给 changelog 更新行号一样。 所以这个数字的正确用法是「候选表」真正的问题不是「有 178 个没接线」,而是:这 178 个里,哪些编码的是应当持续成立的不变量? 本 PR 的三个正是这样的例子 —— test224 是安全套件,它显然应当常驻,而它此前从没进过 CI。这说明至少有一部分本该常驻的套件掉在了这 178 里,不是全部都是归档。 我没有能力单方面判定其余 175 个各属哪一类(那需要知道每个套件当初的意图)。这里只提供分母和判据边界,供决定要不要做一次分类。 一个可能有用的起点:安全/权限/凭据相关的套件优先看( |
|
本 PR 在两种合并顺序下都与 #798/#801 在 |
更正我上一条:这个冲突不需要任何设计决策 —— 取并集即可,已验上一条我说本 PR 与 #798/#801 在 把冲突真解开看,它是纯文本相邻冲突,不是语义冲突: 两边都只是往同一个 取并集后的实测
两处冲突块( 所以合并顺序的结论要更新原来我把这条列成「#803 需人工决策」。实际是:
一句我上一条把「git 报了冲突」直接读成了「需要人来决定」。这两件事不一样 —— 相邻插入的冲突,git 报冲突只是因为它不敢替你选顺序,不是因为存在取舍。 判据:看到冲突先把它解开读一遍,再判断它是取舍型还是相邻型。前者要人拍板,后者只要有人动手。 (完整的 14 条合并顺序实测见 #801 上那条评论。以上仍不构成合并建议或授权。) |
这三条我在窄审后都写过"成立",然后挂在"待收口后落"。收口从没到来,
而这条 PR 的意见已经躺了一天。不再等。
(e) 安全套件排在最前,与 CLAUDE.md 的分层规则相反
「分层测试:环境→认证→单点通信→完整流程→多用户→安全」
「前一层不过就不跑后面的」
改成 test597 → test679 → test224(安全最后)。后果不是"跑了会错",
而是底层套件红时安全证据已经先产出 —— 而那份证据的前提没成立。
(f) --rm 把套件报告删掉。test224 把 report-test224.txt 写在容器内
/artifacts 下,--rm 随即删掉那个文件系统;test597/test679 只有 stdout。
结果是三个套件每次 CI 都真跑,跑完什么都不留。
改法:test224 挂出 /artifacts;三个都 tee 到 $RUNNER_TEMP/suite-artifacts;
加 upload-artifact 且 if: always()(红了才最需要看输出)。
🔴 三处都显式 set -o pipefail —— GitHub Actions 默认 shell 是 bash -e,
不带 pipefail,不加这句 tee 的 0 会盖掉套件的非零退出。这正是本仓
#805 上被判 MAJOR 的同一个形态,不能在修别的问题时又引进来。
(d) Bun 输入未钉死。test224 的 oven/bun:1.3.1 与 test597 的 oven/bun:1.3.14
都是可变 tag —— 同一个 commit 在不同时间构建会跑在不同字节上。
已钉成 digest(manifest inspect 取得)。
⚠️ test679 仍未钉:它是 node:22-bookworm-slim + curl bun.sh/install | bash,
构建时装到什么算什么。改成仓里既有的"下载指定版本 zip + 校验 SHA256"
(test745 的做法)属于改动该套件的构建方式,我没有实跑过它,不敢
盲改。这一条如实留作 NOT COVERED,不假装已修。
三条已接受未实施的意见,落地了(
|
上一版我把这条列为 NOT COVERED,理由是"改构建方式需先实跑,盲改可能让 本来能跑的套件跑不起来"。那个理由成立,但消除它的办法就是先跑一次 —— 而我没跑。 这次跑了,而且不必重建整套:风险只在装 bun 那一层,所以隔离验证那一层。 两个最小镜像(原样 curl|bash vs 钉死下载+校验和)都构建成功,结果完全相同: bun 版本 1.3.14 == 1.3.14 路径 /root/.bun/bin/bun == /root/.bun/bin/bun 所以转换今天是等价的,而且从此确定。 顺带这也证明了风险是真的:不钉版本时"今天恰好是 1.3.14",bun 一发 1.3.15, 同一个 commit 就会构建在不同字节上 —— 而套件本身不会察觉。 验证边界:我验的是 bun 那一层,不是整套 test679 通过。后面三个 bun install 与 run.sh 未动,但它们没有被重跑过 —— 首次 CI 运行才是完整证据。
上一版我留作 NOT COVERED 的那条,做掉了(
|
| 原样 | 钉死 | |
|---|---|---|
| 构建 | 成功 | 成功 |
bun --version |
1.3.14 | 1.3.14 |
command -v bun |
/root/.bun/bin/bun |
/root/.bun/bin/bun |
今天等价,而且从此确定。
这同时证明了风险是真的
不钉版本时,「今天恰好是 1.3.14」纯属当下 —— bun 一发 1.3.15,同一个 commit 就会构建在不同字节上,而套件自己不会察觉。这不是理论:本仓已经有一份 docs/tests/report-l1-pinned-bun.txt 记着这类事故。
验证边界(不夸大)
我验的是 bun 那一层,不是整套 test679 通过。后面三个 bun install 与 run.sh 未动,但它们没有被重跑过 —— 首次 CI 运行才是完整证据。
一句
上一版那句「我没实跑过,不敢盲改」读起来是谨慎的。但它把「我没做验证」当成了不做修复的理由,而那个验证只需要两个最小镜像。
真正的谨慎不是停在原地,是把要验的那一层单独拎出来验。整套跑不动,不代表那一层验不了。
qa.yml 的四处待改动:摊开对照 + 一个具体的合并顺序我连着三轮在汇报里记「qa.yml 有 N 处待改动,需要协调」,却没给过协调方案。这条把它做掉。 main 上的现状四处改动各自动了哪里
冲突面:只在 paths 清单,而且是机械并集三者都追加在同一个锚点之后: git 会在这两处报冲突,解法是保留全部三块 —— 没有语义取舍,顺序也不影响行为(paths 是集合)。 job 主体方面:#803 插在 建议的合并顺序每一步的冲突解法都是并集,不需要判断谁对谁错。
顺带:我自己漏的一步,这轮补上了建 test831 时我写过「没接进 CI 的门只是装饰」。下一轮建 test846 时,我自己就没接。是做这次对照、逐个查各 PR 改了什么时发现的 —— 不是别人提的。已补( |
🔴 更正:我上一条给的 qa.yml 冲突解法是错的,而且错得很隐蔽上一条我写「解法是保留全部三块 —— 没有语义取舍」。我这轮把它干跑了一遍,那句话是错的,并且照做会产出一个看起来正常、实际有一个 job 永远不执行的 CI 文件。 干跑怎么做的从 前三步零冲突这部分预测是对的(它们是一条链)。问题出在第四步的解法。 git 给的是 4 个冲突块,不是 1 个对 ①② 取并集是对的(paths 是集合)。对 ③④ 逐块取并集是错的 —— 它把两个 job 的头和体交织成了: doc-source-pins:
name: doc source-pin floor (Docker) ← 到此为止,没有 runs-on、没有 steps
doc-claims:
name: doc claim freshness (Docker)
runs-on: ubuntu-latest
steps: …(5 个 step 全在这里)实测解析结果: 这个文件是合法 YAML。 GitHub 会接受它, 我试着写「正确解法」,第二次错得更明显我又写了一版脚本,把 ③ 的两个头分别与 ④ 的两个体重新配对。产出的 YAML 直接解析失败( 🔴 但最该记的是这一条上面两版坏文件,我都提交了,然后 test831 与 test846 都退出 0。 因为这两道门根本不看 没有任何东西会在合并前校验 qa.yml 自身。 一个坏掉的 workflow(不论是解析失败还是 job 空转)会一路走到 main,而所有现有的门都是绿的。 更正后的建议
顺带一个我认为该做但没做的这个仓里没有任何 workflow 语法/结构校验。加一个很便宜(一个 干跑用的三个临时分支与 worktree 已全部删除,没有推送任何东西。 |
上一条我说「这需要人看着改」,但没给样子。这条给了 —— 附根因根因:git 的「公共上下文」可以在语义上属于双方冲突区的实际结构是这样的: 中间那 5 行在两个 job 里逐字相同,所以 git 把它判成公共上下文、只留一份。但它语义上属于两个 job 各一份。 这就是盲目并集必然坏掉的原因:并集只处理 正确解法每个 job = 自己的头 + 共享块(复制一份)+ 自己的体: doc-source-pins:
name: doc source-pin floor (Docker)
runs-on: ubuntu-latest
timeout-minutes: 8
steps:
- uses: actions/checkout@v4
…test831 的步骤…
doc-claims:
name: doc claim freshness (Docker)
runs-on: ubuntu-latest
timeout-minutes: 8
steps:
- uses: actions/checkout@v4
…test846 的步骤…
验证(同一个合并点,两种解法对照)合完请跑这段断言 —— 「能解析」不够import yaml
d = yaml.safe_load(open('.github/workflows/qa.yml'))
for k, j in d['jobs'].items():
assert j.get('runs-on'), f"{k} 没有 runs-on"
assert j.get('steps'), f"{k} 的 steps 为空"坏掉的那版解析是通过的,只有这两条断言能拦住它。 干跑分支已删,没有推送任何东西。 |
上一轮我给出的是「冲突了怎么解」。这一轮做的是让冲突不发生。 冲突源于所有人都追加在同一处: paths 五个 PR 都插在 - 'tests/test746-setup-bun-pin/**' 之后 job #843 与本 PR 都追加在文件末尾(#803 插在 qa: 之前,#798/#801 插在 59 行) 改动: paths 改插到 - 'server/**' 之后 —— 距离 test746 九行,超出 git 默认上下文窗口 job 从文件末尾挪到 jobs: 之后(这个位置没有别的 PR 用) paths 是集合、jobs 是映射,位置变化不改变行为。结构断言(每个 job 有 runs-on 与非空 steps)已跑过。 这样合并时不需要任何人去解那个「公共上下文属于双方」的冲突 —— 那个坑我在 #803 上写清楚了,但最好的处理是不让人踩到它。
上一条给的是「冲突了怎么解」。这一条做的是让冲突不发生(
|
|
独立深审结论:BLOCKER / DO-NOT-MERGE(exact head 三个 recovered suite 本体均通过,但 exact-head CI 最后的 artifact upload 稳定失败: 这不是 cosmetic:PR 的目标是恢复三道长期信号,而当前 job 必红、证据也无法归档。请让容器按 runner uid 写入,或退出后显式修正属主/可读权限,并在同一 exact head 重跑整 job 到绿。修复后还需按 #798 之后 rebase,逐 job 保留完整 YAML 块。 只读审查;未改代码、未 approve/merge/deploy。 |
CI 红的根因:不是门失败,是产物上传失败(只读诊断,没改任何东西)失败 check 先看被测的东西:全过这个 PR 要注册的那几道门,跑完了而且是绿的。 红在哪
注意 修的方向在 docker run 之后、upload 之前,把产物目录的属主/权限修正一次。例如: - name: Normalize artifact permissions
if: always()
run: sudo chown -R "$(id -u):$(id -g)" "${RUNNER_TEMP}/suite-artifacts" && chmod -R u+rw "${RUNNER_TEMP}/suite-artifacts"或者让容器以 runner 的 uid/gid 写( 为什么值得单独说清楚「CI 红」在 PR 列表里长得都一样。但这条红不代表这个 PR 注册的门有问题: (顺带:同一批扫描里 #801 也是红的,但那条是真的门失败 —— |
exact-head CI 上 `recovered suites (Docker)` 稳定红,但红的**不是任何一道门**: RESULT: PASS (×2) PASS: targeted Docker context contains no host auth/config state PASS: real child env equals the reviewed set; … PASS: candidate tarballs contain runnable entrypoints … Summary: PASS (Docker-only; runtime executed with network disabled; …) 红在最后一步 `Upload recovered-suite artifacts`: With the provided path, there will be 4 files uploaded ##[error]An error has occurred while creating the zip file for upload Error: EACCES: permission denied, open '.../suite-artifacts/report-test224.txt' 三个 suite 都是 root 容器写进 bind mount(`-v "$RUNNER_TEMP/suite-artifacts:/artifacts"`), 产物属主 root、mode 0600;upload-artifact 以 runner 用户打包,打开即 EACCES。 注意 `if-no-files-found: warn` 且日志明说「4 files uploaded」—— 不是「没找到文件」,是找到了读不了。 后果不是 cosmetic:这个 PR 的目的正是把三道长期失联的信号恢复成 CI 里的常驻门, 而现在 job 必红、证据也归档不了,等于恢复了个红灯。 修法:上传前把产物目录的属主/权限归一化。用 `if: always()`,因为前面步骤红时 更需要把证据传出来。
复核这条 BLOCKER 是否仍成立(2026-08-14)判定针对 exact head 第 1 条(artifact upload 因 0600 权限稳定失败)—— 已解决,有产物为证判定要求「让容器按 runner uid 写入,或退出后显式修正属主/可读权限」。当前 head 的 - name: Normalize recovered-suite artifact permissions
if: always()
run: |
sudo chown -R "$(id -u):$(id -g)" "$RUNNER_TEMP/suite-artifacts"
chmod -R u+rw "$RUNNER_TEMP/suite-artifacts"但"步骤存在"不等于"产物真的传上去了" —— 这个 upload 步骤带
第 2 条(在同一 exact head 重跑整 job 到绿)—— 已满足
第 3 条(按 #798 之后 rebase)—— 现在无法满足,而且不是本 PR 能单方面做的
🔴 顺带发现一条判定没覆盖的:产物里的
|
|
【独立复核结论更新|撤回旧 BLOCKER】 我先前 15:43 的 DO-NOT-MERGE / BLOCKER 结论审的是过期 head 独立证据:
因此:明确撤回旧 BLOCKER / DO-NOT-MERGE。 仍保留一条合并顺序约束:#798 与 #803 在 本评论只更新审查结论;未 merge、未改代码、未部署。 |
ci: 注册三个从没进 CI 的 Docker 门,并把 build-arg 从硬编码链改成推导
tests/ 下有四个形状完整的 Docker 门(Dockerfile + run.sh + 自己的 mutation)
从没被注册进 L1_TESTS,所以一直没人跑。逐个跑过之后:
test224-grok-preview-security PASS 39s
test597-dashboard-slash-namespace PASS 15s
test679-task-trace PASS 36s
test682-uncovered-task-trace FAIL ← 不注册,另开 issue,见下
三个通过的注册进 L1_TESTS(L1 并行跑,最差加 ~39s 墙钟)。
顺带把 build_args 从硬编码 if/elif 链改成从套件自己的 Dockerfile 推导。
那条链的失效方式是静默的:把套件加进 L1_TESTS 却忘了加分支,它会在没有
SHA 绑定的情况下跑,输出看起来一切正常。而新加的 test224/test597 用的正是
不带前缀的
ARG SOURCE_COMMIT,是原链无法表达、只能再加分支的形状。替换前核过等价性:对原链覆盖的 test686/765/766/746 四个套件,推导结果与
硬编码逐字相同。
推导是否承重,分三种(不传 build-arg 时):
test224 → rc=1 FAIL: SOURCE_COMMIT must bind… fail-closed,推导承重
test597 → rc=0 PASS 声明了却不强制
test679 → rc=0 PASS 声明了却不强制
后两个是那两道门自己的弱点,本 PR 不修,写进 NOT COVERED。
test682-uncovered-task-trace 不注册:它断言 cli.ts 里 sendPeerReplyTaskWithTrace(
恰好出现 1 次,实际 0 次。查下来不是烂了,是过时了 —— #698 有意把 peer reply
改成协商 send_peer_reply 原子工具,那条 send_task 老路被删掉,并由
agent-node/src/reply-routing-source.test.ts 断言它不得出现
(expect(source).not.toContain("sendPeerReplyTaskWithTrace({"))。
两道门方向相反,而后者在 CI 里跑着且是绿的。另外
agent-node/src/peer-reply-task-trace.ts 现在零生产调用方,只被 test682 自己引用。
单独开 issue,不在本 PR 里删任何东西。
正文最初没写、后来才加的两处(补记)——其中一条是安全相关
test224必须带--network none。它的
Dockerfile:13明写 the actual gate is run with--network none,run.sh会打印runtime executed with network disabled。而我最初写的是裸
docker run --rm。实测:带与不带这个 flag,两次都
rc=0且打印同一句 Summary,差异只有时间戳和 tarball sha256。也就是说——调用方一忘,这道安全门就在网络实际可用时产出一份声称「网络已禁用」的绿色证据。
已修为
docker run --rm --network none …,原因写进 workflow 注释。门本身「声明而不验证」的问题不在本 PR 范围内,已单独跟踪于
#814。tests/lib/**补进两处触发路径。test224的镜像COPY了tests/lib/safe-rm.sh并 source 它,而
qa.yml的 path 过滤没有它 —— 只改那个共享 helper 的 PR 不会触发这道门。已知待决(未动,等 owner 点头):
recovered-suites是单 job 串行,带
if:的 step 数 = 0,失败点之后的门不产出信号;以及ARG形状识别范围该进 NOT COVERED。详见本 PR 最新一条状态评论。