Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 223 additions & 18 deletions .github/workflows/qwen-autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1881,6 +1881,16 @@ jobs:
${{ needs.route.outputs.do_review == 'true' }}
runs-on: 'ubuntu-latest'
timeout-minutes: 15
# A forced scan can write the same status comment as review-address.
# Share its per-PR lock so neither writer can erase the other's state.
# Keep the predicate as narrow as the job's own `if:` — concurrency is
# evaluated BEFORE it, so without the do_review conjunct a dispatch with
# `phase: issue` + `pr_number: N` (route emits pr_number unconditionally)
# would park this skipped job in that PR's shared slot behind a 300-minute
# address round, stalling the issue phase that `needs` it.
concurrency:
group: "qwen-pr-head-write-${{ needs.route.outputs.do_review == 'true' && needs.route.outputs.pr_number || github.run_id }}"
cancel-in-progress: false
Comment on lines +1891 to +1893

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.

[Suggestion] This concurrency group is broader than the job's own if: condition — GitHub evaluates concurrency BEFORE the job if, so runs that will only skip still occupy the shared per-PR slot. route emits pr_number unconditionally from the dispatch input (via sanitize_number), so a workflow_dispatch with phase: issue and pr_number: N resolves the group to qwen-pr-head-write-N even though do_review is false and this job will skip.

Failure scenario: workflow_dispatch with phase: issue + pr_number: N while an address round for PR N is in flight → the skipped review-scan queues on qwen-pr-head-write-N behind the address leg (timeout-minutes: 300, cancel-in-progress: false); issue-autofix declares needs: ['route', 'review-scan'], so the issue phase the operator actually dispatched idles behind an unrelated address round it never interacts with — with only a "queued" badge as explanation. Nothing is cancelled (it self-recovers); the cost is a bounded but unexplained stall of up to the address round's length. The repo's own qwen-triage-workflow.test.js pins exactly this trap: "a predicate that is broader than the job's own condition lets a run that will skip take the shared per-PR slot".

Suggested change
concurrency:
group: 'qwen-pr-head-write-${{ needs.route.outputs.pr_number || github.run_id }}'
cancel-in-progress: false
concurrency:
group: 'qwen-pr-head-write-${{ needs.route.outputs.do_review == ''true'' && needs.route.outputs.pr_number || github.run_id }}'
cancel-in-progress: false
中文说明

[Suggestion] 这个 concurrency group 比 job 自身的 if: 条件更宽——GitHub 在 job if 之前评估 concurrency,因此只会跳过的运行也会占用共享的 per-PR 锁槽。route 会无条件输出 dispatch 输入中的 pr_number(经 sanitize_number),所以 phase: issue + pr_number: Nworkflow_dispatch 仍会把 group 解析为 qwen-pr-head-write-N——即使 do_review 为 false、本 job 只会跳过。

失败场景:PR N 正有一轮 address 在运行时,以 phase: issue + pr_number: N 触发 workflow_dispatch → 被跳过的 review-scan 会在 qwen-pr-head-write-N 上排队,等在该 address 分片之后(timeout-minutes: 300cancel-in-progress: false);由于 issue-autofix 声明 needs: ['route', 'review-scan'],操作员实际派发的 issue 阶段会闲置在一个与之无关、永不会交互的 address 轮次后面——唯一的提示是 "queued" 标记。不会取消(可自愈);代价是最长可达 address 轮次时长的无解释等待。本仓库自己的 qwen-triage-workflow.test.js 恰好钉住了这个陷阱:"比 job 自身条件更宽的谓词会让将要跳过的运行占用共享的 per-PR 槽位"。

建议修复:把 group 收敛到 job 的真实条件(见 suggestion 块),并同步更新 'serializes forced status writes with the matching address job' 中的断言。

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

outputs:
targets: '${{ steps.scan.outputs.targets }}'
has_targets: '${{ steps.scan.outputs.has_targets }}'
Expand All @@ -1907,6 +1917,186 @@ jobs:
}
WORKDIR="$(mktemp -d)"

read_forced_pr_meta() {
local attempt meta
for attempt in 1 2 3; do
Comment on lines +1920 to +1922

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.

[Suggestion] R8-1: The same 3-attempt / linear-backoff / per-attempt-warning retry skeleton is pasted six times inside the one shell block this diff adds (read_forced_pr_meta, read_live_permission, and four loops in report_forced_takeover_blocked: actor lookup, status lookup, PATCH, create-comment). Drift already exists within the diff: the two reader loops warn to stderr, the four reporter loops warn to stdout (both streams are test-pinned). — Failure scenario: any retry-policy change (attempt count, backoff curve, warning format) must be applied consistently in six copies inside one run block; a miss makes the metadata fetch retry differently from the status post that reports its failure, on the same failure path.

Suggested fix: extract one local retry helper beside the new functions and use it for the four plain-command reporter loops (the two reader loops keep their bespoke in-loop validation but share the backoff/warning shape):

retry3() { # <warning-what> <cmd...> — warns per attempt, linear backoff
  local what="$1" attempt; shift
  for attempt in 1 2 3; do
    if "$@"; then return 0; fi
    echo "::warning::${what} (attempt ${attempt}/3)" >&2
    [[ "${attempt}" -lt 3 ]] && sleep "${attempt}"
  done
  return 1
}
中文说明

R8-1:本 diff 新增的同一个 shell 块中,相同的"3 次尝试 / 线性退避 / 每次尝试告警"重试骨架被粘贴了六次(read_forced_pr_metaread_live_permission,以及 report_forced_takeover_blocked 中的四个循环:actor 查询、状态评论查询、PATCH、新建评论)。diff 内部已出现漂移:两个读取器循环向 stderr 告警,四个报告器循环向 stdout 告警(两种流都被测试钉住)。— 失败场景:任何重试策略变更(尝试次数、退避曲线、告警格式)都必须在一个 run 块的六处副本中保持一致;漏改一处会使元数据获取与报告其失败的"状态评论发布"在同一条失败路径上采用不同的重试行为。建议修复:在新函数旁抽取一个本地重试助手(如上),四个纯命令的报告器循环直接复用;两个读取器循环保留各自的循环内校验,但共享退避/告警形状。

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

if meta="$(gh pr view "${FORCED_PR}" --repo "${REPO}" \
--json number,state,author,headRefName,isCrossRepository,baseRefName,labels,maintainerCanModify 2> /dev/null)" \
Comment on lines +1923 to +1924

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.

[Suggestion] R4-3: All four new gh reads discard stderr via 2> /dev/null (meta here, permission ~1748, actor probe ~1781, status lookup ~1803), so a terminal metadata_fetch_failed / permission_lookup_failed run goes red with no root-cause text — the log carries three identical attempt N/3 warnings and one error line, and the fallback jq -e on empty input exits 4 silently (measured). Nothing validates CI_DEV_BOT_PAT before review-scan spends it, so an operator-typo'd PR number, an expired PAT, and a GitHub outage are indistinguishable. — Failure scenario: a red forced scan pages a maintainer; gh's actual error (Could not resolve to a PullRequest…, HTTP 401: Bad credentials, secondary-rate-limit text) was discarded, so the cause cannot be determined without manually re-running the exact gh call. The route job already has the capture-stderr idiom (~470-490). Suggested fix: capture stderr to a temp file and include it in the final warning/error (the route job's api_error_file idiom), or drop the 2> /dev/null.

中文说明

四个新增的 gh 读取都通过 2> /dev/null 丢弃了 stderr(此处是元数据读取,其余在 ~1748 权限、~1781 身份探测、~1803 状态评论查找),因此终态的 metadata_fetch_failed / permission_lookup_failed 会把运行染红却没有任何根因文本——日志里只有三条相同的 attempt N/3 警告和一条错误行,且空输入上的兜底 jq -e 会静默以 4 退出(已实测)。review-scan 花用 CI_DEV_BOT_PAT 之前没有任何凭证校验,所以手滑输错的 PR 号、过期的 PAT 和 GitHub 故障完全无法区分。— 失败场景:一次染红的强制扫描把维护者叫醒;gh 的真实报错(Could not resolve to a PullRequest…HTTP 401: Bad credentials、次级限流文本)已被丢弃,不手工重跑一模一样的 gh 调用就无法定位原因。route job 里已有捕获 stderr 的惯用写法(~470-490)。建议修复:把 stderr 捕获到临时文件并并入最终的 warning/error(route job 的 api_error_file 写法),或去掉 2> /dev/null

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

&& jq -e 'type == "object"

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.

[Suggestion] R1-4: Test gap — the shape-validation negative path of the new readers is never exercised: the retry tests feed only fully-valid payloads or an outright failing gh. Still stands at this commit (mutants re-confirmed): deleting the jq -e shape check makes read_forced_pr_meta accept an exit-0 {}, and deleting the permission regex makes read_live_permission accept an exit-0 none — both mutants keep all 110 tests green. — Failure scenario: a future edit stripping either fail-closed validation ships green, and malformed-but-successful API responses flow into the classifier/admission path instead of being rejected after 3 attempts.

Suggested fix: add two reader scenarios to the existing harness — a gh stub that exits 0 printing {} (expect status 1 and 3 retry warnings) and a stub that exits 0 printing none (expect status 1).

中文说明

测试缺口——新读取器的结构校验负路径从未被执行:重试用例只喂完全合法的 payload 或直接失败的 gh(本轮复查仍成立,变异体再次确认):删除 jq -e 结构检查后 read_forced_pr_meta 会接受 exit 0 的 {};删除权限正则后 read_live_permission 会接受 exit 0 的 none——两种变异下全部 110 个测试仍为绿。— 失败场景:未来某次编辑删掉任一 fail-closed 校验后会静默通过 CI,畸形但成功的 API 响应将流入分类器/准入路径,而不是在 3 次尝试后被拒绝。

建议修复:在现有测试装置中新增两个读取器场景——exit 0 输出 {}gh 桩(断言状态码 1 与 3 次重试警告)、exit 0 输出 none 的桩(断言状态码 1)。

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

and (.number | type == "number")
Comment on lines +1925 to +1926

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.

[Suggestion] R1-4: Still stands — the shape-validation negative path of the new readers is never exercised: the retry tests feed only fully-valid payloads or an outright failing gh. Mutants re-confirmed at this commit: deleting this jq -e clause from read_forced_pr_meta (and separately the permission whitelist regex from read_live_permission) keeps all 110 tests green, while live comparators prove both mutants change behaviour (malformed payload: BASE exit=1 rejected vs mutant exit=0 accepted). — Failure scenario: gh pr view can exit 0 with a partial response; with validation in place that payload is retried and surfaces as metadata_fetch_failed. If a future edit drops the check, a transient API degradation flows into forced_admission_reason and is recorded as a permanent PR property with no retries — and this suite certifies the change green.

Suggested fix: in the existing runReader harness, add a fake gh that exits 0 but prints {} (and one printing a wrong-typed field, e.g. number as a string) — expect status 1 with (attempt 3/3); likewise for read_live_permission with an exit-0 payload of "" or "owner".

中文说明

[Suggestion] R1-4:仍然存在——新读取器的结构校验负路径从未被测试覆盖:重试测试要么喂完全合法的载荷,要么让 gh 直接失败。本提交上重新确认了变异体:删除 read_forced_pr_meta 中的这个 jq -e 子句(以及单独删除 read_live_permission 的权限白名单正则)后,全部 110 个测试仍然通过,而实时对照证明两个变异体都改变了行为(畸形载荷:BASE exit=1 拒绝 vs 变异体 exit=0 接受)。— 失败场景:gh pr view 可能以 exit 0 返回残缺响应;有校验时该载荷会被重试并最终表现为 metadata_fetch_failed。若未来某次编辑删掉了这个检查,一次瞬时 API 降级就会直接流入 forced_admission_reason,被当作 PR 的永久属性记录下来且不再重试——而本测试套件还会给这个改动开绿灯。

建议修复:在现有 runReader 测试框架中,加一个 exit 0 但输出 {} 的假 gh(再一个输出错误类型字段,如把 number 变成字符串)——断言 status 1 且带 (attempt 3/3);对 read_live_permission 同理,喂 exit-0 的 """owner"

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

and (.state | type == "string")
and (.author.login | type == "string")
and (.headRefName | type == "string")
and (.baseRefName | type == "string")
and (.isCrossRepository | type == "boolean")
and (.labels | type == "array")
and (.maintainerCanModify | type == "boolean")' > /dev/null <<< "${meta}"; then
Comment on lines +1932 to +1933

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.

[Suggestion] Test gap (pattern, instance 2/4): the shape-validation negative path of read_forced_pr_meta is never exercised — the retry tests feed only fully-valid payloads or an outright failing gh. Confirmed surviving mutant: dropping the and (.maintainerCanModify | type == "boolean") conjunct keeps the suite green, while a probe shows the original rejects a payload missing only that field (rc=1 → metadata_fetch_failed) and the mutant accepts it (rc=0). read_live_permission has the same gap: no test feeds a successful-but-unrecognized permission value, so widening its regex to accept anything also survives. — Failure scenario: during an API incident or field rename, partial metadata would flow into admission instead of a fail-closed fetch failure, and no test would catch the regression.

Suggested fix: add fake-gh cases that exit 0 with shape-invalid payloads (meta missing maintainerCanModify; permission "" or "unknown") and assert both readers retry to (attempt 3/3) and return 1.

中文说明

测试缺口(模式,第 2/4 处):read_forced_pr_meta 的结构校验负路径从未被执行——重试测试只喂入完全合法的载荷或直接失败的 gh。已确认的存活变异体:删除 and (.maintainerCanModify | type == "boolean") 合取项后套件仍全绿,而探针表明原代码会拒绝仅缺该字段的载荷(rc=1 → metadata_fetch_failed),变异体却接受它(rc=0)。read_live_permission 存在同样缺口:没有测试喂入“成功但不可识别”的权限值,因此把正则放宽为接受任意值同样存活。— 失败场景:API 故障或字段改名期间,残缺元数据会流入准入而不是触发 fail-closed 的获取失败,且没有任何测试能发现该回归。

建议修复:新增退出码为 0 但载荷不合法的 fake-gh 用例(meta 缺 maintainerCanModify;权限为 """unknown"),并断言两个读取器都重试到 (attempt 3/3) 且返回 1。

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

printf '%s' "${meta}"
return 0
fi
echo "::warning::Forced PR #${FORCED_PR} metadata lookup failed (attempt ${attempt}/3)" >&2
[[ "${attempt}" -lt 3 ]] && sleep "${attempt}"
done
return 1
}

# 'none' and HTTP 404 are DEFINITIVE answers, not lookup failures.
# GitHub returns 200 with permission 'none' for logins that exist but
# hold nothing here (bot-type logins such as dependabot[bot], and org
# logins), and 404 for logins that do not exist or are empty. Both
# mean "no write access" — the routine rejection this gate is for.
# Retrying them would burn 3 API calls plus back-off per candidate per
# scheduled tick, forever, and strand the caller on
# 'permission_lookup_failed': a red forced run (exit 1) whose blocked
# comment promises "a later scheduled scan will retry" — a retry that
# can never succeed — while the actionable "grant the fork author
# write access" guidance behind author_permission_* stays unreachable.
# Only genuinely transient answers (5xx, network, auth) retry.
read_live_permission() {
local login="$1" attempt permission err result=''
# An empty login can only 404; skip the call and answer terminally.
if [[ -z "${login}" ]]; then
printf 'none'
return 0
fi
err="$(mktemp)"
for attempt in 1 2 3; do
if permission="$(gh api "repos/${REPO}/collaborators/${login}/permission" --jq '.permission // ""' 2> "${err}")" \
&& [[ "${permission}" =~ ^(admin|maintain|write|triage|read|none)$ ]]; then
result="${permission}"
break
fi
if grep -q 'HTTP 404' "${err}"; then
result='none'
break
fi
# Surface gh's own diagnosis instead of discarding it: a rate
# limit, an expired PAT and a 5xx all look identical otherwise.
echo "::warning::Permission lookup failed for ${login} (attempt ${attempt}/3): $(tr '\n' ' ' < "${err}")" >&2
[[ "${attempt}" -lt 3 ]] && sleep "${attempt}"
done
rm -f "${err}"
[[ -n "${result}" ]] || return 1
printf '%s' "${result}"
}

forced_admission_reason() {
jq -r --arg ab "${AUTOFIX_BOT}" --arg take "${TAKEOVER_LABEL}" --arg skip "${SKIP_LABEL}" '
if (.state // "") != "OPEN" then "not_open"
elif (.baseRefName // "") != "main" then "wrong_base"
elif ([.labels[]?.name] | index($skip) != null) then "skip_label"
Comment on lines +1986 to +1987

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.

[Suggestion] No classifier test case pins skip_label precedence over the fork checks (unmanaged_author, maintainer_edits_disabled), although the workflow comment documents "skip wins over takeover … excluded everywhere". Probe-verified mutant: moving the skip check below the maintainer_edits_disabled check flips a fork PR carrying both labels from skip_label to maintainer_edits_disabled, and all 11 existing assertions stay green (the only skip cases in the test are in-repo). — Failure scenario: after such a reorder, a fork PR carrying both autofix/takeover and autofix/skip with maintainerCanModify: false classifies as maintainer_edits_disabled; the reporter's case filter excludes skip_label but includes maintainer_edits_disabled, so it posts a "⛔ AutoFix blocked" status comment on a PR whose author explicitly opted out.

Suggested fix — one assertion in the classifier test:

expect(
  reason(
    meta('human', ['autofix/takeover', 'autofix/skip'], {
      isCrossRepository: true,
      maintainerCanModify: false,
    }),
  ),
).toBe('skip_label');
中文说明

分类器测试没有任何用例钉住 skip_label 相对 fork 检查(unmanaged_authormaintainer_edits_disabled)的优先级,而工作流注释明确写着 "skip wins over takeover … excluded everywhere"。已通过变异探针验证:把 skip 检查移到 maintainer_edits_disabled 检查之下,同时带两个标签的 fork PR 会从 skip_label 变为 maintainer_edits_disabled,而现有 11 条断言全部仍然通过(现有 skip 用例都是同仓库 PR)。— 失败场景:发生这种重排后,同时带 autofix/takeoverautofix/skipmaintainerCanModify: false 的 fork PR 会被分类为 maintainer_edits_disabled;报告函数的 case 过滤器排除 skip_label 但包含 maintainer_edits_disabled,于是会在作者已明确选择退出的 PR 上发布 "⛔ AutoFix blocked" 状态评论。

建议修复——在分类器测试中加一条断言(见上方代码块)。

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

elif ((((.author.login // "") == $ab) or ([.labels[]?.name] | index($take) != null)) | not) then "unmanaged_author"
elif (.isCrossRepository == true) and (.maintainerCanModify != true) then "maintainer_edits_disabled"
elif (((.isCrossRepository == true) or (.isCrossRepository == false)) | not) then "cross_repo_state_missing"
else "eligible"
end'
}

report_forced_takeover_blocked() {
local reason="$1" actor status_ids status_id body attempt status_lookup_ok next_en next_zh err
[[ "${DRY_RUN}" == 'true' ]] && return 0

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.

[Suggestion] Reverse audit round 3: the reporter's DRY_RUN short-circuit guard has no behavioral test — every reporter scenario passes DRY_RUN=false. Probe-verified: deleting this guard survives 110/110, while a three-arm probe flips on both axes — current code with DRY_RUN=true makes zero gh calls; the guard-deletion mutant with DRY_RUN=true makes 3 calls including the real PATCH. Reachability verified: the workflow_dispatch dry_run input propagates via the route job's dry_run output into this scan step's env, and a manual pr_number + dry_run: true dispatch of a takeover-labeled fork with maintainer_edits_disabled reaches the reporter with DRY_RUN=true — this guard is the sole protection against a write on that path. The code at this commit is correct; only the behavioral proof is missing. — Failure scenario: if a future edit deletes or breaks the guard, a manual dry-run dispatch — contractually "assess/verify, but do not claim, push, or comment" — of a takeover-labeled fork blocked on maintainer_edits_disabled would POST/PATCH a real "AutoFix blocked" status comment, a write in a mode forbidden from writing, while the full suite stays green.

Suggested fix: add one runReporter arm with DRY_RUN: 'true' (takeover-labelled META, terminal reason), asserting status 0 and an empty recorded-calls file — mirroring the existing FAIL_STATUS_LOOKUP assertion.

中文说明

[Suggestion] 反向审计第 3 轮:报告函数的 DRY_RUN 短路守卫没有行为测试——所有报告场景都传 DRY_RUN=false。经探针验证:删除这行守卫后 110/110 测试仍全绿,而三臂探针在两个轴上都能翻转——当前代码在 DRY_RUN=true 时发起 0 次 gh 调用;删除守卫的变异体在 DRY_RUN=true 时发起 3 次调用,包括真实的 PATCH。可达性已验证:workflow_dispatchdry_run 输入经 route job 的 dry_run 输出传入本扫描步骤的 env;对带 takeover 标签且 maintainer_edits_disabled 的 fork 手动执行 pr_number + dry_run: true 分发时,会以 DRY_RUN=true 到达报告函数——这个守卫是该路径上防止写入的唯一防线。本提交的代码是正确的;缺的只是行为性证明。— 失败场景:若未来某次编辑删除或破坏了这个守卫,一次手动 dry-run 分发(按其契约"只评估/验证,不得 claim、push 或评论")会对一个因 maintainer_edits_disabled 被阻塞的带 takeover 标签 fork POST/PATCH 一条真实的 "AutoFix blocked" 状态评论——在本禁止写入的模式下发生了写入,而整个测试套件仍是绿的。

建议修复:新增一个 DRY_RUN: 'true'runReporter 分支(takeover 标签 META、终态原因),断言 status 0 且记录的调用文件为空——与现有 FAIL_STATUS_LOOKUP 断言对称。

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

[[ "$(jq -r --arg ab "${AUTOFIX_BOT}" --arg take "${TAKEOVER_LABEL}" '
((.author.login // "") == $ab) or ([.labels[]?.name] | index($take) != null)
' <<< "${META}")" == 'true' ]] || return 0
Comment on lines +1998 to +2000

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.

[Suggestion] R8-2: This re-derives "PR is managed" with a second copy of the classifier's unmanaged_author predicate — an invariant living in two places that is provably always true at every current call site (every reason reaching this function already passed the same check in forced_admission_reason, and the case filter below rejects the unmanaged reasons anyway). — Failure scenario: if the managed definition ever changes (a second opt-in label, a different author rule), the edit lands in forced_admission_reason and this buried copy keeps applying the old rule: the reporter silently returns 0, the gate exits 0 green, and the blocked-status comment this PR exists to deliver is never posted — with no red run to point at the loss.

Suggested change
[[ "$(jq -r --arg ab "${AUTOFIX_BOT}" --arg take "${TAKEOVER_LABEL}" '
((.author.login // "") == $ab) or ([.labels[]?.name] | index($take) != null)
' <<< "${META}")" == 'true' ]] || return 0
# Managedness already guaranteed by forced_admission_reason for every
# reason the case filter below admits; do not re-derive it here.
中文说明

R8-2:此处用分类器 unmanaged_author 谓词的第二份副本重新推导"该 PR 被托管"——这是一个存在于两处的不变量,且在当前所有调用点上恒为真(到达本函数的每个原因都已在 forced_admission_reason 中通过同样的检查,且下方 case 过滤器本来就会拒绝未托管的原因)。— 失败场景:若"托管"定义日后变更(新增第二个 opt-in 标签、更换作者规则),修改只会落在 forced_admission_reason,而这份深埋的副本仍按旧规则执行:报告器静默返回 0,门禁以绿色 exit 0 结束,本 PR 本应发布的 blocked 状态评论永远不会发出——且没有红色运行可以指向这一丢失。建议修复:删除该 jq 重复检查,以一行注释说明调用方已保证的不变量。

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

case "${reason}" in
permission_lookup_failed|author_permission_*|maintainer_edits_disabled|cross_repo_state_missing) ;;
*) return 0 ;;
Comment on lines +2001 to +2003

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.

[Suggestion] Test gap (pattern, instance 1/4): this terminal/non-terminal gate has no behavioral test — a confirmed surviving mutant proves it. Changing *) return 0 ;; to *) ;; keeps all 110 tests green, but then non-terminal reasons (wrong_base, not_open, …) would post the "⛔ AutoFix blocked … A later scheduled scan will retry" comment, promising a retry that can never succeed. Also unexercised: the actor guard, the PATCH-failure branch, the DRY_RUN/no-label early returns, and the gh pr comment create fallback. — Failure scenario: a future refactor silently breaks the gate and the suite stays green; blocked comments with false retry promises go out for non-terminal rejections.

Suggested fix: parameterize the existing reporter harness — one non-terminal reason (assert rc=0 and zero gh calls), one non-bot actor (rc=1, no PATCH), one PATCH failure (rc=1), one empty comment list (assert gh pr comment is invoked).

中文说明

测试缺口(模式,第 1/4 处):该终态/非终态门没有行为测试——已确认的存活变异体证明了这一点。把 *) return 0 ;; 改为 *) ;; 后全部 110 个测试仍然通过,但非终态原因(wrong_basenot_open 等)将发出 "⛔ AutoFix blocked … A later scheduled scan will retry" 评论,承诺一次永远不会成功的重试。同样未覆盖:actor 守卫、PATCH 失败分支、DRY_RUN/无标签提前返回、gh pr comment 创建兜底。— 失败场景:未来重构悄悄破坏该门而测试套件仍然全绿;非终态拒绝会发出带有虚假重试承诺的 blocked 评论。

建议修复:参数化现有 reporter 测试装置——一个非终态原因(断言 rc=0 且零 gh 调用)、一个非 bot actor(rc=1、无 PATCH)、一次 PATCH 失败(rc=1)、一个空评论列表(断言调用 gh pr comment)。

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

esac
actor=''
for attempt in 1 2 3; do
if actor="$(gh api user --jq '.login' 2> /dev/null)" && [[ -n "${actor}" ]]; then
break
fi
echo "::warning::PAT identity lookup failed (attempt ${attempt}/3)" >&2
[[ "${attempt}" -lt 3 ]] && sleep "${attempt}"
done
if [[ "${actor}" != "${AUTOFIX_BOT}" ]]; then
echo "::warning::Blocked takeover status skipped: PAT authenticates as '${actor:-unknown}'" >&2
return 1
Comment on lines +2013 to +2015

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.

[Suggestion] This PAT-identity guard warns ::warning::Blocked takeover status skipped: ... and returns 1 — but nothing is skipped (return 1 fails every caller: the permission_lookup_failed site adds ::error::...blocked status update failed + exit 1, the other site exits 1 directly), and every sibling PAT check in this file (lines 712-714, 1412-1414, 1599-1601, 1634-1636) uses ::error::CI_DEV_BOT_PAT authenticates as 'X'; expected ${AUTOFIX_BOT}. — error level, naming both actual and expected identity. — Failure scenario: CI_DEV_BOT_PAT is rotated to the wrong account (or vars.AUTOFIX_BOT_LOGIN mis-set); a forced dispatch of a blocked takeover PR reaches this check and the log shows a warning saying 'skipped' followed by a generic failure error — an operator reads 'skipped' as a deliberate no-op and investigates the comment API instead of the PAT configuration. For reasons that would otherwise exit 0 cleanly (author_permission_*, maintainer_edits_disabled), the misconfiguration turns the run red while the log still says 'skipped'.

Suggested change
if [[ "${actor}" != "${AUTOFIX_BOT}" ]]; then
echo "::warning::Blocked takeover status skipped: PAT authenticates as '${actor:-unknown}'"
return 1
if [[ "${actor}" != "${AUTOFIX_BOT}" ]]; then
echo "::error::CI_DEV_BOT_PAT authenticates as '${actor:-unknown}'; expected ${AUTOFIX_BOT}."
return 1
中文说明

[Suggestion] 这个 PAT 身份守卫打出 ::warning::Blocked takeover status skipped: ... 并 return 1——但什么都没被"跳过"(return 1 会让每个调用方失败:permission_lookup_failed 调用点会追加 ::error::...blocked status update failed + exit 1,另一调用点直接 exit 1),而且本文件所有同族 PAT 检查(712-714、1412-1414、1599-1601、1634-1636 行)都用 ::error::CI_DEV_BOT_PAT authenticates as 'X'; expected ${AUTOFIX_BOT}.——error 级别,同时给出实际与期望身份。— 失败场景:CI_DEV_BOT_PAT 被轮换到错误账号(或 vars.AUTOFIX_BOT_LOGIN 配错);一次对被阻塞 takeover PR 的强制分发走到这个检查,日志先显示一条说 "skipped" 的 warning,再跟一条笼统的失败 error——运维会把 "skipped" 读成有意的空操作,转而去查评论 API 而不是 PAT 配置。对原本可以干净 exit 0 的原因(author_permission_*maintainer_edits_disabled),这个配置错误会把运行变红,而日志仍然写着 "skipped"。

(建议修复见上方 suggestion 代码块:改用 error 级别并同时输出实际与期望身份。)

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

fi
if [[ "${reason}" == 'maintainer_edits_disabled' ]]; then
next_en='Re-enable maintainer edits on the fork PR to resume takeover.'
next_zh='请在 fork PR 上重新允许 maintainer edits,以恢复 takeover。'
elif [[ "${reason}" == author_permission_* ]]; then
next_en='Grant the fork author write access, or remove the autofix/takeover label, to resume takeover.'
next_zh='请授予 fork 作者 write 权限,或移除 autofix/takeover 标签,以恢复 takeover。'
else
next_en='A later scheduled scan will retry without advancing the feedback watermark.'
next_zh='后续定时扫描会重试,本次不会推进反馈水位。'
Comment on lines +2023 to +2025

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.

[Suggestion] R4-1: The blocked comment gives an actionable remedy only for maintainer_edits_disabled; a definitive author_permission_* block gets the generic "A later scheduled scan will retry" — a retry that can only succeed via a human action the text never names, while the sibling case spells its remedy out. The round-1 thread on this was fixed only for the fleet-row facet; the maintainer's explicit third-branch request ("grant the fork author write+, or drop autofix/takeover", listed as a pre-landing item) remains unaddressed at this commit — git diff ea34938..HEAD does not touch these text arms. — Failure scenario: a takeover-labeled fork PR whose author holds read/triage → green exit posts "⛔ AutoFix blocked — author_permission_read … A later scheduled scan will retry". Every scan re-derives the same block until someone grants write+ or removes the label; the maintainer is told to wait rather than act, and no writer updates the comment afterward. Suggested fix: add an arm for author_permission_*, e.g. "Grant the fork author write access (or remove the autofix/takeover label) to resume takeover; scheduled scans re-check permission." with a matching Chinese sentence; keep the generic retry sentence for the genuinely transient permission_lookup_failed.

中文说明

blocked 评论只为 maintainer_edits_disabled 给出可操作的补救指引;确定性的 author_permission_* 阻塞拿到的是笼统的"后续定时扫描会重试"——这个重试只有靠人采取行动才能成功,而文案并没有说明该行动,而相邻分支却写明了补救方式。第 1 轮的相关线程只修复了 fleet-row 可见性那一面;维护者明确提出的第三分支请求("给 fork 作者 write+ 权限,或移除 autofix/takeover 标签",列在 pre-landing 清单里)截至本提交仍未处理——git diff ea34938..HEAD 没有触碰这些文案分支。— 失败场景:一个作者只有 read/triage 权限的 takeover fork PR → 绿色退出并发布"⛔ AutoFix blocked — author_permission_read … 后续定时扫描会重试"。在有人授予 write+ 或移除标签之前,每次扫描都会重新推导出同样的阻塞;维护者被告知等待而不是行动,且之后没有任何写入器更新这条评论。建议修复:为 author_permission_* 增加一个分支,例如"授予 fork 作者 write 权限(或移除 autofix/takeover 标签)以恢复 takeover;定时扫描会重新检查权限。"并配对应中文;真正临时的 permission_lookup_failed 保留现有的笼体重试文案。

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复 + 验证证据:author_permission_* 现在明确提示授予 write 权限或移除 autofix/takeover 标签,新增文案分支回归;聚焦用例 1/1 通过,完整 workflow 111/111 断言通过,Prettier 与 git diff --check 通过。

fi
# Every other status writer resolves its run link from
# github.server_url / GITHUB_SERVER_URL. Hardcoding github.com here
# would make the one link this message exists to surface the only
# broken one on a GHES or proxied host.
body="$(printf '<!-- autofix-status -->\n\n⛔ **AutoFix blocked** — takeover admission stopped at `%s`, so no work was started. [View run](%s/%s/actions/runs/%s). %s\n\n<details>\n<summary>中文说明</summary>\n\n⛔ **AutoFix 已阻塞** —— takeover 准入停在 `%s`,因此本轮未开始处理。[查看运行](%s/%s/actions/runs/%s)。%s\n\n</details>' \
"${reason}" "${GITHUB_SERVER_URL}" "${REPO}" "${GITHUB_RUN_ID}" "${next_en}" \
"${reason}" "${GITHUB_SERVER_URL}" "${REPO}" "${GITHUB_RUN_ID}" "${next_zh}")"
status_ids=''
status_lookup_ok=false
err="$(mktemp)"
# Same filter as the sibling upsert in 'Post autofix status comment',
# including its two guards: `// ""` so a single comment with a null
# body cannot abort the whole program (jq exits 5, all three
# attempts fail, and the run reds out WITHOUT posting the very
# status it exists to post), and --arg so a repo-configured
# AUTOFIX_BOT_LOGIN containing " or \ is a mismatch instead of a jq
# parse error. Stays an inline id stream into `tail -1` — it never
# lands in a WORKDIR json file, so the WORKDIR page normalizer
# (add-with-empty-default) must NOT be applied here: it would wrap
# the id stream in an array and break the tail-1 consumer.
# pipefail is set LOCALLY here rather than relied on: this `if`
# must test gh's status, not jq's. A gh failure carrying an HTTP
# status prints the error body to stdout, so jq errors out and the
# retry fires — but a CONNECTION-level failure (TCP reset, TLS
# abort, DNS blip) leaves stdout EMPTY, and `jq -rs` then prints
# nothing and exits 0. Without pipefail that reads as success on
# nothing read: status_lookup_ok=true, the empty id takes the
# writer down the "no status comment yet" branch, and it posts a
# DUPLICATE ⛔ blocked comment beside the stale ✅ one — the exact
# two-status state this function exists to prevent — on a green
# run. `defaults.run.shell: bash` already gives every step in this
# file `-eo pipefail`, so this is redundant today; it is also the
# only guard that survives that default changing or this helper
# being lifted into a step that sets its own options.
for attempt in 1 2 3; do
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')"; then
status_lookup_ok=true
break
fi
# Surface gh's own diagnosis instead of discarding it, exactly as
# read_live_permission does: a rate limit, an expired PAT and a
# 5xx are indistinguishable from 'attempt 3/3' alone.
echo "::warning::Takeover status lookup failed for #${FORCED_PR} (attempt ${attempt}/3): $(tr '\n' ' ' < "${err}")" >&2
[[ "${attempt}" -lt 3 ]] && sleep "${attempt}"
done
rm -f "${err}"
if [[ "${status_lookup_ok}" != 'true' ]]; then
echo "::warning::Failed to read takeover status comments for #${FORCED_PR}" >&2
return 1
fi
status_id="$(tail -1 <<< "${status_ids}")"

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.

[Suggestion] Test-oracle gap: the newest-status-comment selection tail -1 is pinned with a single-id oracle — the fake comments endpoint returns exactly 123 in every reporter scenario (or empty under NO_STATUS_MARKER), where tail -1 and head -1 are identical. Probed divergence: on payload 100\n123, tail -1 selects 123 (newest) while head -1 selects 100 (oldest). — Failure scenario: a PR can accumulate multiple <!-- autofix-status --> comments — the code itself creates that state when a PATCH attempt fails and the gh pr comment fallback later succeeds. A head -1 mutant PATCHes the OLDEST marker comment while the newest, most visible status stays stale — and the suite cannot tell them apart because its oracle has one line.

Suggested fix: have the fake comments endpoint print 100\n123 in a reporter scenario and assert the recorded PATCH hits repos/QwenLM/qwen-code/issues/comments/123.

中文说明

[Suggestion] 测试预言机缺口:最新评论选择 tail -1 被一个单 id 预言机钉住——每个报告函数场景里假评论端点都只返回 123NO_STATUS_MARKER 时为空),此时 tail -1head -1 完全等价。实测分歧:载荷为 100\n123 时,tail -1 选 123(最新),head -1 选 100(最旧)。— 失败场景:一个 PR 可以累积多条 <!-- autofix-status --> 评论——当 PATCH 尝试失败、随后 gh pr comment 兜底成功时,代码自己就会造成这种状态。head -1 变异体会去 PATCH 最旧的标记评论,而最新、最显眼的状态仍然过期——套件无法区分两者,因为预言机只有一行。

建议修复:让某个报告场景的假评论端点输出 100\n123,并断言记录的 PATCH 命中 repos/QwenLM/qwen-code/issues/comments/123

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

if [[ -n "${status_id}" ]]; then
Comment on lines +2079 to +2080

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.

[Suggestion] R1-2: The blocked-state PATCH targets the single <!-- autofix-status --> marker comment without participating in the address job's lifecycle coordination (post_status captures a comment_id at post time; finalize PATCHes that id under if: always()). Still stands at this commit (re-checked). — Failure scenario: a takeover-labeled fork PR is mid-round in review-address; allow-edits is toggled off and a maintainer force-dispatches → this reporter PATCHes the shared marker to "⛔ blocked … no work was started". When the in-flight round ends, finalize PATCHes the same comment back to the round's terminal status — the block becomes invisible; if the round is cancelled before finalize, the false "no work was started" text persists until the next round.

Suggested fix: scope the claim or skip the write when work is in flight — e.g. soften the body to "this run started no work", or skip the PATCH when an address run is live for this PR (the scan step already queries check/run state elsewhere for BUSY_PRS).

中文说明

blocked 状态的 PATCH 直接写入唯一的 <!-- autofix-status --> 标记评论,没有参与 address job 的生命周期协调(post_status 在发布时记录 comment_id,finalize 在 if: always() 下 PATCH 该 id)(本轮复查仍成立)。— 失败场景:某个带 takeover 标签的 fork PR 正在 review-address 轮次中运行;此时 allow-edits 被关闭,maintainer 强制触发扫描 → 本 reporter 把共享标记评论 PATCH 成 "⛔ blocked … no work was started"。在途轮次结束时,finalize 会把同一条评论 PATCH 回该轮次的终态——阻塞状态从此不可见;若该轮次在 finalize 之前被取消,虚假的 "no work was started" 文案会一直保留到下一轮。

建议修复:限定文案范围或在有轮次在跑时跳过写入——例如把文案软化为 "this run started no work",或在该 PR 存在进行中的 address 运行时跳过 PATCH(scan 步骤已在别处为 BUSY_PRS 查询 check/run 状态)。

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

for attempt in 1 2 3; do
Comment on lines +2079 to +2081

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.

[Critical] R1-2: Still stands — escalated from Suggestion on this round's verification. The blocked-status reporter PATCHes the newest shared <!-- autofix-status --> marker comment with no coordination with the review-address job's lifecycle. Reachability verified at this commit: the route job dispatches review-scan on review/label/dispatch events without consulting address-run state; review-scan has no concurrency group; BUSY_PRS is computed below the forced block (~line 2027) and consulted only at target emission (~line 2091) — nothing gates this path. — Failure scenario: a takeover-labeled fork PR has an address round in flight (its announcement already PATCHed this same newest marker comment) when a forced scan blocks on a reportable reason (permission_lookup_failed during a permission-API outage — exactly what the new retries exist to absorb — maintainer_edits_disabled via workflow_dispatch, or author_permission_* after mid-round de-privileging) → this PATCH turns the live "AutoFix is working on this PR" announcement into "⛔ AutoFix blocked … no work was started" for up to review-address's 300-minute timeout; in the opposite ordering, finalize's id-based PATCH silently erases the blocked status. Both directions show a user-visible false status on the PR thread. The file already treats this hazard class as gate-worthy: the finalize step's comment says it must not overwrite another round's status comment, but this new writer is ungated.

Suggested fix: gate the blocked-status write on round state — skip the PATCH when the matched comment body is the in-flight round's live announcement (post a separate comment instead), or hoist the BUSY_PRS computation above the forced block; alternatively give the blocked status its own marker (e.g. <!-- autofix-blocked -->) so the two channels cannot overwrite each other.

中文说明

[Critical] R1-2:仍然存在——经本轮验证,由 Suggestion 升级为 Critical。blocked 状态报告函数会 PATCH 最新的共享 <!-- autofix-status --> 标记评论,且未与 review-address job 的生命周期做任何协调。本提交上已验证可达性:route job 在 review/label/dispatch 事件下分发 review-scan 时不查询 address 运行状态;review-scan 没有 concurrency group;BUSY_PRS 在强制准入块之后(约 2027 行)才计算,且只在输出 targets 时(约 2091 行)才被查询——这条路径没有任何门禁。— 失败场景:某个带 takeover 标签的 fork PR 正有一轮 address 在运行(其公告已 PATCH 了同一条最新标记评论),此时强制扫描因可报告原因被阻塞(权限 API 故障期间的 permission_lookup_failed——正是新增重试要吸收的场景、workflow_dispatch 触发的 maintainer_edits_disabled、或轮次中途作者被降权后的 author_permission_*)→ 这个 PATCH 会把"AutoFix 正在处理此 PR"的实时公告改成"⛔ AutoFix blocked … 未开始处理",最长可持续 review-address 的 300 分钟超时;反过来的顺序下,finalize 基于 id 的 PATCH 又会悄悄抹掉 blocked 状态。两个方向都会在 PR 时间线上向用户展示与事实相反的状态。本文件已将这类危害视为需要设防:finalize 步骤的注释明确说不得覆盖其他轮次的状态评论,但这个新写入器没有门禁。

建议修复:按轮次状态为 blocked 写入设门——当匹配到的评论正文是进行中轮次的实时公告时跳过 PATCH(改为另发一条评论),或把 BUSY_PRS 的计算上移到强制块之前;也可以给 blocked 状态单独的标记(如 <!-- autofix-blocked -->),让两个通道互不覆盖。

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复:forced review-scan 现在与同一 PR 的 review-address 共享 qwen-pr-head-write-<PR> 并发锁,blocked reporter 与 address status/finalize 不再并发覆盖同一状态评论;定时扫描使用 run ID 保持独立。

验证证据:聚焦并发契约回归 1/1 通过;完整 scripts/tests/qwen-autofix-workflow.test.js 111/111 通过;Prettier 与 git diff --check 通过;commit 7170261765

if gh api --method PATCH "repos/${REPO}/issues/comments/${status_id}" -f body="${body}" > /dev/null; then
return 0
fi
echo "::warning::Failed to update blocked takeover status for #${FORCED_PR} (attempt ${attempt}/3)" >&2
[[ "${attempt}" -lt 3 ]] && sleep "${attempt}"
done
else
for attempt in 1 2 3; do
if gh pr comment "${FORCED_PR}" --repo "${REPO}" --body "${body}" > /dev/null; then
return 0
Comment on lines +2089 to +2091

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.

[Suggestion] report_forced_takeover_blocked re-implements the marker-based status-comment upsert that already exists in this same workflow's 'Post autofix status comment' step (~lines 3458-3495) — find the bot's newest <!-- autofix-status --> comment, PATCH it, else create — and the two copies already diverge: selection (slurped [ .[][] ... ] | last vs tail -1 over per-page --jq output — same result today, different mechanism), create path (gh api ... -f body --jq '.id' capturing the id for finalize vs gh pr comment with no id), retry policy (one-shot vs 3x), and marker handling (MARKER variable vs the literal hardcoded twice). — Failure scenario: both implementations manage the SAME per-PR status comment (same marker, same bot author). Any future change to the marker, the newest-comment selection rule, or the create path must now be applied in two places; applied to one, the writers stop agreeing on which comment is THE status comment and rounds start stacking duplicate status comments (the exact failure the existing step's comment warns about) or orphan the blocked comment. The duplication is structurally induced (review-scan is deliberately checkout-free/API-only), which limits the fix options but not the hazard.

Suggested fix: cheapest consistent option — make the scan-side copy semantically identical to the announcement step (same MARKER variable, same selection semantics, same gh api ... -f body create path). Durable option — extract the upsert into a .github/scripts/ helper used by both jobs; that requires giving review-scan a trusted-base checkout, so state that tradeoff explicitly if rejected.

中文说明

[Suggestion] report_forced_takeover_blocked 重新实现了本工作流 'Post autofix status comment' 步骤(约 3458-3495 行)已有的基于标记的状态评论 upsert——找到 bot 最新的 <!-- autofix-status --> 评论并 PATCH,否则新建——而且两份实现已经开始分叉:选择逻辑(slurp 后 [ .[][] ... ] | last vs 对每页 --jq 输出取 tail -1——今天结果相同,机制不同)、创建路径(gh api ... -f body --jq '.id' 捕获 id 供 finalize 使用 vs gh pr comment 不捕获 id)、重试策略(一次性 vs 3 次)、标记处理(MARKER 变量 vs 标记字面量硬编码两次)。— 失败场景:两份实现管理的是同一条每 PR 状态评论(同一标记、同一 bot 作者)。未来任何对标记、最新评论选择规则或创建路径的修改都必须同时落在两处;只改一处,两个写入器就会对"哪条评论才是状态评论"产生分歧,轮次开始堆叠重复的状态评论(正是现有步骤注释所警告的失败)或让 blocked 评论变成孤儿。这种重复是结构性的(review-scan 刻意不 checkout、纯 API),限制了修复选项,但不消除危害。

建议修复:最便宜的一致性选项——让扫描侧副本与公告步骤语义完全一致(同样的 MARKER 变量、同样的选择语义、同样的 gh api ... -f body 创建路径)。持久选项——把 upsert 抽成 .github/scripts/ 辅助脚本供两个 job 使用;这需要给 review-scan 一个可信 base 的 checkout,如拒绝请明确说明该取舍。

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

fi
echo "::warning::Failed to post blocked takeover status for #${FORCED_PR} (attempt ${attempt}/3)" >&2
[[ "${attempt}" -lt 3 ]] && sleep "${attempt}"
done
fi
return 1
}
Comment on lines +2096 to +2098

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.

[Suggestion] R3-1: Still stands — the reporter's terminal WRITE-failure fall-through (this return 1 after both the PATCH loop and the create-comment loop exhaust all 3 attempts) has no behavioral test: the suite covers lookup failure (FAIL_STATUS_LOOKUP → status 1) and transient one-failure retries (FAIL_PATCH_ONCE/FAIL_COMMENT_ONCE → status 0 after retry), but no scenario fails all three write attempts. Additionally, the workflow-level chain (reporter returns 1 → branch exit 1) is pinned only by a job-wide toContain('exit 1'); a mutant flipping that branch to exit 0 keeps 111/111 green (executed). — Failure scenario: a regression making the reporter return 0 on terminal write failure — or the workflow branch exit 0 when the reporter fails — ships green: forced PR hits a terminal blocker, all three status writes fail, the run renders green, and the PR is left with neither work nor a blocked notification — the exact silent failure this PR exists to remove. Suggested fix: add a reporter scenario failing all 3 PATCH (and create) attempts → expect status 1; and anchor the workflow exit by asserting the block spanning if ! report_forced_takeover_blocked … through its fi contains exit 1.

中文说明

仍然存在——报告函数的终态写入失败 fall-through(PATCH 循环和新建评论循环都耗尽 3 次后的这个 return 1)没有行为测试:套件覆盖了查找失败(FAIL_STATUS_LOOKUP → status 1)和临时性单次失败重试(FAIL_PATCH_ONCE/FAIL_COMMENT_ONCE → 重试后 status 0),但没有任何场景让三次写入全部失败。此外,工作流层面的链条(报告函数返回 1 → 分支 exit 1)只被整个 job 范围的 toContain('exit 1') 钉住;把该分支翻转为 exit 0 的突变体依然 111/111 全绿(已执行验证)。— 失败场景:让报告函数在终态写入失败时返回 0 的回归——或让报告函数失败时工作流分支 exit 0 的回归——会在绿灯下上线:强制 PR 命中终态阻塞、三次状态写入全部失败、运行显示绿色,PR 上既没有处理也没有阻塞通知——正是本 PR 要消除的那种静默失败。建议修复:新增一个让 PATCH(和新建)三次全部失败的报告函数场景 → 断言 status 1;并断言从 if ! report_forced_takeover_blocked … 到其 fi 的代码块包含 exit 1,把工作流退出钉牢。

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


# Candidate PRs: open, same-repo, targeting main, and either
# authored by the dev-bot or opted in via TAKEOVER_LABEL. A PR
# carrying SKIP_LABEL is excluded everywhere — skip wins over
Expand All @@ -1916,8 +2106,12 @@ jobs:
# jq's // treats false as empty, so that form is false for EVERY
# input and silently green-no-op'd all forced dispatches.
if [[ -n "${FORCED_PR}" ]]; then
META="$(gh pr view "${FORCED_PR}" --repo "${REPO}" \
--json number,state,author,headRefName,isCrossRepository,baseRefName,labels,maintainerCanModify 2> /dev/null || echo '{}')"
if ! META="$(read_forced_pr_meta)"; then
echo "::error::Forced PR #${FORCED_PR} admission blocked: metadata_fetch_failed"
Comment on lines +2109 to +2110

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.

[Suggestion] read_forced_pr_meta classifies definitive terminal states as transient metadata_fetch_failed — the sibling, in the metadata reader, of the read_live_permission issue already on this PR (open round-4 comment at line 1749, maintainer review suggestion (3)). Probe-verified: gh pr view 999999999 exits 1 with the definitive GraphQL: Could not resolve to a PullRequest error; the verbatim reader retries it 3× (with sleeps) and returns 1, so a typo'd workflow_dispatch number produces ::error::...metadata_fetch_failed and a red run that re-dispatch can never cure. A deleted/ghost PR author (author: null fails the jq -e shape gate) dies the same death. Pre-diff, both inputs exited 0 with a clean rejection (|| echo '{}' → aggregate rejection → exit 0). Admission stays fail-closed; the defect is the failure-mode regression — permanent red runs and a transient-flavored reason string on conditions that never change. — Failure scenario: a maintainer force-dispatches with a typo'd PR number → 3 retries on a definitive answer → red workflow run implying a transient lookup problem; every retry of the dispatch repeats the red run, where pre-diff the same input exited 0 with an informative rejection.

Suggested fix: mirror the terminal/transient split proposed for read_live_permission — treat a definitive gh pr view failure (nonexistent PR) and a valid-but-unexpected shape (e.g. author: null) as terminal (clean exit 0 with a terminal reason such as pr_not_found / author_missing, which the reporter's case filter keeps silent), and reserve retry-then-exit 1 for genuinely transient transport errors.

中文说明

read_forced_pr_meta 把确定性的终态当成临时性的 metadata_fetch_failed 处理——与本 PR 上已在讨论的 read_live_permission 问题(1749 行的 round-4 开放评论、维护者评审建议 (3))同族,发生在元数据读取器上。探针验证:gh pr view 999999999 以确定性错误 GraphQL: Could not resolve to a PullRequest 退出 1;逐字提取的读取函数会重试 3 次(含 sleep)后返回 1,因此 workflow_dispatch 输错 PR 号会得到 ::error::...metadata_fetch_failed 和一次重新分发也无法修复的红色运行。PR 作者账号已注销(author: null 过不了 jq -e 结构校验)也是同样的下场。改动前这两种输入都会 exit 0 并给出干净的拒绝(|| echo '{}' → 笼统拒绝 → exit 0)。准入本身仍是 fail-closed;缺陷是失败模式的回归——对永不改变的条件产生永久红色运行和听起来像临时故障的原因码。— 失败场景:维护者用错误的 PR 号手动触发 → 对确定性应答重试 3 次 → 红色运行暗示临时查询故障;每次重试分发都重复红色运行,而改动前同样的输入会 exit 0 并输出有意义的拒绝信息。

建议修复:与 read_live_permission 建议的终态/临时态拆分一致——把 gh pr view 的确定性失败(PR 不存在)和合法但意外的结构(如 author: null)作为终态处理(以 pr_not_found / author_missing 等终态原因干净地 exit 0,报告函数的 case 过滤器对其保持静默),仅对真正的临时传输错误保留重试后 exit 1。

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

echo "targets=[]" >> "${GITHUB_OUTPUT}"
echo "has_targets=false" >> "${GITHUB_OUTPUT}"
exit 1
fi
# Same admission as the scheduled scan below. In-repo PRs fail
# CLOSED on a missing isCrossRepository field (`.isCrossRepository
# == false`, never a `// true | not` default — jq's // treats false
Expand All @@ -1927,34 +2121,40 @@ jobs:
# gate runs in the shell case just below, mirroring the scan's
# per-candidate permission call) so the real-time route's fork
# pickup is not silently discarded here.
OK="$(jq -r --arg ab "${AUTOFIX_BOT}" --arg take "${TAKEOVER_LABEL}" --arg skip "${SKIP_LABEL}" \
'(((.state // "") == "OPEN")
and (((.author.login // "") == $ab) or ([.labels[]?.name] | index($take) != null))
and ([.labels[]?.name] | index($skip) | not)
and ((.baseRefName // "") == "main")
and (if (.isCrossRepository == true)
then (.maintainerCanModify == true)
else (.isCrossRepository == false)
end))' <<< "${META}")"
ADMISSION_REASON="$(forced_admission_reason <<< "${META}")"
# Fork only: the author must hold write+ RIGHT NOW (the same
# live-privilege rule the scan applies per candidate and
# review-address re-checks before pushing). In-repo PRs are gated
# by author/label alone.
if [[ "${OK}" == 'true' && "$(jq -r '.isCrossRepository == true' <<< "${META}")" == 'true' ]]; then
if [[ "${ADMISSION_REASON}" == 'eligible' && "$(jq -r '.isCrossRepository == true' <<< "${META}")" == 'true' ]]; then

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.

[Suggestion] Test gap (pattern, instance 4/4): the isCrossRepository == true conjunct — the only thing exempting in-repo PRs from the live permission check — is a changed line with no behavioral test; a mutant deleting it survives the full suite (verified at this commit). — Failure scenario: the mutant permission-checks in-repo forced PRs, which the scheduled scan never does (its in-repo loop has no permission call): an in-repo takeover-labeled PR whose author holds only read/triage would be admitted by the scan but rejected by the mutant's forced path as author_permission_read — a terminal reason — so the reporter would post a factually wrong "A later scheduled scan will retry" comment while the real-time review event is silently degraded to throttled-schedule pickup.

Suggested fix: extend the existing forced-target test to replay this gate — assert that an eligible in-repo meta never reaches the permission call and an eligible cross-repo meta does (a small bash replay with a stubbed read_live_permission that records invocation, in the style of the suite's existing extractions).

中文说明

测试缺口(模式,第 4/4 处):isCrossRepository == true 合取项——唯一使仓库内 PR 免于实时权限检查的条件——是被修改的行却没有行为测试;删除该合取项的变异体在整个套件中存活(已在本提交验证)。— 失败场景:变异体会对仓库内强制 PR 做权限检查,而定时扫描从不会这样做(其仓库内循环没有权限调用):一个作者只有 read/triage 权限、带 takeover 标签的仓库内 PR,扫描会准入,而变异体的强制路径会以 author_permission_read(终态原因)拒绝——于是 reporter 会发出与事实不符的 "A later scheduled scan will retry" 评论,同时实时评审事件被悄悄降级为限流的定时拾取。

建议修复:扩展现有 forced-target 测试以重放该门——断言合法的仓库内 meta 永远不会到达权限调用、而合法的跨仓库 meta 会到达(用记录调用的桩 read_live_permission 做一段小型 bash 重放,沿用套件现有的函数提取风格)。

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

FORK_AUTHOR="$(jq -r '.author.login // ""' <<< "${META}")"
FPERM="$(gh api "repos/${REPO}/collaborators/${FORK_AUTHOR}/permission" --jq '.permission // ""' 2> /dev/null || echo '')"
if ! FPERM="$(read_live_permission "${FORK_AUTHOR}")"; then
ADMISSION_REASON='permission_lookup_failed'
report_forced_takeover_blocked "${ADMISSION_REASON}" \
Comment on lines +2132 to +2133

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.

[Suggestion] R1-9: The exit-code contract for terminal blockers is asymmetric and undocumented in the code. Still stands at this commit (re-checked): metadata_fetch_failed exits 1 without invoking the reporter; permission_lookup_failed (here) exits 1 even when the blocked status was posted successfully; deterministic rejections exit 0 after reporting. — Failure scenario: with no comment pinning the intent, a future maintainer "normalizing" the exit codes can break the fleet loop's expectation (one candidate's lookup failure records a blocked row and moves on; a red run means visibility was lost) — nothing in the file warns them.

Suggested fix: document the contract where the paths diverge — lookup failures fail loud (exit 1) because visibility could not be guaranteed, while deterministic rejections exit 0 once the blocked status is posted or deliberately skipped.

中文说明

终态阻塞的退出码契约不对称且代码中无文档(本轮复查仍成立):metadata_fetch_failed 不调用 reporter 直接 exit 1;permission_lookup_failed(此处)即使 blocked 状态已成功发布也 exit 1;确定性拒绝在上报后 exit 0。— 失败场景:由于没有注释钉住该意图,未来某位 maintainer “归一化”退出码时可能破坏舰队循环的预期(单个候选的查询失败记录一条 blocked 行后继续;红色运行代表可见性丢失)——文件里没有任何警告。

建议修复:在路径分叉处补充注释说明契约——查询失败因无法保证可见性而显式失败(exit 1),确定性拒绝在 blocked 状态成功发布或有意跳过时 exit 0。

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

|| echo "::error::Forced PR #${FORCED_PR} blocked status update failed"
Comment on lines +2132 to +2134

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.

[Suggestion] The exit-code contract for terminal blockers is asymmetric and undocumented: metadata_fetch_failed exits 1 unconditionally without invoking the reporter (~line 1818); permission_lookup_failed (here) exits 1 even when the blocked comment posts fine; deterministic rejections exit 1 only if the reporter fails (~line 1859); and the scan path adds a fourth behavior (fleet_row blocked, job green). — Failure scenario: oncall paged on a red run cannot map red run → action without reverse-engineering two call sites 20 lines apart; the identical PR is green or red depending on whether the comment API happened to be healthy; a future maintainer "normalizing" either path silently changes the alert semantics. If the split is intentional (unknown outcome = loud; deterministic rejection = quiet once visible), it needs saying.

Suggested fix: add a one-line comment at each exit 1 stating why that class fails the run, or unify the exit codes across blocker classes.

中文说明

终态阻塞的退出码契约不对称且无文档:metadata_fetch_failed 无条件 exit 1 且不调用 reporter(约 1818 行);permission_lookup_failed(此处)即使 blocked 评论成功发出也 exit 1;确定性拒绝仅在 reporter 失败时 exit 1(约 1859 行);扫描路径还有第四种行为(fleet_row blocked,job 保持绿色)。— 失败场景:oncall 因红色运行被告警时,必须逆向相距 20 行的两个调用点才能弄清红色运行 → 该如何处理;同一个 PR 会因评论 API 恰好是否健康而时绿时红;未来维护者 "统一" 任一路径都会悄悄改变告警语义。如果这种区分是有意为之(未知结果 = 高声失败;确定性拒绝 = 可见后即安静),需要注释说明。

建议修复:在每个 exit 1 处加一行注释说明该类为何使运行失败,或统一各阻塞类别的退出码。

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

echo "::error::Forced PR #${FORCED_PR} admission blocked: ${ADMISSION_REASON}"
Comment on lines +2133 to +2135

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.

[Suggestion] R1-9: Still stands — the exit-code contract for terminal blockers is asymmetric and undocumented in the code. metadata_fetch_failed exits 1 without invoking the reporter; permission_lookup_failed (here) exits 1 even when the blocked comment posts successfully; author_permission_* / maintainer_edits_disabled exit 0 when the comment posts and 1 when it fails. The asymmetry may well be intentional (transient-infrastructure failure → red, definitive policy rejection → green once reported), but no comment in the forced block states it. — Failure scenario: a future maintainer touching the forced path (or the scheduled loop's treatment of exit codes) has no in-code statement of which exits are deliberate: 'fixing' the asymmetry either way silently changes fleet-scan and route behaviour, and a reviewer cannot tell a deliberate contract from a bug — for exactly the exit semantics this PR's fail-loud design rests on.

Suggested fix: add a short comment above the forced-admission block stating the contract: lookup failures exit 1 unconditionally (transient, must alert); deterministic rejections exit 0 once the blocked status is posted (terminal, reported); report failure exits 1 (the blocker could not be made visible).

中文说明

[Suggestion] R1-9:仍然存在——终态阻塞的退出码契约不对称,且代码中没有任何文档。metadata_fetch_failed 不调用报告函数直接 exit 1;permission_lookup_failed(此处)即使 blocked 评论成功发出也 exit 1;author_permission_* / maintainer_edits_disabled 在评论发出后 exit 0,发不出则 exit 1。这种不对称很可能有意为之(瞬时基础设施失败 → 红色;确定性策略拒绝 → 报告后绿色),但强制块里没有任何注释说明。— 失败场景:未来改动强制路径(或定时循环对退出码的处理)的维护者,看不到任何关于哪些退出是有意的说明:无论朝哪个方向"修复"这个不对称,都会悄悄改变舰队扫描与 route 的行为,而审查者无法区分有意的契约和 bug——而这恰是本 PR fail-loud 设计所依赖的退出语义。

建议修复:在强制准入块上方加一条简短注释说明契约:查询失败无条件 exit 1(瞬时错误,必须告警);确定性拒绝在 blocked 状态发出后 exit 0(终态,已报告);报告失败 exit 1(阻塞原因无法被公开)。

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

echo "targets=[]" >> "${GITHUB_OUTPUT}"
echo "has_targets=false" >> "${GITHUB_OUTPUT}"
exit 1
fi
case "${FPERM}" in
admin|maintain|write)
echo "🌿 forced fork PR #${FORCED_PR} admitted (author ${FORK_AUTHOR}=${FPERM})"
;;
*)
echo "🧭 forced fork PR #${FORCED_PR} rejected: author ${FORK_AUTHOR} permission='${FPERM:-none}' below write"
OK='false'
ADMISSION_REASON="author_permission_${FPERM:-none}"
echo "🧭 forced fork PR #${FORCED_PR} rejected: ${ADMISSION_REASON}"
;;
esac
fi
if [[ "${OK}" != "true" ]]; then
echo "❌ #${FORCED_PR} is not an open main-targeting PR owned by ${AUTOFIX_BOT} or labeled ${TAKEOVER_LABEL} (or it carries ${SKIP_LABEL}); a fork PR additionally needs maintainer edits allowed and a live write+ author"
if [[ "${ADMISSION_REASON}" != 'eligible' ]]; then
if ! report_forced_takeover_blocked "${ADMISSION_REASON}"; then
echo "::error::Forced PR #${FORCED_PR} blocked status update failed"
Comment on lines +2151 to +2152

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.

[Suggestion] On the report-failure exit, the admission reason is never logged for maintainer_edits_disabled: permission_lookup_failed is echoed after its report regardless of outcome (~line 1866), author_permission_* is echoed before this branch (~line 1878), but maintainer_edits_disabled reaches this if ! report_forced_takeover_blocked branch without ever having been echoed, and the branch exits 1 BEFORE the rejected: ${ADMISSION_REASON} line — unreachable on failure. — Failure scenario: comment API outage (three failed PATCH/post attempts) during a forced dispatch of a fork PR whose author disabled maintainer edits: the reason exists only in the comment body that failed to post; the log contains in-function retry warnings and then only ::error::Forced PR #N blocked status update failed, exiting 1 — the operator cannot tell from the run which terminal blocker fired, for exactly the blockers this PR exists to surface.

Suggested fix: emit the reason in the failure branch too, e.g. echo "❌ Forced PR #${FORCED_PR} rejected: ${ADMISSION_REASON}" before the report call (or inside its failure branch).

中文说明

[Suggestion] 在报告失败的退出路径上,maintainer_edits_disabled 的准入原因从未被记录到日志:permission_lookup_failed 无论报告结果如何都会在报告后回显(约 1866 行),author_permission_* 在进入本分支前已被回显(约 1878 行),但 maintainer_edits_disabled 到达这个 if ! report_forced_takeover_blocked 分支时从未被回显,而该分支在 rejected: ${ADMISSION_REASON} 那一行之前就 exit 1——失败时根本走不到那行。— 失败场景:对一个作者已关闭 maintainer edits 的 fork PR 强制分发时评论 API 故障(PATCH/发送三次全失败):原因只存在于那条没能发出的评论正文里;日志只有函数内的重试 warning,然后是一句 ::error::Forced PR #N blocked status update failed,exit 1——运维无法从运行日志判断是哪个终态阻塞触发的,而这恰是本 PR 要暴露的那类阻塞。

建议修复:失败分支也输出原因,例如在报告调用之前(或其失败分支内)加 echo "❌ Forced PR #${FORCED_PR} rejected: ${ADMISSION_REASON}"

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

echo "targets=[]" >> "${GITHUB_OUTPUT}"
echo "has_targets=false" >> "${GITHUB_OUTPUT}"
exit 1
fi
echo "❌ Forced PR #${FORCED_PR} rejected: ${ADMISSION_REASON}"
echo "targets=[]" >> "${GITHUB_OUTPUT}"
echo "has_targets=false" >> "${GITHUB_OUTPUT}"
exit 0
Expand Down Expand Up @@ -2000,14 +2200,19 @@ jobs:
# candidates alone exhaust the inspection budget.
while IFS=$'\t' read -r FPR FAUTHOR; do
[[ -z "${FPR}" ]] && continue
FPERM="$(gh api "repos/${REPO}/collaborators/${FAUTHOR}/permission" --jq '.permission // ""' 2> /dev/null || echo '')"
if ! FPERM="$(read_live_permission "${FAUTHOR}")"; then
echo "::warning::Fork takeover candidate #${FPR} blocked: permission_lookup_failed"
fleet_row "${FPR}" 'blocked' 'permission_lookup_failed'
Comment on lines +2203 to +2205

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.

[Suggestion] Test gap (pattern, instance 3/4): this new scan-loop blocked branch has no test — deleting the entire five-line block keeps all 110 tests green (the generic toContain('permission_lookup_failed') assertion is satisfied by the forced-path and reporter occurrences; no fleet-row assertion covers the 'blocked' state). — Failure scenario: if the branch regresses, a transient permission-lookup failure on the scan path silently falls back to the old below-write skip, making a retryable infra failure indistinguishable in the fleet table from genuine permission revocation — losing exactly the blocked-vs-rejected distinction this PR introduces.

Suggested fix: add a static pin next to the existing fleet-row assertions (e.g. expect(reviewScanJob).toContain("fleet_row \"${FPR}\" 'blocked' 'permission_lookup_failed'")), or exercise the candidate loop with a failing fake gh.

中文说明

测试缺口(模式,第 3/4 处):这个新的扫描循环 blocked 分支没有测试——删除整个五行代码块后全部 110 个测试仍全绿(通用的 toContain('permission_lookup_failed') 断言已被强制路径和 reporter 中的出现满足;没有任何 fleet-row 断言覆盖 'blocked' 状态)。— 失败场景:若该分支回归,扫描路径上的临时权限查询失败会悄悄退回旧的 below-write 跳过逻辑,使可重试的基础设施故障在 fleet 表中与真实的权限回收无法区分——恰好丢失本 PR 引入的 blocked 与 rejected 的区分。

建议修复:在现有 fleet-row 断言旁增加静态钉住(例如 expect(reviewScanJob).toContain("fleet_row \"${FPR}\" 'blocked' 'permission_lookup_failed'")),或用失败的 fake gh 实际执行候选循环。

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

Comment on lines +2203 to +2205

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.

[Suggestion] For author_permission_triage/author_permission_read blockers, the blocked comment promises "A later scheduled scan will retry", but those retries are permanently invisible (pattern, instance 2/2). The scheduled scan's below-write branch — the unchanged *) case directly under this new blocked branch — emits a plain echo with no fleet row, while this new branch adds one for the transient case: the diff itself creates the asymmetry inside one loop. — Failure scenario: a takeover-labeled fork PR whose author held write+ is engaged; the author is later demoted to read/triage (offboarding, role change). A review event hits the forced path → author_permission_read → blocked comment posted, exit 0. Every subsequent scan passes the pre-filter, gets read, and logs "skipped … below write" with no fleet row: no fleet table ever shows the PR or why the retry keeps failing, and the blocked comment is never updated until a new forced event.

Suggested fix: mirror the lookup-failure branch in the below-write *) case: fleet_row "${FPR}" 'blocked' "author_permission_${FPERM}" so the promised retry leaves a trail (and/or qualify the comment's retry wording for permission reasons).

中文说明

author_permission_triage/author_permission_read 阻塞,blocked 评论承诺 "A later scheduled scan will retry",但这些重试是永久不可见的(模式,第 2/2 处)。定时扫描的 below-write 分支——就在这个新 blocked 分支下方、未修改的 *) 分支——只输出一条普通 echo,没有 fleet row,而新分支却为临时失败情形添加了 fleet row:diff 本身在同一个循环里制造了不对称。— 失败场景:一个作者原本有 write+ 权限的带 takeover 标签 fork PR 被接管;作者后来被降为 read/triage(离职、角色调整)。评审事件触发强制路径 → author_permission_read → 发出 blocked 评论,exit 0。此后每次扫描都通过预过滤、得到 read、记录 "skipped … below write" 且没有 fleet row:任何 fleet 表都不会显示该 PR 或重试为何一直失败,blocked 评论在下一次强制事件前永远不会更新。

建议修复:在 below-write 的 *) 分支中镜像查询失败分支:fleet_row "${FPR}" 'blocked' "author_permission_${FPERM}",让承诺的重试留下痕迹(并且/或者对权限类原因限定评论中的重试措辞)。

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复 + 验证证据:定时扫描在 fork 作者权限低于 write 时现在写入 blocked / author_permission_* fleet row,并新增回归断言。聚焦用例 1/1 通过;完整 workflow 测试 110/110 断言通过;Prettier 与 git diff --check 通过。

continue
fi
case "${FPERM}" in
admin|maintain|write)
echo "🌿 fork takeover candidate #${FPR} admitted (author ${FAUTHOR}=${FPERM})"
CANDIDATES="${CANDIDATES} ${FPR}"
;;
*)
echo "🧭 fork takeover candidate #${FPR} skipped: author ${FAUTHOR} permission='${FPERM:-none}' below write"
fleet_row "${FPR}" 'blocked' "author_permission_${FPERM:-none}"
;;
esac
done < <(jq -rs --arg skip "${SKIP_LABEL}" '
Expand Down
Loading