Skip to content

fix(ci): gate the fork signal on fields the review payload delivers - #9469

Merged
qqqys merged 4 commits into
QwenLM:mainfrom
qqqys:fix/fork-signal-simple-pr-payload
Aug 22, 2026
Merged

fix(ci): gate the fork signal on fields the review payload delivers#9469
qqqys merged 4 commits into
QwenLM:mainfrom
qqqys:fix/fork-signal-simple-pr-payload

Conversation

@qqqys

@qqqys qqqys commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Removes one conjunct from the Qwen Autofix Fork Signal job's if, and adds a regression test that stops it — or anything like it — from coming back.

The conjunct was github.event.pull_request.maintainer_can_modify == true. That field is not present in a pull_request_review payload. The event carries the simple pull-request object; maintainer_can_modify — along with mergeable, mergeable_state, additions, deletions, changed_files, and the rest of that family — ships only on the full object the pull_request event sends. The expression therefore evaluated to null on every delivery, null == true is false, and the job's gate could never hold.

Why it's needed

The gate has never once opened. Across the 300 runs of this workflow between the bridge shipping (#8676, 2026-08-07) and today:

conclusion runs
skipped 290
cancelled 7
action_required 1
success 0

Because the signal never completes success, the workflow_run-triggered half (qwen-autofix-fork-bridge.yml) has never fired either. The entire bridge has been inert for twelve days. Every fork-PR review in that window was served by the scheduled */10 scan alone — precisely the throttled backstop this bridge exists to get ahead of, and the one the design notes call out as insufficient for real-time pickup.

This is not a theoretical cost. It surfaced because a scheduled-scan stall left takeover-managed fork PRs sitting on unaddressed Critical findings for hours with nothing to pick them up: the direct pull_request_review lane declines fork events by design (no secrets — route logs fork review noted … the next scheduled scan engages), the bridge that exists to cover that decline was inert, and the cron backstop was the only remaining path. Three paths, and the two that were supposed to provide redundancy were the same path.

Why removing it is not a weakened check

The consent check does not move and is not lost — the bridge already re-reads it live, and always has:

gh pr view "${SIGNAL_PR}" --repo "${REPO}" \
  --json number,author,labels,headRefOid,isCrossRepository,maintainerCanModify,state,baseRefName
…
| select(.headRefOid == $sha and .isCrossRepository == true and .maintainerCanModify == true)

That live read was always the authoritative one. Consent can be withdrawn between the review and the dispatch, so a value copied out of the review payload could only ever have been a stale early-out, never the decision. The bridge's own comment already frames these conjuncts as "cheap early-outs, not a security boundary: review-scan re-derives all of it", and the signal's own comment says its gate "decides WHEN, never whether". scripts/tests/qwen-autofix-fork-bridge-workflow.test.js already pins both the live read (expect(bridgeScript).toContain('maintainerCanModify == true')) and a behavioral case for edits turned off since the review.

The signal job also cannot make this call itself. It holds permissions: {}, no secrets and no checkout, deliberately — it runs on a fork-triggered event, so it is built to hold nothing worth stealing. It has no way to ask the API for a field the payload does not carry. Removal is the only correct direction; there is no "read it properly here" option.

Why a regression test, not just the deletion

This class of bug fails silently and is invisible from outside. The job's entire body is one echo; when the gate holds, nothing observable happens beyond a run turning green. So "the gate never opens" and "no fork review happened to qualify this week" look identical in the Actions tab. That is how it survived twelve days and 300 runs. The verbatim gate pin already in this test file did not help — it pins the gate against drift, and this gate was wrong the day it was written.

The new test asserts the gate references no full-object-only field, so a future conjunct reaching for mergeable or changed_files fails here rather than silently closing the gate forever.

Reviewer Test Plan

How to verify

1. Confirm the field is genuinely absent from the payload. From GitHub's published webhook types:

$ npm pack @octokit/webhooks-types && tar -xzf octokit-webhooks-types-*.tgz
$ grep -A8 'interface PullRequestReviewSubmittedEvent' package/schema.d.ts
export interface PullRequestReviewSubmittedEvent {
  action: "submitted";
  review: PullRequestReview;
  pull_request: SimplePullRequest;     <-- simple, not full
  …
}

SimplePullRequest carries exactly these fields, and maintainer_can_modify is not among them:

url id node_id html_url diff_url patch_url issue_url number state locked title
user body created_at updated_at closed_at merged_at merge_commit_sha assignee
assignees requested_reviewers requested_teams labels milestone draft commits_url
review_comments_url review_comment_url comments_url statuses_url head base
_links author_association auto_merge active_lock_reason

The full PullRequest interface — what the pull_request event sends — does carry it, alongside mergeable and additions.

2. Confirm the observed effect. Qwen Autofix Fork Signal, all runs to date: 290 skipped, 7 cancelled, 1 action_required, 0 success. Includes deliveries where every other conjunct is verifiably true — e.g. run titled fork-signal: PR 9384 reviewed by qwen-code-ci-bot at 2026-08-19T01:47:08Z, on a PR that is cross-repo, targets main, is open, carries autofix/takeover, does not carry autofix/skip, has maintainer_can_modify: true on the REST object, and was reviewed by REVIEW_BOT. Skipped.

3. Run the tests.

$ npx vitest run scripts/tests/qwen-autofix-fork-bridge-workflow.test.js
 Test Files  1 passed (1)
      Tests  10 passed (10)

4. Mutation-verify the new test. Re-add the conjunct to the workflow and re-run: 2 failed | 8 passed — the verbatim gate pin and the new payload-field test both catch it. Restore, and it is 10 passed again.

scripts/tests/qwen-autofix-workflow.test.js has one failure, behaviorally replays the stale-duplicate revalidation, including the conflict-only transition, which is a 5s Test timed outpre-existing and unrelated: it reproduces identically on a pristine upstream/main with these changes stashed (1 failed | 179 passed), and this PR does not touch qwen-autofix.yml.

Evidence (Before & After)

N/A — CI plumbing, no user-visible surface.

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux

Environment (optional)

Unit tests only (npx vitest run scripts/tests/…). The behavioral proof is the workflow-run history cited above; the gate itself can only be exercised by a real fork-PR review, which lands the first time this merges.

Risk & Scope

  • Main risk or tradeoff: a takeover-labeled fork PR whose author has turned maintainer edits off now burns one signal run, one bridge run, and one gh pr view before the bridge rejects it, where the gate previously intended to spend nothing. That is the whole cost, it is bounded at one review, and it buys back a path that has never worked. Ordinary contributor fork PRs are unaffected — the bot-authored-or-takeover-labeled conjunct still excludes them, which is where the run-burn concern actually bites.
  • Not validated / out of scope: the gate cannot be exercised locally; the first real proof is a fork-PR review after this merges. Also out of scope: the scheduled-scan stall that made this inertness visible — separate problem, and this PR is what makes it non-fatal rather than what fixes it.
  • Breaking changes / migration notes: none. No inputs, outputs, permissions, or run-name contracts change.

Linked Issues

None.

中文说明

这个 PR 做了什么

Qwen Autofix Fork Signal 这个 job 的 if 条件中删掉一个合取项,并新增一个回归测试,防止它——或任何同类问题——再次出现。

被删掉的是 github.event.pull_request.maintainer_can_modify == true。这个字段在 pull_request_review 的事件负载中并不存在:该事件携带的是简化版的 pull-request 对象;maintainer_can_modify——连同 mergeablemergeable_stateadditionsdeletionschanged_files 等同一族字段——只出现在 pull_request 事件发送的完整对象上。因此该表达式在每一次投递中都求值为 null,而 null == truefalse,这个 job 的门禁永远无法通过。

为什么需要

这个门禁从来没有打开过一次。从该 bridge 上线(#8676,2026-08-07)到今天,这个 workflow 的全部 300 次运行中:

结论 次数
skipped 290
cancelled 7
action_required 1
success 0

由于 signal 从未以 success 结束,其后由 workflow_run 触发的另一半(qwen-autofix-fork-bridge.yml)也从未启动过。整条 bridge 已经失效十二天。这段时间内所有 fork PR 的评审,都只由每 10 分钟一次的定时扫描承担——而那正是这条 bridge 为了抢在其前面而存在的、受限流的兜底路径,也正是设计说明中指出的、不足以支撑实时接管的那一条。

这不是纸面上的代价。问题之所以浮现,是因为一次定时扫描停摆,导致处于 takeover 管理下的 fork PR 带着未处理的 Critical 发现搁置数小时无人接手:直连的 pull_request_review 通道按设计拒绝 fork 事件(无密钥——route 的日志写着 fork review noted … the next scheduled scan engages),本该覆盖这一拒绝的 bridge 处于失效状态,于是定时兜底成了唯一剩下的路径。名义上三条路,而本应互为冗余的两条其实是同一条。

为什么删除它并不等于放松检查

这项同意检查既没有移位,也没有丢失——bridge 一直在用实时数据重新读取它

gh pr view "${SIGNAL_PR}" --repo "${REPO}" \
  --json number,author,labels,headRefOid,isCrossRepository,maintainerCanModify,state,baseRefName
…
| select(.headRefOid == $sha and .isCrossRepository == true and .maintainerCanModify == true)

这次实时读取始终是权威判据。同意可能在评审与派发之间被撤回,因此从评审事件负载里抄来的值,充其量只能是一个陈旧的提前退出条件,从来都不是决策本身。bridge 自己的注释已经将这些合取项定性为「cheap early-outs, not a security boundary: review-scan re-derives all of it」,而 signal 自己的注释也写明其门禁「decides WHEN, never whether」。scripts/tests/qwen-autofix-fork-bridge-workflow.test.js 中已经同时钉住了这次实时读取(expect(bridgeScript).toContain('maintainerCanModify == true'))以及「评审之后关闭了 maintainer edits」这一行为用例。

signal 这个 job 也没有能力自行做出该判断。它持有 permissions: {},无密钥、无 checkout,这是刻意为之——它运行在 fork 触发的事件上,因此被设计成不持有任何值得窃取的东西。它无从向 API 索取一个事件负载并未携带的字段。删除是唯一正确的方向;不存在「在这里正确地读取它」这个选项。

为什么要加回归测试,而不只是删掉

这一类缺陷是静默失败的,从外部完全不可见。该 job 的全部主体只有一条 echo;当门禁通过时,除了一次运行变绿之外不会发生任何可观测的事情。因此「门禁永远不开」与「这周恰好没有符合条件的 fork 评审」在 Actions 页面上看起来一模一样。它正是这样存活了十二天、300 次运行。该测试文件中已有的逐字门禁钉死并没有帮上忙——它钉的是门禁的漂移,而这个门禁在写下的那天就是错的。

新增的测试断言该门禁不引用任何「仅存在于完整对象」的字段,这样将来若有合取项去取 mergeablechanged_files,会在此处失败,而不是悄无声息地把门永久关死。

评审者测试计划

如何验证

1. 确认该字段在事件负载中确实不存在。 取自 GitHub 公布的 webhook 类型定义:

$ npm pack @octokit/webhooks-types && tar -xzf octokit-webhooks-types-*.tgz
$ grep -A8 'interface PullRequestReviewSubmittedEvent' package/schema.d.ts
export interface PullRequestReviewSubmittedEvent {
  action: "submitted";
  review: PullRequestReview;
  pull_request: SimplePullRequest;     <-- 简化版,不是完整版
  …
}

SimplePullRequest 携带的字段恰好是以下这些,其中并无 maintainer_can_modify

url id node_id html_url diff_url patch_url issue_url number state locked title
user body created_at updated_at closed_at merged_at merge_commit_sha assignee
assignees requested_reviewers requested_teams labels milestone draft commits_url
review_comments_url review_comment_url comments_url statuses_url head base
_links author_association auto_merge active_lock_reason

而完整的 PullRequest 接口——即 pull_request 事件所发送的那个——确实携带该字段,同时也携带 mergeableadditions

2. 确认实际观测到的效果。 Qwen Autofix Fork Signal 至今的全部运行:290 次 skipped、7 次 cancelled、1 次 action_required0 次 success。其中包括其余每一个合取项都可验证为真的投递——例如 2026-08-19T01:47:08Z 那次标题为 fork-signal: PR 9384 reviewed by qwen-code-ci-bot 的运行,对应的 PR 跨仓库、目标为 main、处于 open、带有 autofix/takeover、不带 autofix/skip、REST 对象上 maintainer_can_modify: true、评审者为 REVIEW_BOT。结果仍是 skipped。

3. 运行测试。

$ npx vitest run scripts/tests/qwen-autofix-fork-bridge-workflow.test.js
 Test Files  1 passed (1)
      Tests  10 passed (10)

4. 对新测试做变异验证。 把该合取项加回 workflow 后重跑:2 failed | 8 passed——逐字门禁钉死与新增的负载字段测试都会捕获它。恢复后重新变为 10 passed

scripts/tests/qwen-autofix-workflow.test.js 中有一处失败,behaviorally replays the stale-duplicate revalidation, including the conflict-only transition,报的是 5 秒 Test timed out——既有问题且与本 PR 无关:将本次改动 stash 后在干净的 upstream/main 上可原样复现(1 failed | 179 passed),且本 PR 并未改动 qwen-autofix.yml

证据(改动前后)

N/A —— CI 管道改动,无用户可见界面。

测试环境

操作系统 状态
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux

运行环境(可选)

仅单元测试(npx vitest run scripts/tests/…)。行为层面的证据是上文引用的 workflow 运行历史;门禁本身只能由一次真实的 fork PR 评审来触发,即本 PR 合入后的第一次。

风险与范围

  • 主要风险或权衡: 若某个带 autofix/takeover 标签的 fork PR,其作者已关闭 maintainer edits,那么现在会先消耗一次 signal 运行、一次 bridge 运行和一次 gh pr view,随后才被 bridge 拒绝——而原门禁的意图是在此情形下不消耗任何资源。这就是全部代价,其上界为每次评审一次,换回的是一条从未生效过的通路。普通贡献者的 fork PR 不受影响——「由 bot 创建或带 takeover 标签」这一合取项仍将其排除在外,而这才是「消耗运行次数」这一顾虑真正起作用的地方。
  • 未验证 / 不在范围内: 该门禁无法在本地触发;第一次真正的验证是本 PR 合入后的一次 fork PR 评审。同样不在范围内的是:使这一失效状态暴露出来的定时扫描停摆——那是另一个问题,本 PR 的作用是让它不再是致命的,而不是修复它。
  • 破坏性变更 / 迁移说明: 无。输入、输出、权限以及 run-name 契约均未改变。

关联 Issue

无。

`qwen-autofix-fork-signal.yml` gated on
`github.event.pull_request.maintainer_can_modify == true`. That field does
not exist in a `pull_request_review` payload: the event carries the SIMPLE
pull-request object, and `maintainer_can_modify` — like `mergeable`,
`additions`, and `changed_files` — ships only on the full object the
`pull_request` event sends. The expression evaluated to null on every
delivery, `null == true` is false, and the job's `if` could never hold.

Measured on the repository: across the 300 runs between the bridge shipping
(QwenLM#8676, 2026-08-07) and this change, 290 skipped, 7 cancelled, 1
action_required, and 0 success. Not one signal ever reached its step, so the
bridge behind it has never fired either — every fork-PR review has been
served by the scheduled scan alone, which is exactly the throttled backstop
this bridge exists to get ahead of.

The consent check is not lost, and does not move: the bridge already re-reads
it live (`gh pr view --json maintainerCanModify`, then
`select(… .maintainerCanModify == true)`), and that read was always the
authoritative one — consent can be withdrawn between the review and the
dispatch, so a payload copy could only ever have been a stale early-out. The
signal job cannot make that call itself: it holds `permissions: {}`, no
secrets and no checkout, deliberately, because it runs on a fork-triggered
event.

What the removal does cost is one signal + bridge run and one PR read for a
takeover-labeled fork PR whose author has turned maintainer edits off, where
the gate previously intended to spend nothing. Ordinary contributor fork PRs
are unaffected — the bot-authored-or-takeover-labeled conjunct still excludes
them.

Also adds a regression test asserting the gate references no full-object-only
field. A gate like this fails silently: the job's entire body is one echo, so
"never opens" and "no fork review happened to qualify" look identical from
outside, which is why this went twelve days unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Re-run on the new head (77cc5d1), triggered by maintainer request. Gate re-checked from scratch.

  • Template: complete, including the bilingual section ✓
  • Problem — observed, and I re-verified it independently again. The gate's maintainer_can_modify == true conjunct reads a field the pull_request_review payload doesn't carry (the event delivers the simple pull-request object), so it evaluates null == true → false on every delivery. Pulling the workflow's full run history through the API: 500 runs, zero success (481 skipped, 16 cancelled, 1 action_required). And the specific run this PR cites checks out exactly: fork-signal: PR 9384 reviewed by qwen-code-ci-bot at 2026-08-19T01:47:08Z was skipped even though every other conjunct was provably true — cross-repo, base main, open, reviewed by the review bot, and the label timeline shows autofix/takeover was on the PR from 08-18T08:09 until 08-19T09:31, i.e. present at run time, while the live REST object reads maintainerCanModify: true. The dead conjunct is the only possible cause of that skip. This is a real, observed, reproduced dead gate — not a theoretical concern.
  • Direction: aligned. This repairs the real-time fork-review lane of the repo's own autofix automation; the throttled cron backstop was never meant to be the only path.
  • Size: no core-package paths touched (one workflow YAML, one test file; ~34 production lines, ~63 test lines), so the core-module gate doesn't apply.
  • Approach: scope remains exactly right. The signal job holds permissions: {}, no secrets, no checkout by design (verified in the file), so it cannot ask the API for the missing field — removal is the only correct direction. Consent is not weakened: the bridge re-reads maintainerCanModify live before dispatching (verified at qwen-autofix-fork-bridge.yml:144–152), which was always the authoritative check since consent can be withdrawn between review and dispatch. The regression test generalizes the defense to every full-object-only field. The one new commit since the last review (77cc5d1, test-only) addresses the prior round's single nit — details in the Stage 2 comment.
  • Risk: no elevated-risk path matches.

Moving on to code review. 🔍

中文说明

应维护者请求,在新 head(77cc5d1)上重新运行。门禁从头重新检查。

  • **模板:**完整,包含中文对照 ✓
  • 问题——已观测到,且我再次独立复核。 门禁中的 maintainer_can_modify == true 读取的字段在 pull_request_review 负载中并不存在(该事件只携带简化版 pull-request 对象),因此每次投递都求值为 null == true → false。通过 API 拉取该 workflow 的完整运行历史:500 次运行,零次 success(481 次 skipped、16 次 cancelled、1 次 action_required)。PR 引用的那次具体运行也完全对得上:2026-08-19T01:47:08Zfork-signal: PR 9384 reviewed by qwen-code-ci-bot 结果为 skipped,尽管其余每个合取项都可验证为真——跨仓库、目标 main、open、评审者为评审机器人,且标签时间线显示 autofix/takeover08-18T08:09 一直挂到 08-19T09:31(即运行时在场),而 REST 实时对象读数为 maintainerCanModify: true。死合取项是该次 skip 的唯一可能原因。这是一个真实的、已观测且已复现的死门禁,不是理论性问题。
  • **方向:**对齐。这修复的是仓库自身 autofix 自动化的实时 fork 评审通道;受限流的定时兜底从来不应是唯一路径。
  • **规模:**未触及核心包路径(一个 workflow YAML、一个测试文件;生产代码约 34 行、测试约 63 行),核心模块门禁不适用。
  • 方案:范围依然恰到好处。signal job 按设计持有 permissions: {}、无密钥、无 checkout(已在文件中核实),因此没有能力向 API 索取负载中缺失的字段——删除是唯一正确的方向。同意检查也未被削弱:bridge 在派发前会实时重新读取 maintainerCanModify(已在 qwen-autofix-fork-bridge.yml:144–152 核实),而这一直是权威判据,因为同意可能在评审与派发之间被撤回。回归测试将防御泛化到所有完整对象专属字段。上次评审后的唯一一个新提交(77cc5d1,纯测试)处理了上一轮唯一的小问题——详见 Stage 2 评论。
  • **风险:**未命中高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 77cc5d1db187e46ce5f14f0c1bdcda7ca6139105 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Code review

My baseline is unchanged from the first pass: a gate conjunct that can never be true on this event has exactly two sensible fixes — fetch the field properly, or drop the conjunct. Fetching is off the table because the signal job is deliberately powerless (permissions: {}, no secrets, no checkout — it runs on fork-triggered events), so deletion plus a regression test is the minimal correct fix. The PR still matches that baseline; nothing simpler was missed.

What the new commit (77cc5d1, test-only) changes, reviewed in full:

  • It fixes exactly the one nit from the previous round, and nothing else. The earlier version asserted not.toContain(pull_request.${field}) — substring-based, so a future legitimate conjunct like pull_request.merged_at or pull_request.comments_url would have tripped the merged/comments entries. The new version matches on pull_request\.<field>\b instead. The word-boundary anchor is correct for all four prefix collisions (_ is a word character, so merged\b cannot match merged_at), and field names are [a-z_]+ — no regex metacharacters to escape.
  • The matcher is self-tested in both directions. The new rejects a full-object field without rejecting the fields it prefixes case asserts all four prefix pairs pass when legal and fail when not — so the oracle itself is pinned, not just the gate. That's the right shape for a test whose whole job is to fail loud instead of silent.
  • The security model is intact, re-verified against the current tree. Consent still lives only where credentials exist: the bridge's live read (gh pr view --json …maintainerCanModify… + select(… .maintainerCanModify == true) at qwen-autofix-fork-bridge.yml:144–152), plus the direct lane's own maintainerCanModify checks in qwen-autofix.yml. The gate still decides WHEN, never whether. Permissions, steps, run-name, and concurrency block are unchanged.
  • No drive-bys. The delta against the previously reviewed commit is +26/−1 in one test file.

No blockers, no convention violations.

Test evidence (this PR's own CI, read via API — unattended run)

This is an unattended CI run, so I did not build or execute any PR code; the evidence below is the PR's own CI on 77cc5d1db187e46ce5f14f0c1bdcda7ca6139105, fetched through the checks API. The Ubuntu unit leg — the one that runs scripts/tests/ via npm run test:citest:scriptspassed in 21m12s, so both new tests and the updated verbatim gate pin are green in CI. The macOS/Windows unit legs and the integration jobs are skipped by the repo's conditional CI (workflow/test-only change) — GitHub-set conclusions, quoted as-is. The web-shell smoke leg was still in flight at review time; the finalize job updates the table below once CI settles.

Check Conclusion
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ⏳ pending

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Not verified, explicitly:

  • The gate's first live firing after merge. By definition unobservable pre-merge — the gate only opens on a real fork-PR review. No sandboxed lane can settle this one: /tmux drives the TUI (no TUI surface here), and /verify A/B-tests build behaviour, while this change's oracle is a GitHub webhook payload shape that only a real pull_request_review delivery exercises. The pre-merge evidence is as tight as it gets: the skipped-run at 01:47:08Z above proves the dead conjunct is the only failing one, so removing it unblocks the gate by construction. The first qualifying fork review after merge is the oracle.
  • The earlier /verify attempt (on the previous head) died on an infrastructure failure, not a PR defect — no report was produced. The load-bearing question it would have settled (does the suite pin the change?) is answered textually anyway: the verbatim gate pin was updated in lockstep with the gate and the new field test asserts the removal, all inside the 21-minute green Ubuntu suite.
中文说明

代码审查

我的基准判断与首轮一致:一个在此事件上永远不可能为真的合取项只有两种合理修法——正确读取字段,或删掉它。读取在这里不可行,因为 signal job 刻意不持凭证(permissions: {}、无密钥、无 checkout——它运行在 fork 触发的事件上),所以删除加回归测试就是最小正确修法。PR 依然符合该基准,没有遗漏更简路径。

新提交(77cc5d1,纯测试)的改动,已完整审查:

  • 它恰好修复了上一轮唯一的小问题,且仅此而已。 早先版本用 not.toContain(pull_request.${field}) 断言——基于子串,未来正当的合取项如 pull_request.merged_atpull_request.comments_url 会误触 merged/comments 项。新版本改用 pull_request\.<field>\b 匹配。词边界锚点对全部四个前缀冲突都是正确的(_ 是单词字符,因此 merged\b 不可能匹配 merged_at),且字段名均为 [a-z_]+——无需转义任何正则元字符。
  • 匹配器本身做了双向自测。 新增的 rejects a full-object field without rejecting the fields it prefixes 用例对全部四组前缀断言:合法时通过、非法时失败——被钉死的不只是门禁,还有 oracle 本身。对于一个职责就是"响亮失败而非静默失败"的测试,这是正确的形态。
  • 安全模型完好,已在当前树上重新核实。 同意检查只存在于有凭证的地方:bridge 的实时读取(qwen-autofix-fork-bridge.yml:144–152 处的 gh pr view --json …maintainerCanModify… + select(… .maintainerCanModify == true)),以及直连通道在 qwen-autofix.yml 中自己的 maintainerCanModify 检查。门禁依旧只决定"何时",从不决定"是否"。权限、步骤、run-name 与并发块均未改动。
  • 无夹带改动。 相对上次已评审提交的差异为一个测试文件的 +26/−1。

无阻塞项,无规范违规。

测试证据(该 PR 自己的 CI,经 API 读取——无人值守运行)

这是无人值守的 CI 运行,因此我未构建或执行任何 PR 代码;以下证据是 77cc5d1db187e46ce5f14f0c1bdcda7ca6139105 上该 PR 自己的 CI,经 checks API 获取。Ubuntu 单元测试腿——即通过 npm run test:citest:scripts 运行 scripts/tests/ 的那一条——以 21 分 12 秒通过,因此两个新测试与更新后的逐字门禁钉死都在 CI 中为绿。macOS/Windows 单元测试腿与集成测试作业因仓库的条件化 CI 而被 skipped(纯 workflow/测试改动)——以上为 GitHub 设定的结论,原样引用。评审时 web-shell 冒烟腿仍在进行中;CI 落定后 finalize 任务会更新下表。

未验证项,明确列出:

  • 合入后门禁的首次真实触发。 按定义合入前不可观测——门禁只在一次真实的 fork PR 评审时打开。没有任何沙箱通道能定论此事:/tmux 驱动的是 TUI(此处无 TUI 界面),/verify 做的是构建行为的 A/B 对比,而本改动的 oracle 是 GitHub webhook 的负载形状,只有真实的 pull_request_review 投递才能验证。合入前的证据已经是最紧的:上文 01:47:08Z 那次 skipped 运行证明死合取项是唯一失败的合取项,因此删除它必然打开门禁。合入后第一次符合条件的 fork 评审即是 oracle。
  • 早先的 /verify 尝试(针对上一个 head)死于基础设施故障,并非 PR 缺陷——未生成报告。它本可定论的"测试是否承载改动"问题已由文本层面回答:逐字门禁钉死与门禁同步更新,新字段测试断言了删除,全部包含在那条 21 分钟的绿色 Ubuntu 套件中。

Qwen Code · qwen3.8-max

Reviewed at 77cc5d1db187e46ce5f14f0c1bdcda7ca6139105 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — clean across every stage; the one prior nit was addressed exactly, and I independently reproduced the dead gate twice over. Approval is pinned and deferred only because one CI leg is still in flight on the reviewed commit.

Stepping back: this is what a good infrastructure fix looks like, and the second round made it sharper rather than broader. The problem was observed, not hypothesized — on this re-run I pulled the full run history (500 runs, zero success) and verified the cited delivery end to end: the label timeline proves autofix/takeover was on PR 9384 at run time, the live REST object reads maintainerCanModify: true, and the run still landed skipped — so the payload-less conjunct is the only failing one, which makes the fix correct by construction rather than by plausibility.

The delta since the last review is exactly one test-only commit, and it does exactly what was asked: the full-object field guard now matches on a word boundary instead of a substring, with a two-directional self-test proving the matcher rejects pull_request.merged without rejecting pull_request.merged_at. I checked the boundary logic against all four prefix collisions — _ is a word character, so the anchor holds everywhere it needs to. The oracle is now pinned too, not just the gate.

What I'd want to know six months from now is all in the tree: the long job comment records why the conjunct can never live here (payload shape + permissions: {}), and the field-list test turns the next attempt into a loud CI failure instead of a silent always-false gate. If I maintain this, I thank the author.

Not approving in this run for one procedural reason: the Qwen Code CI workflow on 77cc5d1db187e46ce5f14f0c1bdcda7ca6139105 still has the web-shell smoke leg in flight. Approval is deferred until CI lands green on that commit — the finalize job posts the same commit-pinned approval once every check settles, and withholds it if anything lands red or the head moves.

中文说明

回顾全局:这是一次优秀的基础设施修复该有的样子,而且第二轮让它更锋利、而不是更膨胀。问题是观测到的,不是假设的——这次重跑我拉取了完整运行历史(500 次运行,零次 success),并端到端核实了被引用的那次投递:标签时间线证明运行时 PR 9384 上挂着 autofix/takeover,REST 实时对象读数为 maintainerCanModify: true,而运行结果仍是 skipped——因此负载中缺失的那个合取项是唯一失败的合取项,这使得该修复是构造上正确,而不只是看起来合理。

上次评审以来的差异恰好是一个纯测试提交,且它做的恰好是被要求的事:完整对象字段守卫现在按词边界匹配而非子串,并配有一个双向自测,证明匹配器拒绝 pull_request.merged 的同时不会拒绝 pull_request.merged_at。我对照全部四个前缀冲突检查了边界逻辑——_ 是单词字符,所以锚点在每处需要的地方都成立。现在被钉死的不只是门禁,还有 oracle 本身。

六个月后维护它时我需要知道的东西都已在树里:那段长 job 注释记录了该合取项为何永远不能存在于这里(负载形状 + permissions: {}),字段清单测试则把下一次尝试变成响亮的 CI 失败,而不是静默的永假门禁。如果由我来维护,我会感谢作者。

本次不批准,只出于一个程序性原因:77cc5d1db187e46ce5f14f0c1bdcda7ca6139105 上的 Qwen Code CI 工作流仍有一条 web-shell 冒烟腿在运行中。批准推迟到 CI 在该提交上全绿落定——finalize 任务会在所有检查完成后发布同样绑定该提交的批准;若有检查变红或 head 移动,则不会批准。

Qwen Code · qwen3.8-max

Reviewed at 77cc5d1db187e46ce5f14f0c1bdcda7ca6139105 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship — CI landed green after the review. ✅

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix-fork-signal.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

中文说明

已审查——无阻断问题。 建议见行内评论。

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix-fork-signal.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

— qwen3.8-max via Qwen Code /review (v0.21.14)

Comment thread scripts/tests/qwen-autofix-fork-bridge-workflow.test.js
The guard asserted the signal gate references no full-object-only field via
bare substring containment, but four deny-list entries are strict prefixes of
fields the simple pull-request object DOES deliver: `merged` in `merged_at`,
`commits` in `commits_url`, `comments` in `comments_url`, `review_comments` in
`review_comments_url`. A future edit adding a legal conjunct such as
`github.event.pull_request.merged_at == null` would turn the suite red with a
message blaming a full-object-only field — pushing the author to drop the
conjunct or weaken the guard itself.

Anchor each check on a word boundary, and add a test that pins the matcher's
discrimination on all four prefix pairs so the substring form cannot come back
unnoticed.
wenshao
wenshao previously approved these changes Aug 19, 2026
@wenshao

wenshao commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ incomplete — infrastructure failure - workflow run

The verification job did not complete (checkout, runner, or setup error) and produced no report. See the workflow run for details.

中文 — 判定:⚠️ 未完成 · 基础设施故障

验证作业未完成(检出、runner 或初始化错误),未生成报告。详见工作流运行日志。

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on 77cc5d1db187e46ce5f14f0c1bdcda7ca6139105 — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 77cc5d1db187e46ce5f14f0c1bdcda7ca6139105既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix-fork-signal.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

中文说明

已审查——无阻断问题。 建议见行内评论。

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix-fork-signal.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

— qwen3.8-max via Qwen Code /review (v0.21.14)

Comment thread scripts/tests/qwen-autofix-fork-bridge-workflow.test.js
The full-object-only guard matched `pull_request.<field>` literally, so it
only saw the `.` property de-reference. GitHub Actions reaches the same
property through the documented `[ ]` index operator, on any segment of the
path, and `github.event.pull_request['maintainer_can_modify'] == true`
evaluates exactly as the dot form did: the field is absent from the simple
`pull_request_review` payload, `null == true` is false, and the gate never
opens. The guard stayed green through it — a silent replay of the incident
this PR fixes, invisible to the test written to catch it.

Rewrite the index form to the dot form before matching rather than
enumerating spellings, so one matcher covers every combination of the two at
any depth. The word-boundary anchor from R1-1 is unchanged, so the four
delivered fields the deny-list names prefix (`merged_at`, `commits_url`,
`comments_url`, `review_comments_url`) still pass in every spelling.

A `fromJSON(toJSON(github.event.pull_request))` round-trip still evades this;
no textual guard catches that one, and the comment says so.

Mutation-verified, each mutant reddening the tests that pin it:

| mutant | result |
|---|---|
| normalization removed (identity) | 2 failed — bracket and mixed spellings go unseen |
| word boundary dropped | 1 failed — `merged_at` rejected as `merged` |

`npx vitest run --config ./scripts/tests/vitest.config.ts
scripts/tests/qwen-autofix-fork-bridge-workflow.test.js` -> 12 passed (12).
eslint and prettier clean.
@qqqys

qqqys commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /review

@github-actions

Copy link
Copy Markdown
Contributor

Qwen Code review request accepted. Review is queued in workflow run.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix-fork-signal.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:

  • scripts/tests/qwen-autofix-fork-bridge-workflow.test.js:295 — [probe] The field guard is a 12-name blocklist; an allowlist of delivered fields would fail closed on typos and unlisted fields
中文说明

已审查。 建议见行内评论。

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix-fork-signal.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.21.14)

Comment thread scripts/tests/qwen-autofix-fork-bridge-workflow.test.js
R3-1: `asDotAccess` rewrites `pull_request['field']` to the dot form before
matching, and its regex deliberately tolerates whitespace inside the index
(`\[\s*…\s*\]`) because GitHub Actions accepts
`github.event.pull_request[ 'maintainer_can_modify' ]` as a legal expression.
No spelling in `referenceSpellings` carried that whitespace, so the tolerance
was unpinned: deleting both `\s*` left all 12 tests green, and a later gate
edit written in the spaced form would have reached the same absent field and
restored the always-false gate this PR fixes.

Add the spaced-bracket spelling, which both index-operator tests consume.

Mutation-verified: with the two `\s*` deleted from `asDotAccess`, this file
now fails 2 tests ("rejects a full-object field without rejecting the fields
it prefixes" and "sees a full-object field through the index operator");
before this commit the same mutation left 12 passed.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No blocking issues. LGTM! ✅

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix-fork-signal.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:

  • scripts/tests/qwen-autofix-fork-bridge-workflow.test.js:105 — [probe] Guard misses the spaced-dot spelling pull_request . field , which the Actions lexer accepts
  • scripts/tests/qwen-autofix-fork-bridge-workflow.test.js:295 — [probe] Guard misses the camelCase alias maintainerCanModify , the spelling dominant in the surrounding code
中文说明

无阻断问题。LGTM!✅

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix-fork-signal.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.21.14)

@qqqys
qqqys enabled auto-merge August 20, 2026 02:59
@qqqys
qqqys added this pull request to the merge queue Aug 22, 2026
Merged via the queue into QwenLM:main with commit 2a57f86 Aug 22, 2026
69 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.2.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants