ci: add scheduled autofix workflow for stale bug issues#4989
Conversation
…ed issue handling
DragonnZhang
left a comment
There was a problem hiding this comment.
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}" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
[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.
| 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' }} |
There was a problem hiding this comment.
[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.
| ${{ 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 |
There was a problem hiding this comment.
[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
fiAnd 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 |
There was a problem hiding this comment.
[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}")" |
There was a problem hiding this comment.
[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.
| 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)" |
There was a problem hiding this comment.
[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.
| | 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))' \ |
There was a problem hiding this comment.
[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.
| '[ .[] | 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}" |
There was a problem hiding this comment.
[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.
| 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} |
There was a problem hiding this comment.
[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.
| 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
left a comment
There was a problem hiding this comment.
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
🧪 Local runtime verification reportI exercised this workflow's deterministic logic locally in a Verdict: every deterministic guard behaves as designed, and the two earlier review findings are confirmed closed. Summary below; raw logs are collapsed.
1. Bot-comment "unattended" filter — validated against live dataRan the exact scan query and The 7 selected candidates 2. Edge cases (14/14)no-comments / empty 3. AI-output validation — reviewer Finding 1 confirmed closedFed the verbatim "Read decision" step many Airtight proof for the injection case — the multiline payload yields a 4. Verification gate — real local git repo (
|
| 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
ISSUEinenv:— noGITHUB_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 fromcandidates.json; the agent receives onlynumber, title, body, labels, createdAt, url.- Finding 2 (sandbox/tool-allowlist on the develop agent) is present in the current revision:
"sandbox": true+ an explicitcoreToolsallowlist.
Minor, non-blocking observations
- 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. - Deleted-account comments count as "unattended" (§2) — rare, low impact.
- 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 覆盖。
|
@qwen-code /triage |
|
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. 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 Moving on to code review. 🔍 中文说明感谢贡献,@qqqys! 模板完整 ✓ 方向:完全在范围内——这是一个用 qwen-code-action 消化约 180 个陈旧 bug issue 的 CI workflow,是现有只打标签的 triage bot 的自然延伸。仓库已有 方案:范围对于目标来说是合理的。安全模型考虑周到:评估 agent 只有只读工具且无沙箱,开发 agent 有写工具但在沙箱内且不携带 GitHub 凭证,所有特权操作(认领、打标、推送、开 PR、撤回)都是由结构化 agent 输出门控的确定性 bash 步骤。v1 每天一个 issue 的吞吐量上限是正确的。fork 上的 3 次真实 dry run 有力地证明了设计在实践中可行。 一个观察: 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewThe 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 Candidate filtering — correct. The jq expression for "unattended" issues ( Verification gate — pragmatic. Scoping tests to only changed packages ( Withdrawal path — thorough. The failure/cancellation handler correctly distinguishes between "agent gave up honestly" (permanent No critical blockers or AGENTS.md violations found. Real-Scenario TestingThis PR adds a CI workflow file ( The author's three fork dry-runs provide strong end-to-end evidence:
These runs validate the workflow logic, the security model, and the failure paths. 中文说明代码审查Workflow 结构良好,安全模型干净。亮点: 安全模型——扎实。 三层隔离(只读评估 agent 无沙箱 → 沙箱内开发 agent 无 GitHub 凭证 → 确定性 bash 执行所有特权操作)是正确架构。评估 agent 的 候选过滤——正确。 "无人处理"的 jq 表达式( 验证闸门——务实。 将测试范围限定到仅改动的包( 撤回路径——彻底。 失败/取消处理器正确区分"agent 诚实放弃"(永久 未发现关键阻塞项或 AGENTS.md 违规。 真实场景测试此 PR 添加的是 CI workflow 文件,非用户可见的 CLI 行为。基于 tmux 的真实用户测试不适用——没有可驱动的交互功能。 作者在 fork 上的三次 dry run 提供了充分的端到端证据,验证了 workflow 逻辑、安全模型和失败路径。 — Qwen Code · qwen3.7-max |
|
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 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 The code is straightforward — a single 474-line workflow file with clear step names, sensible 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 LGTM. Approving. ✅ 中文说明退一步看全局。 我对这个问题的独立方案是:每日 cron workflow → 扫描陈旧 bug → 只读 agent 评估 → 沙箱 agent 开发 → 确定性闸门 → push/PR。这基本就是这个 PR 交付的内容,而且它还包含几个我起初没想到的细节——用于"无人处理"判定的 bot 评论过滤、针对幻觉 issue 编号的候选列表验证、以及 动机充分:约 180 个仅靠打标签无法推进的陈旧 bug issue。这是 triage 阶梯的自然下一步,且 dogfood 了项目自身的工具。 代码直观——单个 474 行 workflow 文件,步骤名清晰, 如果六个月后需要维护这个 workflow,我会感谢作者。workflow 自文档化,失败路径彻底(认领撤回、标签清理、诚实中止),每天一个 issue 的上限意味着即使出问题,爆炸半径也是有限的。 唯一的小注意点是硬编码的 LGTM,批准。✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
* 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
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/bugissues 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 anautofix/skiplabel so daily scans converge.If an issue is selected, the workflow posts a claim comment (with opt-out instructions) and adds an
autofix/in-progresslabel — 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-runsbuild,typecheck, andlintrepo-wide plustestscoped 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 withFixes #<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 forautofix/skip, and deletes the claim comment, so an issue never looks silently taken.Why it's needed
The repository currently has ~180 open
type/bugissues 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 dogfoodsQwenLM/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
REPOpointed at the fork/upstream for read-only scanning;dry_run=trueso no comments, pushes, or PRs were created — the runs are public):npm run testrepo-wide hit a pre-existing red test in an untouched package — which motivated scoping the gate's tests to changed packages.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).allToolsCancelledguard the TUI path has) → develops a 2-file fix with targeted tests → gate detectspackages/clias 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 withissue_numberto 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
Environment (optional)
GitHub Actions
ubuntu-latest, exercised end to end on a fork with the same secrets layout.Risk & Scope
autofix/skip(or simply commenting/assigning). Every failure path withdraws the claim publicly.GITHUB_TOKENdo not trigger CI workflows; an optionalAUTOFIX_BOT_TOKENsecret (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.OPENAI_API_KEY/OPENAI_BASE_URLsecrets and theQWEN_PR_REVIEW_MODELrepo variable. Theautofix/skipandautofix/in-progresslabels are created automatically on first use.Linked Issues
None.
中文说明
这个 PR 做了什么
新增一个每日定时 workflow:每轮最多挑选一个长期无人处理的 bug issue,用 Qwen Code 自身自主修复,并遵循人类贡献者的同一套规范——先在评论区认领,动代码前先复现,补单元测试,独立验证,最后开 PR 并附 E2E 报告评论。
每轮扫描 open 的
type/bugissue:无 assignee、无关联 PR、14 天无动静,且"无人处理"的判定是没有真人评论——已知 triage bot 账号(qwen-code-ci-bot、github-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/bugissue 无 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,运行记录公开可查):npm run test撞上未改动包里 main 既有的红测试——这促成了"测试只跑改动包"的闸门改进。issue_number=3121:这次评估判定该 issue 的主诉(服务端验证邮件)非 CLI 可修复,返回 no-go,下游步骤全部干净跳过。这促成了评估规则 4(多症状报告按主诉判断,避免边缘修复"假关闭"未解决的 issue)。allToolsCancelled守卫)→ 开发出 2 文件修复及针对性测试 → 闸门识别出仅packages/cli被改动并通过 → PR 正文、E2E 报告与完整 diff 打印并上传为 artifacts。合并后可在 Actions 页安全验证:手动触发并设
dry_run=true(评估+开发+验证,但不评论/不推/不开 PR,artifacts 展示拟提交的 PR),可选issue_number指定目标。风险与范围
autofix/skip标签(或直接评论/assign)即可使其退出,所有失败路径都会公开撤回认领。GITHUB_TOKEN创建的 PR 不会触发 CI,建议配置可选的AUTOFIX_BOT_TOKEN(PAT 或 App token);feature 类 issue 刻意排除在 v1 之外,bug 管线验证成熟后可复用同一套认领/开发/发布机制扩展。OPENAI_API_KEY/OPENAI_BASE_URLsecrets 与QWEN_PR_REVIEW_MODEL仓库变量;autofix/skip与autofix/in-progress标签首次使用时自动创建。