Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
174 changes: 156 additions & 18 deletions .github/workflows/qwen-autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1688,6 +1688,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 @@ -1714,6 +1719,124 @@ 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
}

read_live_permission() {
local login="$1" attempt permission
for attempt in 1 2 3; do
if permission="$(gh api "repos/${REPO}/collaborators/${login}/permission" --jq '.permission // ""' 2> /dev/null)" \
&& [[ "${permission}" =~ ^(admin|maintain|write|triage|read)$ ]]; then
printf '%s' "${permission}"
return 0
fi
echo "::warning::Permission lookup failed for ${login} (attempt ${attempt}/3)" >&2
[[ "${attempt}" -lt 3 ]] && sleep "${attempt}"
done
return 1
}

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 @@ -1723,8 +1846,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 @@ -1734,34 +1861,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 @@ -1807,14 +1940,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