ci: enrich deferred-findings tracking issues with PR context and assignment - #11080
Conversation
…gnment The autofix loop's deferred-findings upsert files one tracking issue per PR for verified review findings whose fixes lie outside that PR's footprint. The issue it created carried a constant title, a bare boilerplate body, and no assignee, so a maintainer looking at the issue could not tell what it was about or who should follow up — over the past week that produced ~100 issues that read as scattered noise. The create path now fetches the PR once and makes the issue self-describing: the title carries the PR title, the body names the PR context (number, title, author), cc's the author, and every rc bullet deep-links to its original review comment. The issue is assigned to the PR author at creation; external contributors are not assignable, so the create retries once without the assignment rather than losing the findings, and the cc in the body is what actually reaches them. All of it is best-effort: a failed PR fetch degrades to the previous bare title and no assignee — metadata must never lose findings. The title is also the lookup's fallback anchor when a maintainer edit drops the body marker. It now accepts both the bare form and the enriched "base: <PR title>" form, guarded by the colon so PR #5's base never prefix-matches PR #50's issue. The rc link suffix is safe on identity because rc dedup is id-anchored; rv/ic bullets deliberately get no link — their cross-round identity IS the rendered line, and a suffix would render every already-persisted rv/ic item as new, a one-time duplicate wave over the existing tracking issues. The PR title is API-derived content published under the bot identity, so it receives the same mention/comment-opener neutralization as the reason rendering (canonical spelling, which moves the script's escape census pin from 1 to 2), and its cap slice happens in jq so a CJK title cannot be byte-cut under a C locale. Tests: the recording gh stub learns the pulls endpoint and an assignment-failure injection; new cases cover the degraded create, bot authors, the assign fallback, enriched-title adoption, the number-prefix collision, and title neutralization. The upsert test spawns ~70 subprocesses, so it gets an explicit 30s bound like its spawn-heavy neighbors instead of flaking on exec-scanning hosts.
|
Thanks for the PR! Template looks good ✓ — every required heading is present, including all three Risk & Scope bullets and the Chinese section. Problem: observed, and I checked it rather than taking the description's word for it. Issue #11074 is live and open right now: authored by Direction: this is the repo's own autofix tooling rather than shipped product, so there is no user-facing contract to weigh and the CHANGELOG signal does not really apply. Two honest questions, neither a blocker:
Size: not applicable — no core paths are touched. 82 production lines ( Approach: the scope feels right, and it lands close to what I would have written independently — fetch the PR once on the create branch only, put the title in the issue title (capped, since GitHub enforces an issue-title length limit and a long CJK title would otherwise 422 the create), name the author in the body, derive the review-comment URL from the id the bullet already carries (no extra call), and extend the lookup's dedupe anchors instead of replacing them so the existing bare-form issues keep getting adopted. I checked the reuse angle rather than assuming it: both call sites — Risk: no elevated risk signals — neither changed file matches the high-risk path list. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ —— 所有必需小标题都在,包括 Risk & Scope 的三个要点和中文说明。 问题:已观测到,而且是我自己去核实的,不是照抄 PR 描述。 issue #11074 现在就在线上、处于 open 状态:作者是 方向: 这是仓库自己的 autofix 工具链,不是发布出去的产品能力,所以没有用户可见契约要权衡,CHANGELOG 信号在这里也基本不适用。两个坦率的问题,都不是阻塞项:
规模: 不适用——没有触及核心路径。生产代码 82 行( 方案: 范围合理,也和我独立想到的做法很接近——只在创建分支拉一次 PR;把 PR 标题放进 issue 标题(做了截断,因为 GitHub 对 issue 标题长度有限制,过长的中文标题会让创建调用 422);在正文写明作者;review 评论链接直接由 bullet 里已有的 id 推导(不额外调用 API);并且是扩展而非替换 lookup 的去重锚点,这样存量的裸格式 issue 仍会被收养。复用这一点我是查过的,不是想当然:两个调用方—— 风险: 无升级风险信号——两个改动文件都不匹配高风险路径清单。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewI wrote down what I would do before reading the diff, and this is essentially it — so most of my time went into checking whether the claims in the comments actually hold, rather than into the shape of the change. They do. The load-bearing claim checks out. Appending a deep link to the The colon guard does what it says. Sanitization is at parity with the existing publish site, and the escape-site count test moving 1 → 2 matches reality — there are now exactly two Nothing else consumes the format. I grepped the title and body strings repo-wide: the only readers are the script and its own test. One thing worth confirming, since it is easy to get backwards: Non-blocking observations
TestingThis is an unattended CI run, so I did not build or execute anything from this PR — the evidence below is the PR's own CI, read through the API once at the reviewed commit. No polling: this is a snapshot, not a settled result. Zero checks were red at fetch time. The job that actually pins this change — Not verified: the unit suite's own result — still in flight at fetch time. The table below is rewritten in place once CI settles, and approval is deferred with it. Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Sandboxed verification would settle the one thing the stub cannot: 中文说明代码审查我在看 diff 之前先写下了自己的做法,结果和这个 PR 基本一致——所以我的时间主要花在核实注释里的那些断言是否真的成立,而不是纠结改动的形态。断言都成立。 最关键的那条断言是对的。 给 冒号守卫确实做到了它声称的事。 净化处理与既有发布点保持一致,escape-site 计数测试从 1 改到 2 也符合实际——脚本里现在正好有两处 没有其他消费方。 我在整个仓库里搜了标题串和正文串:唯一读取它们的就是这个脚本和它自己的测试。 有一点值得确认,因为很容易搞反: 非阻塞观察
测试这是无人值守的 CI 运行,所以我没有构建或执行本 PR 的任何代码——下面的证据来自 PR 自己的 CI,通过 API 在被审查的那个 commit 上读取了一次。没有轮询:这是快照,不是最终结果。 抓取时刻没有任何 check 是红的。真正 pin 住本次改动的作业—— 未验证:单测套件自身的结果——抓取时仍在运行。CI 稳定后下方表格会被就地重写,approve 也随之延后。 (CI 表格见上,未在此重复。) 有一条只能靠沙箱验证来落定,stub 证明不了: — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
Confidence: 4/5 — the mechanism holds up under scrutiny and the tests pin it directly; what is left is observability, one duplicate-issue failure shape, and an ownership question that belongs to whoever owns the autofix loop. Back to what I wrote before reading the diff: I would have built this the same way, and I could not find a materially simpler path that still solves the stated problem. The part I expected to have to push on — whether appending a deep link is safe against a dedupe mechanism that compares rendered text — turned out to be the part already reasoned through correctly, including the asymmetry between Does it solve something anyone cares about? I confirmed #11074 is live, unassigned, and unreadable, and the description's account of roughly a hundred such issues is consistent with what the mechanism does. So yes — this is not a solution looking for a problem. Would I curse this in six months? No. The dedupe anchors were extended rather than replaced, so the ~100 existing issues keep getting adopted; the env contract is unchanged, so neither caller needed touching; and every new branch fails toward "what we do today" rather than toward "findings lost". That is the right priority ordering for a script whose entire job is not losing verified work. What I am still holding, none of it blocking:
Approval is deferred until CI lands green on 中文说明Confidence: 4/5 —— 机制经得起推敲,测试也直接 pin 住了行为;剩下的是可观测性、一个"重复 issue"的失败形态,以及一个归属于 autofix 循环 owner 的判断问题。 回到我在看 diff 之前写下的方案:我会用同样的方式实现,也没能找到明显更简单、又能解决所述问题的路径。我原以为需要施压的那一点——在"比对渲染文本"的去重机制后面追加链接是否安全——恰恰是已经想清楚的那一点,包括 它解决的是有人在意的问题吗?我确认了 #11074 正在线上、无 assignee、不可读,描述里"约一百个这类 issue"的说法与该机制的实际行为一致。所以是的——这不是为方案找问题。 半年后维护它我会骂人吗?不会。去重锚点是扩展而非替换,存量约 100 个 issue 仍会被收养;env 契约未变,两个调用方都不需要改;每条新增分支的失败方向都是"退回今天的行为",而不是"丢掉 findings"。对一个职责就是"不丢失已验证工作"的脚本来说,这个优先级排序是对的。 仍然保留的意见,都不阻塞:
approve 延后到 CI 在 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
End-to-end verification against the real GitHub API (pre-merge, since a I ran the actual script from this branch against a real PR with real review comments on my fork — yiliang114/qwen-code#5, whose three real inline review comments (ids Round 1 — create: yiliang114/qwen-code#10 The deep links resolve to the actual review comments on that PR. Round 2 — re-run with the same findings (+ a reworded re-emission of one): silent exit 0, nothing published — the issue was adopted via its marker and all ids deduped against the stored corpus, including the enriched title form. Round 3 — one genuinely new finding: appended as a single comment to #10 ( Bonus real-world failure path hit along the way: the fork initially had issues disabled, and the first run exercised the LOST-warning degradation against a real One artifact worth naming for reviewers: when I ran round 2 with a mismatched |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.
Not explored to full depth (tool budget reached): "agent 1d": none — no check was cut short by the ceiling.; "agent 6b": did not execute scripts/tests/qwen-autofix-workflow.test.js in this worktree (no node_modules; install skipped to stay within budget).
中文说明
仅完成部分审查,审查缺口已披露。
未审查(原文为英文):the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.
未探索到全部深度(达到工具调用预算):"agent 1d":none — no check was cut short by the ceiling.;"agent 6b":did not execute scripts/tests/qwen-autofix-workflow.test.js in this worktree (no node_modules; install skipped to stay within budget)。
— qwen3.8-max via Qwen Code /review (v0.23.0)
Address review round R1 on the deferred-findings tracking issue.
R1-1 (Critical): the assignee-less retry re-POSTed the non-idempotent
issue create. GitHub silently drops assignees on POST
/repos/{owner}/{repo}/issues for users without push access, so for the
external contributors the retry was named for, the first create returns
201 and the retry is dead code. The failures that DO reach it are the
ambiguous transport ones (connection reset, gateway 502, a read timeout
after the server already committed), where the re-POST mints a second
tracking issue carrying the same marker: the next round's newest-first
lookup adopts the newer one and the first is orphaned forever, publicly
duplicating every finding while the round logs clean success. That is
against this file's own rule that creating a duplicate is worse than
deferring persistence one round. Create once, unconditionally, then
assign best-effort with a separate idempotent call whose failure only
warns — the body's cc mention is what actually reaches an external
contributor.
R1-7 collapses by construction: the create call is now spelled once
instead of three times, and the assignability condition is evaluated once
(ASSIGNABLE, set alongside CC) instead of twice.
R1-2 (Critical): dropped the re-added per-test 30s timeout on this file's
heaviest case. scripts/tests/vitest.config.ts configures testTimeout
90_000 precisely because this case (~14s idle, and heavier in this diff)
exhausted 30s on contended release runners, and 93e1597 (#10870)
removed this exact cap two days ago. Each spawnSync already carries its
own 30s child timeout, which bounds the only real hang risk.
R1-8: pinned the PR-title neutralization, which had no test on any stage —
deleting the entity escape shipped green while a fully
contributor-controlled title published a live @-mention under the bot
identity (GitHub decodes @ before its mention filter). One case now
pins the entity escape, the [\r\n\t] flatten and the .[0:80] slice.
R1-9: pinned the ic side of the deep-link exemption. Only rv witnessed the
"no suffix for review/issue_comment" invariant, so widening the condition
to issue_comment shipped green while re-rendering every already-persisted
ic item as new — the one-time duplicate wave the script's comment warns
against.
Verified: npx vitest run --config ./scripts/tests/vitest.config.ts
scripts/tests/qwen-autofix-workflow.test.js -> 225 passed | 4 skipped.
Each new assertion reds under its own mutation: retry shape restored,
title entity gsub / flatten / slice deleted, suffix widened to ic.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmto8kxx1l2
|
Qwen Code review timed out. Qwen review timed out after 10800 seconds (of the 180-minute budget). For large PRs, retry with a longer timeout by commenting: |
The pulls-endpoint call was the only gh call in upsert-deferred-issue.sh
whose captured stderr nobody read: `|| true` swallowed the exit status and
the `gh_err_reset` before the create call wiped the reason, so a systematic
fetch failure — a fine-grained PAT rotated without pull-requests:read, a
rate limit, a persistent 404 — silently reverted every new tracking issue
to the bare title / no assignee / no cc while the success line still
printed. That is against this file's own header contract (every failure
path warns and exits 0) and the GH_ERR design note (these warnings are the
feature's only signal): every sibling call routes its reason into a
`gh_reason`-bearing warning.
The gate is the call status, not a body field — `jq -e '.number'` would
warn on every healthy round whose PR JSON carries no `.number`. One
non-blocking warning, read before that reset so the create-failure warning
below still reports its own reason; the bare-title degradation and the
never-fail-a-round contract are unchanged.
Pinned in the existing prFetchFailed case (warning text, reason routed
through gh_reason() so the `::` payload arrives neutralized, no raw
`::error::` on stdout) plus a no-false-warning assertion on the healthy
create path. Mutations observed red, then restored: delete the warning,
read raw ${GH_ERR}, and swap in the `.number` gate.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtolfwlblj
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-6 two hand-mirrored neutralization chains — already reported (comment 3940140375), thread resolved, follow-up issue 11128 open
Not reviewed: finding R2-9 — the verifier never ruled on it: the round-3 verification that would have was refused by the review time budget.
Not reviewed: the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.
Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": shellcheck on the changed script — no shellcheck binary in this worktree and node scripts/lint.js --shellcheck downloads one, so I could not measure whether….
Not reviewed: reverse audit — stopped before round 3 by the review time budget.
— [unverified] tag when the loop ended — the verifier never ruled on them, and they are not confirmed.
Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):finding R2-9 — the verifier never ruled on it: the round-3 verification that would have was refused by the review time budget.
未审查(原文为英文):the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.
未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)":shellcheck on the changed script — no shellcheck binary in this worktree and node scripts/lint.js --shellcheck downloads one, so I could not measure whether…。
未审查:反向审计——评审时间预算不足,未能开始第 3 轮。
— [unverified] 标记——验证者从未对它们作出裁决,它们不算已确认。
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
— qwen3.8-max via Qwen Code /review (v0.23.0)
| # 201 unassigned. Failure only warns (persistence already succeeded) and | ||
| # the body's cc mention is what actually reaches them. | ||
| gh_err_reset | ||
| gh api "repos/${REPO}/issues/${NUM}/assignees" -f "assignees[]=${PR_AUTHOR}" \ |
There was a problem hiding this comment.
[Suggestion] This create-time assignment has two unaccounted downstream effects in .github/workflows/qwen-autofix.yml, a file this diff does not touch. (A) It is a PAT-authenticated write that fires an issues.assigned webhook, which that workflow listens on, so every first-time deferral starts a route job that always ends in 🧭 issue event ignored. (B) An assignee removes the tracking issue from the scheduled ready-for-agent scan, which filters no:assignee — while the body this same diff writes at :448 invites exactly that flow.
(A) qwen-autofix.yml:25-29 declares on.issues.types: ['labeled','assigned'] and the route job's if: (:213-214) prefilters only issue_comment, pull_request and pull_request_review, so an issues.assigned event passes on github.repository alone; runs-on (:218-221) resolves to the self-hosted ecs-qwen pool that the file says it reserves because "a hosted backlog queued route past the cron period, and af-005's supersede then starved every scan round". GitHub's "events caused by GITHUB_TOKEN do not start a run" suppression does not apply — the upsert child runs with CI_DEV_BOT_PAT (:6582, from a step whose env is secrets.CI_DEV_BOT_PAT at :5942). The job then spends a gh api …/collaborators/{sender}/permission call (:556) to reach 🧭 issue event ignored (:576), because ASSIGNEE_LOGIN is the human author, never the bot (guarded at :444), and a brand-new tracking issue carries no labels. (B) AUTOFIX_ISSUE_EXCLUDES: 'no:assignee -linked:pr …' (:656) is consumed by the every-10-minutes scan at :994-995, so once this call succeeds the issue never re-enters that scan; if the labeled-event run is superseded by the per-issue concurrency group (:224-227, cancel-in-progress: true), dies on the runner, or is skipped by a gate, nothing retries and the deferral waits for a human to notice. release.yml:1406-1414 shows the repo relies on that scan as a backstop ("so that, if the dispatch below fails, the scheduled ready-for-agent scan can still find it"). Both effects land only where the author IS assignable — an org member or collaborator, i.e. precisely the maintainers who run this flow; for external contributors GitHub silently ignores the assignee.
Witness:
witness: not run — the deciding facts are GitHub webhook delivery for a PAT-authored write and Actions
trigger evaluation, which need a live repository and a workflow dispatch; the closest capability is the
live A/B arm, and this review has no disposable repository (QWEN_REVIEW_SCRATCH_REPO unset).
Read in the tree at HEAD instead: qwen-autofix.yml:25-29, :213-214, :218-221, :224-227, :552, :556, :576,
:656, :994-995, :5942, :6582; release.yml:1406-1414.
Two in-tree witnesses that PAT-authored issue events DO start runs here:
main-ci-failure-issue.yml:191 (GH_TOKEN: secrets.CI_DEV_BOT_PAT) + :205 (--add-assignee "${AUTOFIX_BOT}")
— a designed trigger that only works if a PAT-authored issues.assigned reaches qwen-autofix.yml:552
qwen-triage.yml:510-521 — records as measured history that bot-PAT-created tracking issues
"trigger triaged the bookkeeping issue with a full agent run per deferral" (issue 9264)
qwen-triage.yml:5 listens on ['opened','edited','reopened'] only — not assigned — so its creator
guard does not cover this event, and qwen-autofix.yml has no equivalent
The cheapest fix for both symptoms: drop the create-time assignment and keep the body's cc @${PR_AUTHOR} mention — the comment at :467-468 says the mention "is what actually reaches them", and it is the only channel that works for the non-assignable population, so the assignment buys little and costs both the no-op run and the scan eligibility. If the assignment is kept: for (A) mirror the expression-level prefilter the file already uses for its other high-volume no-op events (:214), e.g. (github.event_name != 'issues' || github.event.action == 'labeled' || github.event.assignee.login == (vars.AUTOFIX_BOT_LOGIN || 'qwen-code-dev-bot')); for (B) the body must stop pointing at the ready-for-agent flow for THIS issue and say to file a per-item issue instead, since the tracking issue is no longer scan-eligible.
The fix rests on three existing facts. qwen-autofix.yml:552 — [[ "${ASSIGNEE_LOGIN}" == "${AUTOFIX_BOT}" ]] && label_is_trigger=true — is a deliberate trigger whose issue is ALSO bot-created (main-ci-failure-issue.yml:191 runs with secrets.CI_DEV_BOT_PAT and :205 does --add-assignee "${AUTOFIX_BOT}"), so a prefilter keyed on the creator alone would kill that path: it must also test the assignee. A fix that keeps the assignment must preserve the "${PR_AUTHOR}" != "${AUTOFIX_BOT}" guard at :444, since assigning an issue to the bot is this repo's takeover trigger. And label_is_trigger at :551 is the only remaining route by which a maintainer can send an already-assigned tracking issue to the agent, so a creator-based guard copied from qwen-triage.yml would close the flow entirely rather than the noise.
Fix witness: scripts/tests/qwen-autofix-workflow.test.js:14235 (expect(created.calls).toContain('-f assignees[]=someone')) and the assignFailed case at :14287-14305 go red when the assign call is removed; the replacement pin is expect(created.calls).not.toContain('/assignees') alongside the surviving expect(created.calls).toContain('cc @someone') at :14238. If the route.if prefilter is added instead, assert that a bot-created issue assigned to a non-bot does not start route while an issue assigned to AUTOFIX_BOT still does — removing either half of the clause must red one of the two.
中文说明
[Suggestion] 这个创建即指派的调用,在本 diff 未改动的 .github/workflows/qwen-autofix.yml 里造成两个未被计入的下游影响。(A)它是以 PAT 认证的写操作,会触发 issues.assigned webhook,而该 workflow 正监听此事件,于是每一次首次 deferral 都会启动一个 route 作业,而该作业总是以 🧭 issue event ignored 结束。(B)一旦 issue 有了 assignee,它就会被排除在每 10 分钟一次的 ready-for-agent 扫描之外(该扫描过滤 no:assignee)——而本 diff 在 448 行写进正文的那句话,恰恰在邀请读者走这个流程。
(A)qwen-autofix.yml:25-29 声明 on.issues.types: ['labeled','assigned'],而 route 作业的 if:(213-214 行)只预过滤 issue_comment、pull_request 与 pull_request_review,因此 issues.assigned 事件仅凭 github.repository 就能通过;runs-on(218-221 行)会解析到自建 ecs-qwen 池——文件注释说明保留该池的原因是"托管队列把 route 排过了 cron 周期,af-005 的 supersede 随后饿死了每一轮扫描"。GitHub 的"由 GITHUB_TOKEN 触发的事件不会启动新运行"这一抑制并不适用:upsert 子进程使用的是 CI_DEV_BOT_PAT(6582 行,来自 5942 行 env 为 secrets.CI_DEV_BOT_PAT 的步骤)。该作业随后花费一次 gh api …/collaborators/{sender}/permission 调用(556 行),最终到达 🧭 issue event ignored(576 行)——因为 ASSIGNEE_LOGIN 是人类作者、永远不是 bot(444 行已守卫),而新建的 tracking issue 不带任何 label。(B)AUTOFIX_ISSUE_EXCLUDES: 'no:assignee -linked:pr …'(656 行)被 994-995 行的每 10 分钟扫描消费,因此本调用一旦成功,该 issue 就再也不会进入那个扫描;如果 labeled 事件触发的那次运行被按 issue 分组的并发组取代(224-227 行,cancel-in-progress: true)、在 runner 上死掉、或被某个门跳过,就没有任何东西会重试,该 deferral 只能等人发现。release.yml:1406-1414 表明本仓库确实把该扫描当作兜底("以便下面的 dispatch 失败时,定时的 ready-for-agent 扫描仍能找到它")。这两个影响都只在作者可被指派时发生——即组织成员或协作者,也正是运行这套流程的维护者;对外部贡献者,GitHub 会静默忽略该 assignee。
建议:两个症状最省事的共同修复是去掉创建即指派,保留正文里的 cc @${PR_AUTHOR} 提及——467-468 行的注释就说明该提及"才是真正能触达他们的方式",且它是唯一对不可指派人群有效的通道,因此指派收益很小,却要同时付出这次空转运行与扫描资格的代价。若保留指派:针对(A),仿照该文件已用于其他高频空转事件的表达式级预过滤(214 行),例如 (github.event_name != 'issues' || github.event.action == 'labeled' || github.event.assignee.login == (vars.AUTOFIX_BOT_LOGIN || 'qwen-code-dev-bot'));针对(B),正文必须不再为这个 issue 指向 ready-for-agent 流程,改为说明应就单条 finding 另开 issue,因为 tracking issue 已不具备扫描资格。
修复约束(三条既有事实):qwen-autofix.yml:552 的 [[ "${ASSIGNEE_LOGIN}" == "${AUTOFIX_BOT}" ]] && label_is_trigger=true 是一个刻意设计的触发路径,其 issue 同样由 bot 创建(main-ci-failure-issue.yml:191 使用 secrets.CI_DEV_BOT_PAT,205 行执行 --add-assignee "${AUTOFIX_BOT}"),因此仅按创建者过滤会杀掉该路径——必须同时判断 assignee。若保留指派,必须保留 444 行的 "${PR_AUTHOR}" != "${AUTOFIX_BOT}" 守卫,因为把 issue 指派给 bot 是本仓库的接管触发条件。而 551 行的 label_is_trigger 是维护者把"已有 assignee 的 tracking issue"交给 agent 的唯一剩余路径,所以照搬 qwen-triage.yml 的按创建者过滤会把整个流程关掉,而不只是关掉噪音。
修复验收:移除指派调用后,scripts/tests/qwen-autofix-workflow.test.js:14235(expect(created.calls).toContain('-f assignees[]=someone'))与 14287-14305 行的 assignFailed 用例会变红;替代钉子是 expect(created.calls).not.toContain('/assignees'),同时保留 14238 行的 expect(created.calls).toContain('cc @someone')。若改为添加 route.if 预过滤,则断言"bot 创建、指派给非 bot 的 issue 不会启动 route"而"指派给 AUTOFIX_BOT 的 issue 仍会启动"——删除该子句的任一半都必须让两条断言之一变红。
— qwen3.8-max via Qwen Code /review (v0.23.0)
There was a problem hiding this comment.
Confirmed from the in-tree evidence (webhook delivery itself was not exercised live — no scratch repository): a PAT-authored issues.assigned event passes the route job's expression-level prefilter, and an assigned tracking issue leaves the no:assignee-filtered ready-for-agent scan. Both candidate fixes, however, sit outside what this loop may land in this PR:
- Drop the create-time assignment — removes a feature this PR deliberately adds (it is in the PR title). The body's
cc @${PR_AUTHOR}is indeed the only channel that reaches the non-assignable population, so the assignment's marginal value is the maintainer population — precisely the population whose assignment triggers your (A) and (B). Whether that trade is worth it is a product call for the author, not this round. - Add the
route.ifprefilter — edits.github/workflows/qwen-autofix.yml, a file this PR never touches and an area this loop may not expand into (CI/verification machinery outside the PR's footprint; the deterministic gate rejects such expansion). Your three constraints are noted and would bind any such change: the prefilter must test the assignee, not just the creator (the main-ci-failure takeover trigger relies on a bot-created, bot-assigned issue); the"${PR_AUTHOR}" != "${AUTOFIX_BOT}"guard must stay; andlabel_is_triggermust remain a live route for already-assigned issues.
Recommendation for the author: if the assignment is worth keeping, land the prefilter plus the body-text adjustment (stop pointing this issue at ready-for-agent) as its own small PR, where the workflow file is in footprint. Leaving this thread open as the decision point.
中文说明
已根据树内证据确认(webhook 投递本身未实际运行——环境中没有 scratch 仓库):以 PAT 触发的 issues.assigned 事件能通过 route 作业的表达式级预过滤;而带上 assignee 的 tracking issue 会被排除在按 no:assignee 过滤的 ready-for-agent 定时扫描之外。然而两个候选修复都超出本循环在本 PR 内可落地的范围:
- 去掉创建即指派——等于移除本 PR 刻意加入的功能(就写在 PR 标题里)。正文的
cc @${PR_AUTHOR}确实是唯一能触达不可指派人群的通道,因此指派的边际价值只在于维护者人群——而恰恰是这群人的指派会触发你提出的 (A) 与 (B)。这个取舍是否值得,是作者的产品决策,不是本轮能定的。 - 添加
route.if预过滤——需要编辑.github/workflows/qwen-autofix.yml,本 PR 从未触碰该文件,且该区域属于本循环不得扩张进入的 CI/验证机制(PR 足迹之外;确定性门禁会拒绝此类扩张)。你给出的三条约束已记录,任何此类改动都必须遵守:预过滤必须判断 assignee 而非仅判断创建者(main-ci-failure 的接管触发依赖"bot 创建且指派给 bot"的 issue);"${PR_AUTHOR}" != "${AUTOFIX_BOT}"守卫必须保留;label_is_trigger必须仍是已指派 issue 的可用路径。
给作者的建议:若指派值得保留,请把预过滤和正文措辞调整(不再为该 issue 指向 ready-for-agent 流程)作为一个独立小 PR 落地,那样 workflow 文件才在足迹内。此线程保持开放,作为决策点。
There was a problem hiding this comment.
Not fixing this in this PR, and deliberately leaving the thread open rather than closing it with a promise - it needs an author/maintainer decision, not a review-fix pass.
On (B), because it decides the shape: create-time assignment IS this PR's stated purpose ("...with PR context and assignment"). The finding's "cheapest fix for both symptoms" is to drop it, which removes the feature the PR exists to add - that is not a call to make silently in a fix commit. The scan-eligibility consequence is real but is the intended trade: the tracking issue now has a named owner, so the every-10-minutes no:assignee ready-for-agent scan is no longer its delivery mechanism, and the labeled route (label_is_trigger at :551) remains - as the finding itself confirms is the surviving path for an already-assigned issue.
On (A): the wasted route job is a genuine cost, but the fix lands in .github/workflows/qwen-autofix.yml, which this diff does not touch, and the finding's own constraints make it non-local - a creator-based prefilter would kill the main-ci-failure-issue.yml:191/:205 PAT-authored --add-assignee "${AUTOFIX_BOT}" trigger, so the clause must also test the assignee, and getting that wrong silently closes this repo's takeover path. Changing autofix triggering repo-wide deserves its own review with a live-repo witness. This finding records witness: not run - the deciding facts (PAT-authored webhook delivery, Actions trigger evaluation) were read in-tree, not measured, and I could not measure them here either; gh search issues for an existing follow-up on issues.assigned route noise returned nothing, so there is no tracked issue to point at, and I am not minting one for an unmeasured no-op job.
What did change this round is adjacent and shrinks the blast radius of the notification this assignment sends: as of 3e05ec6 the contributor-controlled PR title no longer reaches the markdown-rendered body at all, so the body an assigned author is notified about can no longer carry an attacker-chosen link, an image beacon, or an unclosed <details> that folds the findings away. The cc @${PR_AUTHOR} mention is unaffected and remains charset-validated.
Decision left open, either defensible and both outside this diff's footprint: (i) keep the assignment and add the assignee-aware route.if prefilter to qwen-autofix.yml as its own change with a live witness; or (ii) drop the assignment and keep only the body's cc @author - which also means rewording the body, since it currently invites the ready-for-agent flow for an issue that would then stay scan-eligible.
There was a problem hiding this comment.
Split into the two halves, because they land differently.
(B) — fixed in 239efbf. The body this script writes invited the ready-for-agent flow without saying which issue, and the tracking issue is the one place it has no backstop: AUTOFIX_ISSUE_EXCLUDES (qwen-autofix.yml:656, consumed by the scheduled scan at :995) starts with no:assignee, so once the assign at upsert-deferred-issue.sh:506 lands the issue never re-enters that scan — while release.yml:1487 shows the repo counting on it ("if the dispatch below fails, the scheduled ready-for-agent scan can still find it"). The sentence now attaches the flow to the per-item issue a human files from the list; that issue is unassigned, so it keeps the scan. The script carries a comment saying why, and the upsert case pins it per call (toContain('apply the ready-for-agent flow to that issue') at scripts/tests/qwen-autofix-workflow.test.js:14336, plus not.toContain('(or apply the ready-for-agent flow)') at :14338). Mutation-verified: restoring the old parenthetical reds the case.
(A) — confirmed real, left for a maintainer. Re-verified at the head this reply is written against, current line numbers:
on.issues.types: ['labeled','assigned']—qwen-autofix.yml:26-28route.ifprefilters onlyissue_comment/pull_request/pull_request_review—:214, so anissues.assignedevent passes ongithub.repositoryalone and lands on the self-hostedecs-qwenpool (:218)ASSIGNEE_LOGINenv:262;label_is_trigger:550-552; onecollaborators/{sender}/permissioncall:556; exits at🧭 issue event ignored:576- the write is PAT-authenticated, so the GITHUB_TOKEN suppression does not apply:
run_deferred_upsert(.github/scripts/autofix-push-and-report.sh:422) passesGITHUB_TOKEN="${GITHUB_TOKEN}"into the clean child, and thePush and reportstep that executes it (qwen-autofix.yml:5852) sets that tosecrets.CI_DEV_BOT_PAT - per-issue concurrency group with
cancel-in-progress:223-226
Neither exit belongs in this diff:
- Drop the create-time assignment, keep only
cc @author. That reverses what this PR's title promises ("…and assignment") and the behaviour six pinned cases assert (-f assignees[]=someoneat:14321and:14474, plus thenot.toContain('assignees')degradation guards). - Add an expression-level prefilter to
route.if. That edits the routing gate of shared production CI for every autofix route, and it cannot be keyed on the creator alone::552'sASSIGNEE_LOGIN == AUTOFIX_BOTis a deliberate takeover trigger whose issue is also bot-created and bot-assigned under the same PAT (main-ci-failure-issue.yml:191+:205 --add-assignee "${AUTOFIX_BOT}"), so a creator guard copied fromqwen-triage.ymlwould close that path instead of the noise.
Cost as shipped is one no-op route job per first-time deferral, and only where the author is assignable. Leaving this thread open: (A) needs a maintainer to pick between those two, and I did not touch qwen-autofix.yml.
There was a problem hiding this comment.
Filed as #11214 so this survives the merge — a PR review thread is not a durable home for a decision that outlives the PR.
Re-verified at head 1b9b671d3f, current line numbers (the origin/main merge shifted a few from the original finding):
- the write itself:
upsert-deferred-issue.sh:506(ASSIGNABLE == 1gate) →:514(gh api repos/${REPO}/issues/${NUM}/assignees -f "assignees[]=${PR_AUTHOR}") - it is PAT-authenticated, so the GITHUB_TOKEN suppression does not apply:
autofix-push-and-report.sh:422-432passesGITHUB_TOKEN="${GITHUB_TOKEN}"into the clean child, and the enclosing step's env sets that tosecrets.CI_DEV_BOT_PAT(qwen-autofix.yml:813) on.issues.types: ['labeled','assigned']—qwen-autofix.yml:26-28;route.ifprefilters onlyissue_comment/pull_request/pull_request_review—:214, with noissuesclause, so the event passes ongithub.repositoryalone and lands on the reserved self-hostedecs-qwenpool (:218)- it spends one
collaborators/{sender}/permissioncall (:556) and exits at🧭 issue event ignored(:576), becauselabel_is_triggeris false — no labels on a brand-new issue (:551) andASSIGNEE_LOGINis the human author, not the bot (:552)
(B) is no longer open: it was fixed here in 239efbfc9b, which points the ready-for-agent hint at the per-item issue a human files (unassigned, so it keeps the no:assignee scan backstop at qwen-autofix.yml:656 / :994-995) instead of at this now-assigned tracking issue. Only (A) remains.
(A) is deliberately not fixed in this diff, for the reason the dev-bot gave: this PR's footprint is .github/scripts/upsert-deferred-issue.sh + scripts/tests/qwen-autofix-workflow.test.js and nothing else. It never touches .github/workflows/qwen-autofix.yml, and changing autofix triggering repo-wide from a PR about deferred-findings issue bodies is exactly the expansion the deterministic gate rejects. I am not contradicting that adjudication — I am giving it a tracked home.
Its three constraints are recorded in #11214 as binding, and I re-confirmed all three in-tree rather than copying them:
- the prefilter must test the assignee, not just the creator —
:552'sASSIGNEE_LOGIN == AUTOFIX_BOTis a deliberate takeover trigger whose issue is also bot-created under the same PAT (main-ci-failure-issue.yml:191+:205 --add-assignee "${AUTOFIX_BOT}"), so a creator-only guard would close this repo's takeover path instead of the noise - the
"${PR_AUTHOR}" != "${AUTOFIX_BOT}"guard atupsert-deferred-issue.sh:479-483stays label_is_trigger(:550-551) stays a live route for already-assigned issues — which matters more now that (B) makes this tracking issue permanently scan-ineligible
#11214 also carries the two mutually exclusive exits (add the assignee-aware prefilter vs drop the assignment and keep only cc @author) as a maintainer product decision, and keeps the honest witness caveat: the deciding facts are PAT-authored webhook delivery and Actions trigger evaluation, which were read in-tree, not measured. A fix PR should bring a live-repo witness.
Not resolving this thread. Both the dev-bot and my earlier reply on it designated it the open decision point, and (A) still has no chosen exit — resolving it would record a decision nobody has made. #11214 is where the decision gets made; this thread stays open until it does.
There was a problem hiding this comment.
Re-verified at head 439cd8e872 — the head has moved past 1b9b671d3f, which the last reply on this thread was written against. No code changed this round, and the split that reply made still holds.
(B) — still fixed. 239efbfc9b is an ancestor of head (git merge-base --is-ancestor 239efbfc9b HEAD → yes). .github/scripts/upsert-deferred-issue.sh:485-493 points the ready-for-agent flow at the per-item issue a human files from the list, not at this now-assigned tracking issue, so the no:assignee scan backstop (qwen-autofix.yml:656, consumed by the scheduled scan at :995) survives on the issue that needs it.
(A) — still real, still human-gated, still deliberately not fixed in this diff. Re-confirmed in-tree at current line numbers rather than carried over from the earlier replies:
on.issues.typesincludesassigned—qwen-autofix.yml:26-28route.if(:214) has noissuesclause at all — it prefilters onlyissue_comment,pull_requestandpull_request_review— so anissues.assignedevent passes ongithub.repositoryalone and lands on the reserved self-hostedecs-qwenpool (:218)- it then spends one
collaborators/{sender}/permissioncall (:556) and exits at🧭 issue event ignored(:576), becauselabel_is_triggeris false: a brand-new tracking issue carries no labels (:551) andASSIGNEE_LOGINis the human author, never the bot (:552; the assignment is gated atupsert-deferred-issue.sh:480-483, whose"${PR_AUTHOR}" != "${AUTOFIX_BOT}"guard is intact) - the write is PAT-authenticated, so GitHub's "events caused by GITHUB_TOKEN do not start a run" suppression does not apply: the enclosing step's env is
secrets.CI_DEV_BOT_PAT(qwen-autofix.yml:813) andrun_deferred_upsertpassesGITHUB_TOKENthrough into the clean child
Both exits remain outside what this round may land:
- Drop the create-time assignment — reverses the feature this PR's title promises and that the pinned cases assert.
- Add the assignee-aware prefilter to
route.if— edits the routing gate of shared production CI in a file this PR never touches, and it cannot be keyed on the creator alone::552'sASSIGNEE_LOGIN == AUTOFIX_BOTis a deliberate takeover trigger whose issue is also bot-created under the same PAT (main-ci-failure-issue.yml:191+:205 --add-assignee "${AUTOFIX_BOT}"), so a creator-only guard would close this repo's takeover path instead of the noise.
That is a maintainer decision about repo-wide autofix triggering, not a review-fix pass, and it is tracked in #11214 (open, priority/P2, status/waiting-for-feedback) with all three of this finding's constraints recorded as binding.
Not resolving: #11214 has no chosen exit yet, and both an earlier dev-bot reply and the author's replies on this thread designated it the open decision point — resolving would record a decision nobody has made. Scope this round: 0 files changed, worktree still at 439cd8e872, no push.
|
@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. Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
…e create-path guards
Address review round R2 on the deferred-findings tracking issue.
R2-1: the one neutralized PR_TITLE was published to two surfaces with
different renderers. The issue TITLE is stored and rendered as plain
text — no markdown pass, no mention filter — so the entity/@/comment-
opener stages protect nothing there and only corrupt the string this
change exists to make readable (a ZWSP inside `@types` on a routine
dependency-bump title, visible `\-\-` backslashes). Derive two variants
from the one fetch: PR_TITLE_RAW (flatten + cap only) for CREATE_TITLE,
and the fully neutralized PR_TITLE for the markdown-rendered body copy.
The gsub("<!--") census stays at 2 — the raw variant adds no site.
R2-2: the PR_AUTHOR login-charset guard had no witness — deleting it
shipped green while a malformed `.user.login` flowed into both the
deliberate `cc @…` mention in the public body and the `assignees[]`
argument. New badLogin case.
R2-5: the `-n "${NUM}" &&` half of the assign guard had no witness —
dropping it let the create-failure path POST repos/…/issues//assignees
and print a could-not-assign warning immediately before the LOST
warning. Pinned on the existing writeFail case.
R2-3: the enriched-title format was written at creation and re-spelled
220 lines away at the marker-less adoption lookup, each side pinned by
its own hand-typed literal — a one-sided restyle (": " → " - ") left
the suite green while production forked a second tracking issue. Added
a round-trip case that derives the adoption fixture from the recorded
create call, plus a comment at the create site naming the lookup as the
separator's consumer.
Mutation probes observed red, then restored: title reverted to the
escaped variant; body copy swapped to the raw variant; body-chain
flatten deleted; charset guard deleted; NUM guard half dropped;
separator restyled on the create side (including the maintainer
scenario — every hand-typed create-side literal updated, round-trip
still reds) and on the lookup side.
Verified: npx vitest run --config ./scripts/tests/vitest.config.ts
scripts/tests/qwen-autofix-workflow.test.js -> 229 passed.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Address-review round — PR #11080Commit: This round covered the round-2 review findings. Four Suggestions were implemented (one code change plus three test-witness additions), three items stay open as decisions for the author. Diff growth: script +14/−3, tests +86/−19 — within the window budgets. Implemented
Left open — decisions for the author
Resolved earlier-round findings, re-verified at this headR1-1 (single create + separate assign), R1-2 (no per-test 30 s cap on the heaviest case — the remaining Verification
中文说明Address-review 轮次 — PR #11080提交: 本轮处理第 2 轮评审的发现。实施了 4 条 Suggestion(一处代码修改加三处测试见证补齐),3 条作为作者决策保持开放。diff 增长:脚本 +14/−3,测试 +86/−19——在窗口预算内。 已实施
保持开放 — 待作者决策
已解决的更早轮次发现 — 在本 head 重新验证R1-1(单次创建+独立指派)、R1-2(最重用例无 30 秒单测超时——残留的 验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧵 Resolved all 10 selected review thread(s). · 已关闭全部选中的 10 条评审线程。 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.
Reviewed. Suggestions are inline.
3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-5 unbounded markdown surface in the PR-title neutralization — already reported (comment 3941924802)
- R2-4 PR description still specifies the removed assignee-less create retry — already reported (comment 3941924815)
- R2-6 create-time assignment's two downstream effects in qwen-autofix.yml — already reported (comment 3941924819)
Not explored to full depth (tool budget reached): "agent 6b": could not execute scripts/tests/qwen-autofix-workflow.test.js (no node_modules in this review worktree), so the greenness of the new assertions and the case…; "agent 3c": none — I completed the walk within budget. I did not execute the 25k-line test file (expensive, and greenness/portability is the build-lint lane's concern, not ….
Convergence: round 3 posted 4 inline comment(s), 4 of them reported for the first time; the previous round posted 7 (6 new). Findings keep coming back to the same files: .github/scripts/upsert-deferred-issue.sh (findings in rounds 1, 2; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
已审查。 建议见行内评论。
本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未探索到全部深度(达到工具调用预算):"agent 6b":could not execute scripts/tests/qwen-autofix-workflow.test.js (no node_modules in this review worktree), so the greenness of the new assertions and the case…;"agent 3c":none — I completed the walk within budget. I did not execute the 25k-line test file (expensive, and greenness/portability is the build-lint lane's concern, not …。
收敛情况:第 3 轮发布了 4 条行内评论,其中 4 条是首次提出;上一轮发布了 7 条(其中 6 条首次提出)。发现反复回到同一批文件:.github/scripts/upsert-deferred-issue.sh(第 1、2 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.23.0)
…ning shapes
Three review findings on this PR:
- R3-1 (.github/scripts/upsert-deferred-issue.sh): the degradation warning was
gated on the gh call status alone. This fetch passes no --jq, and gh copies a
non-JSON body raw with serverError set only above status 299, so a transparent
proxy answering `200 text/html` exits 0 while all three derivations come back
empty through their `|| true` - the bare title / no cc / no assignee
degradation behind a clean success line, which is exactly what the warning
exists to name. Extend the gate to "the call failed OR both derived strings
are empty" and name that second case in the message, instead of letting
gh_reason() read "no stderr captured" for a call that succeeded. Keying on
both strings being empty cannot false-positive on a real PR object (a
whitespace-only title still flattens to a non-empty " ", a deleted author
leaves the title), where a `.number`-presence gate would warn on every
healthy round.
- R3-2 (scripts/tests): nothing pinned the fetch's ABSENCE on the append path,
so hoisting the 4-line fetch unit above the create/append branch shipped green
while costing an authenticated GET /pulls/N against the bot PAT's rate limit
on every steady-state append round - the path that runs most often - instead
of once per tracking issue's lifetime. Add the negative half to the existing
`appended` case, which already pins the append-only shape.
- R2-2 (scripts/tests): `badLogin` exercised only the space shape of the four
malformed-login shapes its own comment declares the charset guard covers, so
widening `{1,39}` to `{1,}` or admitting `@` into the class both shipped green
while a 40-character or `@`-bearing login flowed into the deliberately
un-defused public `cc @...` mention and into an argv element. Add one fixture
per remaining shape.
Mutation-verified against the whole file: dropping the gate's second half,
dropping the exit-0 reason wording, hoisting the fetch unit above the branch,
and each of the two charset widenings reds the suite (1 failed each); intact
tree is 225 passed / 4 skipped. shellcheck note count goes 10 -> 9, since
$(gh_reason) moved out of the masked echo into a plain assignment.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtp6vibomd
|
🤖 AutoFix ran out of time before finishing (timeout (3600000ms)) (attempt 2/100) — it will retry on the next scan.
See the Qwen Autofix agent step logs for model/tool output. 中文说明🤖 AutoFix 在完成前耗尽了时间(timeout (3600000ms))(第 2/100 次尝试)—— 将在下次扫描时重试。 Run log: https://github.com/QwenLM/qwen-code/actions/runs/34005069100 🧠 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.
3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-5 unbounded markdown surface in the PR-title neutralization — already reported (comment 3941924802)
- R2-6 create-time assignment's two downstream effects in qwen-autofix.yml — already reported (comment 3941924819)
- the ": " title separator hand-spelled at the create and adoption sites instead of one shared constant — already reported (comment 3941924811)
Not reviewed: the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
scripts/tests/qwen-autofix-workflow.test.js:14351 — [review] The assign warning's captured reason is never asserted.github/scripts/upsert-deferred-issue.sh:425 — [review] Login guard is a weaker copy of the repo's canonical one
Convergence: round 4 posted 2 inline comment(s), 1 of them reported for the first time; the previous round posted 4 (4 new). Findings keep coming back to the same files: .github/scripts/upsert-deferred-issue.sh (findings in round 3; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.
收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 4 轮发布了 2 条行内评论,其中 1 条是首次提出;上一轮发布了 4 条(其中 4 条首次提出)。发现反复回到同一批文件:.github/scripts/upsert-deferred-issue.sh(第 3 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.23.0)
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (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: 1019 passed · 1 failed · 1020 total Flakiness gate: 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:1019 通过 · 1 失败 · 1020 总计 抖动门: Verification reportPR #11080 — deep verificationVerdict: The central claim is proven load-bearing by A/B against the base build and survives an 855-assertion hostile-input sweep. The one 中文摘要结论: A/B 结论:中心主张成立且是 load-bearing 的。用独立的 recording- Findings(详见下文 Findings 一节):
Corrections:描述里"该用例实测 ~4.8s"在本容器不成立(base 就已经 37.6s);一条新测试注释称 未覆盖:逐 commit 归因(快照 5 个 commit,shallow 本地只可达 1 个);GitHub 真实渲染与真实 API(无 token、无写操作);assign 调用的真实 wire body(本机 ScopeThe diff touches exactly two files ( Central claim (the behaviour the PR exists to change): on the create path only, fetch the PR once and make the tracking issue self-describing — title Secondary claims (both verified): (a) the Out of scope and not covered: everything listed in Not covered below. Central claim — A/B tableBoth arms run the real script under the production contract lifted from
Result: 80/80 assertions ( Two facts established without A/B, because they are deterministic properties of tools rather than of this diff:
Corrections to the description and to the new test commentsThese are corrections to text, not requests to change code.
Findings1.
|
| head | M8 mutant | |
|---|---|---|
POST repos/o/r/issues with create failing |
1 | 2 |
| second POST's marker + title identical to the first | — | yes |
| stdout | byte-identical | byte-identical |
| exit status | 0 | 0 |
Because stdout is identical, no output assertion can see it — only the recorded call log can. The suite's single create-count assertion lives in the assignFailed case, whose create succeeds, so a retry-on-create-failure never reaches it. Reproduce:
cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260906-071334/m8-adjudicate.mjsWitness: 04-m8-survivor-1-vs-2-create-posts.png. Classification: coverage gap — the behaviour is right, nothing asserts it. Per AGENTS.md a missing test for correct code is a Suggestion, not a Critical. It is worth raising anyway because the un-pinned property guards the outcome this very file ranks worst.
The fixture that would pin it (not applied — advisory only)
The existing writeFail case already sets writeFail: true; it only lacks the create-count assertion that assignFailed applies to its own successful create:
// in the writeFail case
expect(writeFail.calls.split('api repos/o/r/issues -f title=')).toHaveLength(2);I did not apply and re-run this, so I am not claiming it is measured — the harness above proves the mutant is live and that the current suite is blind to it, which is the part that needed evidence.
2. The file's heaviest case now spends 60% of its 90 s budget (Suggestion)
The PR adds 14 runUpsert subprocess invocations (60 → 74) to the single heaviest it() in a 16 k-line file. Measured on this shared runner, with the same command both arms:
| arm | case duration | budget | margin |
|---|---|---|---|
base (HEAD^1 test + script) |
37,606 ms | 90,000 ms | 2.39× |
| head (as shipped) | 53,868 ms | 90,000 ms | 1.67× |
| head, second independent run (matrix control) | 61,570 ms | 90,000 ms | 1.46× |
Δ = +16.3 s (+43%), ≈1.16 s per added subprocess. The budget figure is the PR's own claim and is correct: scripts/tests/vitest.config.ts:50 sets testTimeout: … || 90_000. The PR also adds a comment noting that a 30 s cap on this exact test was removed by #10870 after it timed out on contended release runners, citing runs 33676423730 / 33683912557. This change consumes 31% of the headroom that remained after that incident.
This is a flake-risk observation, not a correctness defect, and I want to bound it precisely: the case passed on every run here, and the skill's own note applies — a shared, loaded runner is the regime where such a test passes, so I cannot reproduce a timeout by repetition. What I can say is that the margin is now 1.46–1.67× on hardware representative of the lane, where the same case was 2.39× before. Reproduce:
cd /__w/qwen-code/qwen-code && time npx vitest run --config ./scripts/tests/vitest.config.ts \
scripts/tests/qwen-autofix-workflow.test.js \
-t 'upserts deferred findings into a per-PR issue that survives the merge'3. Body prose claims something about rc: items that a rv:/ic:-only batch does not have (Nit)
"Each rc: item links back to its original review comment." is emitted unconditionally at creation. For a batch containing only rv: or only ic: items — which produce no rc: bullet at all — the issue body still asserts a property of items it does not contain. Measured: rv-only and ic-only batches both persist, both correctly omit the deep link, and both carry the sentence. Cosmetic only; no dedupe or rendering consequence.
cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260906-071334/obs.mjs # O14. A fetch that exits 0 with stderr content discards that stderr (Nit)
The gate's second half overwrites PR_CTX_REASON unconditionally when PR_FETCH_OK == 1, so if gh wrote anything diagnostic to stderr while still exiting 0, the captured reason is replaced by the generic the call exited 0 but returned no usable PR object. Measured: with the stub emitting gh: a diagnostic that would have named the proxy on stderr and an HTML body, the warning does not contain that text. This is a small observability loss in a change whose purpose is observability; it is strictly better than base, which had no warning at all.
cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260906-071334/obs.mjs # O2Consequences I tested that do not hold
Bounding the above matters more than escalating it, so these are the scarier readings I tried and disproved:
- No injection through the contributor-controlled PR title. The title is fully fork-author-controlled and republished under the bot identity. I swept 111 cut positions across five hostile token classes (
@victimuser,@victim,@victim,<!\-\-victim-->, and a mix), asserting the body carries no live mention, no live@-entity spelling, and no live<!\-\-. Zero survived. Three cut positions leave a trailing bare@where the.[0:80]slice falls between@and its ZWSP; all three are inert because the closing")follows, and@")is not a parseable mention. The sharpest case is a 79-CJK-char title putting@at codepoint 80: the body copy ends in a bare@and no part ofvictimsurvives. The shipped order is escape-then-slice, which is the safe one — slice-then-escape would have left a live mention at exactly these positions. - No cross-PR adoption collision. Driving hostile titles (
Deferred review findings from PR #50,0: Deferred review findings from PR #50,:,:::) into PR TypeError in Authentication Selection Interface #5's created title, PR refactor(cli): update OpenAI API key prompt with Bailian URL #50's marker-less lookup never adopts PR TypeError in Authentication Selection Interface #5's issue and always creates its own, while PR TypeError in Authentication Selection Interface #5 still adopts its own — including after a retitle and past the 80-codepoint cap, since adoption is prefix-based on the base title. - No new public cross-reference target. The accepted-tradeoff list does not mention timeline effects, so I checked the sibling: base already carried the
#Nshorthand in the body, and head carries the same single#5plus an absolute URL to the same PR. The referenced-target set does not grow. (I could not exercise GitHub's real timeline API — see Not covered.) - No duplicate wave over the ~100 existing issues. The corpus was not hand-typed: the base script authored it, then head replayed the same
rc/rv/icfindings against that real base-authored body and re-published 0 items. The widening mutant re-published exactly 2 — thervandiclines — whilercstayed suppressed by its id anchor. Witness:03-duplicate-wave-head-0-vs-mutant-2.png. - No
GH_ERRcross-contamination. Three sequentialghcalls each reset the sink; the assign warning names the assign reason, and when both fetch and create fail, the two warnings name their own distinct reasons. - No codepoint damage. A CJK title slices to exactly 80 codepoints / 240 bytes with no U+FFFD; a 100-emoji title slices to 80 codepoints with no lone surrogate and round-trips as valid UTF-8. The comment's rationale (a bash byte slice would cut a UTF-8 sequence) is borne out.
- Degenerate payloads never lose findings. Array, empty object, empty string, HTML, numeric title, null title, null user, null login, nested-object title, embedded
\u0000: all 10 persist, all exit 0, all issue exactly one create, and the warning fires exactly when nothing usable came back.
Mutation matrix
Witness: 02-mutation-matrix-12-of-13-killed.png. Oracle is the PR's own suite via vitest's JSON reporter; the unmutated control is green (1 passed / 0 failed) and the positive control is killed, so the harness demonstrably can make this suite fail.
| mutant | claim under test | verdict | first failing assertion (qwen-autofix-workflow.test.js) |
|---|---|---|---|
| PC rename the dedupe marker | positive control | KILLED | :14234 marker present in the create call |
| M1 widen deep-link suffix to all sources | rv/ic exemption | KILLED | :14769 perSource (see Correction 2) |
| M2 drop the colon guard in the lookup | enriched adoption | KILLED | :15064 enriched-title adoption |
| M3 drop the login-charset guard | malformed .user.login |
KILLED | :14327 badLogin (space) |
| M4 gate on call status only | second half of the warning gate | KILLED | :14295 prUnusableBody warning |
| M5 delete the context warning | observability | KILLED | :14272 prFetchFailed warning |
M6 raw ${GH_ERR} instead of gh_reason() |
:: neutralisation |
KILLED | :14272 ;;error;; vs ::error:: |
| M7 hoist the fetch above the create/append branch | creation-only context | KILLED | :14455 append issues no pulls/ |
| M8 retry the create on failure | never retried | SURVIVED | none — suite stayed green |
M9 drop -n "${NUM}" from the assign guard |
no assign after failed create | KILLED | :14516 writeFail sees no assignees |
| M10 title uses the escaped copy | split title/body derivation | KILLED | :14387 titledTitle exact match |
M11 widen {1,39} to {1,} |
40-char login | KILLED | :14345 longLogin |
M12 admit @ into the charset class |
a@b login |
KILLED | :14336 atLogin |
12/13 killed, 1 survived, 0 build failures. Every declared mutation claim in the new test comments held except M8's (Correction 3 / Finding 1). Vacuity of the central new test is proven independently: head tests against the base script go red with AssertionError: expected '…' to contain 'api repos/o/r/pulls/5' at :14240 — an assertion mismatch naming expected-vs-actual, not a crash. Only one assertion can surface because vitest aborts the it() at the first failure.
Not covered
- Per-commit attribution. The snapshot lists 5 commits; the depth-2 merge-ref checkout makes 1 reachable (
git rev-list HEAD^1..HEAD^2). A baregit rev-list --countreturns a plausible1at the shallow boundary rather than erroring, so this was checked against the snapshot, not assumed. I verified the aggregateHEAD^1..HEADdiff only; the four earlier commits' individual claims were not separately exercised. - The real GitHub renderer and API. No token and no writes in this job. The design premise that issue titles are stored and rendered as plain text — no markdown pass, no mention filter is what justifies leaving the title unescaped while escaping the body; I verified the script implements that split (title raw, body neutralised, asserted per call so neither surface can be credited with the other's rendering) but I could not observe GitHub actually render a title. Every escaping assertion is against recorded argv, not rendered HTML.
- The real
ghwire body for the assign call.GH_DEBUG=1does not print request bodies in this build, so the body was verified fromgh api --help(the tool's own manifest) plus 6 in-repo precedent files, not from a captured payload. One network probe was made — a POST to a deliberately nonexistent repo path with an invalid token, which returned401 Bad credentialsand had no side effect. - The full 229-test file. I ran two targeted
-tgates: the upsert block (which contains every new behavioural case) andposts a human-handoff marker…(which contains the diff's other changed assertion,scriptEscapeSitestoHaveLength(1)→2; green in 20.9 s). The remaining 227 tests were skipped by the filter and not run. - Repo-wide gates. No
npm run test:scripts, no eslint, no typecheck: the diff touches no TypeScript source, and the two files it touches are covered by the targeted gates above. - shellcheck's repo wrapper is not a blocking gate.
node scripts/lint.js --shellcheckexits 0 despite hundreds of findings across the repo, because its pipeline ends insed, so the status issed's. A clean result there would be weak evidence, so I ran shellcheck 0.11.0 (the repo's pinned version) directly with the repo's exact flags on head and base: head 10 findings (SC2154×4,SC2312×6), base 9 (SC2154×4,SC2312×5). The delta is +1SC2312note at the new assign warning's$(gh_reason)— the identical idiom the file already uses twice at base. No new finding class, no error-severity finding. The gate's liveness was proven by plantingSC2034andSC2164violations and confirming both are reported. - yamllint could not be installed (
pip3: Permission denied). Immaterial: the diff changes no YAML. - Windows / macOS. The script is bash + jq + gh; the repo's own config excludes bash-driven workflow suites on Windows. Only Linux was exercised.
- The ~100 live tracking issues. No network/token, so the migration was simulated by having the base script author the corpus rather than by reading real issues. This reproduces the shape of the migration (base-authored bytes, replayed through head), not the real issues' contents.
- No calibration against a real emitted artifact. This is a script PR, not a workflow-step PR: there is no posted comment or uploaded file whose bytes a replay could be calibrated against, so nothing here claims calibration. What substitutes for it is that every cell drives the shipped script itself, under the invocation contract copied from the workflow, with the base arm as a live control.
- jq version. This container has jq 1.6; I did not establish which version the production runners use. All jq programs exercised here are 1.6-compatible, but a 1.7-only behaviour difference would not have shown up.
Methodology
Everything ran in the CI verify container (node:22-bookworm, node v22.23.2, bash 5.2.15, jq 1.6) against refs/pull/11080/merge at 68b2f87c. The unit under test is the shipped bash script, executed for real — never stubbed — under the env -i child contract transcribed from .github/workflows/qwen-autofix.yml:6577. The only fake is gh: a recording stub (stub-gh.sh) that appends each invocation's exact argv NUL-delimited and emulates --jq (client-side filtering, which the script depends on for --jq '.number' and --jq '.body // ""') and --paginate (one JSON array per page). Assertions read that argv, so they judge the wire rather than the script's own narration. The base arm is git show HEAD^1:.github/scripts/upsert-deferred-issue.sh; the diff changes no package.json or lockfile and no internal workspace package is on the code path, so reusing the installed root node_modules is a clean control and no realpath check was needed. Mutants are single-point string edits with an exactly-one-occurrence precondition and a bash -n check before use; each swaps the working-tree script, runs vitest under the JSON reporter, and restores in a finally, with the final sha256 asserted equal to the recorded pristine value (df9566be…) and git status --porcelain confirmed empty. Harnesses: ab-suite.mjs (80), census.mjs (20), hostile.mjs (855), obs.mjs (22), m8-adjudicate.mjs (8), gates.mjs (35) — 1020 assertions total, all in this directory with their raw logs (logs-*.txt), rerunnable by a maintainer. Two harness bugs were found and fixed during the round and are recorded here because they shaped early output: a brace-bearing ${VAR:-{…}} default in the stub silently appended a stray } to every JSON payload, and a --paginate emulation that double-wrapped the page array; both were stub faults, proven so by isolation before any conclusion was drawn from them.
A third was caught only by its own positive control and is worth naming, because left undetected it would have fabricated a result rather than crashing: the first mutation-matrix run detected "green" with a /Tests 1 passed/ regex, but vitest wraps that summary in ANSI escapes even when redirected, so the regex never matched and the unmutated control reported NOT GREEN. Had the control not been run first, all 13 mutants would have been reported KILLED — a clean 13/13 that meant nothing. The run was discarded, the tree restored and hash-verified, and the oracle replaced with vitest's JSON reporter (testResults[].assertionResults[]) before the matrix was repeated. This is also why the matrix quotes a positive control at all: it is the only thing that distinguishes "the suite covers this" from "my harness never observed the suite".
Flakiness gate log
rounds=5 files=1 skipped=0
file scripts/tests/qwen-autofix-workflow.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/qwen-autofix-workflow.test.js
per-file results (P=pass F=fail I=infra-exit, one letter per run):
scripts/tests/qwen-autofix-workflow.test.js: PPP
verdict: timeout
summary: only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed
--- per-invocation detail (full copy in the artifact) ---
round 1 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
round 2 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
round 3 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
|
🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind What I found before stopping: Round blocked: required checks cannot run — this runner's
|
…item issue The tracking issue this script creates is assigned to the PR author right after creation, and the scheduled ready-for-agent scan filters `no:assignee` (AUTOFIX_ISSUE_EXCLUDES in qwen-autofix.yml), so an issue carrying an assignee never re-enters that scan. The body still invited readers to "apply the ready-for-agent flow" without saying to which issue, which reads as an invitation to label the tracking issue itself - the one place where a label-event run cancelled by the per-issue concurrency group has no scheduled scan left to retry it. Attach the flow to the per-item issue a human files from this list instead: that issue is unassigned, so it keeps the scan backstop. Pinned per call in the upsert case; mutation-verified red when the old parenthetical is restored. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtpq5ybkn4
|
CI attribution for the red Evidence (head
Not blind-fixing it. The push of
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed.
3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R2-6 create-time assignment's issues.assigned webhook self-trigger in qwen-autofix.yml — already reported (comment 3941924819)
- the single-create-POST witness running only where the create succeeds — already recorded (round 5 deferral, scripts/tests/qwen-autofix-workflow.test.js:14456)
- the assign warning's captured reason never asserted — already recorded (round 4 deferral, scripts/tests/qwen-autofix-workflow.test.js:14351)
Not explored to full depth (tool budget reached): "agent 6c": running npm run test:scripts -- -t 'upserts deferred findings' to confirm the new assertions pass on this host.
1 Suggestion(s) were drafted inline past the resolved critical posting floor; the CLI moved them into the deferral list below (floor enforcement).
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
.github/scripts/upsert-deferred-issue.sh:486 — [review] This comment is the head commit's own rationale for pointing the ready-for-agent flow at the per-item issue, and it argues one state of a gate that covers two. It rests on the tracking….github/scripts/upsert-deferred-issue.sh:518 — [review] Success line for the durable write sits behind the assign network call.github/scripts/upsert-deferred-issue.sh:492 — [review] Body tells the cc'd PR author to do something only a maintainer can doscripts/tests/qwen-autofix-workflow.test.js:15330 — [review] Added comment states a 30s child timeout the harness does not have
中文说明
已审查。
本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未探索到全部深度(达到工具调用预算):"agent 6c":running npm run test:scripts -- -t 'upserts deferred findings' to confirm the new assertions pass on this host。
1 条 Suggestion 在已解析的 critical 发布下限之外被起草为行内评论;CLI 已将其移入下方延后清单(下限强制执行)。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.23.0)
|
🔀 Base updated: red check(s) [Lint & Static (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Lint & Static (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
|
CI attribution — Run Evidence from job
Separately, |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- D7-2 the added comment's 30s child-timeout figure — already recorded (round 6 deferral, scripts/tests/qwen-autofix-workflow.test.js:15330)
- D7-3 the assign call's issues.assigned self-trigger in qwen-autofix.yml — already reported (comment 3941924819, R2-6)
Not reviewed: the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.
Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": executing scripts/tests/qwen-autofix-workflow.test.js under vitest — the review worktree has no node_modules , so the ~14 new runUpsert cases were checked …; "agent reverse-audit (round 1)": enumerating runs-on for each lane that runs npm run test:scripts ( ci.yml:767 , release.yml:701 , qwen-triage.yml:3594 ) — I established the new script co…; "agent 2": verifying against the live GitHub API whether an issue title stores C0 control bytes and U+202E or strips them server-side (no authenticated network probe avail…; "agent 4": running scripts/tests/qwen-autofix-workflow.test.js under vitest to measure the heaviest case's total idle runtime and its real margin under the 90 s testTim….
Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round:
scripts/tests/qwen-autofix-workflow.test.js:14527 — [review] No fixture witnesses the codepoint-safety of the jq .[0:80] title cap
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.
未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)":executing scripts/tests/qwen-autofix-workflow.test.js under vitest — the review worktree has no node_modules , so the ~14 new runUpsert cases were checked …;"agent reverse-audit (round 1)":enumerating runs-on for each lane that runs npm run test:scripts ( ci.yml:767 , release.yml:701 , qwen-triage.yml:3594 ) — I established the new script co…;"agent 2":verifying against the live GitHub API whether an issue title stores C0 control bytes and U+202E or strips them server-side (no authenticated network probe avail…;"agent 4":running scripts/tests/qwen-autofix-workflow.test.js under vitest to measure the heaviest case's total idle runtime and its real margin under the 90 s testTim…。
收敛姿态下延后(第 7 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.23.0)
…he assignment comments Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下: Autofix round summary — PR #11080Commit Addressed1. F1 — the never-retried create is now pinned (ic:5557397302 Finding 1; ic:5557642971 F1; ic:5558163896 F1)Both verifiers measured the same survivor: a mutant that re-POSTs the create once on failure stays green, because the suite's only create-count assertion lives in
Mutation probe (the round's witness): temporarily rewrote the create call to retry once on failure → the suite goes red with 2. F2 — the two assignment comments now state the real semantics (ic:5557642971 F2; ic:5558163896 F2)The comment above the separate assign call claimed create-time assignees are "silently dropped for users without push access" with "the create always returned 201 unassigned", and that "Failure only warns". The verifier measured against the live API and found both backwards:
Rewrote the comment at 3. Two overstated wordings softened (ic:5558163896, "Two wordings I'd soften")
Declined (issue-level findings — no threads to reply on, so recorded here)Sandbox Finding 2 — the file's heaviest case spends ~60% of its 90s budget (ic:5557397302)Same shape as inline R3-3, already declined on-thread in two earlier rounds with reasons, and the decline stands: the quantified timeout-margin claim was withdrawn by the reviewer itself, the case passed on every measured run, and splitting the mega-case is a test-architecture refactor of a 25k-line file this CI-plumbing PR does not own — it belongs in its own PR with its own before/after budget measurement. The deterministic gate re-runs the full suite on every round; a real timeout would surface there, not in prose. Sandbox Nit 3 — "Each rc: item links back to its original review comment." is unconditional (ic:5557397302)The sentence quantifies over the Sandbox Nit 4 — an exit-0 fetch discards stderr gh wrote (ic:5557397302)
Left open by design (no action this round)
Failed checks
Conflict notes
VerificationCommands actually run (this harness kills any command at 120 s, which shaped what could run locally — see the last item):
Not run locally, with the reason: the full 中文说明Autofix 本轮总结 —— PR #11080提交 已处理1. F1 —— "create 永不重试"现在有了钉子(ic:5557397302 发现 1;ic:5557642971 F1;ic:5558163896 F1)两位验证者测到了同一个存活变异体:在 create 失败后重发一次 POST 的变异体能保持全绿,因为套件里唯一的 create 次数断言位于
变异探针(本轮见证):临时把 create 调用改成失败后重试一次 → 套件变红,报 2. F2 —— 两处 assignment 注释改为真实语义(ic:5557642971 F2;ic:5558163896 F2)独立 assign 调用上方的注释原先声称:create 时携带的 assignees 对无 push 权限的用户会被"静默丢弃","create 总是返回 201 unassigned",且"失败只会告警"。验证者对真实 API 实测发现两条都说反了:
已把 3. 两处夸大的措辞已软化(ic:5558163896,"两处建议软化的措辞")
已拒绝(issue 级发现——没有线程可回复,故记录在此)沙箱发现 2 —— 文件最重单用例占用 90 秒预算的约 60%(ic:5557397302)与行内 R3-3 同形,已在之前两轮的线程里带理由拒绝,拒绝仍然成立:量化的超时余量结论已被评审者自己撤回,该用例在所有实测运行中都通过,而拆分为巨型用例是对这个 CI 管道 PR 并不拥有的 2.5 万行文件的测试架构重构——应当单独开 PR 并自带前后预算测量。确定性门禁每轮都会重跑完整套件;真发生超时会在那里暴露,而不是在文字里。 沙箱 Nit 3 —— "Each rc: item links back to its original review comment." 无条件出现(ic:5557397302)这句话是对当前 沙箱 Nit 4 —— 退出码 0 的 fetch 丢弃了 gh 写入的 stderr(ic:5557397302)
刻意保持开放(本轮不动)
失败的检查
冲突说明
验证实际运行过的命令(本执行环境会在 120 秒处杀掉任何命令,这决定了本地能跑什么——见最后一条):
未在本地运行及原因:完整 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.
8 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- the assign POST's issues:assigned self-trigger in qwen-autofix.yml (.github/scripts/upsert-deferred-issue.sh:518) — already reported (comment 3941924819, R2-6 (A)); the author filed it as issue 11214
- the added comment's 30s spawnSync child-timeout figure (scripts/tests/qwen-autofix-workflow.test.js:15343) — already recorded (round 6 deferral at test:15330, re-recorded as D7-2 in round 7)
- the 14 new cases appended to the suite's single heaviest it(), spawn count 60 to 74 (scripts/tests/qwen-autofix-workflow.test.js:14354) — already reported (comments 3942634803 and 3943060577, R3-3), declined twice by the author
- no fixture witnessing the codepoint-safety of the jq .[0:80] title cap (scripts/tests/qwen-autofix-workflow.test.js:14527) — already recorded (round 7 deferral)
- the assign warning's captured reason never asserted (scripts/tests/qwen-autofix-workflow.test.js:14258) — already recorded (round 4 deferral at test:14351)
- the author interpolated mid-clause in the published body sentence (.github/scripts/upsert-deferred-issue.sh:471) — already recorded (round 5 deferral)
- the ready-for-agent comment arguing one state of a two-state gate (.github/scripts/upsert-deferred-issue.sh:486) — already recorded (round 6 deferral); re-derived by this round's round-4 auditor and rejected on that ground plus a measured f…
- the review's empty build-and-test scope, both changed files sitting outside every npm workspace (.github/scripts/upsert-deferred-issue.sh:1) — already disclosed (round 5, 6 and 7 bodies)
Not reviewed: build-and-test — Test (ubuntu-latest, Node 22.x) was cancelled at its 120-minute ceiling before reaching npm run test:scripts, Test (macos-latest) and Test (windows-latest) were skipped, and this review's own scoped build-test ran no suite because both changed files sit outside every npm workspace.
Not reviewed: the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.
Not explored to full depth (tool budget reached): "agent 1c": could not execute scripts/tests/qwen-autofix-workflow.test.js to measure the heaviest case's post-diff runtime — the review worktree has no node_modules ( v….
Deferred under the convergence posture (round 8, not a blocker) — recorded, not requested in this round:
.github/scripts/upsert-deferred-issue.sh:455 — [probe] Degradation gate enumerates derivations, not the response contract.github/scripts/upsert-deferred-issue.sh:461 — [probe] Degradation warning promises persistence before the create runs.github/scripts/upsert-deferred-issue.sh:471 — [probe] Degraded-path body prose has no witness for the guard's false side
Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 8 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):build-and-test — Test (ubuntu-latest, Node 22.x) was cancelled at its 120-minute ceiling before reaching npm run test:scripts, Test (macos-latest) and Test (windows-latest) were skipped, and this review's own scoped build-test ran no suite because both changed files sit outside every npm workspace.
未审查(原文为英文):the executable-script lint — .github/scripts/upsert-deferred-issue.sh: shellcheck is not installed.
未探索到全部深度(达到工具调用预算):"agent 1c":could not execute scripts/tests/qwen-autofix-workflow.test.js to measure the heaviest case's post-diff runtime — the review worktree has no node_modules ( v…。
收敛姿态下延后(第 8 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
— qwen3.8-max via Qwen Code /review (v0.23.0)
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round — no actionNo commit this round. The feedback since the last evaluation contains no requested code change, and the two newly red checks are job-level timeout cancellations on the shared ECS pool with no code cause in this diff. Feedback triage
VerificationCommands actually run against the checked-out head (
No files changed; nothing committed. 中文说明Autofix 本轮——无需改动本轮不产生提交。 自上次评估以来的反馈不包含任何要求修改代码的内容,两个新变红的检查均为共享 ECS 资源池上的作业级超时取消,与本 diff 没有代码层面的因果关系。 反馈分诊
验证对已签出的头部提交(
未改动任何文件;未产生提交。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
CI attribution for the two failing required checks: runner-timeout cancellations, not this PR
Test job (run
web-shell E2E Smoke (job The same check is cancelled at the same ~20m2x wall on unrelated PRs, which makes it repo-wide rather than diff-specific:
Decisive for this PR specifically: the live diff is exactly two files, and neither can reach the web-shell E2E surface or npm install time — Zero files in the diff match The two lanes alternate green/timeout across commits of this same PR — the signature of fleet contention, not of a code cause:
main is red in the same window, on the same fleet:
This PR's own test file, run locally at head Green, and ~60 seconds — it cannot account for a 98-minute test phase, and it never ran in the cancelled job anyway. Verdict: category (ii), not caused by this PR — self-hosted runner contention and the job-level timeouts it provokes. This is the non-determinism already tracked in #10490, which the comment above that step ( Nothing is being fixed inside this PR for it, and the failing tests are not in its 2-file diff ( |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round — no actionNo code changes were made this round. The working tree remains at head Feedback dispositions
Verification
中文说明Autofix 本轮 — 无需改动本轮未做任何代码修改。工作树仍停留在 head 反馈处置
验证情况
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
Maintainer verification — real-environment run of
|
| call | result |
|---|---|
POST …/issues -f 'assignees[]=<non-collaborator>' |
HTTP 422 "assignees octocat cannot be assigned to this issue" — no issue created |
POST …/issues/<n>/assignees -f 'assignees[]=<non-collaborator>' |
exit 0, no stderr, assignees unchanged — a silent no-op |
That settles it: assignees on the create call would have lost the findings outright for exactly the external contributors this branch exists for, and the separate call never warns for a non-assignable author — so the body cc really is the mechanism that reaches them, not a belt-and-braces extra.
The deep link is GitHub's canonical form. https://github.com/{repo}/pull/{pr}#discussion_r{id} is byte-identical to the html_url GitHub returns for a review comment; checked live against #11074's own finding id (…/pull/10991#discussion_r3937553394, HTTP 200).
Worst-case title length is fine. Deferred review findings from PR #12345: + 80 CJK codepoints = 95 codepoints / 203 bytes; real GitHub stored and returned it byte-identically.
4. A/B, regression parity, malformed shapes, CI
- The tests are a gate. Merge-base source + this PR's own tests → red at
qwen-autofix-workflow.test.js:14317. - Zero regressions.
npm run test:scripts(82 files) on the same box: merge-base15 failed | 2161 passed, PR head15 failed | 2161 passed— identical. All 15 are pre-existing local-environment failures (uid 0, node_modules drift), including the one insideqwen-autofix-workflow.test.js. - 9/9 malformed PR-object shapes persist the findings (non-string
.title,.user: null, no.user, whitespace title,[], a200error object,dependabot[bot], the autofix bot itself). Only the two genuinely-unusable fetches warn — the&&gate does not warn on a healthy round with an unassignable author. shellcheck --severity=style: 0 findings, both sides.
Findings
F1 — stale count in a sibling comment (nit). .github/scripts/autofix-push-and-report.sh:40 records "26 findings for run-autofix-review-verification.sh, 9 for upsert-deferred-issue.sh, 32 here". This PR takes that script to 10 under --enable=all (one new SC2312 at the gh api …/assignees … || echo line). The lane can't fail on it, but this repo treats comment counts as pinned facts elsewhere, so it's worth the one-character update.
F2 — whitespace-only PR title yields a dangling separator (nit, unreachable today). {"title":" "} passes [[ -n "${PR_TITLE_RAW}" ]], producing Deferred review findings from PR #N: with a trailing : . Harmless — the lookup's startswith($t + ":") still adopts it — and GitHub rejects blank PR titles, so it is only reachable if the fetch ever points at something other than pulls/N. Mentioning it because the script's own comment leans on "a whitespace-only title still flattens to a non-empty " as a desirable property of the warning gate; it is also what produces this.
F3 — the login charset guard's strictness is owned by another file (observation). [[ "${PR_AUTHOR}" =~ ^[A-Za-z0-9-]{1,39}$ ]] is locale-dependent in glibc. Measured on this box:
LC_ALL=C ä=no A=no
LC_ALL=en_US.UTF-8 ä=MATCH A=MATCH
Production is safe: autofix-push-and-report.sh:430 launches the script through /usr/bin/env -i with no LANG/LC_ALL, so it runs in the C locale. Nothing to change — but the guard is the only thing keeping a forged login out of a cc @… mention published under the bot identity, and its strictness silently depends on a property set in a different file. A half-line reference to that (# strict under the env -i C locale the caller pins) would make the coupling visible.
Follow-up, not for this PR. rv:/ic: could carry #pullrequestreview-<id> / #issuecomment-<id> links later; the counterfactual above quantifies the cost of doing it naively (every persisted item re-published once). Doing it properly needs a dedupe migration, which is correctly out of scope here.
One thing to do before merge
Test (ubuntu-latest, Node 22.x) is cancelled at 439cd8e8 — and that is the job that runs npm run test:scripts. Lint & Static and Integration Tests (no-AK) are green, but CI has never executed this PR's new tests at this head. My local run is the substitute, not a replacement for the required check. Please push an empty commit or re-run that job before merging.
Recommendation
Approve. The design decisions I'd normally push back on — an extra API call on a hot path, appending text to lines that a dedupe mechanism compares, publishing contributor-controlled text under a bot identity — each turn out to be the ones the author already reasoned through, and every one of them holds under a real-environment test. F1–F3 are optional polish.
Harness: real gh → local HTTPS GitHub-API stand-in (fault-injecting, request-recording) + the real script; plus real github.com runs on my fork. The fork probe issues and the fixture branch were deleted after capture, so the screenshots are the surviving evidence.
中文版
Maintainer 验证 —— 在真实环境中运行 upsert-deferred-issue.sh @ 439cd8e8
我没有只读 diff,而是搭了两套独立的真实环境,因为这个 PR 的关键论断几乎都是关于 GitHub 真实行为的论断,而 recording-gh 测试套件只能断言"我们告诉 stub 去做什么"。
- 真实
gh2.46.0 → 本地 HTTPS GitHub API 服务 → 真实脚本。 没有 stub:本地受信 CA、GH_HOST=localhost:8443、真实 HTTP、真实jq、真实bash。脚本发出的每个请求都被记录,故障(500、200 text/html、assign-422、create-500)在服务端注入。 - 同一个脚本跑真实
github.com:在我的 fork 上端到端驱动,目标 PR 的标题是 118 码点的恶意 markdown,并带一条真实的行内 review 评论。
结论:机制确实做到了描述所说的事,包括我原本预期会翻车的部分。 下面 3 条发现全是 nit,都不阻塞合并。真正需要在合并前处理的是一个流程问题:CI 从未在当前 head 上跑过本 PR 的测试。
1. 端到端,真实 gh,无 stub(截图 1)
- 创建路径恰好发出所述的四个调用,顺序也一致:lookup →
GET pulls/N→POST issues→POST issues/N/assignees。 - 追加路径发出 0 个
pulls/调用,上下文拉取确实仅限创建路径,最常跑的稳态轮次不会额外消耗速率限制。 - 降级是真的,而且会告警。
pulls500 与pulls200 text/html都回退到裸标题 / 无 assignee / 无 cc,findings 仍然落盘,且各自打印了指明原因的告警。第二个门更有意思:我用真实二进制确认了gh api在200 text/html上退出码为 0 并原样拷贝正文——所以只看PR_FETCH_OK会静默降级。门里( -z TITLE && -z AUTHOR )这一半是承重的。 - 创建只发一次、绝不重试(create-500 → 恰好 1 次 POST、0 次 assignee 调用、LOST 告警 + 原始转储)。assign-422 → 1 次创建 POST、findings 落盘、仅告警。
- CJK 标题、
LC_ALL=C、输入 104 码点 → 输出恰好 80 码点且是合法 UTF-8。同样输入下等价的 bash${x:0:80}字节切片会从 UTF-8 序列中间截断。jq 切片不是风格偏好。
2. 存量约 100 个 tracking issue 的迁移 —— 我最想证伪的风险
我先用 merge-base 脚本建出 tracking issue(裸 rc:/rv:/ic: 行、无深链),再用 PR 脚本以相同 findings 跑一轮:
comments appended : 0 -> 没有重复浪潮
反事实:同样的重放,但把深链后缀放宽到 ic/rv:
comments appended : 1 -> 把 2 条已跟踪条目当作新条目重新发布
所以"只给 review_comment 加链接"不是保守,而是挡在这个 PR 和"存量 issue 上一次性重复浪潮"之间的那道墙。lookup 身份端到端也成立:marker 被编辑掉的增强标题 issue 会被收养、存量裸标题 issue 仍被收养、PR #5 不会前缀误吞 PR #50 的 issue。
3. 真实 github.com —— stub 无法裁决的论断(截图 3、4、2)
标题面是纯文本。 真实跑出的 issue 里,[URGENT: sign in to keep CI green](https://evil.example/phish) <details> 原样显示为字面字符,没有链接、没有折叠。assignee 由独立调用设置,正文有 cc @…,rc: 条目带深链、rv: 条目刻意不带;第 2 轮只追加了新 finding。
正文面不是。 我把同一个标题——按脚本对 agent 文本完全一致的净化方式处理(ZWSP 提及、& 实体、<!-- 开头)——送进 GitHub 自己的 POST /markdown。左边是本 PR 实际发布的正文,右边是"若把标题复制进正文"的结果:markdown 链接原样存活并渲染成活链接;未闭合的 <details> 让 GitHub 注入一个通用 Details 摘要,并把整个 findings 列表嵌进折叠里。脚本注释里的两半论证都成立,退掉 scriptEscapeSites 计数点是正确的钉法。
assignment 语义 —— 为什么必须独立调用。 对真实 API:
| 调用 | 结果 |
|---|---|
POST …/issues -f 'assignees[]=<非协作者>' |
HTTP 422 "assignees octocat cannot be assigned to this issue" —— issue 根本没被创建 |
POST …/issues/<n>/assignees -f 'assignees[]=<非协作者>' |
退出 0、无 stderr、assignees 不变 —— 静默 no-op |
这就定案了:在创建调用上带 assignees,恰恰会让这个分支所服务的外部贡献者场景彻底丢失 findings;而独立调用对不可 assign 的作者根本不会告警——所以正文的 cc 才是真正触达作者的机制,而不是可有可无的补充。
深链就是 GitHub 的规范形式。 https://github.com/{repo}/pull/{pr}#discussion_r{id} 与 GitHub 返回的 html_url 逐字节一致;用 #11074 自己的 finding id 做了线上校验(HTTP 200)。
最坏情况的标题长度没问题。 Deferred review findings from PR #12345: + 80 个 CJK 码点 = 95 码点 / 203 字节,真实 GitHub 逐字节原样存储并返回。
4. A/B、回归对齐、畸形形态、CI(截图 5)
- 测试确实是门。 merge-base 源码 + 本 PR 自己的测试 → 在
qwen-autofix-workflow.test.js:14317变红。 - 零回归。 同一台机器上
npm run test:scripts(82 个文件):merge-base15 failed | 2161 passed,PR head15 failed | 2161 passed,完全一致。15 个失败全是既有的本地环境问题(uid 0、node_modules 漂移),包括qwen-autofix-workflow.test.js里的那一个。 - 9/9 种畸形 PR 对象形态都保住了 findings(非字符串
.title、.user: null、无.user、纯空白标题、[]、200 的错误对象、dependabot[bot]、autofix bot 自己)。只有两种真正不可用的拉取会告警——&&门不会在"健康轮次但作者不可 assign"时误报。 shellcheck --severity=style:两侧均 0 findings。
发现
F1 —— 兄弟文件里的计数已过期(nit)。 .github/scripts/autofix-push-and-report.sh:40 写着"…9 for upsert-deferred-issue.sh…"。本 PR 把该脚本在 --enable=all 下的数量推到 10(新增一个 SC2312,位于 gh api …/assignees … || echo 那行)。该 lane 不会因此失败,但这个仓库在别处把注释里的计数当作被钉住的事实,值得改这一个字符。
F2 —— 纯空白 PR 标题会产生悬空分隔符(nit,当前不可达)。 {"title":" "} 能通过 [[ -n "${PR_TITLE_RAW}" ]],生成末尾带 : 的标题。无害——lookup 的 startswith($t + ":") 仍会收养它——且 GitHub 不接受空白 PR 标题,只有当这次拉取指向 pulls/N 以外的东西时才可达。之所以提,是因为脚本注释把"纯空白标题仍会 flatten 成非空的 "当作告警门的理想性质来依赖;同一性质也导致了这个现象。
F3 —— 登录名字符集守卫的严格性由另一个文件决定(观察)。 [[ "${PR_AUTHOR}" =~ ^[A-Za-z0-9-]{1,39}$ ]] 在 glibc 下依赖 locale。本机实测:
LC_ALL=C ä=no A=no
LC_ALL=en_US.UTF-8 ä=MATCH A=MATCH
生产是安全的:autofix-push-and-report.sh:430 通过 /usr/bin/env -i 且不传 LANG/LC_ALL 启动脚本,因此运行在 C locale。无需改动——但这个守卫是唯一挡住"伪造 login 进入以 bot 身份发布的 cc @… 提及"的东西,而它的严格性却静默地依赖另一个文件设定的属性。加半行引用(# strict under the env -i C locale the caller pins)能让这个耦合可见。
后续(不属于本 PR)。 rv:/ic: 将来可以带 #pullrequestreview-<id> / #issuecomment-<id> 链接;上面的反事实量化了"天真地做"的代价(每条已落盘条目被重新发布一次)。要做对需要一次去重迁移,本 PR 把它排除在范围外是正确的。
合并前需要做的一件事
Test (ubuntu-latest, Node 22.x) 在 439cd8e8 上是 cancelled,而这正是跑 npm run test:scripts 的 job。Lint & Static 与 Integration Tests (no-AK) 是绿的,但 CI 从未在当前 head 上执行过本 PR 的新测试。我的本地运行是替补,不是必需检查的替代品。合并前请推一个空提交或重跑该 job。
建议
Approve。 我通常会去挑战的那几个设计选择——热路径上多一次 API 调用、往被去重机制比对的行上追加文本、以 bot 身份发布贡献者可控文本——结果都是作者已经想清楚的,而且每一条在真实环境测试下都成立。F1–F3 属于可选打磨。
验证环境:真实 gh → 本地 HTTPS GitHub API 替身(可注入故障、记录请求)+ 真实脚本;外加在我 fork 上对真实 github.com 的运行。fork 上的探针 issue 与 fixture 分支已在截图后删除,截图即留存证据。
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (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: 675 passed · 0 failed · 675 total Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:675 通过 · 0 失败 · 675 总计 抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #11080 — deep verification (round 2)Verdict: This is a follow-up round;
中文摘要结论: 这是第二轮:上一轮报告存在,本轮重新实测了其中每一项,而不是对比旧报告。没有任何阻断项。上一轮两条 Suggestion 均已解决或显著减轻:
变异矩阵 12/13 → 19/19 全部击杀,未变异对照 GREEN,正值对照被击杀,工作树 sha256 校验还原。见 A/B 结论:中心主张成立且 load-bearing。真实脚本在 workflow 的 本轮撤回上一轮自己的 Correction 2:上轮称" Findings(详见下文):4 条 Nit —— 标题 flatten 未覆盖 未覆盖:逐 commit 归因(快照 10 个 commit,shallow 本地仅可达 1 个);GitHub 真实渲染与真实 API(无 token、无写操作);assign 调用的真实 wire body;整份 237 个测试的文件(只跑了 diff 触及的两个 Previous-finding statusEvery row was re-measured at
Scope
Central claim: on the create path only, fetch the PR once and make the tracking issue self-describing — title Secondary claims, both verified: (a) the Delta since the last round (the three commits after Central claim — A/B tableBoth arms run the real shipped script, never a stub of it, under the production contract transcribed from
A9 is the sharpest cell and it is the base arm that fails: base's fallback only tests Facts established without A/B, because they are deterministic properties of tools rather than of this diff:
CorrectionsCorrections to text, not requests to change code.
FindingsAll four are Nits. None blocks; none loses findings; every one reproduces with the command given. 1. The title flatten covers
|
| character | reaches the issue TITLE | flattened |
|---|---|---|
\n \r \r\n \t |
no | yes |
\v (VT, U+000B) |
yes | no |
\f (FF, U+000C) |
yes | no |
| U+0085 (NEL) | yes | no |
| U+2028 (LINE SEPARATOR) | yes | no |
| U+2029 (PARAGRAPH SEPARATOR) | yes | no |
In all nine cases the create still succeeds and nothing reaches the body. This is new surface: base never placed any contributor-controlled text in the title, so base has zero exposure here. Bounding it: the consequence is a control character inside a plain-text issue title — cosmetic, and GitHub's own title handling decides the rendering, which I could not observe (see Not covered). It is worth naming only because the block comment says "Flatten and cap only", which reads as broader than the character class delivers.
cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260907-061243/delta.mjs # section D2Minimal suggested fix (NOT applied, NOT measured — advisory only)
- | gsub("[\r\n\t]+"; " ")
+ | gsub("[\r\n\t\u000b\u000c\u0085\u2028\u2029]+"; " ")I did not apply and re-run this, so I am not claiming it is measured. Two things a maintainer should check before taking it: jq 1.6's handling of \u0085 inside a bracket expression, and whether the change perturbs the entityTitle fixture at :14540, which pins the flatten's current output byte for byte.
2. A title-only degradation is silent — the warning gate covers total failure only (Nit)
The gate is PR_FETCH_OK != 1 || ( -z PR_TITLE_RAW && -z PR_AUTHOR ), and the script's own comment scopes it correctly to "a fully degraded round". But that leaves a partially degraded round unannounced, and the || true on both jq derivations swallows the reason. Measured across eight shapes (D11):
.title in the PR object |
jq outcome | issue title | cc / assign | warning |
|---|---|---|---|---|
"" / null / absent |
"" |
BARE | yes / 1 | none |
12345 / true / {"a":1} / ["a"] |
jq error, swallowed by || true |
BARE | yes / 1 | none |
" " (whitespace) |
" " |
ENRICHED | yes / 1 | none — correct, and it confirms the comment's claim |
In all eight the findings persist and exactly one create is issued, so nothing is lost — only the enrichment silently does not happen. Reachability, bounded: of these shapes only an empty/null/absent title is plausible from the real endpoint, and there the silence is arguably right (a PR with no title has nothing to enrich and there is no failure to report). The four non-string types require a structurally unexpected response — the same transparent-proxy class the gate's second half exists to name — and there the swallowed jq error means the round looks healthy. This is the same root cause as carried-forward Finding 4: the reason machinery only covers total failure.
cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260907-061243/delta.mjs # section D113. Carried-forward: "Each rc: item links back…" on batches with no rc: item (Nit)
Unchanged since the last round. rv-only, ic-only and rv+ic batches each render no rc: bullet and no deep link, yet the body still asserts a property of items it does not contain. Cosmetic; no dedupe or rendering consequence, and the exemption itself is correct in all three (verified: no #discussion_r reaches any of them).
cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260907-061243/delta.mjs # section D64. Carried-forward: a fetch that exits 0 with stderr discards that stderr (Nit)
Unchanged since the last round. PR_CTX_REASON is overwritten unconditionally when PR_FETCH_OK == 1, so a diagnostic gh wrote while still exiting 0 is replaced by the generic reason. Strictly better than base, which emits nothing on this path.
cd /__w/qwen-code/qwen-code && node tmp/pr11080-verify-20260907-061243/delta.mjs # section D7Consequences I tested that do not hold
Bounding these matters more than escalating the Nits, so these are the scarier readings I tried and disproved at the new head:
- No injection through the contributor-controlled PR title — the surface is now gone. The last round swept an escape chain; this round there is no chain to break because the title never reaches the body. I swept 44 hostile payload classes (markdown link, image beacon, unclosed and closed
<details>,<!\-\-/-->, raw HTML,<img onerror>, four mention spellings plus\@and ZWSP,::error::,##[, code span, fenced block, heading, table, blockquote, backslash escapes, shell expansion,\u0000, RTL override, CJK, emoji, all six line-break forms,%/+, bare@,--flag-injection,@filename-injection, empty, whitespace-only, 400 chars) with a canary placed first in each title so the 80-codepoint cap cannot make "absent from body" trivially true. Every canary is asserted present in the title argv (a positive control that the fixture reached the code) and absent from the body. Zero survivors across 501 assertions. Six fixtures additionally assert payload-specific witnesses absent —evil.example,URGENT,<details>,beacon.png,</details>,<script>,alert(1),::error::,##[group,victimuser,@,&#64;,@,admin— while the body keeps- rc:7andfrom PR #5 by someone. Witness04-hostile-title-sweep-canary-absent-from-body.png. - No cross-PR adoption collision. Seven hostile titles (
Deferred review findings from PR #50,0: Deferred review findings from PR #50,:,:::,Deferred review findings from PR #5, empty, 200 chars) driven into PR TypeError in Authentication Selection Interface #5's created title: PR refactor(cli): update OpenAI API key prompt with Bailian URL #50's marker-less lookup never adopts PR TypeError in Authentication Selection Interface #5's issue and always creates its own, while PR TypeError in Authentication Selection Interface #5 still adopts its own. The mechanism is structural, not lucky —CREATE_TITLEalways begins with the looking-up PR's own bare title, and the only reader isstartswith($t + ":"), so a hostile suffix cannot forge another PR's prefix. - No duplicate wave over the ~100 existing issues. The corpus was not hand-typed: the base script authored it (rc + rv + ic in one batch), then head replayed the identical findings against that real base-authored body and re-published 0 items. The widening mutant re-published exactly 2 — the
rvandiclines — whilercstayed suppressed by its id anchor. - No codepoint damage. CJK-100, emoji-100, mixed-astral, combining-mark, exact-80, exact-81 and a 79-char-then-
@title all slice to ≤80 codepoints with no U+FFFD, round-trip as valid UTF-8, and remain adoptable by the lookup. The sharpest cut —@landing at codepoint 80 — leaves a bare trailing@in the title with no part ofvictimusersurviving anywhere. - No
GH_ERRcross-contamination. With fetch and create both failing, the two warnings name their own distinct reasons (FETCH-REASON-unique/CREATE-REASON-unique) with neither leaking into the other; the assign warning names the assign reason and is not polluted by an earlier call. - Degenerate payloads never lose findings. 18 shapes (array, empty object, empty string, HTML, five non-string title types, null/absent user, null/numeric/object login, embedded
\u0000, a nested-JSON-string title, whitespace title, a 5 000-char title) all exit 0, all persist, all issue exactly one create, and the warning fires exactly when both derivations came back empty.
Mutation matrix — 19/19 killed
Witness: 02-mutation-matrix-19-of-19-killed.png. Oracle is the PR's own suite via vitest's JSON reporter (testResults[].assertionResults[]) — never a regex over ANSI output, which is how the previous round's first attempt fabricated a fake 13/13. The unmutated control is GREEN (passed=1 failed=0) and the positive control is KILLED, so the harness demonstrably can make this suite fail. Each mutant is a single-point edit with an exactly-one-occurrence precondition and a bash -n check (on a real file — bash -n /dev/stdin is unavailable in this container) before use; each swaps the working-tree script, runs, and restores in a finally, with the final sha256 asserted equal to the recorded pristine a1c904706f542b72… and git status --porcelain confirmed empty. 0 build errors.
| mutant | claim under test | verdict | first red (qwen-autofix-workflow.test.js) |
|---|---|---|---|
| PC rename the dedupe marker | positive control | KILLED | :14311 marker present in the create call |
| M1 widen deep-link suffix to all sources | rv/ic exemption | KILLED | :14905 perSource rv side |
M1b widen to issue_comment only |
adjudicates Correction 2 | KILLED | :14922 icTracked — sole ic witness |
| M2 drop the colon guard in the lookup | enriched adoption | KILLED | :15200 adoption of #44 |
| M3 drop the login-charset guard | malformed .user.login |
KILLED | :14415 badLogin |
| M4 gate on call status only | warning gate's second half | KILLED | :14383 prUnusableBody warning |
| M5 delete the context warning | observability | KILLED | :14360 prFetchFailed warning |
M6 raw ${GH_ERR} instead of gh_reason() |
:: neutralisation |
KILLED | :14360 ;;error;; vs ::error:: (same assertion M5 deletes) |
| M7 make the append path fetch too | creation-only context | KILLED | :14582 append issues no pulls/ |
| M8 retry the create on failure | never retried | KILLED ← was SURVIVED | :14647 expected [ …(3) ] to have a length of 2 but got 3 |
M9 drop -n "${NUM}" from the assign guard |
no assign after failed create | KILLED | :14652 writeFail sees no assignees |
| M10 re-add the retired title→body copy | title kept out of markdown | KILLED | :14512 body must not contain evil.example |
M11 widen {1,39} to {1,} |
40-char login | KILLED | :14446 longLogin |
M12 admit @ into the charset class |
a@b login |
KILLED | :14436 atLogin |
M13 flip the degradation gate's && to || |
title-present / author-rejected | KILLED | :14425 no spurious warning |
M14 delete the [\r\n\t] flatten |
title flatten | KILLED | :14540 expected undefined to be '…PR #5: …' |
M15 delete the .[0:80] cap |
title cap | KILLED | :14540 exact capped string |
| M16 restore the ambiguous ready-for-agent parenthetical | pointer aimed per-item | KILLED | :14335 |
M17 one-sided restyle of the ": " separator |
round-trip write ↔ adopt | KILLED | :14318 |
M18 plant a second gsub("<!\-\-"; …) site |
census stays at 1 | KILLED | :16412 toHaveLength(1) — a pre-existing assertion, see Correction 4 |
Every declared mutation claim in the new test comments held, including the one that failed last round. Vacuity of the central new test is proven twice over (vacuity.mjs, 8/8): the head suite against the base script goes red at :14317 with AssertionError: expected '…' to contain 'api repos/o/r/pulls/5' — an assertion mismatch naming expected-vs-actual, not a crash — and a single-line revert that drops only the pulls fetch (leaving every precondition intact) reds the same assertion. Per the skill's rule on blunt reverts, the fine result is the one to trust: the test is pinned by the change itself.
The census is live, not decorative: the shipped script matches gsub("<!\-\-"; "…") exactly once, and planting a second site makes the same regex count 2. CREATE_TITLE has exactly three occurrences — two writes and one read, at -f title="${CREATE_TITLE}" — so the enriched title has no second consumer.
Not covered
- Per-commit attribution. The snapshot lists 10 commits; the depth-2 merge-ref checkout makes 1 reachable (
git rev-list HEAD^1..HEAD^2→ only439cd8e8, andgit rev-parse --is-shallow-repository→true). A baregit rev-list --countreturns a plausible1at a shallow boundary rather than erroring, so this was checked against the snapshot'scommitsarray, not assumed. The last round could see 5 of its commits' messages; this round the three delta commits were identified from the snapshot's headlines and the aggregate diff only. I verified the aggregateHEAD^1..HEADdiff; the nine earlier commits' individual claims were not separately exercised. baseRefOiddisagreement, named rather than resolved. The snapshot'sbaseRefOidis9c1c41a9…while the merge-ref'sHEAD^1is92a8a8d1….HEAD^2matches the snapshot'sheadRefOidexactly (439cd8e8…), which confirms a merge-ref checkout, soHEAD^1is the base the A/B used, per the CI contract. The snapshot OID was captured at a different moment against a movingmain; I could not fetch to reconcile it (no token).- The real GitHub renderer and API. No token, no writes. The design premise that issue titles are stored and rendered as plain text — no markdown pass, no mention filter is what justifies leaving the title unescaped while keeping it out of the body. I verified the script implements that split (title raw, body never carries it, asserted per call so neither surface can be credited with the other's rendering) but I could not observe GitHub actually render a title. Every escaping assertion is against recorded argv, not rendered HTML. This is also the limit on Finding 1's severity: what GitHub does with a U+2028 in a title is unmeasured here.
- The real
ghwire body for the assign call.GH_DEBUG=1does not print request bodies in this build, so the body was verified fromgh api --help(gh 2.100.0's own manifest) plus 3 occurrences across 2 in-repo-f "labels[]=…"precedents, not from a captured payload. No network call was made against the API this round. - The full 237-test file. Two targeted
-tgates only: the upsert block (which contains every new behavioural case; green, 16.6–17.3 s) andposts a human-handoff marker when review addressing reaches a terminal handoff(which contains thescriptEscapeSitescensus; green, 44 ms per-test / 3.1 s wall). That assertion is unchanged context in the aggregate diff — see Correction 4. Each-trun reportspassed=1 failed=0 skipped=236, i.e. 237 tests in the file, counted from the JSON reporter rather than estimated; the two gates between them exercised 2 distinct tests, leaving 235 unrun. - Repo-wide gates. No
npm run test:scripts, no eslint, no typecheck, nonpm run build: the diff touches no TypeScript source and no package, and the two files it touches are covered by the gates above. - shellcheck ran directly, not through the repo wrapper.
node scripts/lint.js --shellcheckends its pipeline insed, so its exit status issed's and a clean 0 there is weak evidence. I ran shellcheck 0.11.0 with the repo's exact flags (--check-sourced --enable=all --exclude=SC2002,SC2129,SC2310 --severity=style --format=gcc --color=never) on both arms: head 10 findings (SC2154×4 warning,SC2312×6 note), base 9 (SC2154×4,SC2312×5). The delta is +1SC2312note at line 520, the new assign warning's$(gh_reason)— the identical idiom the file already uses at base. No new finding class, no error-severity finding. The gate's liveness was proven by plantingSC2034andSC2164and confirming both are reported. The binary'ssha256was verified againstSHELLCHECK_SHA256['linux.x86_64']inscripts/lint.jsbefore extraction (8c3be12b…4e227198, exact match). - actionlint / yamllint not run. Neither binary is present and
pip3 install --useris not permitted here. Immaterial: the diff changes no YAML. I readqwen-autofix.ymlat:656,:995and:6618-6672but did not lint it. - Windows / macOS. The script is bash + jq + gh; only Linux was exercised. The PR's own table marks both as CI-only.
- The ~100 live tracking issues. No network/token, so the migration was simulated by having the base script author the corpus rather than by reading real issues. This reproduces the shape of the migration (base-authored bytes replayed through head), not the real issues' contents.
- No calibration against a real emitted artifact. This is a script PR, not a workflow-step PR: there is no posted comment or uploaded file whose bytes a replay could be calibrated against, and
previous-report.mdis a report about the script, not an artifact the script emitted. So nothing here claims calibration. What substitutes for it is that every cell drives the shipped script itself, under the invocation contract copied from the workflow, with the base arm as a live control — including the contract's easily-dropped detail that the child's stdout and stderr both go to/dev/nulland only fd 3 survives. - jq version. This container has jq 1.6; I did not establish which version the production runners use. All jq programs exercised here are 1.6-compatible, but a 1.7-only difference would not have shown up. This bears on Finding 1's suggested fix, which I therefore did not apply.
- Duration measurements are single-machine. 3 runs per arm, sequential, in a quiet window after the matrix finished. I cannot reproduce a fast-machine or heavily-contended regime by repetition here; the 3.2× gap against last round's absolute numbers is itself the evidence that load dominates.
Methodology
Everything ran in the CI verify container (node:22-bookworm, node v22.23.2, bash 5.2.15, jq 1.6, gh 2.100.0) against refs/pull/11080/merge at 1dd90b08. The unit under test is the shipped bash script, executed for real — never stubbed — under the env -i child contract transcribed from .github/workflows/qwen-autofix.yml:6627-6655, including the bot-identity precheck and the fd-3-only output channel. The only fake is gh: a recording stub (stub-gh.sh) that appends each invocation's exact argv NUL-delimited with \x1e record separators, and emulates the two behaviours the script depends on — --jq (filter applied to the JSON response) and --paginate (pages merged into one array, never double-wrapped). Assertions read that argv, so they judge the wire rather than the script's narration.
The base arm is git show HEAD^1:.github/scripts/upsert-deferred-issue.sh. Both arms were snapshotted into this directory before any harness ran (script-head.sh a1c904706f542b72…, script-base.sh 6d1064cfb44f5792…) and every harness reads the snapshot, not the live path. That is a deliberate race guard: the mutation matrix swaps the live script in place, so a harness reading it concurrently could pick up a mutant and report it as head behaviour — a race that fabricates a result rather than crashing. The duration harness's base arm ran in a scratch worktree at tmp/base-tree (removed afterwards; git worktree confirmed gone and git status --porcelain empty), with both arms' script shas asserted against the snapshots before timing and the worktree's module resolution checked (readlink -f node_modules resolves inside the worktree, and the diff touches no package, so nothing crosses the workspace boundary). The diff changes no manifest or lockfile — asserted programmatically — so reusing the installed root node_modules is a clean control.
Harnesses, all in this directory with their raw logs and rerunnable by a maintainer: ab-suite.mjs (127 assertions, logs-ab.txt), delta.mjs (501, logs-delta.txt), attribution.mjs (9, logs-attribution.txt), vacuity.mjs (8, logs-vacuity.txt), m1b.mjs (8, logs-m1b.txt), matrix.mjs (20 encoded expectations, logs-matrix.txt + matrix.json), shellcheck-gate.sh (2 liveness checks, logs-shellcheck.txt), durations.mjs (measurements, logs-durations.txt + durations.json), ab-table.mjs (the captured table, logs-ab-table.txt), tally.mjs (derives assertions.json from those artifacts so every count is auditable, logs-tally.txt). 675 assertions, 0 fail.
Four harness defects were found and fixed during the round, and are recorded because each shaped intermediate output; all four were proven to be harness faults before any conclusion was drawn from them. (1) field() looked for a joined -f title= argv element, but bash passes -f and title=… separately — 27 A/B cells reported false reds until the oracle was fixed. (2) The first driver did not replicate production's > /dev/null 2>&1 + exec >&3, producing a spurious Bad file descriptor on stderr; the fd-3 form is now transcribed exactly. (3) The authorDegraded oracle matched a bare ' by ', which the body always contains as "deferred by the autofix loop", so it reported every author as usable and three total-degradation cells looked like gate failures. (4) bash -n /dev/stdin is unavailable in this container, which turned all 19 mutants into BUILD-ERROR on a dry run — caught by running --dry before spending 20 minutes of vitest. The dry run is also what confirmed all 19 edit targets occur exactly once.
Flakiness gate log
rounds=5 files=1 skipped=0
file scripts/tests/qwen-autofix-workflow.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/qwen-autofix-workflow.test.js
per-file results (P=pass F=fail I=infra-exit, one letter per run):
scripts/tests/qwen-autofix-workflow.test.js: PPPPP
verdict: pass
summary: 1 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
round 2 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
round 3 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
round 4 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
round 5 · scripts/tests/qwen-autofix-workflow.test.js: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qqqys
left a comment
There was a problem hiding this comment.
APPROVE (verified at head 439cd8e)
Historical items
Never any CHANGES_REQUESTED on this PR — six review rounds, all Suggestions; the final round at this head posted zero new findings and carried eight, each dispositioned (the assign self-trigger was filed as issue #11214, the ready-for-agent two-state argument was rejected with a measurement, the added-cases count was declined twice with reasoning, the rest are assertion-strength notes). The human maintainer approved this head before this pass.
My Critical-only scan (injection and persistence surfaces)
The enrichment treats contributor-controlled input with the discipline the threat requires, and I verified each claim against the code rather than the comments:
- The PR title reaches only the issue title (plain-text surface) — never the markdown body — with the exact live-phishing/
<details>-collapse/dedup-corruption vectors spelled out as the reason; body-internal strings are either auto-linked (PR #N) or charset-validated (^[A-Za-z0-9-]{1,39}$on the login before the deliberate@mention), and path/reason escaping keeps its existing chain with the rc deep-link suffix appended after the 500-char cap and justified safe by the id-anchored dedup identity (rv/ic deliberately unsuffixed to avoid a duplicate wave). - Title-cap slicing moved into jq (codepoint-safe) instead of a bash byte slice; the enriched title's lookup fallback (
startswith($t + ":")) is guarded by the colon and paired with the creation site, stated where someone would restyle one and break the other. - Persistence stays non-idempotent-safe: one create call, never retried (duplicate-orphan reasoning explicit), assignment split out so it can never take the create down, external-author assignment decline handled by the body cc, every degradation path warns loudly instead of silently reverting to bare mode — and the warning reads before the error-reset that would wipe the reason.
The +372 lines are harness pins of the above (stub PR objects, refusal arms); I noted the round's own disclosure that Test (ubuntu-latest) was cancelled by this week's pool overrun before reaching test:scripts at this head, so those pins are unobserved in CI — a shared-infrastructure condition, non-attributable and non-gating per policy; CI at head otherwise shows 14 green and zero failures.
|
Released in v0.23.1. |














What this PR does
The autofix loop files one tracking issue per PR for verified review findings whose fixes lie outside that PR's footprint (
upsert-deferred-issue.sh). Before this change the issue carried a constant title (Deferred review findings from PR #N), a bare boilerplate body, and no assignee, so nobody could tell at a glance what the issue was about or who should follow up. After this change the create path fetches the PR once and makes the issue self-describing:Deferred review findings from PR #10991: refactor(daemon): decouple extension activation refresh.[URGENT](https://evil.example/phish)would render as a live attacker-chosen link inside bot-authored text, and an unclosed<details>would fold the findings list away from the raw-body dedupe corpus. The title copy is therefore flattened and capped for the title and never copied into the body;.github/scripts/upsert-deferred-issue.shrecords the rule asDo NOT re-add it; if a title ever has to appear in the body, put it in a code span, never in prose.POST /repos/{owner}/{repo}/issuesis not idempotent, and the failures that would reach a retry are the ambiguous ones (connection reset, gateway 502, a read timeout after the server already committed), so re-POSTing can mint a second tracking issue carrying the same marker that the next round's newest-first lookup orphans forever. A rejected assignment only warns — the body'scc @authormention already reaches them.rc:bullet deep-links to its original review comment (…/pull/N#discussion_r<id>), so the bare internal comment id is no longer the only handle.Everything is best-effort: a PR-context fetch that fails or returns nothing usable degrades to the previous bare title, no assignee and no cc, and warns — metadata must never lose findings. The append path is unchanged and makes no
pulls/call at all (the context fetch is creation-only).Why it's needed
Over the past week this mechanism created ~100 tracking issues (e.g. #11074), and each one reads as scattered noise: a title that says nothing about the content, a body of
- rc:<internal id>bullets with no way back to the original review comment, and no assignee. A maintainer has to open the source PR and hunt through its review threads to figure out what a tracking issue even refers to, and nobody is pointed at for follow-up. This PR puts the context and the owner on the issue itself.Reviewer Test Plan
How to verify
The recording-gh test suite pins the behavior end to end; run the upsert block (pass
--config, asnpm run test:scriptsdoes: the bare form resolves the root config'sscriptsproject, which has no config file of its own and so falls back to vitest's 5s defaulttestTimeout— this case measures ~4.8s and times out):npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js -t 'upserts deferred findings into a per-PR issue that survives the merge'New cases cover: enriched title + assignee + deep link on the create call; degraded create (failed PR fetch → bare title, no assignee, still persisted, plus a
could not fetch PR #5 contextwarning naming the captured reason); degraded create when the fetch exits 0 with an unusable body (200 text/html→ same degradation, warning says the call returned no usable PR object); assignment rejected (separate assign call fails →could not assignwarning, exactly one create POST, findings still persisted); bot-authored PR (never self-assigned, no cc); malformed.user.loginshapes (a space, an@, 40 characters) never reaching thecc @…mention or theassignees[]argument; enriched-title adoption by the lookup with the colon guard (PR #5's base never prefix-adopts PR #50's issue); the PR title reaching the plain-text issue TITLE raw (flatten + 80-codepoint cap only:[URGENT](https://evil.example/y) <details> fix @fooandping @admin @x+ 100As both come back verbatim apart from the newline fold and the cap) and reaching the markdown-rendered body in no spelling —evil.example,URGENT,<details>,fix @fooand the ZWSP-neutralized@fooare all asserted absent from-f body=whilefrom PR #5 by someonestays;expect(scriptEscapeSites).toHaveLength(1)keeps the retired title-neutralization site retired, so re-adding one has to be deliberate; the ready-for-agent pointer in the body attaching to the per-item issue rather than to this tracking issue (which now carries an assignee and is therefore filtered out of the scheduledno:assigneescan); and the append path issuing nopulls/call.The lookup's title fallback also keeps adopting existing bare-form issues (exact match) — pinned by the pre-existing marker-stripped case.
Evidence (Before & After)
Before (live issue #11074):
After (local dry-run of the script against a recording gh stub):
Tested on
Environment (optional)
Unit-test level only: the script runs against a recording
ghstub under vitest (script is bash + jq + gh, same as before — no new runtime dependencies).Risk & Scope
GET pulls/N) on the create branch only, best-effort with degradation to the old form. Assignment can be rejected for external authors — handled by the separate best-effort assign call (warning only) plus the cc mention, so the author is still reached.rv:/ic:bullets deliberately get no link — their cross-round identity IS the rendered line, and a suffix would re-publish every already-persisted rv/ic item once across existing issues.Linked Issues
Reference: #11074 is the motivating example of the current form.
中文说明
这个 PR 做了什么
autofix 循环会为每个 PR 建一个 tracking issue,记录那些"已验证属实、但修复超出该 PR 改动范围"的 review 发现(
upsert-deferred-issue.sh)。改动前这个 issue 只有固定标题(Deferred review findings from PR #N)、一段裸模板正文、没有 assignee,一眼看不出说的是什么、该谁跟进。改动后创建路径会拉一次 PR 元数据,让 issue 自描述:Deferred review findings from PR #10991: refactor(daemon): decouple extension activation refresh。[URGENT](https://evil.example/phish)会在 bot 署名的文本里渲染成可点击、由攻击者选定目标的链接,未闭合的<details>会把 findings 列表折叠起来、从原始正文的去重语料里消失。因此标题只做 flatten 与截断后进入 issue 标题,绝不副本进正文;.github/scripts/upsert-deferred-issue.sh把这条规则写成了Do NOT re-add it; if a title ever has to appear in the body, put it in a code span, never in prose。POST /repos/{owner}/{repo}/issues不幂等,而能走到重试的恰恰是模糊失败(连接重置、网关 502、服务端已提交后的读超时),重新 POST 可能铸出第二个带相同 marker 的 tracking issue,被下一轮的 newest-first lookup 永久孤儿化。assignment 被拒只告警——正文的cc @author提及已经能触达作者。rc:附原评论深链(…/pull/N#discussion_r<id>),内部评论 id 不再是唯一线索。全部 best-effort:PR 元数据拉取失败或返回内容不可用时降级回旧的裸标题、无 assignee、无 cc,并发出告警——元数据问题绝不能丢 findings。追加(append)路径完全不变,且根本不发
pulls/请求(上下文拉取仅限创建路径)。为什么需要
过去一周该机制建了约 100 个 tracking issue(例如 #11074),每个都显得散乱:标题看不出内容、正文是一串没有回链的
- rc:<内部 id>、没有 assignee。maintainer 必须打开源 PR 翻 review 线程才能搞清 issue 指的是什么,也没有人被指向去跟进。这个 PR 把上下文和责任人直接放到 issue 上。评审验证计划
如何验证
recording-gh 测试套件端到端 pin 住了行为,跑 upsert 测试块(要带
--config,与npm run test:scripts一致:不带时命中的是根配置的scriptsproject,而它自己没有配置文件,于是回退到 vitest 默认的 5stestTimeout——这个用例实测 ~4.8s,会超时):npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js -t 'upserts deferred findings into a per-PR issue that survives the merge'新增用例覆盖:创建调用的增强标题 + assignee + 深链;降级创建(PR 拉取失败 → 裸标题、无 assignee、仍落盘,并输出
could not fetch PR #5 context告警并带上捕获到的原因);拉取退出码 0 但正文不可用的降级创建(200 text/html→ 同样降级,告警改为说明该调用没有返回可用的 PR 对象);assignment 被拒(独立的 assign 调用失败 →could not assign告警、恰好一次创建 POST、findings 仍落盘);bot 自己的 PR(永不自我 assign、不 cc);畸形.user.login(含空格、含@、40 字符)绝不进入cc @…提及与assignees[]参数;lookup 对增强标题的收养及冒号守卫(#5的基础标题不会前缀误吞#50的 issue);PR 标题以原样(仅 flatten 与 80 码点截断:[URGENT](https://evil.example/y) <details> fix @foo与ping @admin @x+ 100 个A除换行折叠与截断外均原样返回)进入纯文本的 issue 标题,且以任何拼写都不进入 markdown 渲染的正文——evil.example、URGENT、<details>、fix @foo以及 ZWSP 净化后的@foo均被断言不出现在-f body=中,同时from PR #5 by someone保留;expect(scriptEscapeSites).toHaveLength(1)把已退掉的标题净化点钉住,重新加回必须是有意为之;正文里的 ready-for-agent 指引挂在单条 finding 自建的那个 issue 上,而不是这个 tracking issue(它现在带 assignee,因而被定时扫描的no:assignee过滤掉);以及 append 路径不发pulls/请求。lookup 的标题兜底仍会收养存量的裸格式 issue(精确匹配)——由既有的 marker-stripped 用例 pin 住。
前后证据
改动前(线上 issue #11074):
改动后(本地用 recording gh stub 跑脚本的 dry-run 输出):
测试环境
运行环境(可选)
仅单测层面:脚本在 vitest 下对 recording
ghstub 运行(bash + jq + gh,与之前一致,无新运行时依赖)。风险与范围
GET pulls/N),best-effort、失败降级回旧形态。外部作者可能无法被 assign——由"独立的尽力 assign 调用(失败只告警)+ 正文 cc"兜底,作者仍能收到通知。rv:/ic:条目刻意不加链接——它们的跨轮身份就是渲染行本身,加后缀会把所有已落盘的 rv/ic 条目一次性重新发布,在存量 issue 上造成重复。关联 Issue
参考:#11074 是当前形态的动机示例。