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
137 changes: 98 additions & 39 deletions .github/workflows/qwen-autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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-'
Expand All @@ -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:
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -804,16 +827,16 @@ 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.
GITHUB_TOKEN: '${{ secrets.CI_DEV_BOT_PAT }}'
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)"
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -936,17 +959,18 @@ 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
CANDIDATES="${FORCED_PR}"
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='[]'
Expand All @@ -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')"
Expand All @@ -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"
Expand Down Expand Up @@ -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}")"
Expand Down Expand Up @@ -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.
Expand All @@ -1200,7 +1246,11 @@ jobs:
| select((.user.login // "") != $ab)
| select(((.author_association // "") | IN($trust[])) or (.user.login // "") == $rb)
| select((.body // "") | test("<!-- (autofix-eval|qwen-triage|qwen-review-suggestion-summary|pr-force-push|qwen-review-ack) ") | not) | .created_at))
| max // ""' "${WORKDIR}/rv.json" "${WORKDIR}/rc.json" "${WORKDIR}/ic.json")"
+ (.[3] | map(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)
| (.completedAt // .updatedAt // "")))
| max // ""' "${WORKDIR}/rv.json" "${WORKDIR}/rc.json" "${WORKDIR}/ic.json" "${WORKDIR}/checks.json")"
[[ -z "${NEWEST}" ]] && NEWEST="${WATERMARK}"
echo "newest=${NEWEST}" >> "${GITHUB_OUTPUT}"

Expand Down Expand Up @@ -1244,6 +1294,15 @@ jobs:
| select((.body // "") | test("<!-- (autofix-eval|qwen-triage|qwen-review-suggestion-summary|pr-force-push|qwen-review-ack) ") | not)
| "- @\(.user.login): \(.body // "" | gsub("\r"; ""))"' \
"${WORKDIR}/ic.json"
echo
echo "## Failed checks"
Comment thread
yiliang114 marked this conversation as resolved.
jq -r --arg wm "${WATERMARK}" '
.[]
| 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)
| "- \((.workflowName // "external check") | gsub("[^A-Za-z0-9 _./()-]"; "") | .[0:80]): \(.conclusion // .state // "?")"' \
"${WORKDIR}/checks.json"
} > "${WORKDIR}/feedback.md"
echo '--- feedback.md ---'
cat "${WORKDIR}/feedback.md"
Expand Down
Loading
Loading