Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
200 changes: 182 additions & 18 deletions .github/workflows/qwen-autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1691,6 +1691,11 @@ 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.
concurrency:
group: 'qwen-pr-head-write-${{ needs.route.outputs.pr_number || github.run_id }}'
cancel-in-progress: false
outputs:
targets: '${{ steps.scan.outputs.targets }}'
has_targets: '${{ steps.scan.outputs.has_targets }}'
Expand All @@ -1717,6 +1722,150 @@ jobs:
}
WORKDIR="$(mktemp -d)"

read_forced_pr_meta() {
local attempt meta
for attempt in 1 2 3; do
if meta="$(gh pr view "${FORCED_PR}" --repo "${REPO}" \
--json number,state,author,headRefName,isCrossRepository,baseRefName,labels,maintainerCanModify 2> /dev/null)" \
&& jq -e 'type == "object"
and (.number | type == "number")
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
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"
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
[[ "${DRY_RUN}" == 'true' ]] && return 0
[[ "$(jq -r --arg ab "${AUTOFIX_BOT}" --arg take "${TAKEOVER_LABEL}" '
((.author.login // "") == $ab) or ([.labels[]?.name] | index($take) != null)
' <<< "${META}")" == 'true' ]] || return 0
case "${reason}" in
permission_lookup_failed|author_permission_*|maintainer_edits_disabled|cross_repo_state_missing) ;;
*) return 0 ;;
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)"
[[ "${attempt}" -lt 3 ]] && sleep "${attempt}"
done
if [[ "${actor}" != "${AUTOFIX_BOT}" ]]; then
echo "::warning::Blocked takeover status skipped: PAT authenticates as '${actor:-unknown}'"
return 1
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 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
body="$(printf '<!-- autofix-status -->\n\n⛔ **AutoFix blocked** — takeover admission stopped at `%s`, so no work was started. [View run](https://github.com/%s/actions/runs/%s). %s\n\n<details>\n<summary>中文说明</summary>\n\n⛔ **AutoFix 已阻塞** —— takeover 准入停在 `%s`,因此本轮未开始处理。[查看运行](https://github.com/%s/actions/runs/%s)。%s\n\n</details>' "${reason}" "${REPO}" "${GITHUB_RUN_ID}" "${next_en}" "${reason}" "${REPO}" "${GITHUB_RUN_ID}" "${next_zh}")"
status_ids=''
status_lookup_ok=false
for attempt in 1 2 3; do
if status_ids="$(gh api "repos/${REPO}/issues/${FORCED_PR}/comments" --paginate \
--jq ".[] | select(.user.login == \"${AUTOFIX_BOT}\") | select(.body | contains(\"<!-- autofix-status -->\")) | .id" 2> /dev/null)"; then
status_lookup_ok=true
break
fi
echo "::warning::Takeover status lookup failed for #${FORCED_PR} (attempt ${attempt}/3)"
[[ "${attempt}" -lt 3 ]] && sleep "${attempt}"
done
if [[ "${status_lookup_ok}" != 'true' ]]; then
echo "::warning::Failed to read takeover status comments for #${FORCED_PR}"
return 1
fi
status_id="$(tail -1 <<< "${status_ids}")"
if [[ -n "${status_id}" ]]; then
for attempt in 1 2 3; do
Comment on lines +2079 to +2081

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)"
[[ "${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
fi
echo "::warning::Failed to post blocked takeover status for #${FORCED_PR} (attempt ${attempt}/3)"
[[ "${attempt}" -lt 3 ]] && sleep "${attempt}"
done
fi
return 1
}

# 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 @@ -1726,8 +1875,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"
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 @@ -1737,34 +1890,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
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}" \
|| echo "::error::Forced PR #${FORCED_PR} blocked status update failed"
echo "::error::Forced PR #${FORCED_PR} admission blocked: ${ADMISSION_REASON}"
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"
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 @@ -1810,14 +1969,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 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
Loading