diff --git a/.github/workflows/qwen-autofix.yml b/.github/workflows/qwen-autofix.yml index ee5d56cde3..ae46b4803a 100644 --- a/.github/workflows/qwen-autofix.yml +++ b/.github/workflows/qwen-autofix.yml @@ -8,14 +8,13 @@ name: 'Qwen Autofix' # The lifecycle is asynchronous — a PR is opened in one run and its review is # addressed in a later run once a reviewer has weighed in — so each scheduled # tick runs only the phase(s) that make sense, decided by the `route` job: -# • every 4h → review phase (sweep the bot's open PRs) -# • every 12h (00/12 UTC) → also the issue phase (locate + fix one new bug) -# • issues:labeled → issue phase when ready label, state, and sender match -# • pull_request_review → review phase (real-time feedback loop for bot PRs) -# • workflow_dispatch → force a phase, an issue, or a PR +# • every 10m → review phase; issue phase only if no PR needs work +# • issues:labeled → issue phase when ready label, state, and sender match +# • pull_request_review → review phase for submitted feedback on bot PRs +# • workflow_dispatch → force a phase, an issue, or a PR # # Every GitHub write (issue/PR comments, labels, branch push, PR create) goes -# through CI_DEV_BOT_PAT so the bot acts as a single identity, qwen-code-dev-bot. +# through CI_DEV_BOT_PAT so the bot acts as the configured autofix identity. # PAT label writes can emit issues:labeled events; the route guards below make # those runs exit unless the label, issue state, and ready label all match. on: @@ -26,8 +25,7 @@ on: types: - 'submitted' schedule: - - cron: '0 0,12 * * *' # Issue + review every 12h; must match route SCHEDULE check - - cron: '0 4,8,16,20 * * *' # Review only between issue runs + - cron: '*/10 * * * *' # Review first; issue fallback only when no PR needs work workflow_dispatch: inputs: phase: @@ -64,7 +62,7 @@ permissions: env: # Identity of the autofix bot. All open in-repo PRs authored by this bot are # eligible for the review phase — not limited to autofix/issue-* branches. - AUTOFIX_BOT: 'qwen-code-dev-bot' + AUTOFIX_BOT: "${{ vars.AUTOFIX_BOT_LOGIN || 'qwen-code-dev-bot' }}" # Branch-name prefix used by the issue phase when creating PRs. Also used # for duplicate-PR detection and issue-number extraction from branch names. BRANCH_PREFIX: 'autofix/issue-' @@ -77,7 +75,9 @@ env: TRUSTED_ASSOC: '["OWNER", "MEMBER", "COLLABORATOR"]' # Hard cap on automated review-address rounds per PR. After this the bot stops # and leaves the PR for a human. - MAX_ROUNDS: '3' + MAX_ROUNDS: '5' + # Do not claim more issues when too many existing autofix PRs are still open. + MAX_OPEN_AUTOFIX_PRS: '5' jobs: # --------------------------------------------------------------------------- @@ -147,8 +147,9 @@ jobs: if [[ "${EVENT_NAME}" == 'schedule' || "${EVENT_NAME}" == 'workflow_dispatch' ]]; then DO_REVIEW=true fi - # Must match the issue-phase cron string on the schedule trigger. - if [[ "${EVENT_NAME}" == 'schedule' && "${SCHEDULE}" == '0 0,12 * * *' ]]; then + # Scheduled runs scan review PRs first; issue-autofix runs only + # when review-scan reports no target. + if [[ "${EVENT_NAME}" == 'schedule' ]]; then DO_ISSUE=true fi # Real-time review triggers: only process in-repo bot PRs with @@ -248,9 +249,13 @@ jobs: # ISSUE PHASE — locate one maintainer-ready issue, fix it, open a PR. # =========================================================================== issue-autofix: - needs: 'route' + needs: ['route', 'review-scan'] if: |- - ${{ needs.route.outputs.do_issue == 'true' }} + ${{ + always() && + needs.route.outputs.do_issue == 'true' && + (github.event_name != 'schedule' || (needs.review-scan.result == 'success' && needs.review-scan.outputs.has_targets != 'true')) + }} runs-on: 'ubuntu-latest' timeout-minutes: 180 concurrency: @@ -371,6 +376,7 @@ jobs: FORCED_ISSUE: '${{ needs.route.outputs.issue_number || github.event.issue.number }}' run: |- mkdir -p "${WORKDIR}" + OPEN_AUTOFIX_PR_COUNT=0 if [[ -n "${FORCED_ISSUE}" ]]; then echo "🎯 Forced issue #${FORCED_ISSUE}" @@ -406,34 +412,51 @@ jobs: fi fi else - echo "🔍 Ready-for-agent issues (newest first)..." - if ! gh issue list --repo "${REPO}" \ + if ! gh pr list --repo "${REPO}" --state open --author "${AUTOFIX_BOT}" \ + --limit 100 --json number,headRefName,isCrossRepository > "${WORKDIR}/open-autofix-prs.json"; then + echo "::warning::Open autofix PR scan failed; proceeding without WIP-cap enforcement." + else + OPEN_AUTOFIX_PR_COUNT="$(jq --arg p "${BRANCH_PREFIX}" \ + '[.[] | select((.isCrossRepository != true) and ((.headRefName // "") | startswith($p)))] | length' \ + "${WORKDIR}/open-autofix-prs.json")" + fi + + if [[ "${OPEN_AUTOFIX_PR_COUNT}" -ge "${MAX_OPEN_AUTOFIX_PRS}" ]]; then + echo "⏭️ ${OPEN_AUTOFIX_PR_COUNT} open autofix PR(s) already exist; WIP limit is ${MAX_OPEN_AUTOFIX_PRS}; skipping issue fallback." + jq -n -c '[]' > "${WORKDIR}/candidates.json" + else + echo "🔍 Ready-for-agent issues (newest first)..." + if ! gh issue list --repo "${REPO}" \ --search "is:open is:issue label:${READY_FOR_AGENT_LABEL} label:${AUTOFIX_APPROVED_LABEL} ${AUTOFIX_ISSUE_EXCLUDES}" \ --limit 30 --json number,title,body,labels,createdAt,url \ > "${WORKDIR}/scan.json"; then - echo "::warning::Ready-for-agent issue scan failed; falling back to an empty candidate list." - jq -n -c '[]' > "${WORKDIR}/candidates.json" - else - if ! jq -c '.[0:10] | map(. + {autofixTier: 1})' \ - "${WORKDIR}/scan.json" > "${WORKDIR}/candidates.json"; then - echo "::warning::Ready-for-agent result processing failed; falling back to an empty candidate list." + echo "::warning::Ready-for-agent issue scan failed; falling back to an empty candidate list." jq -n -c '[]' > "${WORKDIR}/candidates.json" + else + if ! jq -c '.[0:10] | map(. + {autofixTier: 1})' \ + "${WORKDIR}/scan.json" > "${WORKDIR}/candidates.json"; then + echo "::warning::Ready-for-agent result processing failed; falling back to an empty candidate list." + jq -n -c '[]' > "${WORKDIR}/candidates.json" + fi fi fi fi COUNT="$(jq length "${WORKDIR}/candidates.json")" if [[ "${COUNT}" -gt 0 ]]; then - if ! gh pr list --repo "${REPO}" --state open --author "${AUTOFIX_BOT}" \ - --limit 100 --json number,headRefName > "${WORKDIR}/open-autofix-prs.json"; then + if [[ -s "${WORKDIR}/open-autofix-prs.json" ]]; then + echo "ℹ️ Reusing open autofix PR scan for duplicate-PR annotation." + elif ! gh pr list --repo "${REPO}" --state open --author "${AUTOFIX_BOT}" \ + --limit 100 --json number,headRefName,isCrossRepository > "${WORKDIR}/open-autofix-prs.json"; then echo "::warning::Open autofix PR scan failed; candidates will proceed without duplicate-PR annotation." - else + fi + if [[ -s "${WORKDIR}/open-autofix-prs.json" ]]; then if ! jq -c --arg p "${BRANCH_PREFIX}" --slurpfile prs "${WORKDIR}/open-autofix-prs.json" ' ($prs[0] // []) as $prs | map( ($p + (.number | tostring)) as $branch | ( - first($prs[] | select((.headRefName // "") == $branch) | { + first($prs[] | select((.isCrossRepository != true) and ((.headRefName // "") == $branch)) | { number, headRefName }) // null @@ -804,8 +827,8 @@ jobs: if: |- ${{ steps.decision.outputs.go_issue != '' && needs.route.outputs.dry_run != 'true' }} env: - # CI_DEV_BOT_PAT (the qwen-code-dev-bot PAT) opens the PR as - # qwen-code-dev-bot. This is required: the default GITHUB_TOKEN is + # CI_DEV_BOT_PAT opens the PR as the configured autofix bot. This is + # required: the default GITHUB_TOKEN is # blocked from creating PRs ("GitHub Actions is not permitted to # create or approve pull requests"), and PRs it does create do not # trigger CI. The bot PAT clears both problems. @@ -813,7 +836,7 @@ jobs: ISSUE: '${{ steps.decision.outputs.go_issue }}' run: |- if [[ -z "${GITHUB_TOKEN}" ]]; then - echo '::error::CI_DEV_BOT_PAT is required to publish the PR as qwen-code-dev-bot.' + echo '::error::CI_DEV_BOT_PAT is required to publish the PR as the autofix bot.' exit 1 fi api_error_file="$(mktemp)" @@ -901,8 +924,8 @@ jobs: fi # =========================================================================== - # REVIEW PHASE (scan) — find every autofix PR with new, unaddressed feedback - # (or a base conflict) and emit them as a matrix. Cheap: GitHub API only. + # REVIEW PHASE (scan) — find one autofix PR with new, unaddressed feedback + # (or a base conflict) and emit it as a matrix. Cheap: GitHub API only. # =========================================================================== review-scan: needs: 'route' @@ -936,6 +959,7 @@ jobs: and ((.baseRefName // "") == "main"))' <<< "${META}")" if [[ "${OK}" != "true" ]]; then echo "❌ #${FORCED_PR} is not an open in-repo main-targeting PR owned by ${AUTOFIX_BOT}" + echo "targets=[]" >> "${GITHUB_OUTPUT}" echo "has_targets=false" >> "${GITHUB_OUTPUT}" exit 0 fi @@ -943,10 +967,10 @@ jobs: else gh pr list --repo "${REPO}" --state open --author "${AUTOFIX_BOT}" \ --base main \ - --limit 100 --json number,headRefName > "${WORKDIR}/bot-prs.json" - # gh pr list with --author on the same repo only returns in-repo PRs, - # and --base main limits to main-targeting PRs. - CANDIDATES="$(jq -r '.[] | .number' "${WORKDIR}/bot-prs.json")" + --limit 100 --json number,headRefName,isCrossRepository > "${WORKDIR}/bot-prs.json" + CANDIDATES="$(jq -r \ + '.[] | select(.isCrossRepository != true) | .number' \ + "${WORKDIR}/bot-prs.json")" fi TARGETS='[]' @@ -959,7 +983,18 @@ jobs: ISSUE="${PR}" fi HEAD_SHA="$(gh api "repos/${REPO}/pulls/${PR}" --jq '.head.sha')" - + CHECKS_JSON="$(gh pr view "${PR}" --repo "${REPO}" \ + --json statusCheckRollup --jq '.statusCheckRollup // []' 2> /dev/null || echo '[]')" + HAS_PENDING_CHECKS="$(jq -r ' + [ .[] + | select((.status // .state // "") | IN("QUEUED", "IN_PROGRESS", "PENDING", "WAITING", "REQUESTED")) + | select(((.workflowName // "") != "Qwen Autofix") or (((.name // "") | startswith("review-address")))) ] + | length > 0 + ' <<< "${CHECKS_JSON}")" + if [[ "${HAS_PENDING_CHECKS}" == "true" ]]; then + echo "⏳ #${PR}: PR has pending checks; skipping until the current verification finishes" + continue + fi # Push watermark: the PR's last push. Feedback older than this was in # front of the agent on a previous round. PUSH_WM="$(gh api "repos/${REPO}/commits/${HEAD_SHA}" --jq '.commit.committer.date')" @@ -983,6 +1018,13 @@ jobs: echo "🚧 #${PR}: hit MAX_ROUNDS (${ROUND}/${MAX_ROUNDS}) — leaving for a human" continue fi + N_FAILED_CHECKS="$(jq --arg wm "${EFF_WM}" ' + [ .[] + | select((.conclusion // .state // "") | IN("FAILURE", "FAILED", "ERROR", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED")) + | select(((.workflowName // "") != "Qwen Autofix") or (((.name // "") | startswith("review-address")))) + | select((.completedAt // .updatedAt // "") > $wm) ] + | length + ' <<< "${CHECKS_JSON}")" gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate > "${WORKDIR}/rv.json" gh api "repos/${REPO}/pulls/${PR}/comments" --paginate > "${WORKDIR}/rc.json" @@ -1021,17 +1063,18 @@ jobs: HAS_CONFLICT='false' if [[ "${MERGEABLE}" == "CONFLICTING" ]]; then HAS_CONFLICT='true'; fi - if [[ "${N_REVIEWS}" -eq 0 && "${N_COMMENTS}" -eq 0 && "${N_ISSUE_COMMENTS}" -eq 0 && "${HAS_CONFLICT}" != "true" ]]; then + if [[ "${N_REVIEWS}" -eq 0 && "${N_COMMENTS}" -eq 0 && "${N_ISSUE_COMMENTS}" -eq 0 && "${N_FAILED_CHECKS}" -eq 0 && "${HAS_CONFLICT}" != "true" ]]; then echo "✅ #${PR}: nothing new since ${EFF_WM} (conflict=${HAS_CONFLICT})" continue fi - echo "🔎 #${PR}: ${N_REVIEWS} review(s) + ${N_COMMENTS} inline + ${N_ISSUE_COMMENTS} issue comment(s) new, conflict=${HAS_CONFLICT}, round=${ROUND}" + echo "🔎 #${PR}: ${N_REVIEWS} review(s) + ${N_COMMENTS} inline + ${N_ISSUE_COMMENTS} issue comment(s) + ${N_FAILED_CHECKS} failed check(s) new, conflict=${HAS_CONFLICT}, round=${ROUND}" TARGETS="$(jq -c \ --arg pr "${PR}" --arg branch "${BRANCH}" --arg issue "${ISSUE}" \ --arg round "${ROUND}" --arg wm "${EFF_WM}" \ '. + [{pr: $pr, branch: $branch, issue: $issue, round: $round, watermark: $wm}]' \ <<< "${TARGETS}")" + break # one PR per scheduled scan done COUNT="$(jq 'length' <<< "${TARGETS}")" @@ -1181,6 +1224,9 @@ jobs: gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate > "${WORKDIR}/rv.json" gh api "repos/${REPO}/pulls/${PR}/comments" --paginate > "${WORKDIR}/rc.json" gh api "repos/${REPO}/issues/${PR}/comments" --paginate > "${WORKDIR}/ic.json" + gh pr view "${PR}" --repo "${REPO}" \ + --json statusCheckRollup --jq '.statusCheckRollup // []' > "${WORKDIR}/checks.json" \ + 2> /dev/null || echo '[]' > "${WORKDIR}/checks.json" # Newest actionable feedback timestamp — stamped into the eval marker so # the next scan knows everything up to here has been considered. @@ -1200,7 +1246,11 @@ jobs: | select((.user.login // "") != $ab) | select(((.author_association // "") | IN($trust[])) or (.user.login // "") == $rb) | select((.body // "") | test("