Skip to content

ci: 注册三个从没进 CI 的 Docker 门,build-arg 改推导(第四个已过时,另开 issue) - #803

Open
vansin wants to merge 8 commits into
mainfrom
ci/register-orphan-suites
Open

ci: 注册三个从没进 CI 的 Docker 门,build-arg 改推导(第四个已过时,另开 issue)#803
vansin wants to merge 8 commits into
mainfrom
ci/register-orphan-suites

Conversation

@vansin

@vansin vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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 里删任何东西。


正文最初没写、后来才加的两处(补记)——其中一条是安全相关

  1. 🔴 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
  2. tests/lib/** 补进两处触发路径。 test224 的镜像 COPYtests/lib/safe-rm.sh 并 source 它,
    qa.yml 的 path 过滤没有它 —— 只改那个共享 helper 的 PR 不会触发这道门。

已知待决(未动,等 owner 点头):recovered-suites 是单 job 串行,
if: 的 step 数 = 0,失败点之后的门不产出信号;以及 ARG 形状识别范围该进 NOT COVERED。
详见本 PR 最新一条状态评论。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

CI diagnosis — DO-NOT-MERGE current HEAD 47f17d77619973fd4cab1f6457118441bb4af6c6.

Failing check: L0 + L1 (report-only), run https://github.com/sleep2agi/agent-network/actions/runs/31656458158/job/94311893856. Raw sequence is decisive:

L1 registry snapshot ...
· build qa-cli-01-hub-start
Process completed with exit code 1

No Docker build log or suite output follows because Docker was never invoked.

Root cause is introduced by this PR: scripts/qa.sh runs under set -euo pipefail, and the new assignment

arg_name=$(grep ... Dockerfile | head -1 | awk ...)

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 (qa-cli-01-hub-start). The intended later if [[ -n "$arg_name" ]] is unreachable. This is not an Actions/network flake and not evidence that any of the three newly registered suites failed.

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 || true that would also hide a missing/unreadable Dockerfile. Then rerun the exact L0+L1 job and the claimed equivalence matrix for the four existing build-arg suites plus one no-ARG suite. Current report cannot be accepted because the actual CI path exits before exercising its main claim.

No code change made by reviewer; waiting for an append-only author fix and new frozen coordinates.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread scripts/qa.sh Outdated
Comment on lines +159 to +160
arg_name=$(grep -oE '^ARG (SOURCE_COMMIT|TEST[0-9]+_SOURCE_COMMIT)' \
"tests/$t/Dockerfile" 2>/dev/null | head -1 | awk '{print $2}')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread scripts/qa.sh Outdated
# 下面三个是 2026-08-13 扫出来的:形状完整的 Docker 门(Dockerfile + run.sh
# + 自己的 mutation),但从没被注册进 L1_TESTS,所以一直没人跑。
# 本地实测三个都 PASS(39s / 15s / 36s;L1 是并行跑的)。
"test224-grok-preview-security"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread scripts/qa.sh Outdated
Comment on lines +80 to +82
"test224-grok-preview-security"
"test597-dashboard-slash-namespace"
"test679-task-trace"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread scripts/qa.sh Outdated
Comment on lines +80 to +82
"test224-grok-preview-security"
"test597-dashboard-slash-namespace"
"test679-task-trace"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Second gate on new HEAD cf783526cb4d3b2fbabbb685f16a31805cab7c5a: still DO-NOT-MERGE pending truthful exact-source evidence.

The control-flow fix is narrow (one scripts/qa.sh delta) and addresses the observed zero-match exit. Current-main virtual merge was also checked correctly: merge-tree bfd613c7... preserves #802’s qa-180 Dockerfile and report byte-for-byte, so this PR does not revert #802.

However, the committed report says source_commit=034f00647d42d38d5086d7fc057eb7824a441791. That is the PR base: it contains neither the orphan-suite registration/generic inference commit (47f17d77) nor the zero-match fix (cf783526). The report therefore cannot prove current HEAD, and specifically cannot contain an execution of the fix added after it. An in-progress GitHub Actions run may prove current behavior transiently, but does not repair the repo’s false durable coordinate.

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: ... || true is zero-safe but also suppresses parser errors; missing Dockerfiles still fail at the later Docker build, while malformed/unrecognized ARGs can remain unbound for weak suites. A future meta-gate should validate declared binding names rather than infer-and-ignore. No author-branch edit made.

@vansin
vansin marked this pull request as ready for review August 13, 2026 01:15
@vansin
vansin force-pushed the ci/register-orphan-suites branch from cad8d07 to 354bf67 Compare August 13, 2026 01:16
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Review gate on current HEAD 354bf67998605ce17c0c78d16b40fc8793bf5a76: DO-NOT-MERGE pending a truthful durable report.

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 recovered-suites job. The exact Actions job is green (test224, test597, test679 all built and ran successfully; job 94314095687 completed in 2m44s), and L0+L1 plus both package-unit jobs are green. This supports the design.

It does not repair the committed report. docs/tests/report-register-orphan-suites.txt still says source_commit=034f00647d42d38d5086d7fc057eb7824a441791, the PR base. That commit contains none of the registration, zero-match fix, or the new independent-job topology. Therefore the durable repository evidence still points at a tree incapable of producing its claims.

Required closeout: append a report-only child after the full current check set finishes. It must distinguish the immutable source HEAD 354bf679... from the GitHub PR virtual-merge SHA used as $GITHUB_SHA, record the recovered-suites job/log coordinates and results, preserve the test597/test679 SOURCE_COMMIT enforcement limitation, and state that qa.yml remains report-only unless branch protection changes. Do not rewrite or relabel the base run.

No reviewer branch edit or merge performed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread .github/workflows/qa.yml
- 'tests/test725-agent-node-unit-ci/**'
- 'tests/test745-agent-network-unit-ci/**'
- 'tests/test746-setup-bun-pin/**'
- 'tests/test224-grok-preview-security/**'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread .github/workflows/qa.yml Outdated
steps:
- uses: actions/checkout@v4

- name: Build test224-grok-preview-security

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread .github/workflows/qa.yml Outdated
-f tests/test224-grok-preview-security/Dockerfile .

- name: Run test224-grok-preview-security
run: docker run --rm anet-test224-grok-preview-security

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 不修。
@vansin
vansin force-pushed the ci/register-orphan-suites branch from 2d4bf19 to 6b1953a Compare August 13, 2026 02:19
两条都是独立审(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 里三个都会跑完,
  排序不影响是否产出证据。
@vansin
vansin force-pushed the ci/register-orphan-suites branch from 6b1953a to c933f5f Compare August 13, 2026 02:33
vansin pushed a commit that referenced this pull request Aug 13, 2026
独立审(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 在解决的问题,不是本门的判据。
vansin pushed a commit that referenced this pull request Aug 13, 2026
自查发现本报告里有两个不同的 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
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

状态更新:上面那条 DO-NOT-MERGE 针对的是 head 354bf679,已被取代

那条评论提出的是 set -euo pipefail + 无匹配 grep 导致赋值早退、Docker 根本没跑。
该问题已修(命令替换末尾加 || true),并且我把三个套件从 L1 撤出、改放独立 job

当前坐标(冻结中,我不再改动)

source     aeec4b9c…   (代码提交)
report     c933f5fc…   (report-only 子提交)
base       034f0064    (current main)

witnessed-red 是在真脚本上做的:去掉 || true → rc=1 且日志停在
· build qa-cli-01-hub-start,与当初 CI 的症状逐字一致;加回后三种 Dockerfile 形状
(无 ARG / TEST765_ 前缀 ARG / 裸 ARG SOURCE_COMMIT)各取一个跑真 qa.sh --l1,ALL PASS in 84s

近期审查发现(经 commhub 收到,转录到此;我的转录,非他们的终审)

一条我认为是真的、且我该改 —— 但冻结中没动手:

recovered-suites单 job 串行;GHA 默认前一步失败会跳过后续
test224 红时,test597 / test679 不产出信号

我核了自己那个 job:7 个 step(checkout + build/run ×3),带 if: 的数量 = 0
所以它不造假绿(job 已红),但我在 PR 正文里按「三个门都跑」描述,那句话不成立

证据强度说准:「step 都不带条件」是实测 grep;「无条件 step 会被跳过」是 GHA 文档语义——
我在本仓找不到直接实证(近 80 个 run 里只有 1 个失败,且其失败点之后没有可观察的无条件 step)。

我的判断:这三个门互相独立(安全扫描 / dashboard 路由 / task-trace),又都是长期没人跑的孤儿门,
该要全量诊断而非 fail-fast,最小改法是给后续 step 加 if: always()等 owner 点头再改。

交叉核确认的其余点:三个套件的 ARG 形状与 qa.yml 传参完全吻合;
test224--network none 只加在 Run 步、Build 保持联网,编排正确;
三套件 COPY 的输入在 pull/push paths 两侧均覆盖;
|| true 正确封住无 ARG 时的 pipefail 早退。

一条我之前没意识到的边界:那段推导的 grep 只识别列首、无默认值的 ARG 两种形状;
未来出现缩进或 ARG X=default 会静默不传。当前 L1 分母没有这种形状,不是缺陷,但该进 NOT COVERED
同样等点头再改。

待决清单(共 2 处,均未动)

  1. 后续 step 加 if: always()(或拆 matrix);
  2. NOT COVERED 补上「ARG 形状识别范围」这一句。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

五条 P1 对当前 head c933f5fc 复核完毕:3 已修,2 仍在

审查针对 47f17d77 / cad8d076,坐标已过期,逐条对当前 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 待复审收口后一并落。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

补裁第 7 条 P1 —— 我上一轮漏了它,「五条全部裁定」那句话是错的

上一轮我说本 PR 是「五条 P1」并逐条给了裁定。实际是六条 P1 + 一条 P2。 我当时用 ... | head -6 列评论,第 7 条被截掉了,而它恰好是 P1。

(这是同一个坑在本轮内第四次出现:head -N 用在「有没有某个东西」的查询上,会把「没有」和「有但被截断」变成同一个输出。前三次分别咬在 --network none 的核对、comments 字段、以及 find 吞 stderr 上。)


qa.yml:100 Preserve recovered-suite reports outside removed containers —— 成立

当前 head c933f5fc 上,三个 Run 步骤全是裸 --rm:

:106  run: docker run --rm --network none anet-test224-grok-preview-security
:116  run: docker run --rm anet-test597-dashboard-slash-namespace
:126  run: docker run --rm anet-test679-task-trace

没有 -v 挂载、没有 docker cp、没有 actions/upload-artifact

test224 把 report-test224.txt 写在容器内 /artifacts 下,--rm 随即把那个文件系统删掉;test597 同样;test679 的输出只是 stdout。

结果是:这三个套件每次 CI 都真的跑了,但跑完什么都不留。 仓里能看到的只有更早提交进来的旧报告 —— 而那些报告与本次执行无关。

为什么这条比看起来重要

本仓的约定是实测要落 docs/tests/report-testN.txt。这条与 #809 上那条 P1(「干净容器的 capture 需要落成 report 制品」)是同一个根因:

只存在于易失上下文里的证据,不是证据。

#809 那次的代价已经具体化了:因为没有 report,「零连接」这个关键前提没被写下来,于是一个空闲 hub 的单样本被我写成了关于稳定线的安全结论(见 #809)。

这里的风险形态一样 —— 门跑绿了,但没有任何东西能证明它这次跑的是什么、在什么条件下绿的。等到某次它变红,或者有人质疑某个结论时,回不去。

修法

三个 Run 步骤都需要把产物带出容器,再落成仓内制品或 CI artifact:挂 -v "$PWD/docs/tests:/artifacts"(或跑完 docker cp),并配 actions/upload-artifact。test679 需要先让它把结果写成文件而不是只打 stdout。


本 PR 更正后的裁定表

# 内容 裁定
a test224 --network none 已修
b tests/lib/** 路径 已修
c 容忍无 ARG SOURCE_COMMIT 已修
d 三套件 Bun 输入未钉死 仍在
e 安全套件排在最前 仍在
f --rm 销毁套件报告 仍在(本次补裁)
(P2)注册套件变更时触发 QA 未单独裁定

冻结中未改分支。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

给这条 PR 补一个分母:200 个有 Dockerfile 的套件里,178 个不被任何自动 runner 引用

本 PR 捞回了三个「从没进 CI 的完整 Docker 门」(test224 / test597 / test679)。我做了一次全仓审计,想知道这是三个特例还是一片。

测量

有 Dockerfile 的套件            200
被 scripts/qa.sh 或任一 workflow 文本引用   22
不被引用                        178

但 178 不是缺陷数 —— 先说清楚判据的边界

我第一版的判据是「名字出现在 qa.sh 或 workflow 里才算接线」。这个前提部分不成立,查了才知道:

tests/run-parallel.sh 是按编号发现套件的,不靠硬编码名字:

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 或 qa.sh 调用它 —— 全仓只有 tests/README.mddocs/open-source-quality-review.md 两处文档提到它。所以它是手动工具,不改变「这 178 个不进 CI」这个结论,只改变「它们完全跑不了」这个说法(手动仍可跑)。

更要紧的是:其中大多数本来就不该常驻

命名形态 test<issue#>-<topic> 说明它们多是一次性验证套件 —— 当时为某个 issue 建、跑过、留了 report。这类套件不是「孤儿」,是归档

这跟我在 #831 上分的那两类是同一个区分:

参考页 pin / 常驻门 changelog pin / 一次性验证套件
描述的是 现在的行为 当时发生了什么
该怎么办 保持有效 保持原样

把 178 个全部接进 CI 会是另一种错,就像给 changelog 更新行号一样。

所以这个数字的正确用法是「候选表」

真正的问题不是「有 178 个没接线」,而是:这 178 个里,哪些编码的是应当持续成立的不变量?

本 PR 的三个正是这样的例子 —— test224 是安全套件,它显然应当常驻,而它此前从没进过 CI。这说明至少有一部分本该常驻的套件掉在了这 178 里,不是全部都是归档。

我没有能力单方面判定其余 175 个各属哪一类(那需要知道每个套件当初的意图)。这里只提供分母和判据边界,供决定要不要做一次分类。

一个可能有用的起点:安全/权限/凭据相关的套件优先看(test3-securitytest9-permissionstest30-v0.8-auth-deprecationtest631-private-config-permissionstest637-database-url-guardtest660-explicit-prod-db-optin 等),因为这类不变量通常不会因为 issue 关闭而失效。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

本 PR 在两种合并顺序下#798/#801.github/workflows/qa.yml 上冲突 —— 是真交叠,不是顺序问题。完整的 14 条合并就绪度与顺序实测见 #801 (comment) 。不构成合并建议或授权。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

更正我上一条:这个冲突不需要任何设计决策 —— 取并集即可,已验

上一条我说本 PR 与 #798/#801qa.yml 上是「真交叠,需人工解,或先决定 qa.yml 的最终形态」。后半句说重了。

把冲突真解开看,它是纯文本相邻冲突,不是语义冲突:

<<<<<<< HEAD   (#798 加的)
      - 'tests/test798-server-unit-ci/**'
      # test798 的镜像 COPY 了 test601 的 race-worker.ts …
      - 'tests/test601-hub-scheduled-tasks/**'
=======        (#803 加的)
      - 'tests/test224-grok-preview-security/**'
      - 'tests/test597-dashboard-slash-namespace/**'
      - 'tests/test679-task-trace/**'
      - 'tests/lib/**'
>>>>>>>

两边都只是往同一个 paths 列表里加不同的条目。 git 冲突是因为插入点相邻,不是因为内容互斥。

取并集后的实测

校验 结果
残留冲突标记 0
YAML 解析 通过
job 数 7
test798 / test601(来自 #798) 7 / 4 处
test224 / test597 / test679 / tests/lib(来自 #803) 8 / 7 / 7 / 2 处

两处冲突块(pull_request.pathspush.paths)形状相同,同样处理。

所以合并顺序的结论要更新

原来我把这条列成「#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,不假装已修。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

三条已接受未实施的意见,落地了(c933f5fc → 9a9389a2)—— 其中一条如实留作未修

同样先说为什么拖到现在:这三条我在窄审后都写过「成立」,然后统一挂在「待收口后落」。收口从没到来,意见躺了一天。不再等。

(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-artifactif: always()(红了才最需要看输出)。

🔴 三处都显式 set -o pipefail。GitHub Actions 默认 shell 是 bash -e,不带 pipefail —— 不加这句,tee 的 0 会盖掉套件的非零退出。

这正是本仓 #805 上被判 MAJOR 的同一个形态。 在修「证据留不下」的时候把「退出码被吞」引进来,等于用一个修复换一个缺陷。

(d) Bun 输入 —— 修了两个,第三个如实留作未修

test224   FROM oven/bun:1.3.1   → @sha256:9c5d3c92…   已钉
test597   FROM oven/bun:1.3.14  → @sha256:e10577f0…   已钉
test679   node:22 + curl bun.sh/install | bash        ⚠ 仍未钉

前两个是可变 tag,同一个 commit 在不同时间构建会跑在不同字节上;digest 由 docker buildx imagetools inspect 取得。

test679 我没有改。 它要改成仓里既有的做法(test745:下载指定版本 zip + 校验 BUN_LINUX_X64_SHA256)属于改动该套件的构建方式,而我没有实跑过它 —— 盲改可能让一个本来能跑的套件跑不起来。

这一条如实留作 NOT COVERED,不假装已修。 要修,需要有人先在容器里跑通改造后的版本。


一句

三条里两条(e/f)是我自己引进的问题,一条(d)是既有的。而 (f) 的修法本身差点又引进第四个 —— tee 不加 pipefail。

修一个问题时最容易犯的,是自己刚修过的那一类。 今天这已经是第四次。

上一版我把这条列为 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 运行才是完整证据。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

上一版我留作 NOT COVERED 的那条,做掉了(9a9389a2 → 4a00e683)

上一版我把 test679 的 curl -fsSL https://bun.sh/install | bash 列为未修,理由是:

改成仓里既有的做法属于改动该套件的构建方式,而我没有实跑过它 —— 盲改可能让一个本来能跑的套件跑不起来。

那个理由成立,但消除它的办法就是先跑一次 —— 而我没跑。

这次跑了,而且不必重建整套

test679 的构建要 COPY 三个包再跑三次 bun install,很重。但风险只在装 bun 那一层,所以隔离验证那一层:两个最小镜像,只有第 3 行不同。

原样  curl -fsSL https://bun.sh/install | bash
钉死  下载 bun-v1.3.14 zip + sha256sum -c + 解到 /root/.bun/bin

结果:

原样 钉死
构建 成功 成功
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 installrun.sh 未动,但它们没有被重跑过 —— 首次 CI 运行才是完整证据。


一句

上一版那句「我没实跑过,不敢盲改」读起来是谨慎的。但它把「我没做验证」当成了不做修复的理由,而那个验证只需要两个最小镜像。

真正的谨慎不是停在原地,是把要验的那一层单独拎出来验。整套跑不动,不代表那一层验不了。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

qa.yml 的四处待改动:摊开对照 + 一个具体的合并顺序

我连着三轮在汇报里记「qa.yml 有 N 处待改动,需要协调」,却没给过协调方案。这条把它做掉。

main 上的现状

.github/workflows/qa.yml   107 行,3 个 job
  12  on:            (pull_request.paths 与 push.paths 两份清单)
  44  jobs:
  45    agent-network-unit
  62    agent-node-unit
  79    qa

四处改动各自动了哪里

来源 paths 清单 job 主体
#803 两份各 +4 行 第 76 行插入 80 行(recovered-suites,落在 qa: 之前)
#843 两份各 +4 行 文件末尾 +22 行(doc-source-pins)
#846 两份各 +3 行 文件末尾 +22 行(doc-claims)
#31 的接线建议 尚未成 PR

冲突面:只在 paths 清单,而且是机械并集

三者都追加在同一个锚点之后:

      - 'tests/test746-setup-bun-pin/**'      ← 锚点,两份清单里各出现一次
+     …#803 的 4 行 / #843 的 4 行 / #846 的 3 行…

git 会在这两处报冲突,解法是保留全部三块 —— 没有语义取舍,顺序也不影响行为(paths 是集合)。

job 主体方面:#803 插在 qa: 之前,#843#846 都追加在文件末尾。#803 与另外两者不冲突;#843#846 都在末尾,会冲突,但同样是「两个 job 都留下」。

建议的合并顺序

1. #803   (主体最大、插入位置最特殊,先落省得后面反复 rebase)
2. #843   (末尾追加 doc-source-pins;paths 取并集)
3. #846   (末尾追加 doc-claims;paths 取并集)

每一步的冲突解法都是并集,不需要判断谁对谁错。

⚠️ #843 还有一个非 qa.yml 的合并动作项:它引入的 docs/doc-source-pins-baseline.txt#810 / #834 耦合(那两个 PR 会让基线里的条目失效)。详见 #843 上的那条测量 —— 合完跑 python3 scripts/check-doc-source-pins.py . --write-baseline 即可。

顺带:我自己漏的一步,这轮补上了

建 test831 时我写过「没接进 CI 的门只是装饰」。下一轮建 test846 时,我自己就没接。是做这次对照、逐个查各 PR 改了什么时发现的 —— 不是别人提的。已补(46aef155),形状与 #843 一致。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

🔴 更正:我上一条给的 qa.yml 冲突解法是错的,而且错得很隐蔽

上一条我写「解法是保留全部三块 —— 没有语义取舍」。我这轮把它干跑了一遍,那句话是错的,并且照做会产出一个看起来正常、实际有一个 job 永远不执行的 CI 文件。

干跑怎么做的

origin/main 起一个临时分支,按我建议的顺序依次合 #843 → #844 → #845 → #846:

合 ci/doc-source-pin-guard        冲突 0
合 docs/mcp-tools-symbol-anchors  冲突 0
合 docs/mcp-tools-inline-anchors  冲突 0
合 docs/stale-issue-review        冲突 1  → .github/workflows/qa.yml

前三步零冲突这部分预测是对的(它们是一条链)。问题出在第四步的解法。

git 给的是 4 个冲突块,不是 1 个

#1 paths     ours 4 行 / theirs 3 行
#2 paths     ours 4 行 / theirs 3 行
#3 job-body  ours 2 行 / theirs 2 行     ← 两个 job 的**头部**
#4 job-body  ours 14 行 / theirs 13 行   ← 两个 job 的**主体**

对 ①② 取并集是对的(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 全在这里)

实测解析结果:

doc-source-pins    runs-on=None        steps=0
doc-claims         runs-on=ubuntu-latest  steps=5

这个文件是合法 YAML。 GitHub 会接受它,doc-source-pins 会作为一个什么都不做的 job 出现在检查列表里 —— 名字在、绿灯亮、门从没跑过。

我试着写「正确解法」,第二次错得更明显

我又写了一版脚本,把 ③ 的两个头分别与 ④ 的两个体重新配对。产出的 YAML 直接解析失败(expected <block end> at line 124)。

🔴 但最该记的是这一条

上面两版坏文件,我都提交了,然后 test831 与 test846 都退出 0。

因为这两道门根本不看 qa.yml。实测全仓:

.github/ 下的 workflow 与脚本、scripts/、tests/  中
  yamllint / actionlint 命中 = 0 个文件

没有任何东西会在合并前校验 qa.yml 自身。 一个坏掉的 workflow(不论是解析失败还是 job 空转)会一路走到 main,而所有现有的门都是绿的。

更正后的建议

  1. paths 两块:取并集,顺序无关(这部分我原来的说法成立)。
  2. job 主体两块:不能逐块并集。 必须把属于同一个 job 的碎片重新拼成完整块,再依次放置。这需要人看着改,脚本化容易出上面两种错的任意一种。
  3. 合完之后至少跑一次 yaml.safe_load 并断言每个 job 都有 runs-on 与非空 steps —— 光"能解析"不够,steps=0 也能解析。

顺带一个我认为该做但没做的

这个仓里没有任何 workflow 语法/结构校验。加一个很便宜(一个 actionlint 或十行 python 断言),而它能挡住的正是「job 名字在、但 steps 为空」这类假绿。我没有直接加,因为 qa.yml 现在有 5 个 PR 在改,再塞一个改动进去只会让协调更难 —— 建议等这批合完再加。

干跑用的三个临时分支与 worktree 已全部删除,没有推送任何东西

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

上一条我说「这需要人看着改」,但没给样子。这条给了 —— 附根因

根因:git 的「公共上下文」可以在语义上属于双方

冲突区的实际结构是这样的:

<<<<<<< HEAD
  doc-source-pins:
    name: doc source-pin floor (Docker)
=======
  doc-claims:
    name: doc claim freshness (Docker)
>>>>>>>
    runs-on: ubuntu-latest          ← git 认为这是公共上下文,只保留一份
    timeout-minutes: 8
    steps:
      - uses: actions/checkout@v4

<<<<<<< HEAD
      …test831 的 build/run 步骤…
=======
      …test846 的 build/run 步骤…
>>>>>>>

中间那 5 行在两个 job 里逐字相同,所以 git 把它判成公共上下文、只留一份。但它语义上属于两个 job 各一份

这就是盲目并集必然坏掉的原因:并集只处理 <<</===/>>> 之间的内容,而错在那之外 —— 它把一份共享文本留给了后面那个 job,前面那个 job 于是只剩一行 name

正确解法

每个 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 的步骤…

paths 那两块仍然是取并集(这部分我原来的说法成立)。

验证(同一个合并点,两种解法对照)

盲目并集      doc-source-pins steps=0   ← 合法 YAML,GitHub 接受,门从不跑
本次解法      doc-source-pins steps=3
              doc-claims      steps=3
              5 个 job 全部 runs-on 非空 + steps 非空
              test831 退出码 0(4 处 MUTATION_RED)
              test846 退出码 0(3 处 MUTATION_RED)

合完请跑这段断言 —— 「能解析」不够

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 为空"

坏掉的那版解析是通过的,只有这两条断言能拦住它。

干跑分支已删,没有推送任何东西

vansin pushed a commit that referenced this pull request Aug 13, 2026
上一轮我给出的是「冲突了怎么解」。这一轮做的是让冲突不发生。

冲突源于所有人都追加在同一处:
  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 上写清楚了,但最好的处理是不让人踩到它。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

上一条给的是「冲突了怎么解」。这一条做的是让冲突不发生(1d00bd1f)

我前两轮在这里写了根因(git 把两个 job 共享的 runs-on/steps:/checkout 判成公共上下文只留一份)和可抄的正确解法。但最好的处理不是把坑说清楚,是不让人踩到它

冲突的来源:所有人都挤在同一个位置

paths   #798 #801 #803 #843 #846  五个 PR 全部追加在 - 'tests/test746-setup-bun-pin/**' 之后
job     #798/#801 插在第 59 行     #803 插在 qa: 之前
        #843 追加在文件末尾        #846 也追加在文件末尾   ← 撞的就是这里

改动:把 #846 挪到没人用的位置

paths   改插到 - 'server/**' 之后 —— 距离 test746 九行,超出 git 默认的上下文窗口
job     从文件末尾挪到 jobs: 之后

paths 是集合、jobs 是映射,位置变化不改变行为。改完跑了结构断言(每个 job 有 runs-on 与非空 steps)。

干跑结果

#843 → #844 → #845 → #846 合:

合 #846 之后  冲突文件 = 0        (原来是 1,且是危险的 job-body 交织那种)
jobs = doc-claims / agent-network-unit / agent-node-unit / qa / doc-source-pins
结构不合格的 job = 无

再把 #803 也合进来:

冲突块 2 个,归因后 两块都是 #843 × #803,而且都是 paths 条目
  ours   = #843 的 - 'docs-site/**' …
  theirs = #803 的 - 'tests/test224-grok-preview-security/**' …
#846 已完全不参与

剩下的 #843 × #803 是安全的那一类 —— 纯 paths、取并集即可,没有 job 主体交织。

解成并集后的五合一结果(干净树,无冲突标记):

HEAD 脏度 = 0
jobs = doc-claims / agent-network-unit / agent-node-unit / recovered-suites / qa / doc-source-pins
结构不合格的 job = 无
paths pr/push = 21 / 20
test831 退出码 0(4 处 MUTATION_RED)
test846 退出码 0(3 处 MUTATION_RED)

一个我没当证据用的中间结果

第一次跑门时树上还带着冲突标记,两道门照样退出 0 —— 因为它们不读 qa.yml。那次的退出码说明不了任何事,我没有拿它当证据,重新在干净树上跑了一遍。

这正是我在这条 PR 上早先记过的那个盲点:门绿不代表 workflow 文件是好的。

干跑用的临时分支与 worktree 已删,没有推送任何东西。

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

独立深审结论:BLOCKER / DO-NOT-MERGE(exact head 4a00e683…)。

三个 recovered suite 本体均通过,但 exact-head CI 最后的 artifact upload 稳定失败:report-test224.txt 由 root 容器写成 mode 0600,qa.yml 将宿主 artifacts 目录 bind mount 后直接交给 upload-artifact,Actions 原始错误为 EACCES: permission denied, open .../suite-artifacts/report-test224.txt

这不是 cosmetic:PR 的目标是恢复三道长期信号,而当前 job 必红、证据也无法归档。请让容器按 runner uid 写入,或退出后显式修正属主/可读权限,并在同一 exact head 重跑整 job 到绿。修复后还需按 #798 之后 rebase,逐 job 保留完整 YAML 块。

只读审查;未改代码、未 approve/merge/deploy。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

CI 红的根因:不是门失败,是产物上传失败(只读诊断,没改任何东西)

失败 check recovered suites (Docker),失败步骤是 Upload recovered-suite artifacts ——
不是任何一道测试步骤。

先看被测的东西:全过

RESULT: PASS                                                        (×2)
PASS: targeted Docker context contains no host auth/config state
PASS: real child env equals the reviewed set; text boundaries redact; config/session dirs are 0700 …
PASS: candidate tarballs contain runnable entrypoints and force publishConfig.tag=preview
PASS: tarballs, extracted payloads, build output, test output, and report contain zero synthetic marker bytes
Summary: PASS (Docker-only; runtime executed with network disabled; no real credential was read)

这个 PR 要注册的那几道门,跑完了而且是绿的

红在哪

With the provided path, there will be 4 files uploaded
Artifact name is valid!
Root directory input is valid!
Beginning upload of artifact content to blob storage
##[error]An error has occurred while creating the zip file for upload
Error: EACCES: permission denied, open '/home/runner/work/_temp/suite-artifacts/report-test224.txt'

actions/upload-artifact@v4runner 用户身份打包,而
suite-artifacts/report-test224.txtDocker 容器写出来的 —— 属主/权限不匹配,
打开即 EACCES,zip 创建失败,整个 job 判红。

注意 if-no-files-found: warn,而且日志明说「there will be 4 files uploaded」——
所以不是「没找到文件」那种情况,文件在,只是读不了。

修的方向

在 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 写(docker run --user "$(id -u):$(id -g)"),
或用 docker cp 之后显式 chmod。具体取哪种由维护者定 —— 我只定位到 EACCES 这一层。

为什么值得单独说清楚

「CI 红」在 PR 列表里长得都一样。但这条红不代表这个 PR 注册的门有问题:
门全绿,红的是把报告传出来的那一步。如果按「红了就先搁着」处理,
这个 PR 会因为一个和它主张无关的权限问题被无限期挂起。

(顺带:同一批扫描里 #801 也是红的,但那条是真的门失败 ——
TEST798_RUNSH_BLOB 没被传进去,详见我在 #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()`,因为前面步骤红时
更需要把证据传出来。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

复核这条 BLOCKER 是否仍成立(2026-08-14)

判定针对 exact head 4a00e683…,当前 head 已是 134af215,所以逐条重跑了一遍。
先披露:那次权限修复是我推的(134af215),所以第 1 条我不做主观判断,只贴可复验的事实。

第 1 条(artifact upload 因 0600 权限稳定失败)—— 已解决,有产物为证

判定要求「让容器按 runner uid 写入,或退出后显式修正属主/可读权限」。当前 head 的 qa.yml:158-161:

- 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 步骤带 if-no-files-found: warn,
目录是空的它也会绿。所以我直接去查产物,而不是看 job 颜色:

run 31720356471(anet QA (v0),head_sha=134af215cedd)
  产物:recovered-suite-artifacts   6831 字节   expired=false

解包后:
  report-test224.txt   1117 字节     ← 正是当初报 EACCES 的那个文件
  test224.log          1117 字节
  test597.log         14787 字节
  test679.log          3684 字节

report-test224.txt 头部:
  # test224 — Grok preview credential and package gate
  date: 2026-08-13T16:25:04+00:00
  network: disabled by runner

recovered suites (Docker)每一步都是 success,含 Normalize…Upload…

顺带记一个我自己差点栽的坑:我第一次查产物查的是 lint 那个 workflow 的 run(0 个产物),
差点得出"仍未解决"。同一个 head 上有 6 个 run,得挑 anet QA (v0) 那个

第 2 条(在同一 exact head 重跑整 job 到绿)—— 已满足

134af215 上 11 个 check 全部 SUCCESS,含 recovered suites (Docker)
agent-node unitagent-network unit、两个 e2e 与三道 lint 门。

第 3 条(按 #798 之后 rebase)—— 现在无法满足,而且不是本 PR 能单方面做的

git merge-base --is-ancestor pr/798 pr/803  → 否
git merge-tree pr/798 pr/803                → 冲突:.github/workflows/qa.yml

#798 本身还没合进 main,所以"在 #798 之后 rebase"是一条合并顺序要求,
要等 #798 落地才能执行。这一条不该继续挂在本 PR 头上当 DO-NOT-MERGE 的理由 ——
它是 merge queue 的排序问题,建议按 #856 里那份顺序处理。

🔴 顺带发现一条判定没覆盖的:产物里的 source_commit 指向一个仓库里不存在的提交

report-test224.txt:  source_commit=4e3f28e62a6cb2dfe7a7bd887af2cf66e716d154

git cat-file -t 4e3f28e6…                 → 本地不存在
gh api …/commits/4e3f28e6…                → "Merge 134af215… into 034f0064"

成因在 qa.yml:132:

--build-arg SOURCE_COMMIT="$GITHUB_SHA"

pull_request 事件下 GITHUB_SHAGitHub 临时合并提交,不是 PR head。

这不算错:被测的确实是那棵合并树,记它比记 head 更准。但它有两个实际后果:

  1. 拿这个 SHA 去 git show 会失败,除非先 git fetch origin refs/pull/803/merge;
  2. PR 关闭后那个 ref 会消失,归档下来的产物就失去了可解析的溯源锚点。

建议两个都记(改动很小):

--build-arg SOURCE_COMMIT="$GITHUB_SHA" \
--build-arg SOURCE_HEAD_SHA="${{ github.event.pull_request.head.sha || github.sha }}"

push 事件下 GITHUB_SHA 就是真实提交,所以这只影响 PR 上的运行。
这条独立于原判定的三条,不挡合并,但会影响以后拿这些产物当证据时能不能复原现场。

建议

原 BLOCKER 的第 1、2 条已由 134af215 解决且我用产物本体验过;第 3 条是合并顺序,
不是本 PR 的缺陷。建议把判定从 DO-NOT-MERGE 降为「可合,但须排在 #798 之后并解 qa.yml 冲突」。

(只读:未改本 PR、未 approve、未 merge;产物是通过 API 下载到临时目录读的。
第 1 条涉及我自己推的提交,判断请以上面的产物清单和步骤结论为准,不以我的结论为准。)

@vansin

vansin commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

【独立复核结论更新|撤回旧 BLOCKER】

我先前 15:43 的 DO-NOT-MERGE / BLOCKER 结论审的是过期 head 4a00e6833e1cf2c726e7d1b348d8310ff788e889,不再适用于当前 PR。现按远端当前 head 134af215ceddebc14a2f8830b22dc1e0d69ac082 重新核验,结论更新为:CLEAN,技术上可合

独立证据:

  • current head 的 11 个 checks 全部 SUCCESS;
  • 重新下载 Actions run 31720356471recovered-suite-artifacts
  • report-test224.txt 实际存在,1117 bytes,下载后 mode 0644,内容以 Summary: PASS 收口;
  • 4a00e683 → 134af215 的唯一 delta 是 .github/workflows/qa.yml 新增 always-run 的权限归一化:上传前 chown runner uid/gid,并 chmod u+rw。这与真实可下载 artifact 共同证明旧 EACCES 已闭合。

因此:明确撤回旧 BLOCKER / DO-NOT-MERGE。

仍保留一条合并顺序约束:#798#803qa.yml 的 pull_request/push paths 两处有真实文本冲突。应先合 #798,再把 #803 rebase 到更新后的 main,人工保留双方触发路径并重新确认 checks;不要直接按两个旧 head 连续合并。

本评论只更新审查结论;未 merge、未改代码、未部署。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants