Repository navigation
fix(ci): surface blocked autofix takeover admission - #8410
Conversation
Code reviewReviewed What I verified independently
Nice touches: the explicit Suggestions1. The blocked comment tells maintainers to wait, for blockers that will never clear (L1779-L1785) Only Worth a third branch for 2. The bot's own fork PRs get no blocked status at all (L1769) [[ "$(jq -r --arg take "${TAKEOVER_LABEL}" '[.labels[]?.name] | index($take) != null' <<< "${META}")" == 'true' ]] || return 0The workflow deliberately manages the bot's own fork PRs without a label (header L24-L29: "The bot's OWN fork PRs … are auto-managed WITHOUT a label when allow-edits is on"), and 3. Terminal API answers are retried and then reported as transient (L1740-L1752, L1717-L1738)
Either produces 3 sleeps, Splitting "the call failed" (retry) from "the call answered and the answer is terminal" (classify, 4. Both readers discard the API error ( The stated goal is making blockers visible, but the only trace left is 5.
6. jq program interpolation deviates from this file's own convention (L1791) --jq ".[] | select(.user.login == \"${AUTOFIX_BOT}\") | select(.body | contains(\"<!-- autofix-status -->\")) | .id"The identical lookup at L3466-L3470 uses 7. Dead After 8. Outputs written immediately before
Test coverageThe behavioural harness is a real step up from string assertions, and neutralising
SummaryNo Critical. The classifier rewrite is sound and I could not find an admission-widening path. The substantive concerns are all on the new reporting surface rather than the gate: #1 and #2 mean the "make terminal blockers visible" promise is only partly delivered (wrong guidance for permission blockers, no coverage for the bot's own fork PRs), and #3/#4 mean some terminal conditions are laundered into transient-looking red runs with no diagnostic in the log. Those three plus the |
Review:
|
Two review blockers on the forced takeover admission gate.
R7-1: `read_live_permission` whitelisted only admin/maintain/write/triage/
read, so GitHub's definitive answers for "holds nothing here" never matched.
Bot-type logins (dependabot[bot], github-actions[bot], renovate[bot]) and org
logins return HTTP 200 with permission 'none'; nonexistent or empty logins
return 404. Both burned three API calls plus back-off and then returned
`permission_lookup_failed`, so the forced path exited 1 (a red run) instead of
the routine `author_permission_none` rejection, the blocked comment promised
"a later scheduled scan will retry" — a retry that can never succeed — and the
actionable "grant the fork author write access" guidance behind
author_permission_* was unreachable. `author_permission_${FPERM:-none}` could
never render `none`. The scheduled loop re-paid the same cost per candidate
per tick, permanently.
Accept 'none', answer HTTP 404 terminally, skip the call for an empty login,
and keep the retry budget for genuinely transient answers (5xx, network, auth)
so a legitimate write-holder is never silently rejected. gh's own stderr now
rides along in the warning instead of going to /dev/null — a rate limit, an
expired PAT and a 5xx were indistinguishable before.
R6-1: the new blocked-status comment lookup is the 14th `--paginate` code
site, but the deliberate site-count pin still asserted 13, failing
`Test (ubuntu-latest, Node 22.x)` deterministically. Bump it to 14 and record
why this site stays out of the `jq -s 'add // []'` normalizer: it consumes the
page stream inline via `--jq ... | .id` into `tail -1` and never lands in a
WORKDIR json file, so wrapping it in an array would break the tail-1 consumer.
R6-2: pin the forced-admission wiring (reader -> classifier -> live-permission
gate -> reporter) end to end, plus fixtures for none, 404, empty login, every
grant level, and a transient 5xx.
Review round 2 —
|
Addresses review round 2 on e49aade. - Status-comment lookup now uses the same jq filter as the sibling upsert in 'Post autofix status comment': `// ""` guards so one comment with a null body cannot abort the program (verified: the old filter exits 5, so all three attempts fail and the run reds out without posting the very status it exists to post), and --arg for AUTOFIX_BOT so a repo-configured login containing " or \ is a mismatch rather than a jq parse error. - The lookup no longer sends gh's stderr to /dev/null, matching the rule read_live_permission states 60 lines above. - The blocked body resolves its run link from GITHUB_SERVER_URL like every other status writer, instead of hardcoding github.com. - All four reporter warnings go to stderr like the two reader helpers, so the reporter stays safe to wrap in $( ). Tests: the reporter and reader harnesses now run under production shell options (`set -eo pipefail`) and call through `|| exit $?` so errexit is suspended inside the helper exactly as the `if !` call sites do. New cases cover the null-bodied comment page, the PAT-identity mismatch branch, the in-repo pass-through (no collaborator call), and the permission_lookup_failed red exit. Each kills a probe-verified mutant that previously survived: dropping the `// ""` guard, dropping the `isCrossRepository == true` conjunct, and flipping that `exit 1` to `exit 0`. Verified: npm run test:scripts 946 passed / 9 skipped, 46 files; prettier --check clean; eslint clean; git diff --check clean.
|
已修复(commit Fixed in Fixed
Tests
Each new case kills a mutant that previously survived — I re-ran the suite against all three:
Verification
Not changed, on purpose#1 (shared |
|
@qwen-code /triage |
…enLM#8410) Review round 9 raised two Suggestion-level findings on the forced admission path; both are addressed here. R9-1 (.github/workflows/qwen-autofix.yml:1698) — the review-scan concurrency group was broader than the job's own `if:`. GitHub evaluates concurrency before the job condition, and `route` emits `pr_number` unconditionally from the dispatch input, so a `workflow_dispatch` with `phase: issue` + `pr_number: N` resolved the group to `qwen-pr-head-write-N` even though `do_review` is false and the job only skips. Because `issue-autofix` declares `needs: ['route', 'review-scan']`, the skipped leg could queue behind an in-flight address round for PR N (`timeout-minutes: 300`, `cancel-in-progress: false`) and stall the issue phase the operator actually dispatched, with only a "queued" badge to explain it. The group now carries the same `do_review == 'true'` conjunct, so a run that will skip falls to the per-run `github.run_id` group. The forced path's serialization against review-address is unchanged. R9-2 (scripts/tests/qwen-autofix-workflow.test.js:4034) — the pin asserted both jobs' groups as independent literals and never compared the two prefixes, so renaming one side while updating its literal in the same block would ship green with the lost-update race on the status comment silently reopened. The test now extracts both prefixes and asserts equality, mirroring `groupOf` in qwen-resolve-workflow.test.js, and a new case pins the group predicate against the job's own `if:` block. Verified: qwen-autofix / qwen-resolve / qwen-triage workflow suites 266/266 pass. Both new assertions were mutation-checked — reverting the group to the broad predicate fails 2 tests, and renaming only review-scan's prefix fails the equality assertion. Prettier and ESLint clean (the group is double-quoted because the expression embeds 'true', which Prettier will not leave escaped).
|
已修复 round 9 的两条 Suggestion(commit Fixed both round-9 suggestions in R9-1 — R9-2 — Verification
Note on style: the group is now double-quoted. Its expression embeds |
|
@qwen-code /triage |
Maintainer verification — local real-stack run of
|
| Injected condition | main (before) |
PR #8410 (after) |
|---|---|---|
Fork permission read: 500 → write |
rejected, dispatch lost | admitted after 1 retry, exit 0 |
| Fork permission read: 500 ×3 | silent reject, exit 0, stale ✅ status stays | permission_lookup_failed, existing status comment PATCHed to the blocked body, exit 1 |
| Permission = HTTP 404 | rejected (leaks {"message":"Not Found"} into the log) |
author_permission_none, exactly 1 API call, routine exit 0 |
Permission = none / read (200) |
same aggregate message | author_permission_none / author_permission_read, 1 call |
| Maintainer edits off | generic rejection | maintainer_edits_disabled + "Re-enable maintainer edits…" guidance |
gh pr view 500 → 200 |
|| echo '{}' → silent green no-op |
retried, admitted |
gh pr view 500 ×3 |
silent green no-op | metadata_fetch_failed, exit 1 |
| Status PATCH 500 ×3 | n/a | 3 bounded attempts, ::error::blocked status update failed, exit 1 |
| Status comment absent | n/a | posts a new one (addComment) with the same marker |
| PAT authenticates as a human | n/a | refuses to write, ::error::, exit 1 |
DRY_RUN=true |
n/a | no write at all (ledger confirms), exit 1 |
not_open / wrong_base / skip_label / unmanaged_author |
one aggregate message | distinct reason codes, no status write (label/reason gate holds) |
| Scheduled scan, fork permission 500 ×3 | skipped … below write, no fleet row |
blocked / permission_lookup_failed fleet row in the run summary |
Scheduled scan, permission read / 404 |
no fleet row | blocked / author_permission_read / author_permission_none |
Edge probes that also passed: a null-body comment in the list does not abort the jq filter (the // "" guard holds); an AUTOFIX_BOT_LOGIN containing " and \ is a mismatch, not a jq parse error (the --arg guard holds); a status comment living on page 2 is found correctly through --paginate + .[][]; the run link is built from GITHUB_SERVER_URL, and the bilingual body renders as intended (screenshot 1).
The PR's own suite: 117/117 pass on the head commit.
🐞 Defect found — connection-level failure on the status read is neither retried nor surfaced
report_forced_takeover_blocked reads the status comments through a pipeline:
if status_ids="$(gh api ".../comments" --paginate 2> "${err}" |
jq -rs --arg ab "${AUTOFIX_BOT}" --arg m '<!-- autofix-status -->' '…')"; then
status_lookup_ok=trueThe review-scan step does not set pipefail (the sibling Post autofix status comment step does — it starts with set -uo pipefail; this one only mentions pipefail in a comment). So the if tests jq's exit status, not gh's. When gh fails with an HTTP status it prints the error body to stdout and jq errors out, so the retry works — that path is fine. But when gh fails at the connection level (TCP reset / TLS abort / DNS blip) stdout is empty, jq -rs happily produces nothing and exits 0.
Verified end-to-end (scenario: terminal author_permission_none, status read answered with a TCP RST):
- 1 status read instead of 3 — the documented bounded retry never happens
status_lookup_ok=truealthough nothing was read- a duplicate
⛔ AutoFix blockedcomment is posted while the stale✅ round 3 pushedcomment stays — the exact two-status state this PR exists to remove - the run exits 0, green
Re-running the identical scenario with the one-line change below gives 3 bounded retries, gh's real diagnosis in the warning, Failed to read takeover status comments, and exit 1 — i.e. the contract the PR description states:
if status_ids="$(set -o pipefail; gh api ".../comments" --paginate 2> "${err}" |
jq -rs …)"; thenSmaller notes (non-blocking)
-
npm run test:scriptsexits 1 on macOS. On this head,scripts/tests/qwen-autofix-workflow.test.jsreports 117/117 passed but the process still exits 1 on an unhandled[vitest-worker]: Timeout calling "onTaskUpdate". Deterministic here — 3/3 runs on macOS (file runtime 62–86 s); the same file on the merge-base is exit 0, 112 passed, 55 s. Linux CI is green, and theTest (macos-latest)job wasskippedin that run, so this is currently unobserved by CI rather than proven safe there. Worth a look, since the file is not excluded on darwin (only onwin32). -
A 404 on the forced PR lookup is a terminal answer treated as transient.
read_forced_pr_metaretries anygh pr viewfailure three times, so a mistypedpr_numberin a manual dispatch burns 3 calls + back-off and then reds out withmetadata_fetch_failed— a transient-sounding code for a permanent condition. This is the same distinction the PR already draws for permissions (e49aade); mirroring it here (grep -q 'HTTP 404'→pr_not_found) would be consistent. -
The
jq -eschema gate on the metadata is effectively unreachable through realgh.gh pr view --jsonalways emits every requested field, so the "malformed metadata" branch cannot fire in practice — a deleted-account author comes through aslogin: "app/"(still a string), and the run then correctly ends atauthor_permission_none. Harmless belt-and-braces, but the transient-vs-malformed coverage it implies is theoretical.
Not covered
- The
concurrency:group change inbf6d7965is a job-level expression, evaluated by the Actions engine before the step runs — it is outside this harness. I only reviewed it by reading; its semantics (do_review != 'true'→github.run_id) look right, but it is untested here. - Everything downstream of admission (
review-address, the push path, the agent round itself). - Real GitHub rate-limit / secondary-limit behaviour.
Verdict: the admission changes do what the description says, before/after is clearly demonstrable, and the reason codes + status write behave correctly under every failure mode I could inject. I'd like the pipefail fix in before merge; the three notes can follow up.
中文版本
维护者验证 —— 在本地对 review-scan 做真实环境运行
我没有只读 diff,而是把这个 PR 的准入路径在本地真实跑了一遍。结论:PR 声称的行为属实且已验证;同时harness 也发现了新版 blocked-status 写入逻辑里的一个真实缺陷,外加两三个合并前值得看一眼的小问题。
验证方法(harness)
目标是:网络层以上的东西一律不打桩。
Scan for PRs with new feedback这一步的run:脚本块从.github/workflows/qwen-autofix.yml里逐字节抽出(不做任何改写,只去掉块缩进),交给bash执行。- 运行在 Linux 容器里(对齐
runs-on: ubuntu-latest:GNUdate -d、bash 5.2),使用真实ghCLI v2.96.0 与jq1.7.1。 - 容器内把
api.github.com指向127.0.0.1,由本地服务通过真实 TLS 提供(自签证书装进容器信任库)。因此gh自己的参数解析、--paginate的 Link 头翻页、--jq过滤、GraphQL 编码与错误处理全部原样执行 —— 只有源站的应答是我们控制的。 - workflow 顶层
env:也是从 YAML 抽取的,没有任何常量是手抄的。 - 每一次 HTTP 请求都记入 ledger,所以断言基于真正发出去的请求,而不是日志文本。
31 个场景 × {main 基线, PR head} = 64 次完整步骤执行,303 次真实 HTTP 请求。
验证的 head 为 bf6d7965(其 review-scan 的 run: 块与 5d45d9f3 逐字节相同);基线为 merge-base 89b3d5ea。
确认有效的部分
| 注入条件 | main(修改前) |
PR #8410(修改后) |
|---|---|---|
fork 权限查询:500 → write |
拒绝,本次调度丢失 | 重试一次后准入,exit 0 |
| fork 权限查询:连续 3 次 500 | 静默拒绝、exit 0,旧的 ✅ 状态仍在 | permission_lookup_failed,现有状态评论被 PATCH 成 blocked 内容,exit 1 |
| 权限返回 HTTP 404 | 拒绝(且把 {"message":"Not Found"} 泄进日志) |
author_permission_none,只调用 1 次 API,常规拒绝 exit 0 |
权限为 none / read(200) |
同一句笼统信息 | author_permission_none / author_permission_read,1 次调用 |
| 未允许 maintainer edits | 笼统拒绝 | maintainer_edits_disabled + “请重新允许 maintainer edits” 指引 |
gh pr view 500 → 200 |
|| echo '{}' → 静默绿色空跑 |
重试后准入 |
gh pr view 连续 3 次 500 |
静默绿色空跑 | metadata_fetch_failed,exit 1 |
| 状态 PATCH 连续 3 次 500 | 无此路径 | 3 次有界重试,::error::blocked status update failed,exit 1 |
| 不存在状态评论 | 无此路径 | 以相同 marker 新建一条(addComment) |
| PAT 身份是人类账号 | 无此路径 | 拒绝写入,::error::,exit 1 |
DRY_RUN=true |
无此路径 | 完全不写(ledger 可证),exit 1 |
not_open / wrong_base / skip_label / unmanaged_author |
一句笼统信息 | 各自独立原因码,且不写状态评论(标签/原因门控生效) |
| 定时扫描,fork 权限连续 3 次 500 | skipped … below write,无 fleet 行 |
run summary 出现 blocked / permission_lookup_failed fleet 行 |
定时扫描,权限 read / 404 |
无 fleet 行 | blocked / author_permission_read / author_permission_none |
另外通过的边界探针:评论列表里存在 body 为 null 的评论不会让 jq 中断(// "" 护栏有效);AUTOFIX_BOT_LOGIN 含 " 和 \ 时是不匹配而非 jq 解析错误(--arg 护栏有效);状态评论位于第 2 页时经 --paginate + .[][] 也能正确定位;run 链接来自 GITHUB_SERVER_URL,中英双语正文渲染符合预期(见截图 1)。
PR 自带测试:head 提交上 117/117 通过。
🐞 发现的缺陷 —— 连接层失败时,状态评论读取既不重试也不暴露
report_forced_takeover_blocked 通过管道读取状态评论:
if status_ids="$(gh api ".../comments" --paginate 2> "${err}" |
jq -rs --arg ab "${AUTOFIX_BOT}" --arg m '<!-- autofix-status -->' '…')"; then
status_lookup_ok=truereview-scan 这一步没有 pipefail(兄弟步骤 Post autofix status comment 有 —— 它以 set -uo pipefail 开头;本步骤里 pipefail 只出现在注释中)。因此 if 判断的是 jq 的退出码而非 gh 的。当 gh 因 HTTP 状态码失败时,它会把错误体打到 stdout,jq 随之报错,所以那条路径没问题。但当 gh 在连接层失败(TCP reset / TLS 中断 / DNS 抖动)时,stdout 是空的,jq -rs 什么都不输出并以 0 退出。
端到端验证结果(场景:终态 author_permission_none,状态评论读取被 TCP RST 打断):
- 状态读取只发生 1 次而非 3 次 —— 文档承诺的有界重试根本没发生
- 明明什么都没读到,
status_lookup_ok却为true - 新发了一条重复的
⛔ AutoFix blocked评论,而旧的✅ round 3 pushed仍然留在 PR 上 —— 正是本 PR 想消除的“两条状态并存”状态 - 本次运行 exit 0,绿色
把同一场景换成下面这一行改动后重跑:3 次有界重试、warning 里带出 gh 的真实诊断、Failed to read takeover status comments、exit 1 —— 即 PR 描述所声明的契约:
if status_ids="$(set -o pipefail; gh api ".../comments" --paginate 2> "${err}" |
jq -rs …)"; then其他较小的问题(不阻塞)
-
npm run test:scripts在 macOS 上 exit 1。 在本 head 上,scripts/tests/qwen-autofix-workflow.test.js报告 117/117 通过,但进程仍以 1 退出,原因是未处理的[vitest-worker]: Timeout calling "onTaskUpdate"。本机可稳定复现 —— macOS 上 3/3(该文件耗时 62–86 秒);同一文件在 merge-base 上是 exit 0、112 通过、55 秒。Linux CI 是绿的,且那次绿色运行里Test (macos-latest)是skipped,所以这属于“CI 目前没观察到”,而不是“已证明在 macOS 上没事”。该文件只在win32被排除、darwin 并未排除,值得看一下。 -
强制 PR 元数据查询遇到 404 是终态,却被当作临时错误。
read_forced_pr_meta对任何gh pr view失败都重试三次,因此手动 dispatch 时写错pr_number会白白消耗 3 次调用加退避,最后以metadata_fetch_failed变红 —— 用一个听起来像“临时”的原因码描述一个永久状态。这正是本 PR 已经为权限查询做出的区分(e49aade);在这里同样处理(grep -q 'HTTP 404'→pr_not_found)会更一致。 -
元数据上的
jq -e结构校验,在真实gh下实际上不可达。gh pr view --json总会输出所请求的全部字段,所以“畸形元数据”分支在实践中不会触发 —— 账号已注销的作者会以login: "app/"出现(仍是字符串),随后流程正确地停在author_permission_none。作为兜底无害,但它所暗示的“临时失败 vs 结构畸形”的覆盖只是理论上的。
未覆盖范围
bf6d7965里的concurrency:改动属于 job 级表达式,由 Actions 引擎在步骤运行前求值,不在本 harness 范围内。我只是读过:其语义(do_review != 'true'→github.run_id)看起来正确,但这里没有实测。- 准入之后的一切(
review-address、推送路径、agent 轮次本身)。 - GitHub 真实的 rate limit / secondary limit 行为。
结论: 准入相关改动确实做到了描述所声称的事情,前后对比清晰可证,且在我能注入的每种失败模式下,原因码与状态写入行为都正确。希望合并前先修掉 pipefail 这一处;其余三点可以后续跟进。
…LM#8410) Maintainer verification of the forced-admission path found the one `if` in `report_forced_takeover_blocked` that tests a PIPELINE rather than a single command: if status_ids="$(gh api .../comments --paginate 2> "${err}" | jq -rs ...)" A gh failure carrying an HTTP status prints the error body to stdout, so jq chokes on it and the bounded retry fires. A CONNECTION-level failure (TCP reset, TLS abort, DNS blip) leaves stdout empty — `jq -rs` then prints nothing and exits 0. Absent pipefail that reads as success on nothing read: the loop breaks on attempt 1, `status_lookup_ok` goes true, and the empty id routes the writer to its "no status comment yet" branch, posting a DUPLICATE blocked comment beside the stale one, run green. Set the option locally on that command substitution. `defaults.run.shell: bash` expands to `bash --noprofile --norc -eo pipefail`, so the step is already pipefail on a real runner and this is redundant today; it is the only guard that survives that default changing or the helper moving to a step that sets its own options. Tests: replay the same connection-level failure with the ambient pipefail dropped and pin 3 bounded reads, exit 1, and no comment written; add the HTTP-status half so the two failure shapes stay distinguishable and the local option cannot be "simplified" away as carried by jq; pin both the `set -o pipefail` and `defaults.run.shell: 'bash'` textually, since the ambient half is what every other gh|jq writer in this file relies on without saying so. Both mutations verified to fail the suite.
|
已修复(commit Fixed the blocking The change
if status_ids="$(set -o pipefail; gh api "repos/${REPO}/issues/${FORCED_PR}/comments" --paginate 2> "${err}" |
jq -rs --arg ab "${AUTOFIX_BOT}" --arg m '<!-- autofix-status -->' \
'.[][] | select((.user.login // "") == $ab) | select((.body // "") | contains($m)) | .id')"; thenOne factual note on severity, since it changes what the fix is for rather than whether to take it: this file sets VerificationBoth halves of the failure are now replayed against the extracted helper, and I checked each new assertion bites by mutating the source:
The second row is the new one: it is the case that fails without the local option. Mutation checks —
The third row is there so the Commands run on
Note 1 (macOS |
Picks up main so `Test (ubuntu-latest, Node 22.x)` stops failing. CI checks out `refs/pull/8410/head` but runs the base branch's `ci.yml`, so main's "Check voice guard mirror sync" step ran `npm run check:voice-guard-sync` against this branch's older `package.json`, which predates that script (added in QwenLM#8350) -> `npm error Missing script`, exit 1. Nothing in this PR caused it; the branch was simply 17 commits behind. Merge resolution: both sides had added a `const reviewAddressJob` in `scripts/tests/qwen-autofix-workflow.test.js`, in different hunks, so git merged them textually into a duplicate `const` (SyntaxError at import, whole suite unloadable). Kept main's bounded slice `(?=\n {2}[a-z][a-z0-9-]*:\n|$)` and dropped this branch's older unbounded `[\s\S]*$` form — main's is strictly more general (it still allows EOF, so it keeps working while review-address is the last job, and it shrinks correctly once a job is appended after it).
|
已修复(merge commit Fixed in merge commit 1.
|
Re-verification after
|
bf6d7965 (no local pipefail) |
d8d66b5 ($(set -o pipefail; …)) |
|
|---|---|---|
runner shell -eo pipefail — production |
✔ 3/3 reads, nothing written, exit 1 | ✔ 3/3 reads, nothing written, exit 1 |
| ambient pipefail dropped — hypothetical | ✘ 1/3 reads, duplicate ⛔ comment, exit 0 | ✔ 3/3 reads, nothing written, exit 1 |
The failure lives only in the bottom-left cell. So: not a production bug, and I should not have called it one. The fix is still worth keeping for the reason the commit gives — it is the only guard that does not depend on a workflow-level default staying put or on this helper never moving to a step that sets its own options — and the two new tests (connection-level vs HTTP-level failure, with ambient pipefail dropped) pin exactly the right distinction. Pinning defaults.run.shell: 'bash' textually is a good call too: the other gh … | jq … writers in this file silently depend on it.
Also worth noting for anyone else building a harness against this file: -e is part of that same default, and I re-checked it — it changes nothing in this step, because every call site of the new helpers is an if ! / || context, which suspends errexit inside the call.
Full matrix re-run under the runner shell
All 31 scenarios × {merge-base 037b4d9, head dfc6ec5}, now with bash --noprofile --norc -eo pipefail. Every admission outcome, reason code, retry count and status write is byte-identical to my first run — so the whole "Confirmed working" table in my earlier comment stands as written, only now on a faithful shell:
- transient permission 500 →
write: admitted after one retry - permission 500 ×3:
permission_lookup_failed, existing status comment PATCHed to the blocked body, exit 1 - permission 404 /
none/read: terminal, 1 API call,author_permission_*, routine exit 0 maintainer_edits_disabled,metadata_fetch_failed, PAT-identity refusal,DRY_RUNwrites nothing, distinct reason codes fornot_open/wrong_base/skip_label/unmanaged_author, scheduled-scanblockedfleet rows — all unchanged- edge probes unchanged: null comment body,
AUTOFIX_BOT_LOGINcontaining"and\, status comment on page 2,GITHUB_SERVER_URL-derived run link
The PR's own suite on dfc6ec5: 120/120 pass.
Status of my other three notes
test:scriptsexit 1 on macOS — still there, but I have to soften how I described it. On this head it is intermittent: 2 of 6 runs exit 1 with the unhandled[vitest-worker]: Timeout calling "onTaskUpdate", always on the slower runs (~78 s file runtime vs ~58 s when green); tests are 120/120 either way. On the earlier head I saw 3/3, so "deterministic" was an artefact of a small sample. Still worth a look — the file is only excluded onwin32, and this is a process-level non-zero exit, not a test failure.- 404 on the forced PR lookup treated as transient — open.
read_forced_pr_metastill retries a nonexistentpr_numberthree times and reportsmetadata_fetch_failed. Verified again ondfc6ec5: 3pr_viewcalls, exit 1. jq -emetadata schema gate unreachable through realgh— open, and harmless. Informational only.
Verdict
Unchanged apart from the retraction: the admission changes do what the description says, and I could not find a production-reachable failure in the forced or scheduled path across 31 injected conditions. My earlier "I'd like the pipefail fix in before merge" was based on my harness bug, not on the code — d8d66b5 is a good defensive addition rather than a required fix. From my side this is ready to merge; notes 1–3 are follow-up material.
中文版本
d8d66b5 之后的复验 —— 以及对我自己报告的更正
针对 dfc6ec5 重跑了整套 harness(merge-base 现为 037b4d9)。先说更正,因为这是我的问题。
⚠️ 我对 pipefail 那条发现的严重性判断是错的
d8d66b5 的 commit message 是对的,我的报告不对:本 workflow 设置了 defaults.run.shell: 'bash',GitHub 会把它展开成 bash --noprofile --norc -eo pipefail。而我的 harness 是用普通 bash 启动抽出来的步骤的。 正是这一处不忠实,让状态读取的问题看起来在生产环境可达。实际上并不可达 —— 环境里本来就有的 pipefail 在任何真实 runner 上都已经覆盖了它。
我把 harness 改成完全按 runner 的方式启动步骤,并把该场景重跑成一个 2×2:
bf6d7965(无局部 pipefail) |
d8d66b5($(set -o pipefail; …)) |
|
|---|---|---|
runner shell -eo pipefail —— 生产 |
✔ 读 3/3 次,不写入,exit 1 | ✔ 读 3/3 次,不写入,exit 1 |
| 去掉环境 pipefail —— 假设 | ✘ 只读 1/3 次,重复 ⛔ 评论,exit 0 | ✔ 读 3/3 次,不写入,exit 1 |
失败只存在于左下角那一格。所以:这不是生产缺陷,我不该那样定性。 这个修复仍然值得保留,理由就是 commit 里写的 —— 它是唯一不依赖「workflow 级默认值保持不变」或「这个 helper 永远不被挪到自设选项的步骤里」的护栏;而新增的两个测试(连接层失败 vs HTTP 层失败,且都去掉环境 pipefail)正好钉住了这个区分。把 defaults.run.shell: 'bash' 做文本断言也是对的:本文件里其他 gh … | jq … 写入者都在无声地依赖它。
另外提醒任何要为这个文件搭 harness 的人:-e 也是同一个默认值的一部分,我一并复核过 —— 它在这一步没有任何影响,因为新 helper 的每一处调用点都是 if ! / || 上下文,会在调用内部挂起 errexit。
在 runner shell 下重跑完整矩阵
全部 31 个场景 × {merge-base 037b4d9,head dfc6ec5},这次用 bash --noprofile --norc -eo pipefail。所有准入结果、原因码、重试次数、状态写入与我第一轮完全一致 —— 因此前一条评论里那张「确认有效」的表格原样成立,只是这次跑在忠实的 shell 上:
- 权限查询 500 →
write:重试一次后准入 - 权限连续 3 次 500:
permission_lookup_failed,现有状态评论被 PATCH 成 blocked 内容,exit 1 - 权限 404 /
none/read:终态,只调 1 次 API,author_permission_*,常规 exit 0 maintainer_edits_disabled、metadata_fetch_failed、PAT 身份拒写、DRY_RUN不写入、not_open/wrong_base/skip_label/unmanaged_author各自独立原因码、定时扫描的blockedfleet 行 —— 全部不变- 边界探针不变:body 为 null 的评论、含
"与\的AUTOFIX_BOT_LOGIN、位于第 2 页的状态评论、来自GITHUB_SERVER_URL的 run 链接
PR 自带测试在 dfc6ec5 上:120/120 通过。
其余三条的状态
- macOS 上
test:scriptsexit 1 —— 依然存在,但我得修正之前的描述。在这个 head 上是间歇性的:6 次里 2 次 exit 1(未处理的[vitest-worker]: Timeout calling "onTaskUpdate"),且都发生在较慢的那几次(文件耗时约 78 秒,绿色时约 58 秒);两种情况下测试都是 120/120。之前的 head 上我看到 3/3,所以「稳定复现」是样本太小的假象。仍值得看一下 —— 该文件只在win32被排除,而这是进程级的非零退出,不是测试失败。 - 强制 PR 查询的 404 被当作临时错误 —— 仍未处理。
read_forced_pr_meta对不存在的pr_number仍重试三次并报metadata_fetch_failed。在dfc6ec5上再次验证:3 次pr_view调用,exit 1。 - 元数据的
jq -e结构校验在真实gh下不可达 —— 仍未处理,且无害,仅供参考。
结论
除了那处撤回之外结论不变:准入相关改动确实做到了描述所声称的事情,在 31 种注入条件下,我没能在强制路径或定时路径中找到生产可达的失败。我之前说「希望合并前先修掉 pipefail」是基于我自己的 harness bug,而不是代码本身 —— d8d66b5 属于良好的防御性补强,而非必需修复。就我这边而言可以合并;1–3 三条作为后续跟进即可。
|
@qwen-code /triage |



What this PR does
This change makes forced AutoFix review admission explicit and fail-closed. Live pull-request metadata and fork-author permissions are validated and retried up to three times, each rejection receives a stable reason code, and takeover pull requests update their existing AutoFix status to a bilingual blocked state when admission cannot safely continue. Scheduled fork discovery reuses the same permission reader.
The regression coverage executes the admission classifier, transient and terminal lookup paths, the GitHub status-comment update, its failure path, and the complete review-scan shell syntax.
Why it's needed
A trusted Critical review on a managed takeover pull request could route into AutoFix but then be silently rejected by the forced scan after a transient metadata or permission lookup. The prior success status remained visible even though the feedback was not addressed. This keeps
review-scanauthoritative while making transient failures recoverable and terminal blockers visible without advancing the feedback watermark.Reviewer Test Plan
How to verify
Confirm that a takeover-labelled fork remains eligible when live metadata and write permission are available after a transient API failure. Confirm that malformed or repeatedly unavailable metadata and permission responses stop admission after three attempts. For an identifiable takeover PR with a terminal permission blocker, confirm that the existing AutoFix status comment is updated with the specific reason and run link; if that comment cannot be read or updated, the scan must fail visibly.
Evidence (Before & After)
Before: forced-scan lookup failures collapsed into one aggregate rejection and could leave the previous successful takeover status visible.
After: admission emits stable reason codes, retries bounded live reads, and behaviorally verifies the blocked-status PATCH and its failure semantics.
Tested on
Environment (optional)
Node.js 22 workspace. The focused new regression passes independently; all 110 workflow assertions passed in the complete local run, although the local Vitest worker reported an RPC timeout after publishing the passing results. Type checking, formatting, shell syntax, and diff checks pass.
Risk & Scope
review-addresswrite-safety recheck are intentionally unchanged; hosted GitHub Actions CI will provide Linux coverage.Linked Issues
Fixes #8409
中文说明
本 PR 的改动
本次修改让强制触发的 AutoFix 评审准入具备明确原因并保持写入侧 fail-closed。PR 实时元数据和 fork 作者权限都会校验响应结构并最多重试三次;每种拒绝都有稳定原因码;takeover PR 无法安全继续时,会把现有 AutoFix 状态更新为中英双语的 blocked 状态。定时扫描 fork 候选也复用同一权限读取逻辑。
回归测试会实际执行准入分类、临时和终态查询失败、GitHub 状态评论更新及其失败路径,并检查完整 review-scan shell 语法。
为什么需要
受信任的 Critical 评审可以把已接管 PR 路由进 AutoFix,但强制扫描可能因为一次临时元数据或权限查询失败而静默拒绝,旧的成功状态仍然保留,看起来像反馈已经被处理。本修改继续以
review-scan作为权威实时准入,同时让临时错误可恢复、终态阻塞可见,并且不推进反馈水位。Reviewer Test Plan
如何验证
确认带 takeover 标签的 fork PR 在元数据和 write 权限首次查询临时失败、后续成功时仍可准入。确认元数据或权限响应畸形、连续不可用时会在三次后停止。对于元数据可识别但权限查询最终失败的 takeover PR,确认现有 AutoFix 状态会更新为包含明确原因和运行链接的 blocked 状态;如果状态评论无法读取或更新,扫描必须明确失败。
证据(修改前后)
修改前:强制扫描的查询失败会合并成一个笼统拒绝,并可能继续显示上一轮成功的 takeover 状态。
修改后:准入输出稳定原因码,实时查询做有界重试,并通过行为测试验证 blocked 状态 PATCH 及其失败语义。
测试平台
环境(可选)
Node.js 22 工作区。新增聚焦回归可独立通过;完整本地运行的 110 个工作流断言全部通过,但本地 Vitest worker 在发布通过结果后报告了一次 RPC 超时。类型检查、格式检查、shell 语法和 diff 检查均通过。
风险与范围
review-address独立的最终写安全复核保持不变;由 GitHub Actions CI 提供 Linux 覆盖。关联 Issue
Fixes #8409