Uh oh!
There was an error while loading. Please reload this page.
fix(pm): give ci-failure's transport probe the repo-scoped second stage - #10157
Conversation
Fixes#9966 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
…eError Found by the ablation itself: with stage 2 removed the 5xx case read `unwell.repo.status` off a null and crashed, so the harness never printed the class-4 failure it was there to show. A pin that crashes hides its siblings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
Uh oh!
There was an error while loading. Please reload this page.
os-zhuang
commented
Aug 20, 2026
Dequeued on |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32368087612 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9966
ci-failure.mjsgathered only the account-scoped observation (/rate_limit) and handed it toclassifyTransportProbewith no repo-scoped stage, then walked repo-scoped Actions paths throughrest(). This adopts the second stage #9963 built — per-caller, which is the shape it was built for.The container class is real here, and the card's stated consequence is REFUTED — measured before fixing
The dispatch expected this container to refuse every repo-scoped read. It does not. Measured 2026-08-20, one container, seconds apart:
The refusal is per-repository, not per-session. That does not weaken the card — it sharpens it. A seat pointing this file at a sibling repo (
PM_SWEEP_REPO=objectstack-ai/objectui— the cross-repo task CLAUDE.md describes, where a fix spansframeworkandobjectui) is a live specimen, and the account-scoped reading it green-lights on is byte-for-byte the healthy Routine runner's.What it cost is worse than the card predicted.#9966 expected the exit-3 reading to "degrade into an UNDETERMINED or a raw HTTP number". Measured on
origin/mainat2d3860df9:Node exits 1 on an uncaught exception, and
1in this file's own exit table is RED — "the assertion text was retrieved for EVERY failing check. The output is the answer." A caller branching on$?, which the header instructs it to do, read a transport refusal as a confident verdict about the tree from a container that had not read one byte of it. That is why the fix is a probe stage and not acatcharound the walk: exit 3 has to be reached before anything is read, or the answer is only a politer wrong one.Reverse verification — direction predicted in writing first, then run
--sha, class-4 repo, beforereachable, then uncaught throw, exit 1 (worse than predicted — see above)--sha, class-4 repo, afterPREREQUISITE NOT MET — the transport authenticates but repo-scoped reads are refused--sha, enabled repo, before vs afterGREEN — all 29 check-run(s) completed, none failedboth--self-testwith stage 2 ablatedexpected "repo-scope-refused" / actual "reachable"The no-regression leg is not byte-identical, and the reason is not this change:
51 rows on the sha, 22 supersededbecame53 rows, 24 supersededbecause the live board moved between the two runs. The grouped answer (29 check-runs), the verdict and the exit code are identical.Ablation mechanics. No build or
dist/is involved — this file is executed by path and imports its sibling by relative specifier, so there is noexportsresolution that could serve a stale artifact. The mutation was confirmed on disk each way by anchored grep rather than by an editor's exit code: mutated legABLATION-9966= 1 andif (!needsRepoProbe(account))= 0; restore leg the inverse, plusgit diff --stat= 0 lines against the commit.The ablation also found a defect in the pin itself: with stage 2 removed the 5xx case read
unwell.repo.statusoff anulland crashed, so the harness never printed the class-4 failure it existed to show. A pin that crashes hides its siblings — the assertions are now crash-safe, and that is the second commit.The design fork the card left open — settled by measurement, not by preference
Per-caller, and the deciding evidence is that the two gatherings are already not the same. Stage 1 differs today, on
main:check-half-states.mjsre-probes anonymously whenfirst.status !== 200.ci-failure.mjsre-probes when the status is non-200 orrateLimitRemaining === 0.So "both callers now need the same two stages" is true of stage 2 only. A shared
probeTransportwould have to pick one of those stage-1 policies and silently change the other file's request pattern — unpinning one of the two self-tests' assumptions to remove a duplication that is one function long. The sequencing rule itself is shared:needsRepoProbeis imported, not re-derived.The change
probeRepoRead()— oneGET /repos/{owner}/{repo}. NotGET /user: measured above,/useranswers 200 in the refusing container, so an "is this a real endpoint" probe green-lights the very class this stage names. What must be exercised is the scope.probeTransport()fires stage 2 only when stage 1 already returnedreachable(needsRepoProbe, imported) — exactly the path that used to green without repo-scoped evidence. The three failing classes short-circuit and cost precisely what they cost before; only the previously-green path spends one core request more.--self-testdrives them offline. It opens no socket, as the header promises.nullby design), so theprobe === nullbranch now prints the stage-2 reading too — otherwise the reader sees two healthy/rate_limitlines and no explanation of what failed to classify.Self-test
Nine cases added, none rewritten or weakened.
selfTestbecomesasyncbecause the pin drives the gathering — that is where this file's defect lived, and a pin exercising onlyclassifyTransportProbewould restate #9946's self-test instead of covering this file. The added cases pin the verdict, the request sequencing (['rate:token', 'repo']), the short-circuit for failing classes, the 404 and 5xx branches, the tokenless path, and — the regression pin proper — that the same observations with no repo reading still classify asreachable, which is what makes the class-4 fixture a pin rather than a restatement of the fix.Out of scope, filed not fixed
EXIT_RED. This change removes the class-4 path into it (exit 3 now precedes the walk), but a refusal arriving after the probe passed is untouched, and choosing its exit code (2 vs 3) is a decision this card did not scope. Corroborated twice: a 403 on a refused repo, and a 422 on a sha GitHub has never seen.check-half-states' vocabulary ("the sweep", "the board read"), and this PR routes them to a caller that does neither. The wording lives in the other file, outside this card's declared surface.Neither is addressed here; both remain open.
Gates — run on
9e7aefd, the final commit, each quoted from the gate's own verdict linenode scripts/pm/dispatch-gates.mjswith no args derived the set from the real diff (1 path, merge base2d3860df9) and named exactly the two families the dispatch listed — no additions.node scripts/check-cross-package-test-inputs.mjs—OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.(exit 0)node scripts/check-nul-bytes.mjs—check-nul-bytes: OK (scanned 6066 text file(s) -- 6066 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).(exit 0)node scripts/pm/check-half-states.mjs --self-test—✓ check-half-states self-test: 469 cases pass.(exit 0) — not path-derived; run because this change now importsneedsRepoProbefrom it.node scripts/pm/ci-failure.mjs --self-test—OK self-test: … the transport probe runs its two stages in order …(exit 0)Every exit code was captured before any pipe, inside the locked command. The tree was byte-identical to
9e7aefdwhen they ran.Labelled
skip-changeset:scripts/pm/**is internal PM tooling and publishes nothing (#9963 / #9945 precedent).Generated by Claude Code