fix(ci): make autofix busy detection fail closed and mark dispatched PRs - #9329
Conversation
Reviews on merged/closed PRs have nothing to address, yet each one started an autofix run that spun up a runner only to exit no-op. Observed 2026-08-16: 24+ finding-reply reviews on merged QwenLM#9222 and 26 runs on merged QwenLM#9189 within minutes (issue QwenLM#9296). Add a PR open-state clause to the route prefilter; the scheduled scan remains the backstop, and address-time revalidation already drops targets whose PR closed after dispatch.
Silent API failures in the busy-PR enumeration re-dispatched PRs whose address legs were already running or queued (issue QwenLM#9296): each duplicate burned one build-cli (~5 min) before cancelling a queued sibling leg through the per-PR group's latest-wins queue. - Any enumeration failure (run list or per-run jobs view) now empties the scan's candidate set for this pass; a forced dispatch keeps its explicit-override semantics. - Stamp a pending commit-status marker (qwen-autofix/dispatch-pending) on the PR head at dispatch and treat it as busy while fresher than 30 minutes; the address leg re-stamps it success on checkout. This covers the scan->build-cli window where the matrix leg does not exist in the live-run jobs view yet. Commit statuses only: the check-run creation API needs a GitHub App, and the workflow authenticates with a PAT. Refs QwenLM#9296
|
Re-run after the autofix loop pushed Template looks good ✓ Problem: observed, not theoretical. Dated evidence from 2026-08-16 (scans at 11:27Z/11:37Z re-dispatched #9255/#9027 while their address legs had been running/queued for 3–12 minutes), quantified cost (one build-cli burned per duplicate, cancelled sibling legs via the latest-wins queue feeding the 59% cancellation rate), tracked as the first P1 item of #9296. The round-2 commit does not change the problem statement. Direction: aligned. This is the repo's own autofix fleet burning CI minutes on duplicate dispatches, and the change stays scoped to the workflow's busy-detection path. The design note on commit statuses vs check-runs (PAT gets HTTP 403 on check-run creation) shows the alternatives were actually evaluated. Internal CI tooling — no external CHANGELOG reference applies; the area is squarely relevant. Size: not applicable — no core-module paths touched. Production logic is the workflow file (+165/−27 including round 2); the other file (+434/−2) is the workflow's contract-test suite and is excluded from size accounting. Well under every advisory threshold. Approach: scope still right, and round 2 is strictly review-driven — it adds exactly the fixes the first passes asked for (the Risk: no elevated risk signals — no high-risk paths matched. The fail-closed tradeoff (one skipped cron tick per transient API hiccup) remains bounded and cheaper than a duplicate leg. Moving on to code review. 🔍 中文说明autofix 循环推送 模板完整 ✓ 问题:已观测到,非理论性问题。带日期的证据(2026-08-16,11:27Z/11:37Z 的 scan 在 #9255/#9027 的 address leg 已运行/排队 3–12 分钟时重复派发),代价已量化(每个重复 leg 浪费一次 build-cli,latest-wins 队列取消兄弟 leg 推高 59% 取消率),属于 #9296 的第一个 P1 项。第二轮提交未改变问题陈述。 方向:对齐。这是仓库自身 autofix fleet 在重复派发上浪费 CI 时间,改动范围限于工作流的 busy 检测路径。commit status 与 check-run 的设计说明(PAT 创建 check-run 返回 403)表明确实评估过备选方案。内部 CI 工具,不适用外部 CHANGELOG 参照;该领域完全相关。 规模:不适用——未触及核心模块路径。生产逻辑为工作流文件(+165/−27,含第二轮);另一文件(+434/−2)是工作流契约测试套件,不计入规模。远低于所有提示阈值。 方案:范围依然合理,且第二轮完全由评审驱动——恰好补齐了前几轮要求的修复( 风险:无升级风险信号——未命中高风险路径。fail-closed 的代价(一次瞬时 API 故障跳过一轮 cron tick)有上限,且比重复 leg 便宜。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 |
Code reviewThis is the second pass; the first reviewed Verified at
Remaining items — all non-blocking, all logged for human follow-up (the autofix loop is in critical-only mode and correctly will not touch them):
Marker lifecycle with the TTL-exemption path: sequenceDiagram
participant P1 as Fleet scan
participant P2 as Commit status on PR head
participant P3 as build-cli
participant P4 as Address leg
P1->>P2: stamp PENDING at dispatch
Note over P1,P4: visibility window - leg not yet in jobs view
P1->>P3: emit target, build CLI bundle
P3->>P4: matrix leg materializes
P4->>P2: re-stamp SUCCESS at checkout or discard
Note over P2: after the 30m TTL the marker blocks nothing anywhere
TestingUnattended CI run — no PR code was built or executed here; the evidence below is the PR's own CI results fetched via the API. tmux: N/A — workflow-only change with no product surface. The yamllint/shellcheck/stubbed-gh harness results in the description are the author's local verification, attributed as such; the CI evidence for the contract suite is the green CI at the reviewed commit is fully settled and all green — both Sandboxed lanes: a
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 中文说明代码审查:这是第二次审查;第一次审查 已在 遗留项(均不阻塞,均已记录待人工跟进):① leg 未物化时滞留的 PENDING status 不会从 PR 页面清除(状态不过期,仅视觉残留;派发正确性不受影响,TTL 与门豁免都会使其老化)。② 元数据获取失败的丢弃无法触发释放(两个守卫输入都不可知,设计上 fail-closed;代价是 30 分钟 TTL 自愈的有界跳过)。③ 生命周期测试钉住跨点一致性但写入点仅钉形状——突变分析表明 state 翻转/错误 sha 变量/丢失 continue 可通过套件;现代码正确,属测试加固。④ 显式 workflow_dispatch 撞上新鲜滞留标记需等 TTL——与既有 busy 跳过行为一致且自愈。⑤ 卫生:body 仍写"两部分"而 diff 含三处改动,路由门已在工作流内注释。 测试:无人值守 CI 运行,未构建或执行 PR 代码;证据为经 API 获取的 PR 自身 CI 结果。tmux 不适用(纯工作流改动,无产品界面)。描述中的本地验证结果仅作转述;契约套件的 CI 证据是下方绿色的 ubuntu Test 检查(含三个新行为回放测试)。被审 commit 的 CI 已全部完成且全绿——两个 pull_request 事件工作流运行均成功,零失败零待运行;macOS/Windows 测试腿按 fork PR 矩阵限制跳过。沙箱通道:本次 triage 触发的 /verify 正在运行并将自行发布报告,但其对比的 CLI 构建产物与本 diff 无关;scan 循环只能合并后经 fleet 表观测,故直接说明实证路径而不指向无法运行此代码的通道。 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — every finding from the first pass is resolved in round 2, including the stranded-marker Critical, with regression tests that fail against the pre-fix branch; what remains is known-bounded, self-healing, and logged for human follow-up. Stepping back: this PR is in better shape than the head the first pass reviewed. The one real reservation back then — the orphan window actually running on the 330-minute stale-check clock instead of the promised 30 minutes — is fixed with an exemption that is exactly the marker's own context, and the fix was reproduced before it was written (the replay returns the blocking value on the pre-round branch where the test asserts the released one). Every edge I chased at the new head degrades the same way: a stranded status lingers visually on the PR page but can't block dispatch; a failed metadata fetch strands the marker but the TTL releases it; an explicit dispatch into a fresh marker waits at most one TTL, same as the existing busy skip. The failure budget everywhere is "skip one scan tick", never "duplicate dispatch". The remaining nits — the body still saying "two parts", the stamp-site test pins that would survive a state-flip mutation, the follow-up on the shepherd duplication — are recorded above and in the review thread; none is worth another round trip on a production CI workflow where the human review already landed (a maintainer approval stands on this head). The fork-refactor guardrail does not apply (fix, not refactor), and the author is a maintainer. CI is fully settled and green on the reviewed commit, so no deferral. Approving pinned to 中文说明评分 4/5:第一轮的所有发现均已在第二轮解决,包括滞留标记 Critical,且带有对修复前分支失败的回归测试;遗留项均为已知有界、可自愈、已记录待人工跟进。 退一步看:这个 PR 的状态好于第一轮审查的 head。当时唯一的实质性保留意见——孤儿窗口实际运行在 330 分钟 stale 时钟而非承诺的 30 分钟——已通过恰好限定为标记自身 context 的豁免修复,且修复先复现后落码(回放对修复前分支返回阻塞值而测试断言放行值)。新 head 上追查的每个边缘都以同样方式降级:滞留 status 仅在 PR 页面视觉残留、不阻塞派发;元数据获取失败使标记滞留但 TTL 会放行;显式派发撞上新鲜标记最多加等一个 TTL,与既有 busy 跳过一致。所有失败预算都是"跳过一轮 scan tick",而非"重复派发"。 遗留的小问题——body 仍写"两部分"、打标记点测试可被 state 翻转突变通过、shepherd 重复的后续跟进——已记录在上文与评审线程中;对于一个已有人工评审落地的生产 CI 工作流,这些不值得再走一轮。fork-refactor 护栏不适用(fix 而非 refactor),作者为维护者。被审 commit 的 CI 已全部完成且全绿,无需推迟。批准固定于 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: reverse audit — stopped before round 6 by the review time budget.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:反向审计——评审时间预算不足,未能开始第 6 轮。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| # the leg does not exist in the live-run jobs view, so an overlapping scan | ||
| # would re-dispatch the same PR. The scan treats a PENDING marker fresher | ||
| # than DISPATCH_STATUS_TTL_MINUTES as busy (a run that dies before the leg | ||
| # materializes leaves a marker that expires by age). Commit statuses only — |
There was a problem hiding this comment.
[Critical] A stranded dispatch-pending PENDING marker is additionally caught by the pre-existing HAS_PENDING_CHECKS gate (~line 3067), which ages pending entries out with PENDING_STALE_MIN=330 — so the marker blocks the PR for up to ~330 minutes, not the 30-minute TTL documented here ("expires by age"). StatusContext entries carry no .workflowName (they pass the != "Qwen Autofix" carve-out) and no .name (not in NON_BLOCKING_CHECKS=["review-pr"]), and their startedAt stays inside the 330-minute PENDING_CUTOFF. The diff never exempts its own status context from that gate — Failure scenario: a scan stamps PENDING, the run dies before the leg materializes (build-cli failure/cancellation); after 30 min the new TTL check correctly passes, but HAS_PENDING_CHECKS still matches and every scan skips the PR as "active checks in flight" for up to ~330 min (self-heals only at 330 min or a moved head). The block covers scheduled AND forced scans (no FORCED_PR exemption at that gate), and /retry cannot clear a commit status, so there is no in-band lever.
Witness (live probe on this PR's head):
with a PENDING probe status present: the workflow's exact HAS_PENDING_CHECKS filter
returned the entry -> HAS_PENDING_CHECKS=true
age-threshold arm (captured gh StatusContext shape):
marker age=29m | new-30m-marker-check-fires=yes | HAS_PENDING_CHECKS=true
marker age=60m | new-30m-marker-check-fires=no | HAS_PENDING_CHECKS=true
marker age=331m | new-30m-marker-check-fires=no | HAS_PENDING_CHECKS=false
Suggested fix — exempt the marker's context from HAS_PENDING_CHECKS (NON_BLOCKING_CHECKS cannot express this: it matches .name, which StatusContext entries lack):
# in the HAS_PENDING_CHECKS jq: add --arg ctx "${DISPATCH_STATUS_CONTEXT}"
| select((.context // "") != $ctx)中文说明
【严重】 滞留的 dispatch-pending PENDING 标记还会被既有的 HAS_PENDING_CHECKS 门(约 3067 行)命中:该门用 PENDING_STALE_MIN=330 作为过期阈值——因此标记会把 PR 阻塞最长约 330 分钟,而不是此处文档承诺的 30 分钟("expires by age")。StatusContext 条目没有 .workflowName(能通过 != "Qwen Autofix" 豁免),也没有 .name(不在 NON_BLOCKING_CHECKS=["review-pr"] 中),且其 startedAt 在 330 分钟的 PENDING_CUTOFF 之内。diff 没有把自身的 status context 从该门中排除——失败场景:scan 打上 PENDING 后 run 在 leg 物化前死亡(build-cli 失败/被取消);30 分钟后新的 TTL 检查正确放行,但 HAS_PENDING_CHECKS 仍然命中,之后每轮 scan 都会以 "active checks in flight" 跳过该 PR,最长约 330 分钟(只在 330 分钟到期或 head 移动时自愈)。该阻塞对定时 scan 和 forced scan 都生效(该门没有 FORCED_PR 豁免),且 /retry 无法清除 commit status,因此没有任何带内解法。
证据(在本 PR head 上的 live 探针):存在 PENDING probe status 时,工作流原样的 HAS_PENDING_CHECKS 过滤器返回该条目(HAS_PENDING_CHECKS=true);年龄阈值实验(用捕获的真实 gh StatusContext 形态):标记 29 分钟 → 新 30 分钟检查命中且 HAS_PENDING_CHECKS=true;60 分钟 → 30 分钟检查放行但 HAS_PENDING_CHECKS=true;331 分钟 → HAS_PENDING_CHECKS=false。
建议修复:把标记的 context 从 HAS_PENDING_CHECKS 中排除(NON_BLOCKING_CHECKS 表达不了:它匹配的是 StatusContext 没有的 .name)——在该门的 jq 中加 --arg ctx "${DISPATCH_STATUS_CONTEXT}" 和 | select((.context // "") != $ctx)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| if [[ -z "${FORCED_PR}" ]]; then | ||
| CANDIDATES='' |
There was a problem hiding this comment.
[Suggestion] The fail-closed exemption is broader than its stated justification: FORCED_PR is set for every trusted pull_request_review scan too (route emits pr_number for them; the job-level comment ~line 3330 documents FORCED_PR "is ALSO set for every trusted pull_request_review … which is not an explicit dispatch"), so event-driven review scans stay fail-open on enumeration failure, while the added FAIL-CLOSED comment says any enumeration failure empties THIS scan's candidate set — Failure scenario: an enumeration failure (rate limit / 5xx — the condition that produced the 2026-08-16 duplicates) during a trusted-review-event scan leaves BUSY_PRS empty; if the in-flight leg already materialized and re-stamped the marker SUCCESS, the review-event scan re-dispatches the same PR — one wasted build-cli (~5 min) plus a cancelled queued sibling via latest-wins, the exact duplicate this PR closes for scheduled scans. Bounded, but on a path the PR title claims to close.
Suggested fix — narrow the exemption to genuine explicit dispatches; in-file precedent exists (the cap-refused gate ~line 3365 already splits on EVENT_NAME):
if [[ -z "${FORCED_PR}" || "${EVENT_NAME}" != 'workflow_dispatch' ]]; then
CANDIDATES=''
fi(or document in the FAIL-CLOSED comment that event-driven scans deliberately remain fail-open)
中文说明
【建议】 fail-closed 豁免比其声明的理由更宽:FORCED_PR 对每个受信任的 pull_request_review scan 同样会被设置(route 会为它们输出 pr_number;约 3330 行的 job 级注释明确写道 FORCED_PR "is ALSO set for every trusted pull_request_review … which is not an explicit dispatch"),因此事件驱动的 review scan 在枚举失败时仍是 fail-open,而新增的 FAIL-CLOSED 注释声称任何枚举失败都会清空本轮 scan 的候选集——失败场景:受信任 review 事件 scan 期间发生枚举失败(限流/5xx——正是 2026-08-16 产生重复派发的条件),BUSY_PRS 为空;若在途 leg 已物化并把标记重打为 SUCCESS,review 事件 scan 会重复派发同一 PR——浪费一次 build-cli(约 5 分钟)并经 latest-wins 取消排队的兄弟 leg,正是本 PR 为定时 scan 消除的那种重复。有界,但发生在 PR 标题声称要关闭的路径上。
建议修复:把豁免收窄到真正的显式派发;文件内已有先例(约 3365 行的 cap 拒绝门已经用 EVENT_NAME 做了同样的拆分):if [[ -z "${FORCED_PR}" || "${EVENT_NAME}" != 'workflow_dispatch' ]]; then CANDIDATES=''; fi(或在 FAIL-CLOSED 注释中说明事件驱动 scan 有意保持 fail-open)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| done <<< "$(sort -u <<< "${LIVE_RUNS}")" | ||
| fi | ||
| if [[ "${BUSY_ENUM_OK}" != '1' ]]; then | ||
| echo "::warning::busy-PR enumeration failed (run list or jobs view unreadable) — failing closed: no scan targets dispatched this pass" |
There was a problem hiding this comment.
[Suggestion] Fail-closed emptying emits has_targets=false with exit 0, which downstream is indistinguishable from "no PR needs work" — a transient enumeration failure can flip the scheduled issue phase on against the workflow's declared ordering ("issue phase only if no PR needs work") — Failure scenario: on a schedule run, gh run list/gh run view fails; CANDIDATES='' → TARGETS=[] → has_targets=false, but the step still exits 0, so issue-autofix's clause needs.review-scan.result == 'success' && needs.review-scan.outputs.has_targets != 'true' (~line 753) passes and route sets DO_ISSUE=true on schedule — the bot claims a ready issue and starts an up-to-180-minute agent run while PRs sit with unaddressed review feedback. The old fail-open code never emptied CANDIDATES, so enumeration failure previously still yielded has_targets=true and suppressed the issue phase; this diff introduces the inversion.
Witness (probe of the verbatim extracted scan step, stubbed gh, schedule env):
fail arm (gh run list exit 1): ::warning, exit code 0, targets=[], has_targets=false
ok arm (same canned fleet): has_targets=true, targets=[{"pr":"101",...}], PENDING stamped
-> flips on enumeration health alone
Suggested fix — emit a distinct signal on the fail-closed branch and gate the issue phase on it:
# next to the warning:
echo "enum_failed=true" >> "${GITHUB_OUTPUT}"
# and in issue-autofix's if: (and its concurrency predicate ~line 793):
&& needs.review-scan.outputs.enum_failed != 'true'(or exit 1 on enumeration failure, which the existing result == 'success' conjunct already treats as issue-phase-suppressing)
中文说明
【建议】 fail-closed 清空候选集会以 exit 0 输出 has_targets=false,下游无法区分它与 "没有 PR 需要处理"——一次瞬时枚举失败可能把定时 issue 阶段翻转开启,违反工作流声明的顺序("issue phase only if no PR needs work")——失败场景:schedule run 中 gh run list/gh run view 失败;CANDIDATES='' → TARGETS=[] → has_targets=false,但 step 仍以 0 退出,于是 issue-autofix 的条件 needs.review-scan.result == 'success' && needs.review-scan.outputs.has_targets != 'true'(约 753 行)通过,route 在 schedule 上置 DO_ISSUE=true——bot 认领一个 ready issue 并启动最长 180 分钟的 agent run,而 PR 的 review 反馈无人处理。旧的 fail-open 代码从不清空 CANDIDATES,枚举失败时仍会得到 has_targets=true 并抑制 issue 阶段;这个反转是本 diff 引入的。
证据(对原样提取的 scan step 做探针,stub gh、schedule 环境):失败臂(gh run list exit 1)→ ::warning、exit 0、targets=[]、has_targets=false;正常臂(同一模拟 fleet)→ has_targets=true、targets=[{"pr":"101",...}]、打上 PENDING——仅随枚举健康状态翻转。
建议修复:在 fail-closed 分支输出独立信号并让 issue 阶段对其设门:在 warning 旁 echo "enum_failed=true" >> "${GITHUB_OUTPUT}",并在 issue-autofix 的 if:(及约 793 行的并发谓词)中加 && needs.review-scan.outputs.enum_failed != 'true'(或在枚举失败时 exit 1,既有的 result == 'success' 合取项已把它视为抑制 issue 阶段)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| gh api "repos/${REPO}/statuses/${CHECKED_OUT_HEAD}" -X POST \ | ||
| -f state="success" -f context="${DISPATCH_STATUS_CONTEXT}" \ |
There was a problem hiding this comment.
[Suggestion] The release stamp runs only after checkout; the prepare step's three eligibility-discard exits (4568 INELIGIBLE ladder, 4596 fork fetch failure, 4620 fork push preflight) all precede it, and a repo-wide grep shows exactly two statuses/ call sites (3698 stamp, 4640 release) — so every deliberate same-repo discard strands the dispatch-pending marker at PENDING for the full 30-minute TTL with no leg in existence — Failure scenario: a scan dispatches a same-repo PR and stamps PENDING; while the leg sits queued, a transient gh pr view API error hits the address-time recheck (PR_LIVE='{}' → INELIGIBLE "metadata fetch failed (API error) — fail-closed"), or a maintainer applies autofix/skip, or the base/branch changes. Prepare exits 0 before checkout, nothing releases the marker, and the next scan's marker check skips the still-valid PR as "dispatch pending" for up to 30 minutes — contradicting the discard path's own documented promise at 4511-4513 ("fail closed — the next scan re-emits a still-valid target"). Until the Critical above is fixed, the same stranded marker additionally blocks via HAS_PENDING_CHECKS for up to ~330 minutes.
Witness: not run — cross-run timed scenario against live GitHub state; settled by complete static enumeration: exits 4568/4596/4620 all precede the only release site (4640); grep 'repos/${REPO}/statuses/' across all workflows returns only 3698 (stamp) and 4640 (release); the discard-path re-emit promise sits at 4511-4513.
Suggested fix — release on the discard path too: add headRefOid to the PR_LIVE --json list and, before the INELIGIBLE exit 0, best-effort stamp success on it for same-repo heads (guard with LIVE_XREPO == "false", mirroring the dispatch-side HEAD_REPO_FULL == REPO guard, || true like this site).
中文说明
【建议】 release 重打只在 checkout 之后运行;prepare step 的三个资格丢弃出口(4568 INELIGIBLE 阶梯、4596 fork fetch 失败、4620 fork push 预检)都在它之前,且全仓库 grep 显示 statuses/ 恰有两个调用点(3698 stamp、4640 release)——因此每一次同仓库的主动丢弃都会把 dispatch-pending 标记滞留在 PENDING 状态,持续整个 30 分钟 TTL,且不存在任何 leg——失败场景:scan 派发一个同仓库 PR 并打上 PENDING;leg 排队期间,一次瞬时的 gh pr view API 错误命中 address-time 复查(PR_LIVE='{}' → INELIGIBLE "metadata fetch failed (API error) — fail-closed"),或维护者打上 autofix/skip,或 base/分支变化。prepare 在 checkout 前以 0 退出,没有任何东西释放标记,下一轮 scan 的标记检查会把仍然有效的 PR 以 "dispatch pending" 跳过最长 30 分钟——与丢弃路径自己在 4511-4513 行文档化的承诺("fail closed — the next scan re-emits a still-valid target")相矛盾。在上面的 Critical 修复之前,同一滞留标记还会经 HAS_PENDING_CHECKS 额外阻塞最长约 330 分钟。
证据:未运行——跨 run 的定时场景依赖 live GitHub 状态;以完全静态枚举定案:出口 4568/4596/4620 全部位于唯一 release 点(4640)之前;跨所有工作流 grep repos/${REPO}/statuses/ 只返回 3698(stamp)与 4640(release);丢弃路径的"下一轮重新发出"承诺在 4511-4513 行。
建议修复:丢弃路径也释放标记——把 headRefOid 加进 PR_LIVE 的 --json 列表,在 INELIGIBLE exit 0 之前对同仓库 head 尽力重打 success(用 LIVE_XREPO == "false" 做守卫,镜像 dispatch 侧的 HEAD_REPO_FULL == REPO 守卫,|| true 同此处)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| if [[ "${HEAD_REPO_FULL}" == "${REPO}" ]]; then | ||
| gh api "repos/${REPO}/statuses/${LIVE_HEAD}" -X POST \ |
There was a problem hiding this comment.
[Suggestion] Both new commit-status writes bypass the file's own DRY_RUN write discipline: a workflow_dispatch with dry_run: true stamps real, PR-visible PENDING statuses. Every other write in the scan step is DRY_RUN-guarded (forced-takeover blocked report ~2476, deferred-review ack ~3094, cap-paused notice/label ~3436, stale-base update ~3549) — Failure scenario: a maintainer fires a dry-run dispatch to observe the loop; the scan stamps a real PENDING status on every selected same-repo head. Normally the dry-run leg's prepare step releases it — but review-address needs build-cli without always(), so if build-cli fails on the dry run, all legs are skipped and nothing ever releases the stamps: real scans then skip those PRs for ≤30 min via the marker check and, via the gate interaction reported above, up to ~330 min — a test mode that was supposed to touch nothing blocks real dispatches for hours.
Witness: not run — the load-bearing residual is Actions' needs-failure skip semantics and live commit-status effects; verified statically at the reviewed commit: review-address if: is has_targets == 'true' only (no always()), the two statuses/ writes are the only ones in the repo, and every comparable write in the scan step carries a DRY_RUN guard.
Suggested fix — guard both stamps:
if [[ "${DRY_RUN}" != "true" && "${HEAD_REPO_FULL}" == "${REPO}" ]]; then(the prepare step's env currently lacks DRY_RUN — add DRY_RUN: '${{ needs.route.outputs.dry_run }}' there; skipping the stamp in dry runs degrades duplicate protection to the pre-PR baseline the comment below names as the surviving fallback)
中文说明
【建议】 两处新的 commit-status 写入都绕过了文件自身的 DRY_RUN 写入纪律:dry_run: true 的 workflow_dispatch 会打上真实、PR 可见的 PENDING status。scan step 的其他所有写入都有 DRY_RUN 守卫(forced-takeover 拦截上报约 2476、deferred-review ack 约 3094、cap 暂停通知/标签约 3436、过期 base 更新约 3549)——失败场景:维护者发起 dry-run 派发以观察循环;scan 会给每个选中的同仓库 head 打上真实 PENDING status。通常 dry-run leg 的 prepare step 会释放它——但 review-address 依赖 build-cli 且没有 always(),所以 dry run 中 build-cli 一旦失败,所有 leg 都被跳过,没有任何东西释放这些 stamp:真实 scan 随后会经标记检查跳过这些 PR ≤30 分钟,再经上面报告的 gate 交互最长约 330 分钟——一个本应不产生任何副作用的测试模式阻塞真实派发长达数小时。
证据:未运行——承重残留是 Actions 的 needs 失败跳过语义与 live commit-status 效果;在受审 commit 上静态验证:review-address 的 if: 仅有 has_targets == 'true'(无 always()),两处 statuses/ 写入是全仓库仅有的,scan step 中所有可比写入都带 DRY_RUN 守卫。
建议修复:给两处 stamp 加守卫 if [[ "${DRY_RUN}" != "true" && "${HEAD_REPO_FULL}" == "${REPO}" ]]; then(prepare step 的 env 目前没有 DRY_RUN——需加 DRY_RUN: '${{ needs.route.outputs.dry_run }}';dry run 中不打 stamp 会把去重保护降级为下方注释所列的 PR 前基线兜底)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| # already in PR_META. Unlike the in-memory busy skip this runs | ||
| # after the metadata fetch, so it consumes inspection budget; | ||
| # acceptable because the case is rare (a PR dispatched <30m ago). | ||
| if jq -e --arg ctx "${DISPATCH_STATUS_CONTEXT}" --arg cut "${DISPATCH_CUTOFF}" ' |
There was a problem hiding this comment.
[Suggestion] The dispatch-pending marker mechanism — this scan-side skip predicate, the PENDING stamp (~3698), and the SUCCESS re-stamp (~4640) — is pinned by nothing; no test references the context, the TTL, or the statuses/ endpoint (part of the same coverage gap as the comment on the enumeration block above) — Failure scenario: concrete surviving mutations: flipping > $cut to < $cut makes fresh markers pass and stale markers block, silently re-opening the duplicate-dispatch window this PR closes; a stamp/check/release context mismatch leaves the marker permanently unread; the suite pins the same-shaped comparison in the check-staleness filter explicitly because "a flipped comparison … is caught, not just a removed constant", but not here.
Suggested fix — replay the jq predicate over fixture rollups (fresh PENDING → skip; stale PENDING / SUCCESS / missing rollup → pass) and string-pin both stamp calls' state=/context= arguments against DISPATCH_STATUS_CONTEXT so writer/reader identity agreement fails in CI, not in production (the fork-bridge test uses exactly this cross-site pinning pattern).
中文说明
【建议】 dispatch-pending 标记机制——scan 侧这个跳过谓词、PENDING stamp(约 3698)、SUCCESS 重打(约 4640)——没有任何固定;测试中零处引用该 context、TTL 或 statuses/ 端点(与上方枚举块的覆盖缺口同属一处)——失败场景:可存活的具体变异——把 > $cut 翻成 < $cut 会让新鲜标记放行、过期标记阻塞,悄悄重开本 PR 要关闭的重复派发窗口;stamp/check/release 三方 context 不一致会让标记永远无人读取;套件对 check 过期过滤器里同形的比较特意做了固定,理由正是"翻转的比较也要能被抓到,而不只是删常量",但这里没有。
建议修复:用夹具 rollup 回放该 jq 谓词(新鲜 PENDING → 跳过;过期 PENDING / SUCCESS / rollup 缺失 → 放行),并把两处 stamp 调用的 state=/context= 参数与 DISPATCH_STATUS_CONTEXT 做字符串互锁,让写入方/读取方一致性在 CI 里失败而不是在生产里失败(fork-bridge 测试正是这种跨点互锁模式的现成例子)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| -f description="address leg started (run ${GITHUB_RUN_ID})" \ | ||
| > /dev/null 2>&1 || true |
There was a problem hiding this comment.
[Suggestion] Sibling asymmetry: the dispatch-time stamp guards same-repo heads (HEAD_REPO_FULL == REPO), but this leg-side SUCCESS re-stamp — which builds the same API call — carries no equivalent guard, even though the fork idiom exists ~60 lines above ([[ "${HEAD_REPO:-${REPO}}" != "${REPO}" ]] at ~4579) and HEAD_REPO is in scope in this step — Failure scenario: fork legs are reachable (forced fork admission ~2621; scheduled-scan admission of fork PRs). On a fork leg, CHECKED_OUT_HEAD came from the fork, so the POST targets a sha absent from the base repo's object store → 422 on every fork leg, swallowed by || true (one wasted API call each), contradicting the invariant the stamp side documents ("Same-repo heads only get a stamp"). If the fork head commit does exist in the base object store, it instead paints SUCCESS for a context never stamped PENDING there.
Suggested fix — mirror the stamp side:
if [[ "${HEAD_REPO:-${REPO}}" == "${REPO}" ]]; then
gh api "repos/${REPO}/statuses/${CHECKED_OUT_HEAD}" -X POST ...
fi中文说明
【建议】 兄弟不对称:dispatch 侧的 stamp 有同仓库守卫(HEAD_REPO_FULL == REPO),但 leg 侧这个 SUCCESS 重打——构造的是同一个 API 调用——没有等价守卫,而 fork 惯用写法就在约 60 行上方(约 4579 行的 [[ "${HEAD_REPO:-${REPO}}" != "${REPO}" ]]),且 HEAD_REPO 在本 step 作用域内——失败场景:fork leg 可达(forced fork 准入约 2621;定时 scan 对 fork PR 的准入)。fork leg 上 CHECKED_OUT_HEAD 来自 fork,POST 指向 base 仓库对象库中不存在的 sha → 每个 fork leg 都 422,被 || true 吞掉(每次浪费一个 API 调用),与 stamp 侧文档化的不变量("Same-repo heads only get a stamp")矛盾。若 fork head commit 恰存在于 base 对象库,则会为一个从未在该 sha 上打过 PENDING 的 context 涂上 SUCCESS。
建议修复:镜像 stamp 侧——if [[ "${HEAD_REPO:-${REPO}}" == "${REPO}" ]]; then gh api ... fi。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| if [[ "${BUSY_ENUM_OK}" == '1' ]]; then | ||
| while IFS= read -r LIVE_RUN; do | ||
| [[ -z "${LIVE_RUN}" ]] && continue | ||
| if ! BUSY_OUT="$(gh run view "${LIVE_RUN}" --repo "${REPO}" --json jobs \ |
There was a problem hiding this comment.
[Suggestion] The busy-set enumeration this diff rewrites exists near-verbatim in .github/workflows/qwen-fleet-shepherd.yml (~lines 297-323, SHEP_BUSY/BUSY_OK): identical capture("^review-address \\((?<pr>[0-9]+),") regex, identical accumulation into a space-delimited busy string, already fail-closed — now two divergently wrapped copies of the same busy definition (the shepherd copy has no knowledge of the new dispatch-pending window) — Failure scenario: a rename/reformat of the review-address matrix job naming, or a busy-definition change, fixed on one side only silently empties the other side's busy set → re-dispatch of a PR already being addressed, the exact duplicate-build-cli cost (~5 min each) this PR exists to eliminate.
Suggested fix: extract the enumeration into a shared script (e.g. .github/scripts/list-busy-autofix-prs.sh) called from both workflows, or at minimum add cross-referencing comments at both sites so a regex or semantics change lands in both.
中文说明
【建议】 本 diff 重写的 busy 集合枚举在 .github/workflows/qwen-fleet-shepherd.yml(约 297-323 行,SHEP_BUSY/BUSY_OK)里有近乎逐字的副本:相同的 capture("^review-address \\((?<pr>[0-9]+),") 正则、相同的空格分隔 busy 串累积、且已是 fail-closed——现在同一 busy 定义有了两个包装方式已经分叉的副本(shepherd 副本不知道新的 dispatch-pending 窗口)——失败场景:review-address matrix job 命名被改名/改格式,或 busy 定义变更,只修一侧会悄悄清空另一侧的 busy 集合 → 重复派发正在处理中的 PR,正是本 PR 要消除的重复 build-cli 成本(每次约 5 分钟)。
建议修复:把枚举抽成共享脚本(如 .github/scripts/list-busy-autofix-prs.sh)供两个工作流调用,或至少在两处加交叉引用注释,确保正则或语义变更同时落到两侧。
— qwen3.8-max via Qwen Code /review (v0.21.13)
There was a problem hiding this comment.
Verified and deferred to the follow-up queue — the duplication is real: .github/workflows/qwen-fleet-shepherd.yml (~lines 297-323) carries the same capture("^review-address \\((?<pr>[0-9]+),") busy-set enumeration, already fail-closed via BUSY_OK, and it does not know about the new dispatch-pending window. Both suggested fixes (extracting a shared .github/scripts/list-busy-autofix-prs.sh, or adding cross-referencing comments at both sites) require touching the shepherd workflow and/or creating a new shared script — areas this PR has never touched, so implementing them here would expand the round outside the PR's footprint. The follow-up issue tracks it; the thread stays open.
中文说明
已核实并推迟到后续跟进队列——重复属实:.github/workflows/qwen-fleet-shepherd.yml(约 297-323 行)携带相同的 capture("^review-address \\((?<pr>[0-9]+),") busy 集枚举,已通过 BUSY_OK fail-closed,但不知道新的 dispatch-pending 窗口。两种建议修复(抽取共享的 .github/scripts/list-busy-autofix-prs.sh,或在两处加交叉引用注释)都需要触碰 shepherd 工作流和/或新建共享脚本——均为本 PR 从未触碰的区域,在此实现会把本轮改动扩出 PR 足迹。跟进 issue 会跟踪此项;线程保持打开。
| while IFS= read -r LIVE_RUN; do | ||
| [[ -z "${LIVE_RUN}" ]] && continue | ||
| if ! BUSY_OUT="$(gh run view "${LIVE_RUN}" --repo "${REPO}" --json jobs \ | ||
| --jq '.jobs[] | select(.status != "completed") | .name | capture("^review-address \\((?<pr>[0-9]+),") | .pr' 2> /dev/null)"; then |
There was a problem hiding this comment.
[Suggestion] The fail-closed path discards every underlying error detail — both enumeration calls redirect 2> /dev/null — so the warning below cannot say which call failed or why. Transient API instability and PAT degradation produce identical opaque log output and need different responses (wait a tick vs fix the token) — Failure scenario: in a persistent-failure case (PAT expiry/revoked scope, sustained Actions API outage), every scan silently stops dispatching behind the same fixed warning; the oncall responder seeing "autofix stopped working" has no error text in the job log and must manually re-run gh run list / gh run view on the runner to discover the cause.
Suggested fix — capture stderr instead of discarding it (in-file precedent: the route job captures API errors via api_error_file="$(mktemp)" ~line 497 and includes them in its warning), and append the tail to the ::warning:: and fleet_row detail.
中文说明
【建议】 fail-closed 路径丢弃了全部底层错误细节——两处枚举调用都 2> /dev/null——因此下方的 warning 说不出是哪个调用失败、为何失败。瞬时 API 抖动与 PAT 失效产生完全相同的不透明日志,却需要不同的处置(等一轮 tick vs 修 token)——失败场景:持续失败时(PAT 过期/权限被收回、Actions API 持续故障),每轮 scan 都静默停止派发、只留同一句固定 warning;看到 "autofix 不工作了" 的 oncall 在 job 日志里找不到任何错误文本,只能上 runner 手工重跑 gh run list / gh run view 找原因。
建议修复:捕获 stderr 而不是丢弃(文件内先例:route job 约 497 行用 api_error_file="$(mktemp)" 捕获 API 错误并写进 warning),把尾部追加进 ::warning:: 与 fleet_row 详情。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| # already drops targets whose PR closed after dispatch. | ||
| if: |- | ||
| ${{ github.repository == 'QwenLM/qwen-code' && (github.event_name != 'issue_comment' || (github.event.issue.pull_request && (startsWith(github.event.comment.body, '@qwen-code /takeover') || startsWith(github.event.comment.body, '@qwen-code /retry')))) && (github.event_name != 'pull_request' || github.event.label.name == 'autofix/takeover') }} | ||
| ${{ github.repository == 'QwenLM/qwen-code' && (github.event_name != 'issue_comment' || (github.event.issue.pull_request && (startsWith(github.event.comment.body, '@qwen-code /takeover') || startsWith(github.event.comment.body, '@qwen-code /retry')))) && (github.event_name != 'pull_request' || github.event.label.name == 'autofix/takeover') && (github.event_name != 'pull_request_review' || github.event.pull_request.state == 'open') }} |
There was a problem hiding this comment.
[Suggestion] This gate-clause hunk re-implements the P0 route gate already merged into main via PR #9299 (commit 5492009bb2): the branch forked at b744248656 before that merge and carries its own byte-identical copy as commit 3a09e73fad (verified: git show of both commits produces patch-identical changes; main is 24 commits ahead; git merge-tree resolves cleanly). The PR description says "this PR is the busy-detection hardening only" and does not mention the gate — Failure scenario: reviewers spend attention on ~13 already-shipped lines in Files changed, and the branch sits 24 commits behind main. The merge itself absorbs the duplicate cleanly, so the cost is pre-merge: diff noise and misdescribed scope.
Suggested fix: rebase the branch onto current main — patch-id identity drops 3a09e73fad, leaving a 1-commit PR containing only the busy-detection changes.
中文说明
【建议】 这个门子句 hunk 重复实现了已经通过 PR #9299(commit 5492009bb2)合入 main 的 P0 route 门:分支在 b744248656 处分叉,早于那次合并,并以 commit 3a09e73fad 携带了一份字节级相同的副本(已验证:两个 commit 的 git show 补丁完全一致;main 领先 24 个 commit;git merge-tree 干净合并)。PR 描述写的是 "this PR is the busy-detection hardening only",并未提及这个门——失败场景:reviewer 要在 Files changed 里花注意力看约 13 行已发布的内容,且分支落后 main 24 个 commit。合并本身能干净吸收这份重复,所以成本在合并前:diff 噪音与失实的范围描述。
建议修复:把分支 rebase 到当前 main——patch-id 相同会自动丢掉 3a09e73fad,得到一个只含 busy 检测改动的单 commit PR。
— qwen3.8-max via Qwen Code /review (v0.21.13)
There was a problem hiding this comment.
Declined with evidence. The underlying facts are verified: 3a09e73fad on this branch and 5492009bb2 on main (via #9299) share patch-id 5d0a0dba5db3a618cdbe6d3128f42ae7f878dbf8, and git merge-tree --write-tree origin/main HEAD resolves cleanly. But the suggested rebase is history rewriting, which this headless autofix flow forbids (additive commits only). The additive alternative — merging main into the branch — is discouraged when there is no conflict to resolve, and would pull ~32 unrelated commits into a live CI workflow branch to remove ~13 lines of diff noise that the eventual merge absorbs cleanly (patch-id identity drops the duplicate at squash/merge time). The residual cost is pre-merge reviewer attention on already-shipped lines; the PR-description scope note is owned by the workflow's PR-preparation step, which this round cannot rewrite either.
中文说明
以证据拒绝。 底层事实已核实:本分支的 3a09e73fad 与 main 上的 5492009bb2(经 #9299)共享 patch-id 5d0a0dba5db3a618cdbe6d3128f42ae7f878dbf8,且 git merge-tree --write-tree origin/main HEAD 干净合并。但建议的 rebase 属于历史重写,本 headless autofix 流程禁止该操作(只允许追加式提交)。追加式替代方案——把 main 合并进分支——在没有冲突需要解决时不被鼓励,且会把约 32 个无关提交拉进一条生产 CI 工作流分支,只为去掉约 13 行最终合并会干净吸收的 diff 噪音(patch-id 相同会在 squash/merge 时自动丢掉重复提交)。残留成本仅是 reviewer 在合并前要在已发布行上花的注意力;PR 描述的范围说明由工作流的 PR 准备步骤负责,本轮同样无法改写。
Exempt the marker's status context from the HAS_PENDING_CHECKS gate (a stranded marker otherwise blocked the PR for up to ~330 minutes, not the documented 30-minute TTL), release it on the address-time discard path, guard every status write same-repo and dry-run, narrow the fail-closed carve-out to explicit workflow_dispatch dispatches, emit enum_failed so an emptied candidate set cannot flip the scheduled issue phase on, and carry the enumeration error tail in the fail-closed warning. Pin all of it behaviorally in the workflow contract tests.
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Autofix review round — PR #9329Round outcome: 1 Critical + 9 suggestions implemented in code, 1 suggestion deferred to the follow-up queue, 1 suggestion declined with evidence. One commit: Feedback points and dispositionsCritical (resolved in code)
Suggestions implemented
Suggestion deferred to the follow-up queue
Suggestion declined with evidence
Review body
Files changed
VerificationCommands actually run this round, in order:
中文说明Autofix 审查轮次 — PR #9329本轮结果:1 个 Critical + 9 条建议已在代码中实现,1 条建议推迟到后续跟进队列,1 条建议以证据拒绝。 单个提交: 反馈点与处置Critical(已在代码中解决)
已实现的建议
推迟到后续跟进队列的建议
以证据拒绝的建议
审查正文
变更文件
验证本轮实际执行的命令,按顺序:
🧭 Gate advisory — this round modified areas outside the PR footprint (machine-measured, not agent-authored):
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 10.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
.github/workflows/qwen-autofix.yml:317 — [review] New route-gate open-PR clause is pinned by no test.github/workflows/qwen-autofix.yml:2951 — [review] Marker check blocks the explicit workflow_dispatch override lane on a stranded marker.github/workflows/qwen-autofix.yml:2951 — [review] Head move during the build-cli window leaves the new head's rollup marker-less (skip side)
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:反向审计——在 10 轮的反审轮数上限内未收敛。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| # skipped when build-cli fails), and a stranded real PENDING | ||
| # would then block real scans — duplicate protection degrades to | ||
| # those same surviving layers. | ||
| if [[ "${DRY_RUN}" != "true" && "${HEAD_REPO_FULL}" == "${REPO}" ]]; then |
There was a problem hiding this comment.
[Suggestion] A stamped dispatch-pending status is never cleared on GitHub when the leg never materializes: review-address is needs: ['route', 'review-scan', 'build-cli'] with default success-gating, so a build-cli failure skips the leg entirely and neither release site (discard at ~4592, checkout at ~4678) ever runs; run cancellation or a prepare-step crash before checkout yields the same. Commit statuses do not expire, so the PR page shows qwen-autofix/dispatch-pending = pending indefinitely while the head sha is unchanged. Automation itself is unaffected after the 30-minute TTL (the reader and the HAS_PENDING_CHECKS exemption both age it out correctly), so this is PR-page hygiene and a misleading status, not dispatch correctness — but the stamp guard already mitigates the exact analogous dry-run case, leaving the non-dry variant unhandled. — Failure scenario: scan dispatches a same-repo PR and stamps PENDING; build-cli then fails (a path the stamp-site comment itself names); review-address is skipped, neither release site runs; humans and status-watching tooling see a permanent pending status announcing a leg that is never coming.
Suggested fix — re-stamp the dispatched targets' heads with a non-success state from a finalizer that runs when the leg does not materialize:
# e.g. a small job with `if: failure()` keyed on build-cli;
# the target list is already in needs.review-scan.outputs.targets
finalize-dispatch-markers:
needs: ['review-scan', 'build-cli']
if: ${{ failure() && needs.review-scan.outputs.has_targets == 'true' }}
# for each target: gh api repos/${REPO}/statuses/${head} -X POST
# -f state="error" -f context="${DISPATCH_STATUS_CONTEXT}"
# -f description="dispatch did not materialize (run ...)"中文说明
【建议】 当 leg 始终没有物化时,已打上的 dispatch-pending status 在 GitHub 上永远不会被清除:review-address 是 needs: ['route', 'review-scan', 'build-cli'] 且默认 success 门控,因此 build-cli 失败会整体跳过 leg,两个释放点(~4592 的丢弃释放、~4678 的 checkout 释放)都不会执行;run 被取消或 prepare 步骤在 checkout 前崩溃同样如此。commit status 不会过期,因此在 head sha 不变时 PR 页面会无限期显示 qwen-autofix/dispatch-pending = pending。30 分钟 TTL 之后自动化本身不受影响(读取端与 HAS_PENDING_CHECKS 豁免都会按年龄放行),所以这是 PR 页面卫生与误导性状态问题,而非派发正确性问题——但 stamp 守卫已经为完全类似的 dry-run 情形做了缓解,非 dry-run 变体却没有处理。失败场景:scan 派发一个同仓库 PR 并打上 PENDING;随后 build-cli 失败(stamp 处注释自己点名的路径);review-address 被跳过,两个释放点都不执行;人类与关注 status 的工具会看到一个永久的 pending 状态,宣称一个永远不会到来的 leg。
建议修复:用一个在 leg 未物化时运行的收尾 job,把已派发 target 的 head 重打为非 success 状态(例如 if: failure() 挂在 build-cli 上的小 job;target 列表已经在 needs.review-scan.outputs.targets 中)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| # pending for the full TTL with no leg coming — against the | ||
| # re-emit promise above. Same-repo heads only (fork heads were | ||
| # never stamped) and never in a dry run (nothing was stamped). | ||
| if [[ "${DRY_RUN}" != "true" && "${LIVE_XREPO}" == "false" && -n "${LIVE_HEAD_OID}" ]]; then |
There was a problem hiding this comment.
[Suggestion] The discard-path marker release cannot fire on the one discard reason where the target is still valid and the re-emit promise applies — a failed live-metadata fetch. That failure leaves both guard inputs unknowable: on PR_LIVE == '{}', has("isCrossRepository") is false so LIVE_XREPO defaults to true, and .headRefOid // "" yields empty LIVE_HEAD_OID — the guard fails on both inputs, and the checkout-path release is unreachable because this discard exits first. Every scan inside the TTL then sees the fresh PENDING marker and skips the PR as busy. The block's own comment exists precisely to prevent this shape ("a same-repo head would keep being skipped as dispatch-pending for the full TTL with no leg coming — against the re-emit promise above"). — Failure scenario: same-repo PR stamped PENDING; the leg's recheck gh pr view fails transiently (rate limit / 5xx) → INELIGIBLE='metadata fetch failed (API error) — fail-closed' discard → release guard no-ops → the PR is skipped as dispatch-pending for up to the full 30-minute TTL plus a tick instead of being re-emitted on the next tick (self-heals at the TTL).
Witness (verbatim replay of this recheck block with a recording gh stub): failed-fetch arm → discarded (stale=true): true, release writes: ""; control arm (fetch OK, PR closed meanwhile) → release writes: "…statuses/deadbeefcafe -X POST -f state=success -f context=qwen-autofix/dispatch-pending". Enumerating the ladder: fetch-failure is the only discard reason whose release cannot fire — every other same-repo discard yields known LIVE_XREPO=false and a non-empty LIVE_HEAD_OID.
Suggested fix — carry the dispatch-time head sha into the target record and release the known-stamped sha instead of depending on the live fetch that just failed:
# scan side: add the sha already in hand to the target row
# '. + [{..., head_oid: $head}]' (LIVE_HEAD is live at selection)
# leg side, on the discard path:
DISCARD_OID="${LIVE_HEAD_OID:-${TARGET_HEAD_OID}}"
if [[ "${DRY_RUN}" != "true" && ( "${LIVE_XREPO}" == "false" || -n "${TARGET_HEAD_OID}" ) && -n "${DISCARD_OID}" ]]; then
# release on the dispatch-time sha when the live fetch failedor state in the block's comment that a metadata-failure discard deliberately accepts the TTL-bounded delay.
中文说明
【建议】 丢弃路径的标记释放在唯一一个"target 仍然有效、re-emit 承诺适用"的丢弃原因上无法触发——即 live 元数据获取失败。该失败让守卫的两个输入都不可知:PR_LIVE == '{}' 时 has("isCrossRepository") 为 false,LIVE_XREPO 默认为 true;.headRefOid // "" 得到空的 LIVE_HEAD_OID——守卫在两个输入上都不成立,而 checkout 路径的释放不可达,因为此丢弃分支先退出。TTL 之内的每轮 scan 都会看到新鲜的 PENDING 标记并把该 PR 当作 busy 跳过。本块自己的注释正是为了防止这个形态("同仓库 head 会在整个 TTL 内因 dispatch-pending 持续被跳过而没有 leg 到来——违背上面的 re-emit 承诺")。失败场景:同仓库 PR 被打上 PENDING;leg 的 recheck gh pr view 瞬时失败(限流/5xx)→ INELIGIBLE='metadata fetch failed (API error) — fail-closed' 丢弃 → 释放守卫空转 → 该 PR 被跳过最长 30 分钟 TTL 加一个 tick,而不是下一个 tick 重新派发(TTL 到期自愈)。
证据(用带记录功能的 gh stub 对本 recheck 块做逐字回放):获取失败臂 → discarded (stale=true): true、release writes: "";对照臂(获取成功、期间 PR 被关闭)→ release writes: "…statuses/deadbeefcafe -X POST -f state=success -f context=qwen-autofix/dispatch-pending"。枚举整个 INELIGIBLE 阶梯:获取失败是唯一一个释放无法触发的丢弃原因——其他同仓库丢弃都得到已知的 LIVE_XREPO=false 与非空 LIVE_HEAD_OID。
建议修复:把派发时已知的 head sha 带入 target 记录,在 live 获取失败时释放这个已知被打过标记的 sha(而不是依赖刚刚失败的 live 获取);或在本块注释中声明"元数据失败的丢弃有意接受 TTL 有界的延迟"。
— qwen3.8-max via Qwen Code /review (v0.21.13)
| // the same way). | ||
| const stampSites = | ||
| workflow.match( | ||
| /gh api "repos\/\$\{REPO\}\/statuses\/[^"]+" -X POST \\\n\s*-f state="\w+" -f context="\$\{DISPATCH_STATUS_CONTEXT\}"/g, |
There was a problem hiding this comment.
[Suggestion] The marker lifecycle test pins cross-site identity (all three writers + both readers bind ${DISPATCH_STATUS_CONTEXT}) but not per-site semantics: the pin regex -f state="\w+" accepts any state, statuses\/[^"]+ accepts any sha variable, and the skip branch's continue is asserted nowhere. Three one-line workflow mutations each kill the PR's central protection yet survive the entire suite — while the discard-path release IS behaviorally pinned (writes.log assertions); the stamp writer and checkout release are shape-only. — Failure scenario: (a) stamp flips -f state="pending" → "success" — real markers never match select(.state == "PENDING"), so the dispatch→materialization window reopens: suite passes 177/177. (b) stamp targets a wrong sha variable — the marker never lands in the head's rollup: 392/392 pass across all six workflow test files. (c) the continue is dropped from the marker-skip branch — a busy PR still gets dispatched: 392/392 pass. Comparator proven alive: a sentinel DISPATCH_STATUS_TTL_MINUTES 30→31 mutation fails the lifecycle test.
Suggested fix — pin the per-site semantics:
// the scan stamp must be state=pending on statuses/${LIVE_HEAD}:
expect(reviewScanJob).toContain(
'gh api "repos/${REPO}/statuses/${LIVE_HEAD}" -X POST \\\n -f state="pending" -f context="${DISPATCH_STATUS_CONTEXT}"',
);
// both release sites state=success (add the checkout one the
// discard replay already proves), and pin the skip branch body
// (or replay the stamp line with a recording gh stub, exactly
// as the discard release is replayed)中文说明
【建议】 标记生命周期测试固定的是跨点同一性(三处写入与两处读取都绑定 ${DISPATCH_STATUS_CONTEXT}),但没有固定每处的语义:固定用的正则 -f state="\w+" 接受任何 state,statuses\/[^"]+ 接受任何 sha 变量,跳过分支里的 continue 没有任何断言。三处单行工作流变异每一个都会摧毁本 PR 的核心保护,却都能通过整个测试套件——而丢弃路径的释放是有行为级固定的(writes.log 断言);stamp 写入与 checkout 释放只有形状级固定。失败场景:(a) stamp 翻转为 -f state="success" —— 真实标记永远匹配不上 select(.state == "PENDING"),派发→物化窗口重新打开:套件 177/177 通过。(b) stamp 指向错误的 sha 变量——标记永远不会落在 head 的 rollup 里:全部六个 workflow 测试文件 392/392 通过。(c) 从标记跳过分支删掉 continue —— busy 的 PR 仍会被派发:392/392 通过。比较器活性已证明:哨兵变异 DISPATCH_STATUS_TTL_MINUTES 30→31 会让生命周期测试失败。
建议修复:固定每处语义——scan 的 stamp 必须是 statuses/${LIVE_HEAD} 上 state=pending;两处释放都是 state=success(把丢弃回放已经证明的 checkout 释放也加上);并固定跳过分支的主体(或像丢弃释放那样,用带记录功能的 gh stub 回放 stamp 行)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
|
@qwen-code /triage |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round: no action takenThis round found no actionable feedback:
Critical-only mode is active because the PR's diff grew past this counting window's growth budget (test lines over budget; this is a growth-only engagement). The deferred non-Critical automated-reviewer items listed in the feedback are an audit record for human follow-up — per the workflow rules, they are not modified, resolved, or replied to in this round. Posting No code changes were made and no commit was created. 中文说明Autofix 审查轮次:未采取任何操作本轮没有可处理的反馈:
由于本 PR 的 diff 增长超出了当前计数窗口的增长预算(测试行数超出预算;本次为纯增长触发),已进入仅处理 Critical 的模式。反馈中列出的被延迟的非 Critical 自动审查条目是供人工跟进的审计记录——按照工作流规则,本轮不会修改、关闭或回复这些条目。如果维护者希望重新启用建议处理,评论 本轮未做任何代码改动,也未创建提交。 Deferred non-Critical feedbackCritical-only mode is active: the PR's diff grew src 39 / test 432 net lines beyond this counting window's baseline (budgets: 400/400). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback continues to flow unaffected during a growth-only engagement (the per-author batch budget applies only after 5 change-producing rounds). (
中文说明已进入仅处理 Critical 的模式:本计数窗口内 diff 净增长已达 源码 39 / 测试 432 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。纯增长触发期间维护者反馈照常流动(按作者的批次预算仅在完成 5 个产生改动的轮次后生效)。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 117 passed · 0 failed · 117 total 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:117 通过 · 0 失败 · 117 总计 Verification reportVerification report — PR #9329
|
| cell | base (control) | head | result |
|---|---|---|---|
| healthy enum, one busy leg | candidates kept, busy=101 |
identical | equivalence ✓ |
| healthy enum, no live runs | candidates kept, busy empty | identical | equivalence ✓ |
| run-list failure | candidates KEPT, busy silently empty (fail-open) | candidates EMPTIED, enum_failed=true, fleet fail-closed, error tail in warning |
flip ✓ |
| jobs-view failure (measured incident shape) | busy legs invisible, would re-dispatch over live legs | candidates emptied, enum_failed=true, broke after failing view |
flip ✓ |
list failure + explicit workflow_dispatch |
kept | kept (carve-out), enum_failed still signalled |
carve-out ✓ |
list failure + FORCED_PR from trusted pull_request_review |
kept | emptied (carve-out narrowed to explicit dispatches) | narrowing ✓ |
| first status query fails, second healthy | degrades to partial view | fails closed on first failure (stated tradeoff) | tradeoff ✓ |
Marker mechanism (base has zero references to the marker; all of it is new):
| surface | cells | result |
|---|---|---|
| scan-side skip predicate | fresh PENDING→skip; stale→pass; exactly-at-cutoff→pass (strict >); +1s→skip; SUCCESS→pass; foreign context→pass; empty rollup→pass; missing rollup key→pass; CheckRun-typed entry→pass; null startedAt→pass; month-boundary compares |
12/12 ✓ |
| HAS_PENDING_CHECKS exemption (A/B) | base gate: stranded marker alone blocks (the ~330-min TTL bug); head gate: stranded marker blocks nobody, genuine in-flight check still blocks (alone and beside marker), stuck >330m aged out, foreign PENDING context still blocks, Qwen-Autofix review-address leg still blocks, nonblocking-listed check doesn't | 8/8 ✓ |
| dispatch-time stamp | same-repo non-dry-run stamps once on the live head with state=pending + marker context; fork→none; dry-run→none; failed stamp→warning, exit 0 |
8/8 ✓ |
| checkout-path release | same-repo stamps success on checked-out head; fork→none; dry-run→none; unset HEAD_REPO falls back to REPO |
5/5 ✓ |
| discard-path release | same-repo discard releases on live head; fork→none; missing sha→none; dry-run→none | 5/5 ✓ |
Wiring (19/19): enum_failed output exposed and consumed inside the schedule clause of both the issue-phase if: and its concurrency group (non-schedule events bypass it); DRY_RUN wired into the prepare-branch step; exactly 3 status-POST sites, all binding ${DISPATCH_STATUS_CONTEXT}, read by the same variable twice; base PR_META fetch already included statusCheckRollup (the check side costs zero extra API calls, as claimed).
Reviewer Test Plan walkthrough
- Fail-closed reads — the plan says API failures "cannot be forced on demand"; the stub-gh harness forced them anyway (cells above). Reads as claimed. ✓
- Second scan within 30 min skips — predicate replayed over fixture rollups (fresh PENDING → skip, exact log line
⏳ #${PR}: dispatch pending (marker fresher than …) — skippingpresent at scan step line 3068). Shape reproduced by replay; the live double-scan trigger (real dispatch + build-cli window) is not reproduced end-to-end — no GitHub credential in this sandbox. - Orphan marker expires by age — stale PENDING (>30m) → dispatchable, exactly-at-cutoff boundary probed. Same replay caveat.
- Fork PRs unstamped — all three write sites refuse fork heads and dry runs in replay. ✓
Findings (non-blocking)
F1 (nit) — fail-closed warning overstates in the explicit-dispatch carve-out. In the fail-closed branch the ::warning::… no scan targets dispatched this pass and the fleet row "scan dispatch skipped this pass" are emitted before the carve-out if, so an explicit workflow_dispatch that keeps its candidates (A/B cell 5) logs "no targets dispatched" while targets are in fact dispatched. Log wording only; the enum_failed signal and the carve-out behavior are correct. Evidence: logs/enum-ab.txt cell 5 (warning present, candidates kept).
F2 (completeness) — the stamp description= strings are the one unpinned axis. Mutation M7 (description text changed) survives the whole suite by design of the pins. Harmless prose, not a contract; noted for completeness, not as a merge condition.
Mutation matrix (new contract tests)
Witness: evidence/03-mutation-matrix.png (live run), logs/mutation-matrix.txt.
| mutant | expected | observed | caught by |
|---|---|---|---|
| M1 revert enum block to base | RED | RED | enum test (block absent) |
| M2 carve-out widened to review events | RED | RED | enum test (string pin) |
M3 drop enum_failed echo |
RED | RED | enum test, behavioral (expected '' to contain 'enum_failed=true') |
M4 freshness > → < |
RED | RED | lifecycle test |
| M5 drop HAS_PENDING_CHECKS exemption | RED | RED | lifecycle test |
| M6 drop discard-path release | RED | RED | both marker tests (write-log + stamp-site census 3→2) |
| M7 description text changed | SURVIVE | SURVIVE | — (F2) |
M8 discard release state=success→pending (all pinned strings intact) |
RED | RED | release test, behavioral (expected … to contain 'state=success') |
No mutant regressed a killed test to survived; the two behavioral-layer kills (M3, M8) prove the replay assertions fire independently of the string pins. Every mutant run restored the tree (git status clean after each).
Targeted gates
- Contract suite at head: 179/179 pass (
evidence/04-contract-suite-green.png). One vitest workeronTaskUpdateRPC timeout reported as an unhandled error — reproduced identically on the base suite (176/176 + same error, A/A control), so pre-existing/environmental on this loaded runner, not the PR. - Base suite at base (gate-live control): 176/176 pass.
- actionlint 1.7.12 (repo-pinned, sha-verified install): exit 0 on all workflows.
- shellcheck 0.11.0 at
--severity=erroron the two changed run blocks (extracted verbatim,# shellcheck shell=bashdirective added): 0 findings; 2 pre-existing style warnings (export X="$(mktemp …)"SC2155) also present at base. bash -non both extracted run blocks: clean.
Not covered
- Per-commit attribution: depth-2 checkout reaches only 1 of the 3 commits in the metadata; commit 1's route-gate clause (
pull_request_reviewon closed PRs) is already in the base tip, so the aggregate diff is commits 2+3. Verified the aggregate only. - Live end-to-end of the double-scan window and orphan-marker expiry (plan steps 2–3): requires a GitHub PAT + real dispatches; this sandbox has none. The replays reproduce the decision predicates and log shapes, not the trigger.
- yamllint 1.35.1: container has no
pip3/pipmodule; could not install. Substituted actionlint (which parses the workflow YAML) plus strictyamlparsing of both files in every harness. - Repo-wide gates (ESLint, prettier, full test suite) not re-run — CI covers them; this round scoped to the changed surface.
- The repo's own
--shellcheckgate lints standalone.shfiles only and runs actionlint with-shellcheck=; embedded workflow bash is not shellchecked by any repo gate, so the error-severity run above is mine, not the repo's.
Methodology
Environment: node:22-bookworm CI verify container, merge-ref checkout at depth 2, npm ci + build pre-done. All harnesses (enum-ab.mjs, marker.mjs, wiring.mjs, mutate.mjs, matrix-assert.mjs, extract-steps.mjs) live in this artifact dir; raw logs in logs/. Blocks were carved from yaml.parse()d workflow text (never retyped) and replayed under bash --noprofile --norc with PATH-stubbed gh/recording stubs; mutations were applied to a scratch copy of the working-tree YAML and restored+verified after each. Assertion counts: enum-ab 40, marker 41, wiring 19, matrix-assert 8, gates 9 (3 new tests green, full suite green, base A/A suite green, actionlint, shellcheck-error, bash -n ×2) = 117 pass / 0 fail.
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round: no action takenThis round found no actionable feedback on PR #9329 (head
Critical-only mode is active because the PR's diff grew past this counting window's growth budget (src 39 / test 432 net lines vs 400/400 budgets; test lines over budget — a growth-only engagement). The one item in the deferred non-Critical section is the bot's own triage verification-in-progress comment, an audit record for human follow-up — per the workflow rules it is not modified, resolved, or replied to in this round. A maintainer can post No code changes were made and no commit was created. The branch stays at 中文说明Autofix 审查轮次:未采取任何操作本轮在 PR #9329(head
由于本 PR 的 diff 增长超出了当前计数窗口的增长预算(源码 39 / 测试 432 净行,预算 400/400;测试行数超出预算——本次为纯增长触发),已进入仅处理 Critical 的模式。被延迟的非 Critical 区域中唯一的条目是 bot 自身的 triage 沙箱验证进行中的评论,属于供人工跟进的审计记录——按照工作流规则,本轮不会修改、关闭或回复该条目。维护者可以评论 本轮未做任何代码改动,也未创建提交。分支保持在 Deferred non-Critical feedbackCritical-only mode is active: the PR's diff grew src 39 / test 432 net lines beyond this counting window's baseline (budgets: 400/400). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback continues to flow unaffected during a growth-only engagement (the per-author batch budget applies only after 5 change-producing rounds). (
中文说明已进入仅处理 Critical 的模式:本计数窗口内 diff 净增长已达 源码 39 / 测试 432 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。纯增长触发期间维护者反馈照常流动(按作者的批次预算仅在完成 5 个产生改动的轮次后生效)。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
Released in v0.21.14. |




What this PR does
Hardens the autofix busy-PR detection in the fleet scan, in two parts:
Fail-closed enumeration. The busy-PR skip reads live runs and their job lists via the API. Previously every failure there was swallowed silently (fail-open), and the scan dispatched as if nothing was busy. Now any enumeration failure (the run list, or one run's jobs view) empties this scan's candidate set with a warning; the next tick retries with fresh reads. A forced dispatch keeps its explicit-override semantics and is not emptied.
Dispatch-pending marker. Between dispatch and leg materialization there is a visibility window: build-cli runs in between, and the matrix leg does not exist in the live-run jobs view until it expands. The scan now stamps a pending commit-status marker (
qwen-autofix/dispatch-pending) on the PR head when it dispatches a target, and treats a marker fresher than 30 minutes as busy. The address leg re-stamps the marker success on checkout; a run that dies before materializing leaves a marker that expires by age. The check piggybacks on the statusCheckRollup already fetched per candidate — zero extra API calls. Same-repo heads only: a fork head sha cannot carry a status in this repo, so fork duplicates stay covered by the address-time revalidation.Why it's needed
Measured on 2026-08-16 (full evidence in #9296): the busy enumeration missed address legs that had been running or queued for 3–12 minutes, and overlapping scans re-dispatched the same PRs. Each duplicate burned one build-cli (~5 min) and then cancelled a queued sibling leg through the per-PR group's latest-wins queue; the cancelled parent runs were a major contributor to the workflow's 59% cancellation rate. A duplicate costs far more than one skipped scan pass, so the enumeration now fails closed; the marker closes the remaining window where no leg exists to see yet.
Design note: the marker was originally planned as a queued check-run, but the check-run creation API requires GitHub App authentication (PAT gets HTTP 403), and this workflow authenticates with a PAT. Commit statuses work with the existing credential and surface the same fields (context, state, startedAt) in the PR's statusCheckRollup, which made them the natural substrate.
Reviewer Test Plan
How to verify
This is a CI-workflow change; behavior is observable in scan logs and run statistics:
gh run list/gh run viewfailure sets the flag, and the flag empties the candidate set unless a forced PR is set.⏳ #<PR>: dispatch pending … — skippingand abusyfleet row for it, instead of re-dispatching. After the leg checks out, the marker flips to success and subsequent scans inspect the PR normally.Local verification performed: the full workflow parses as YAML and passes yamllint 1.35.1 (the CI-pinned version) with zero findings; both changed run blocks pass
bash -nand shellcheck at error severity; the marker's jq filter was tested against real statusCheckRollup shapes captured from a live probe (fresh pending → busy, stale pending → free, success → free, missing rollup → free); the fail-closed block was driven through a stubbed-gh harness covering healthy enumeration, run-list failure (fleet emptied), run-list failure with forced PR (candidates kept), and jobs-view failure (fleet emptied). The status-based marker round-trip (create → rollup shape → re-stamp) was probed live against a scratch fork.Evidence (Before & After)
Before: 2026-08-16 — scans at 11:27Z and 11:37Z re-dispatched #9255/#9027 while address legs for those PRs already existed (running 12 minutes / queued 3 minutes); the duplicate legs queued behind the originals and were cancelled by the latest-wins queue after wasting their build-cli runs. After: such overlaps are skipped at scan time via the jobs enumeration (fail-closed when unreadable) plus the dispatch-pending marker (covers the pre-materialization window).
Tested on
Environment (optional)
N/A — workflow change only; the workflow runs on ubuntu-latest.
Risk & Scope
Linked Issues
Part of #9296 (first P1 item). No closing keyword — the issue still tracks the replies-batching and P2 items.
中文说明
这个 PR 做了什么
加固 fleet scan 的 busy-PR 检测,分两部分:
fail-closed 枚举:busy 跳过逻辑通过 API 读取存活 run 及其 job 列表,此前所有失败被静默吞掉(fail-open),scan 会当作没有 busy 照常派发。现在任何枚举失败(run 列表、或某个 run 的 jobs 视图)都会清空本轮 scan 的候选集并告警,下一个 tick 用新鲜读取重试。forced 派发保留显式覆盖语义,不被清空。
dispatch-pending 标记:派发与 leg 物化之间存在可见性窗口——中间隔着 build-cli,matrix leg 在展开前不存在于 live-run jobs 视图。scan 派发 target 时在 PR head 打一个 pending 的 commit status 标记(
qwen-autofix/dispatch-pending),30 分钟内的新鲜标记视为 busy。address leg 在 checkout 后把标记重打为 success;run 在物化前死掉留下的标记按年龄过期。检查直接复用每个候选已经获取的 statusCheckRollup——零额外 API 调用。仅限同仓库 head:fork 的 head sha 无法在本仓库承载 status,fork 重复派发继续由 address-time revalidation 兜底。为什么需要
2026-08-16 实测(完整证据见 #9296):busy 枚举漏检了已运行/排队 3–12 分钟的 address leg,并发 scan 重复派发同一 PR。每个重复 leg 浪费一次 build-cli(约 5 分钟),然后通过 per-PR 组的 latest-wins 队列取消先排队的兄弟 leg;被取消的父 run 是工作流 59% 取消率的主要贡献者。重复派发的代价远高于跳过一轮 scan,因此枚举改为 fail-closed;标记则补上"leg 尚不存在"的剩余窗口。
设计说明:标记原计划用 queued 状态的 check-run,但 check-run 创建 API 需要 GitHub App 认证(PAT 返回 HTTP 403),而本工作流用 PAT 认证。commit status 用现有凭据即可创建,且在 PR 的 statusCheckRollup 中暴露相同字段(context、state、startedAt),是天然的载体。
Reviewer 测试计划
如何验证
这是 CI 工作流变更,行为体现在 scan 日志与 run 统计:
gh run list/gh run view失败置位标志,标志清空候选集,除非存在 forced PR。⏳ #<PR>: dispatch pending … — skipping和 busy fleet 行,而不是重复派发。leg checkout 后标记翻转为 success,后续 scan 正常检查该 PR。已完成的本地验证:完整工作流 YAML 解析通过,yamllint 1.35.1(CI 固定版本)零问题;两个改动的 run 块通过
bash -n和 error 级 shellcheck;标记 jq 过滤器用 live 探测捕获的真实 statusCheckRollup 形态测试(新鲜 pending → busy、过期 pending → 放行、success → 放行、rollup 缺失 → 放行);fail-closed 块用 stub gh 的 harness 覆盖了健康枚举、run 列表失败(fleet 清空)、run 列表失败但有 forced PR(候选保留)、jobs 视图失败(fleet 清空);status 标记的创建→rollup 形态→重打全流程在临时 fork 上 live 探测过。证据(改动前后)
改动前:2026-08-16——11:27Z 与 11:37Z 的 scan 在 #9255/#9027 的 address leg 已存在(运行 12 分钟/排队 3 分钟)时重复派发;重复 leg 排在原 leg 后面,浪费 build-cli 后被 latest-wins 队列取消。改动后:这类重叠在 scan 时经 jobs 枚举(不可读时 fail-closed)加 dispatch-pending 标记(覆盖物化前窗口)被跳过。
测试环境
环境(可选)
N/A——仅工作流变更;工作流运行在 ubuntu-latest。
风险与范围
关联 Issue
属于 #9296(第一个 P1 项)。不使用关闭关键字——该 issue 还跟踪回复批量化与 P2 项。