Skip to content

ci: add scheduled autofix workflow for stale bug issues#4989

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
qqqys:ci/scheduled-issue-autofix
Jun 15, 2026
Merged

ci: add scheduled autofix workflow for stale bug issues#4989
wenshao merged 4 commits into
QwenLM:mainfrom
qqqys:ci/scheduled-issue-autofix

Conversation

@qqqys

@qqqys qqqys commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a daily scheduled workflow that picks at most one stale, unattended bug report and tries to fix it autonomously with Qwen Code itself, following the same conventions human contributors follow: claim the issue in the comments first, reproduce the bug before touching code, add unit tests, verify deterministically, and open a PR with an E2E report comment.

Each run scans for open type/bug issues with no assignee, no linked PR, and no activity for 14 days, where "unattended" means no human comments — comments from known triage-bot accounts (qwen-code-ci-bot, github-actions, etc.) don't count as engagement since they appear on most new issues. An assessment agent with a read-mostly toolset judges the top candidates: is it a coherent, real bug; is it reproducible headlessly on Linux CI; is the fix well-scoped; and for multi-symptom reports, would fixing the in-scope symptom actually resolve the reporter's primary complaint? It picks at most one and is free to pick none. Permanently ineligible issues (wrong platform, not a code bug) get an autofix/skip label so daily scans converge.

If an issue is selected, the workflow posts a claim comment (with opt-out instructions) and adds an autofix/in-progress label — the label is what future scans key off, so an issue can never be double-claimed. A development agent then follows the repository's reproduce-first bugfix workflow: demonstrate the bug, apply a minimal root-cause fix, add collocated vitest tests, rebuild, re-verify. It is explicitly allowed to abort honestly if it cannot reproduce or loses confidence. Before anything is published, a deterministic verification gate — plain bash, independent of the agent — re-runs build, typecheck, and lint repo-wide plus test scoped to the packages the diff touches (so a pre-existing red test elsewhere on main cannot block every fix). Only then is the branch pushed and a PR opened with Fixes #<n>, plus the E2E evidence posted as a separate comment. If anything fails after the claim, the workflow withdraws it publicly: posts what the agent learned, swaps the label for autofix/skip, and deletes the claim comment, so an issue never looks silently taken.

Why it's needed

The repository currently has ~180 open type/bug issues with no assignee that have sat untouched for 14+ days, a large share with no human engagement at all. The existing triage bots label them, but nothing moves them forward. This workflow is the next rung on that ladder: it dogfoods QwenLM/qwen-code-action (the same runner the triage workflows use) as an autonomous engineer working the long tail, one issue per day.

Security: issue text is untrusted input processed by an agent, so containment is structural rather than prompt-only. The assessment agent runs with an allowlisted read-mostly toolset, and the development agent has no GitHub credentials at all — every gh-authenticated action (claim, label, push, PR, withdraw) is a deterministic workflow step gated on structured agent outputs.

Reviewer Test Plan

How to verify

The full pipeline was exercised with three real dry runs on a fork (same workflow, job guard and REPO pointed at the fork/upstream for read-only scanning; dry_run=true so no comments, pushes, or PRs were created — the runs are public):

  1. Run 1 — full scan found 7 unattended candidates (bot-comment filtering verified against live data); the agent picked US user -- created account, but never received verification email #3121, produced a complete 5-line fix with red-green unit tests in ~18 min. The verification gate then correctly blocked publishing because npm run test repo-wide hit a pre-existing red test in an untouched package — which motivated scoping the gate's tests to changed packages.
  2. Run 2 — forced issue_number=3121: assessment this time judged the issue's primary complaint (server-side verification email) as not CLI-fixable and returned no-go; all downstream steps were skipped cleanly. This motivated assessment rule 4 (judge multi-symptom reports by the primary complaint, so a tangential fix never auto-closes an unresolved issue).
  3. Run 3 — full green path with the final workflow: scan → assessment no-goes US user -- created account, but never received verification email #3121 per rule 4 (flagging the tangential React warning for a human to split out) and selects Cannot stop Qwen after you reject a command #3153 with a concrete root cause (ACP session path missing the allToolsCancelled guard the TUI path has) → develops a 2-file fix with targeted tests → gate detects packages/cli as the only changed package and passes → PR body, E2E report, and full diff printed and uploaded as artifacts.

After merge, the same can be verified safely from the Actions tab: run the workflow manually with dry_run=true (assess + develop + verify, but no comment/push/PR; artifacts show the would-be PR), optionally with issue_number to force a target.

Evidence (Before & After)

N/A — new CI workflow, no user-visible behavior change. The three dry-run links above contain the step logs, the generated PR bodies, and full diffs as artifacts.

Tested on

OS Status
🍏 macOS N/A
🪟 Windows N/A
🐧 Linux

Environment (optional)

GitHub Actions ubuntu-latest, exercised end to end on a fork with the same secrets layout.

Risk & Scope

  • Main risk or tradeoff: an autonomous agent acting on untrusted issue text. Contained structurally: the development agent has no GitHub credentials, all privileged operations are deterministic bash steps, throughput is capped at one issue per day, and any maintainer can opt an issue out by adding autofix/skip (or simply commenting/assigning). Every failure path withdraws the claim publicly.
  • Not validated / out of scope: PRs created with the default GITHUB_TOKEN do not trigger CI workflows; an optional AUTOFIX_BOT_TOKEN secret (PAT or App token) is recommended so autofix PRs run CI. Feature requests are deliberately out of scope for v1 — the same claim/develop/publish machinery can be extended once the bug pipeline proves itself.
  • Breaking changes / migration notes: none. Reuses the existing OPENAI_API_KEY / OPENAI_BASE_URL secrets and the QWEN_PR_REVIEW_MODEL repo variable. The autofix/skip and autofix/in-progress labels are created automatically on first use.

Linked Issues

None.

中文说明

这个 PR 做了什么

新增一个每日定时 workflow:每轮最多挑选一个长期无人处理的 bug issue,用 Qwen Code 自身自主修复,并遵循人类贡献者的同一套规范——先在评论区认领,动代码前先复现,补单元测试,独立验证,最后开 PR 并附 E2E 报告评论。

每轮扫描 open 的 type/bug issue:无 assignee、无关联 PR、14 天无动静,且"无人处理"的判定是没有真人评论——已知 triage bot 账号(qwen-code-ci-botgithub-actions 等)的评论不算,因为它们几乎出现在每个新 issue 上。评估 agent(只读工具白名单)逐个判断:是否连贯真实的 bug;能否在 Linux CI 无头环境复现;修复是否小切口;对多症状报告,修复可及的症状是否真正解决报告者的主诉。每轮最多选一个,也可以一个不选。结构性不可修复的 issue(平台限定、非代码 bug)会打上 autofix/skip 标签,使每日扫描收敛。

选中后,workflow 发认领评论(附退出方式说明)并打 autofix/in-progress 标签——后续扫描以标签为准,issue 不会被重复认领。开发 agent 按仓库"先复现"的 bugfix 流程执行:复现 bug、最小化根因修复、补 vitest 测试、重建、复验,无法复现或失去信心时允许诚实弃权。发布前有一道与 agent 无关的确定性验证闸门:全仓库重跑 build/typecheck/lint,测试只跑 diff 涉及的包(main 上其他包的既有红测试不会阻塞所有修复)。全部通过才推分支、开带 Fixes #<n> 的 PR,并单独评论 E2E 证据。认领后任一环节失败都会公开撤回:发布 agent 的发现、把标签换成 autofix/skip、删除认领评论,保证 issue 不会处于"被占着却没人做"的状态。

为什么需要

仓库目前有约 180 个 open 的 type/bug issue 无 assignee 且 14 天以上无动静,其中很大一部分完全没有真人参与。现有 triage bot 只打标签,没有任何机制推动它们前进。这个 workflow 是 triage 体系的下一级:用 QwenLM/qwen-code-action(与现有 triage workflow 同款 runner)dogfood Qwen Code 自身,每天消化一个长尾 issue。

安全方面:issue 正文是经过 agent 处理的不可信输入,因此采用结构性隔离而非仅靠 prompt 约束。评估 agent 只有只读白名单工具;开发 agent 没有任何 GitHub 凭证——所有 gh 鉴权操作(认领、打标、push、开 PR、撤回)都是确定性的 workflow 步骤,由结构化的 agent 产出来 gate。

审阅者测试计划

完整管线已在 fork 上用三轮真实 dry run 验证(同一 workflow,job guard 与 REPO 指向 fork/upstream 做只读扫描;dry_run=true 不产生任何评论、push 或 PR,运行记录公开可查):

  1. 第一轮——完整扫描得到 7 个候选(bot 评论过滤经真实数据验证);agent 选中 US user -- created account, but never received verification email #3121,约 18 分钟产出 5 行修复及红绿单测。验证闸门随后正确拦截了发布:全仓库 npm run test 撞上未改动包里 main 既有的红测试——这促成了"测试只跑改动包"的闸门改进。
  2. 第二轮——强制 issue_number=3121:这次评估判定该 issue 的主诉(服务端验证邮件)非 CLI 可修复,返回 no-go,下游步骤全部干净跳过。这促成了评估规则 4(多症状报告按主诉判断,避免边缘修复"假关闭"未解决的 issue)。
  3. 第三轮——最终版全绿通路:扫描 → 评估按规则 4 对 US user -- created account, but never received verification email #3121 no-go(并注明 React 警告应由人工拆分)、选中 Cannot stop Qwen after you reject a command #3153 并给出具体根因(ACP session 路径缺少 TUI 路径已有的 allToolsCancelled 守卫)→ 开发出 2 文件修复及针对性测试 → 闸门识别出仅 packages/cli 被改动并通过 → PR 正文、E2E 报告与完整 diff 打印并上传为 artifacts。

合并后可在 Actions 页安全验证:手动触发并设 dry_run=true(评估+开发+验证,但不评论/不推/不开 PR,artifacts 展示拟提交的 PR),可选 issue_number 指定目标。

风险与范围

  • 主要风险:自主 agent 处理不可信的 issue 文本。已结构性遏制:开发 agent 无 GitHub 凭证,所有特权操作为确定性 bash 步骤,吞吐量限制为每天一个 issue,maintainer 给 issue 加 autofix/skip 标签(或直接评论/assign)即可使其退出,所有失败路径都会公开撤回认领。
  • 未验证/范围外:默认 GITHUB_TOKEN 创建的 PR 不会触发 CI,建议配置可选的 AUTOFIX_BOT_TOKEN(PAT 或 App token);feature 类 issue 刻意排除在 v1 之外,bug 管线验证成熟后可复用同一套认领/开发/发布机制扩展。
  • 破坏性变更:无。复用现有 OPENAI_API_KEY / OPENAI_BASE_URL secrets 与 QWEN_PR_REVIEW_MODEL 仓库变量;autofix/skipautofix/in-progress 标签首次使用时自动创建。

@DragonnZhang DragonnZhang left a comment

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.

Automated Review — Security Findings

This PR adds a scheduled GitHub Actions workflow for autonomous bug fixing. The overall architecture is sound (assess → claim → develop → verify → publish), but there are two security issues that should be addressed before merging.

Finding 1 (High): Unvalidated AI model output written to GITHUB_OUTPUT

The go_issue step output is sourced from decision.json, which is generated by an AI model that processes untrusted issue text (titles and bodies). The value is written to GITHUB_OUTPUT without any validation that it is a numeric issue number.

If the AI model is tricked via prompt injection in a malicious issue body, the go field in decision.json could contain newlines or shell metacharacters. A newline in the value would inject arbitrary key-value pairs into GITHUB_OUTPUT (e.g., go_issue=1234\nskip_ci=true), potentially altering the behavior of subsequent steps.

Additionally, ${{ steps.decision.outputs.go_issue }} is expanded by GitHub Actions into the YAML of the "Develop fix" step's prompt (lines 251, 267, 286, 291) before the shell sees it. If the value contains YAML-special characters or newlines, it could modify the prompt content or break the step.

Suggested fix — validate that GO is a positive integer before writing it to GITHUB_OUTPUT:

GO="$(jq -r '.go // empty' "${WORKDIR}/decision.json")"
if [[ -n "${GO}" ]] && ! [[ "${GO}" =~ ^[0-9]+$ ]]; then
  echo "❌ Invalid go value: ${GO}"
  echo "go_issue=" >> "${GITHUB_OUTPUT}"
  exit 0
fi
echo "go_issue=${GO}" >> "${GITHUB_OUTPUT}"

Finding 2 (Medium): "Develop fix" step runs unsandboxed with unrestricted tool access

The "Develop fix" step (line 245) uses "sandbox": false with no coreTools restriction, giving the AI agent full access to all tools including arbitrary shell commands. This agent processes untrusted issue text. While the prompt instructs the agent to treat issue text as untrusted, a successful prompt injection could escalate to arbitrary code execution on the CI runner, which has contents: write permission and access to OPENAI_API_KEY.

By contrast, the "Assess candidates" step restricts tools to read-only commands. Consider adding a coreTools allowlist for the "Develop fix" step as well, or at minimum enabling sandboxing.


Both findings relate to the same root cause: output from an AI model that processes untrusted input is trusted and used in security-sensitive contexts without validation.

fi

GO="$(jq -r '.go // empty' "${WORKDIR}/decision.json")"
echo "go_issue=${GO}" >> "${GITHUB_OUTPUT}"

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.

Security: Unvalidated AI output written to GITHUB_OUTPUT

GO comes from parsing decision.json, which is generated by an AI model processing untrusted issue text. Without validation that GO is a positive integer, a crafted issue body could trick the model into returning a value containing newlines, injecting arbitrary key-value pairs into GITHUB_OUTPUT. This same value is later expanded via ${{ steps.decision.outputs.go_issue }} into YAML strings (lines 251, 267, 286, 291), where non-numeric content could break the prompt or step configuration.

Add validation: [[ "${GO}" =~ ^[0-9]+$ ]] before writing to GITHUB_OUTPUT, and reject non-matching values.

settings_json: |-
{
"maxSessionTurns": 400,
"sandbox": false

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.

Security: Unrestricted agent with no sandbox processing untrusted input

The "Develop fix" step runs with sandbox: false and no coreTools restriction, giving the agent full shell access on the CI runner. The agent processes issue titles and bodies (untrusted user input). The "Assess candidates" step above restricts tools to read-only commands; consider applying a similar coreTools allowlist here, or at minimum enabling sandboxing, to limit the blast radius of a successful prompt injection.

${DETAIL}"
gh issue edit "${ISSUE}" --repo "${REPO}" \
--remove-label 'autofix/in-progress' --add-label 'autofix/skip' || true
if [[ -n "${COMMENT_ID}" ]]; 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.

[Critical] The withdraw step unconditionally adds autofix/skip on any failure after claim succeeds, including transient CI failures (runner OOM, npm registry outage, flaky test in a touched package). Since the scan query excludes -label:autofix/skip, a fixable bug is permanently blocked from future autofix attempts.

Suggested change
if [[ -n "${COMMENT_ID}" ]]; then
if [[ -f "${WORKDIR}/failure.md" ]]; then
gh issue edit "${ISSUE}" --repo "${REPO}" \
--remove-label 'autofix/in-progress' --add-label 'autofix/skip' || true
else
gh issue edit "${ISSUE}" --repo "${REPO}" \
--remove-label 'autofix/in-progress' || true
fi

— qwen3.7-max via Qwen Code /review


- name: 'Withdraw claim on failure'
if: |-
${{ failure() && steps.claim.outcome == 'success' }}

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] failure() returns false when a job is cancelled (timeout fires, manual cancel, runner reclaim). In all these cases, the autofix/in-progress label persists permanently and the scan query excludes that label, so the issue is never re-scanned.

Suggested change
${{ failure() && steps.claim.outcome == 'success' }}
${{ (failure() || cancelled()) && steps.claim.outcome == 'success' }}

— qwen3.7-max via Qwen Code /review

--description 'Not eligible for the scheduled autofix agent' \
--color 'ededed' 2> /dev/null || true
jq -c '(.skip // [])[] | select(.permanent == true)' "${WORKDIR}/decision.json" \
| while read -r row; do

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] The skip-labeling loop runs even when dry_run is true. The dry_run input only gates the "Claim issue" and "Publish PR" steps, but labeling is also a mutating operation — a dry-run trial permanently labels issues with autofix/skip, silently shrinking the candidate pool for future real runs.

Suggested fix: guard the labeling block with a dry_run check:

if [[ "${DRY_RUN}" != "true" ]]; then
  jq -c '(.skip // [])[] | select(.permanent == true)' "${WORKDIR}/decision.json" \
    | while read -r row; do
        ...
      done
fi

And pass DRY_RUN: '${{ inputs.dry_run }}' as an env var to this step.

— qwen3.7-max via Qwen Code /review

fi

GO="$(jq -r '.go // empty' "${WORKDIR}/decision.json")"
if [[ -n "${GO}" && ! "${GO}" =~ ^[1-9][0-9]*$ ]]; 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.

[Critical] The go issue number is validated as a positive integer but never checked against candidates.json. A hallucinating or prompt-injection-influenced assessment agent could return any valid issue number, bypassing all scan filters (staleness, unattended, label exclusions, no-assignee). The workflow would claim and develop a fix for an arbitrary issue.

Suggested fix: after extracting GO, validate it against the candidate list:

CANDIDATE_NUMS="$(jq -r '.[].number' "${WORKDIR}/candidates.json")"
if [[ -n "${GO}" ]] && ! grep -qx "${GO}" <<< "${CANDIDATE_NUMS}"; then
  echo "❌ Assessment selected issue #${GO} which is not in the candidate list"
  echo "go_issue=" >> "${GITHUB_OUTPUT}"
  exit 0
fi

— qwen3.7-max via Qwen Code /review

jq -c '(.skip // [])[] | select(.permanent == true)' "${WORKDIR}/decision.json" \
| while read -r row; do
NUM="$(jq -r '.number' <<< "${row}")"
echo "🏷️ Skipping #${NUM} permanently: $(jq -r '.reason' <<< "${row}")"

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 skip-list number field lacks the numeric validation applied to go (line 192). A hallucinated value passes directly to gh issue edit "${NUM}", potentially labeling an unrelated issue with autofix/skip.

Suggested change
echo "🏷️ Skipping #${NUM} permanently: $(jq -r '.reason' <<< "${row}")"
NUM="$(jq -r '.number' <<< "${row}")"
if [[ ! "${NUM}" =~ ^[1-9][0-9]*$ ]]; then
echo "⚠️ Invalid skip number: ${NUM}"
continue
fi

— qwen3.7-max via Qwen Code /review

# red or flaky test elsewhere on main must not block every fix.
# Cross-package regressions are covered by regular CI on the PR.
CHANGED_PKGS="$(git diff --name-only "origin/main...${BRANCH}" \
| grep -oE '^packages/[^/]+' | sort -u)"

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] grep -oE returns exit code 1 when no lines match. Since shell: bash applies set -eo pipefail by default, this kills the script before the if [[ -z ... ]] guard on line 372 is reached. A legitimate fix touching only root-level files (eslint config, tsconfig, Dockerfile) produces an opaque failure.

Suggested change
| grep -oE '^packages/[^/]+' | sort -u)"
CHANGED_PKGS="$(git diff --name-only "origin/main...${BRANCH}" \
| grep -oE '^packages/[^/]+' | sort -u || true)"

— qwen3.7-max via Qwen Code /review

# Triage bots comment on most new issues, so "unattended" means:
# no comments at all, or every commenter is a known bot account.
jq -c --argjson bots "${KNOWN_BOTS}" \
'[ .[] | select([(.comments // [])[].author.login] - $bots == []) ] | .[0:10] | map(del(.comments))' \

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 jq bot filter doesn't handle null author.login values from deleted or ghost GitHub accounts. Since null is not in $bots, the array subtraction leaves it in place, and the issue is incorrectly classified as human-attended — silently excluded from candidates forever.

Suggested change
'[ .[] | select([(.comments // [])[].author.login] - $bots == []) ] | .[0:10] | map(del(.comments))' \
jq -c --argjson bots "${KNOWN_BOTS}" \
'[ .[] | select(([ (.comments // [])[].author.login ] | map(select(. != null))) - $bots == []) ] | .[0:10] | map(del(.comments))' \
"${WORKDIR}/scan.json" > "${WORKDIR}/candidates.json"

— qwen3.7-max via Qwen Code /review

run: |-
BRANCH="autofix/issue-${ISSUE}"
git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${REPO}.git"
git push origin "${BRANCH}"

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] git push origin (non-force) fails if a stale remote branch from a prior attempt on the same issue exists. The push failure triggers withdraw, which adds autofix/skip, making the issue un-retryable without manual branch cleanup.

Suggested change
git push origin "${BRANCH}"
git push --force-with-lease origin "${BRANCH}"

— qwen3.7-max via Qwen Code /review

DETAIL='The run failed before producing a verified fix.'
LABEL_ARGS=(--remove-label 'autofix/in-progress')
fi
gh issue comment "${ISSUE}" --repo "${REPO}" --body "🤖 Withdrawing the claim above — the automated fix attempt did not succeed; ${REASON}

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] gh issue comment here lacks || true, but the next two commands (lines 471 and 473) both have it. Under the GitHub Actions default shell (bash -eo pipefail), if this gh issue comment call fails (rate limit, transient API error, network timeout), set -e aborts the script before the label cleanup on line 471 runs. The issue is then left with autofix/in-progress permanently, which also removes it from future scan eligibility (the search query excludes -label:autofix/in-progress). The issue becomes silently stuck with no automated or human-visible resolution path.

Suggested change
gh issue comment "${ISSUE}" --repo "${REPO}" --body "🤖 Withdrawing the claim above — the automated fix attempt did not succeed; ${REASON}
gh issue comment "${ISSUE}" --repo "${REPO}" --body "🤖 Withdrawing the claim above — the automated fix attempt did not succeed; ${REASON}
What the agent found, in case it helps a human contributor:
${DETAIL}" || true

— claude-sonnet-4-20250514 via Qwen Code /review

@DragonnZhang DragonnZhang left a comment

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.

No review findings on this revision. The incremental changes since the last review (reordering gh issue edit before gh issue comment and adding || true) correctly address the previously reported issues. All input validation (issue number regex, candidate list membership), error handling (|| true on cleanup commands), dry_run gating on mutating operations, failure/cancellation handling (failure() || cancelled()), and security containment (sandbox on develop step, restricted tools on assess step) are properly implemented. Downgraded from Approve to Comment: CI check review-pr is still in progress. -- claude-sonnet-4-20250514 via Qwen Code /review

@wenshao

wenshao commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

🧪 Local runtime verification report

I exercised this workflow's deterministic logic locally in a tmux harness to complement the author's three fork dry-runs. Because the workflow is a single GitHub Actions file, "runtime verification" here means extracting the bash/jq/git logic verbatim from the YAML and running it against (a) the live QwenLM/qwen-code repo, read-only, (b) crafted edge-case inputs, and (c) a real local git repo, plus a full actionlint + shellcheck pass. No comments, labels, pushes, or PRs were created against the repo — every privileged call was stubbed.

Verdict: every deterministic guard behaves as designed, and the two earlier review findings are confirmed closed. Summary below; raw logs are collapsed.

# Area (verbatim source) Result
1 Scan search + bot-comment filter on live repo (L44, L83-99) ✅ 30 scanned → 7 candidates, 0/30 mismatches vs ground truth
2 Bot-filter jq on synthetic edge cases (L92-94) 14/14
3 decision.json validation + skip-label guards (L186-229) 17/17 + airtight injection proof
4 comment_id extraction + verification gate (L244, L354-397) ✅ 3/3 URLs, 7/7 gate scenarios
5 actionlint + shellcheck on whole file 0 findings (shellcheck integration proven active)
6 Token isolation / candidate shaping (security claims) ✅ develop+verify steps carry no GITHUB_TOKEN

1. Bot-comment "unattended" filter — validated against live data

Ran the exact scan query and jq filter against the live repo, then cross-checked each verdict with an independent derivation (a grep -vE over distinct comment authors, not reusing the workflow's jq):

ISSUE    #cmts  selected?  expected   non-bot commenters
3979     1      select     select     ,                      <- bot-only comment, correctly kept
3949     0      select     select     ,
3914     8      exclude    exclude    astrostl danalec fenris hailinzeng svilendotorg
4317     4      exclude    exclude    byx1728 pomelo-nwu
...
MISMATCHES=0   (across all 30 scanned issues)

The 7 selected candidates [3979, 3949, 3884, 3424, 3184, 3153, 3121] genuinely have zero human comments; all 23 issues with ≥1 human commenter were excluded. The set-difference approach (authors - bots == []) handles duplicate bots and the github-actions[bot] bracket variant correctly.

2. Edge cases (14/14)

no-comments / empty comments / missing key / single bot / github-actions[bot] / multiple distinct bots / same bot repeated → select; one human, bot+human, human+bot → exclude. One behavioral note (not a bug): a comment from a deleted account (author: null) is treated as unattended.

3. AI-output validation — reviewer Finding 1 confirmed closed

Fed the verbatim "Read decision" step many decision.json variants (GITHUB_OUTPUT → temp file, gh stubbed):

valid in-candidate (3153)   -> go_issue=3153   ✅
valid int NOT candidate     -> go_issue=        (rejected: not in candidate list)
zero / negative / float     -> go_issue=        (regex ^[1-9][0-9]*$)
"3153; rm -rf /"            -> go_issue=        (rejected)
"3153\nmalicious=true"      -> go_issue=        (rejected)  <- GITHUB_OUTPUT injection

Airtight proof for the injection case — the multiline payload yields a GITHUB_OUTPUT of exactly one line (go_issue=); neither malicious=true nor skip_ci ever reaches the file. The permanent-skip loop only labels numbers that are both positive integers and in the candidate list (x;whoami and out-of-list 88888 are rejected with a warning), and applies no labels when dry_run=true.

4. Verification gate — real local git repo (npm stubbed)

scenario result
fix touches packages/cli only ✅ PASS, CHANGED_PKGS=packages/cli
fix touches core and cli ✅ PASS, both tested
branch missing / no diff vs main / missing pr-body.md ❌ FAIL (correct)
only a root (non-package) file changed ❌ FAIL "does not touch any package"
agent wrote failure.md ❌ FAIL "aborted intentionally"

comment_id="${COMMENT_URL##*-}" extracts the id correctly even for repos whose name contains -.

5. Lint

actionlint 1.7.7 + shellcheck 0.10.0: 0 errors (0 parse, 0 expression, 0 shell). I proved the shellcheck integration is actually active by injecting a deliberate echo $UNDEF into a copy and watching it raise SC2086 — the real file stays clean.

6. Security-claim spot checks

  • Develop fix and Verification gate steps carry only ISSUE in env:no GITHUB_TOKEN, matching "the development agent has no GitHub credentials." Tokens are confined to scan/assess/decision/claim/publish/withdraw (all deterministic steps).
  • map(del(.comments)) strips comment data from candidates.json; the agent receives only number, title, body, labels, createdAt, url.
  • Finding 2 (sandbox/tool-allowlist on the develop agent) is present in the current revision: "sandbox": true + an explicit coreTools allowlist.

Minor, non-blocking observations

  1. Scan window: --limit 30 + sort:created-desc + .[0:10] means each run considers only the 30 newest-created stale bugs. With label-based convergence and one-issue/day cadence this is fine, but a steady influx of new bugs could keep the oldest of the ~180 backlog out of the window for a long time. Worth a comment if that long tail is a goal.
  2. Deleted-account comments count as "unattended" (§2) — rare, low impact.
  3. Root-only fixes are rejected by the gate as touching no package. Harmless for this repo (all code lives under packages/*); just be aware a fix limited to a root script/config would be blocked.

What this does not cover

Agent reasoning quality, the real QwenLM/qwen-code-action runtime, GitHub-Actions expression evaluation, and live secrets — those are exercised by the author's three linked fork dry-runs and by CI on PRs the workflow opens. This report covers the deterministic scaffolding that wraps the agent.

Harness details

Logic was copied verbatim from the PR's YAML (cited line numbers) into standalone scripts and driven in a dedicated tmux session. Live data via gh issue list (read-only). Gate tested on a throwaway bare-repo + clone under /var/tmp. All gh/npm calls stubbed to logging no-ops; nothing was mutated on QwenLM/qwen-code.


中文小结

在本地用 tmux 把本 PR workflow 里的 bash/jq/git 逻辑逐行原样抽出来跑真实验证(只读访问线上仓库 + 构造边界输入 + 真实本地 git 仓库 + actionlint/shellcheck),全程未对仓库产生任何评论/标签/push/PR。结论:所有确定性逻辑与安全护栏均按预期工作,此前两条 review 意见已确认修复

  • bot 评论过滤:线上 30 个 issue,过滤出 7 个候选,与独立复核零误判;只有 bot 评论的 plan mode下,qwen code完成回复后在ghostty终端会出现不停闪屏 #3979 正确保留,有真人评论的全部排除。
  • AI 输出校验(Finding 1):go 为非正整数、命令注入串、含换行的 GITHUB_OUTPUT 注入载荷全部被拒,注入用例下 GITHUB_OUTPUT 严格只有一行 go_issue=,无泄漏。
  • 跳过标签:只给"既是正整数又在候选列表内"的 issue 打 autofix/skip;dry_run=true 时不打任何标签。
  • 验证闸门:改动包检测、无改动/缺产物/分支缺失/failure.md 等场景全部正确。
  • lint:actionlint+shellcheck 零告警(已通过注入故意错误证明 shellcheck 确实在跑)。
  • 安全:develop / verify 两步 env 里没有 GITHUB_TOKEN,与"开发 agent 无 GitHub 凭证"一致。

未覆盖:agent 推理质量、qwen-code-action 真实运行、GHA 表达式求值、线上 secrets —— 这些由作者在 fork 上的三次 dry-run 与 PR CI 覆盖。

@wenshao

wenshao commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for this PR, @qqqys!

Template looks good ✓

On direction: this is squarely within scope — it's a CI workflow that dogfoods qwen-code-action to work the long tail of ~180 stale bug issues. It's the natural next step after the existing triage bots that only label. roadmap/background-automation label exists in the repo, which confirms the area is actively tracked.

On approach: the scope feels right for what it's trying to do. The security model is well-considered: assessment agent has a read-only toolset with no sandbox, development agent has a write toolset inside a sandbox but carries no GitHub credentials, and all privileged operations (claim, label, push, PR, withdraw) are deterministic bash steps gated on structured agent outputs. One-issue-per-day throughput cap is the right call for v1. The fork dry-runs (3 runs with real issues) are strong evidence the design works in practice.

One observation before diving into code: the KNOWN_BOTS list is hardcoded in the workflow. If new bot accounts are added later (e.g., a Dependabot-like service), someone has to remember to update this list. A repo variable would be more maintainable, but hardcoding is fine for v1 — it's explicit and easy to find.

Moving on to code review. 🔍

中文说明

感谢贡献,@qqqys

模板完整 ✓

方向:完全在范围内——这是一个用 qwen-code-action 消化约 180 个陈旧 bug issue 的 CI workflow,是现有只打标签的 triage bot 的自然延伸。仓库已有 roadmap/background-automation 标签,说明该方向已被积极跟踪。

方案:范围对于目标来说是合理的。安全模型考虑周到:评估 agent 只有只读工具且无沙箱,开发 agent 有写工具但在沙箱内且不携带 GitHub 凭证,所有特权操作(认领、打标、推送、开 PR、撤回)都是由结构化 agent 输出门控的确定性 bash 步骤。v1 每天一个 issue 的吞吐量上限是正确的。fork 上的 3 次真实 dry run 有力地证明了设计在实践中可行。

一个观察:KNOWN_BOTS 列表硬编码在 workflow 中。如果后续添加新 bot 账号(如类 Dependabot 服务),需要有人记得更新。用 repo variable 更可维护,但 v1 硬编码也可以——明确且容易找到。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

The workflow is well-structured and follows a clean security model. Here's what stands out:

Security model — solid. The three-tier isolation (read-only assessor with no sandbox → sandboxed developer with no GitHub creds → deterministic bash for all privileged ops) is the right architecture. The assessment agent's coreTools are all read-only (read_file, glob, search_file_content, cat, git log/diff, gh issue view/search), and the development agent's coreTools are write-capable but it carries no GITHUB_TOKEN. The claim/push/PR/withdraw steps are plain bash gated on structured outputs — the agent can't forge a PR or post a comment even if it wanted to.

Candidate filtering — correct. The jq expression for "unattended" issues (commenters - known_bots == []) correctly handles both zero-comment issues and all-bot-comment issues. The candidate-list validation before acting on the assessment's go decision (checking the selected issue number is actually in the candidate list) is a good defense against hallucinated issue numbers.

Verification gate — pragmatic. Scoping tests to only changed packages (git diff --name-only | grep packages/) is the right call — a pre-existing red test in an untouched package shouldn't block every autofix. Cross-package regressions are covered by regular CI on the PR.

Withdrawal path — thorough. The failure/cancellation handler correctly distinguishes between "agent gave up honestly" (permanent autofix/skip) and "infrastructure failure" (removes in-progress, leaves for future retry). Deleting the claim comment prevents the issue from looking silently taken.

No critical blockers or AGENTS.md violations found.

Real-Scenario Testing

This PR adds a CI workflow file (.github/workflows/qwen-scheduled-issue-autofix.yml), not user-facing CLI behavior. Tmux-based real-user testing is not applicable — there's no interactive feature to drive.

The author's three fork dry-runs provide strong end-to-end evidence:

  1. Run 1 — full scan found 7 candidates, picked US user -- created account, but never received verification email #3121, produced a 5-line fix in ~18 min. Verification gate correctly blocked publishing due to a pre-existing red test (motivated the scoped-test improvement).
  2. Run 2 — forced issue US user -- created account, but never received verification email #3121: assessment judged the primary complaint as not CLI-fixable, returned no-go. All downstream steps skipped cleanly.
  3. Run 3 — full green path: scan → assessment → develop → gate → PR. Complete artifacts uploaded.

These runs validate the workflow logic, the security model, and the failure paths.

中文说明

代码审查

Workflow 结构良好,安全模型干净。亮点:

安全模型——扎实。 三层隔离(只读评估 agent 无沙箱 → 沙箱内开发 agent 无 GitHub 凭证 → 确定性 bash 执行所有特权操作)是正确架构。评估 agent 的 coreTools 全部只读,开发 agent 有写能力但不携带 GITHUB_TOKEN。认领/推送/PR/撤回步骤是由结构化输出门控的纯 bash——agent 即使想也无法伪造 PR 或发评论。

候选过滤——正确。 "无人处理"的 jq 表达式(commenters - known_bots == [])正确处理零评论和全部为 bot 评论两种情况。在执行评估的 go 决策前验证候选列表(检查选中的 issue 编号确实在候选列表中),是对幻觉 issue 编号的良好防御。

验证闸门——务实。 将测试范围限定到仅改动的包(git diff --name-only | grep packages/)是正确选择——未触及包中的既有红测试不应阻塞所有自动修复。跨包回归由 PR 上的常规 CI 覆盖。

撤回路径——彻底。 失败/取消处理器正确区分"agent 诚实放弃"(永久 autofix/skip)和"基础设施故障"(移除 in-progress,留给未来重试)。删除认领评论防止 issue 看起来被默默占用。

未发现关键阻塞项或 AGENTS.md 违规。

真实场景测试

此 PR 添加的是 CI workflow 文件,非用户可见的 CLI 行为。基于 tmux 的真实用户测试不适用——没有可驱动的交互功能。

作者在 fork 上的三次 dry run 提供了充分的端到端证据,验证了 workflow 逻辑、安全模型和失败路径。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Stepping back to look at the whole picture.

My independent proposal for this problem would have been: daily cron workflow → scan stale bugs → read-only agent assesses → sandboxed agent develops → deterministic gate → push/PR. That's essentially what this PR delivers, with several details I wouldn't have thought of upfront — bot-comment filtering for "unattended" judgment, candidate-list validation against hallucinated issue numbers, and the AUTOFIX_BOT_TOKEN fallback so PRs can trigger CI.

The motivation is strong: ~180 stale bug issues that labeling alone won't move. This is the natural next step in the triage ladder, and it dogfoods the project's own tooling. The roadmap/background-automation label confirms the area is on the roadmap.

The code is straightforward — a single 474-line workflow file with clear step names, sensible if conditions, and good inline comments explaining the why. The security model (three-tier isolation: read-only assessor → sandboxed developer → deterministic privileged ops) is structurally sound, not just prompt-level. The three fork dry-runs demonstrate all paths: success, assessment-no-go, and verification-gate-blocked.

If I had to maintain this in six months, I'd thank the author. The workflow is self-documenting, the failure paths are thorough (claim withdrawal, label cleanup, honest abort), and the one-issue-per-day cap means blast radius is bounded even if something goes wrong.

The only minor note is the hardcoded KNOWN_BOTS list (mentioned in Stage 1), which is fine for v1 and easy to refactor later if needed.

LGTM. Approving. ✅

中文说明

退一步看全局。

我对这个问题的独立方案是:每日 cron workflow → 扫描陈旧 bug → 只读 agent 评估 → 沙箱 agent 开发 → 确定性闸门 → push/PR。这基本就是这个 PR 交付的内容,而且它还包含几个我起初没想到的细节——用于"无人处理"判定的 bot 评论过滤、针对幻觉 issue 编号的候选列表验证、以及 AUTOFIX_BOT_TOKEN 回退使 PR 可以触发 CI。

动机充分:约 180 个仅靠打标签无法推进的陈旧 bug issue。这是 triage 阶梯的自然下一步,且 dogfood 了项目自身的工具。roadmap/background-automation 标签确认该方向已在路线图中。

代码直观——单个 474 行 workflow 文件,步骤名清晰,if 条件合理,内联注释解释了 why。安全模型(三层隔离:只读评估 → 沙箱开发 → 确定性特权操作)在结构上是可靠的,不仅仅依赖 prompt 层面。三次 fork dry run 验证了所有路径:成功、评估 no-go、验证闸门拦截。

如果六个月后需要维护这个 workflow,我会感谢作者。workflow 自文档化,失败路径彻底(认领撤回、标签清理、诚实中止),每天一个 issue 的上限意味着即使出问题,爆炸半径也是有限的。

唯一的小注意点是硬编码的 KNOWN_BOTS 列表(Stage 1 已提到),v1 可以接受,后续如有需要也容易重构。

LGTM,批准。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

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.

LGTM, looks ready to ship. ✅

@wenshao
wenshao requested a review from yiliang114 June 14, 2026 18:01
@wenshao
wenshao merged commit 02bd82a into QwenLM:main Jun 15, 2026
47 of 48 checks passed
doudouOUC pushed a commit that referenced this pull request Jun 15, 2026
* feat(workflow): add Qwen Scheduled Issue Autofix workflow for automated issue handling

* fix(ci): harden scheduled autofix workflow

* fix(ci): harden scheduled autofix recovery

* fix(ci): keep autofix cleanup best effort
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants