Skip to content
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f51482f
feat(ci): route non-functional PRs to a triage-only review path (#7411)
yiliang114 Aug 14, 2026
75ced85
chore(ci): retrigger failed checks
yiliang114 Aug 15, 2026
b80a1a9
fix(ci): pin the triage-only skip to the triaged head SHA (#9219)
yiliang114 Aug 15, 2026
949e494
Merge remote-tracking branch 'origin/main' into HEAD
yiliang114 Aug 15, 2026
102cefb
fix(ci): authenticate triage-only markers
yiliang114 Aug 15, 2026
0760f41
fix(ci): drop the printf|grep pipes from the triage-only skip gate
yiliang114 Aug 15, 2026
1430f84
test(ci): align triage marker pin assertion
yiliang114 Aug 15, 2026
5c02b55
fix(ci): close triage-route review round (#9193)
yiliang114 Aug 15, 2026
a577d64
fix(ci): make skip-gate read failures diagnosable + correct doc ratio…
yiliang114 Aug 16, 2026
f9e2e1e
test(ci): pin --paginate on the skip gate's list reads (#9193)
yiliang114 Aug 16, 2026
ef9b6f4
fix(ci): carve whitespace-is-syntax files out of the triage-only rout…
yiliang114 Aug 16, 2026
5516a2a
docs(ci): cover the label-failure branch in the Stage 1f fail-closed …
yiliang114 Aug 16, 2026
8687fd9
Merge remote-tracking branch 'origin/main' into HEAD
yiliang114 Aug 16, 2026
742ed71
fix(ci): pin triage-only skips to base
yiliang114 Aug 16, 2026
ee43e1c
fix(ci): strip stale status/on-hold when the triage pin no longer mat…
yiliang114 Aug 16, 2026
0966c62
fix(ci): pin the on-hold marker to a dedicated comment and harden Sta…
yiliang114 Aug 16, 2026
e3f6a6d
fix(ci): honor triage retarget and manual holds
yiliang114 Aug 16, 2026
cc37df5
test(ci): pin triage skip label retention
yiliang114 Aug 16, 2026
3354b1a
test(ci): make the stale-label-removal failure branch reachable (#9193)
yiliang114 Aug 16, 2026
66645bc
fix(ci): close the edited-trigger race and the base TOCTOU (#9193)
yiliang114 Aug 16, 2026
3b87dae
fix(ci): authenticate the stale-marker invalidation as the marker author
yiliang114 Aug 16, 2026
090742c
Merge branch 'main' into feat/7411-triage-only-route
yiliang114 Aug 18, 2026
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
74 changes: 74 additions & 0 deletions .github/workflows/qwen-code-pr-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,8 @@ jobs:
id: 'context'
env:
TRIGGER_BODY: "${{ github.event.comment.body || github.event.review.body || '' }}"
# Read-only label lookup for the triage-only skip below.
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
run: |-
set -euo pipefail
DEFAULT_TIMEOUT_MINUTES=180
Expand Down Expand Up @@ -539,6 +541,78 @@ jobs:
exit 1
fi

# Triage-only route (#7411): the triage skill routes PRs it
# classifies as behavior-neutral maintenance (renames, formatting,
# comment/JSDoc-only) to a triage summary instead of the full
# review, marking them with `status/on-hold` (pr-workflow.md,
# Stage 1f). The automatic lane honours that mark; check the LIVE
# labels, not the event payload — the label lands after the event
# fires, and this step runs again after the review delay. Explicit
# asks (/review comments, review_requested, dispatch) never reach
# this check, and removing the label re-enables the automatic lane
# on the next push. A failed label read skips the optimisation and
# lets the review run (fail open: the worst case is one extra full
# review, never a silently skipped one).
#
# The label alone is NOT sufficient (#9193): Stage 1f applies it
# once at open time and, until a stale pin strips it, it never
# leaves the exempt list — so a PR that earns it with a benign v1
# could silently skip review for every later push of arbitrary
# content. Skip only while the triage run's marker comment is
# pinned to the LIVE head/base SHA pair; a push or base retarget
# after triage moves them (triage re-runs on neither), stale pin ->
# the new diff goes to the review lane, which also strips the
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
# label. A manually applied "parked" label has no marker and never
# skips. Unreadable head/base fails open like the label read.
if [ "$AUTO_REVIEW" = "true" ]; then
# No `printf | grep -q` pipes below: under `set -o pipefail`
# the -q consumer exits at the first match and closes the
# pipe, so the producer (printf builtin or gh) dies on EPIPE
# with 141 and pipefail surfaces that as FALSE even when the
# marker matches — on busy PRs (>64 KB of bot comment bodies)
# the skip would silently never fire. Herestrings and [[ == ]]
# pattern matches have no pipe and no early-exit producer.
#
# Read failures stay fail-open but are never silent: each
# failed read writes its own summary line, so a skip disabled
# by an API failure, rate-limit window, or token regression is
# diagnosable from the run artifacts — and a failed read is
# never reported as a stale pin, which would assert a mismatch
# that was never observed (R5-4, #9193 review).
LABELS_READ_OK=true
LABELS="$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels?per_page=100" --paginate --jq '.[].name' 2>/dev/null)" || LABELS_READ_OK=false
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
if [ "$LABELS_READ_OK" = false ]; then
echo "status/on-hold skip gate: label read failed — failing open, running the review lane (#9193)." >> "$GITHUB_STEP_SUMMARY"
elif grep -Fxq 'status/on-hold' <<< "$LABELS"; then
PR_REFS="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq '[.head.sha, .base.sha] | @tsv' 2>/dev/null || true)"
IFS=$'\t' read -r HEAD_SHA BASE_SHA <<< "$PR_REFS"
MARKERS_READ_OK=true
MARKERS="$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments?per_page=100" --paginate --jq '.[] | select(.user.login == "github-actions[bot]" or .user.login == "qwen-code-ci-bot") | select(.body | startswith("<!-- qwen-triage on-hold sha=")) | .body' 2>/dev/null)" || MARKERS_READ_OK=false
if [ -n "$HEAD_SHA" ] && [ -n "$BASE_SHA" ] && [ "$MARKERS_READ_OK" = true ] && [[ "$MARKERS" == *"qwen-triage on-hold sha=${HEAD_SHA} base=${BASE_SHA}"* ]]; then
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
echo "Skipping automatic review: PR #${PR_NUMBER} carries status/on-hold pinned to head ${HEAD_SHA} and base ${BASE_SHA} (triage-only route). A maintainer can run @qwen-code /review explicitly." >> "$GITHUB_STEP_SUMMARY"
echo "should_run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ -z "$HEAD_SHA" ] || [ -z "$BASE_SHA" ]; then
echo "status/on-hold present but the live head/base SHA was unreadable — failing open, running the review lane (#9193)." >> "$GITHUB_STEP_SUMMARY"
elif [ "$MARKERS_READ_OK" = false ]; then
echo "status/on-hold present but the marker comments were unreadable — failing open, running the review lane (#9193)." >> "$GITHUB_STEP_SUMMARY"
else
echo "status/on-hold present but not pinned to the live head/base (${HEAD_SHA}/${BASE_SHA}) — running the review lane (#9193)." >> "$GITHUB_STEP_SUMMARY"
# The stale pin means the triage-only exemption no longer
# matches the live diff, so strip the label too. It sits in
# stale.yml's exempt-pr-labels and Stage 1f applies it only
# once at open time — without this the PR would skip the
# review lane AND never age out via the stale bot.
if gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels/status%2Fon-hold" -X DELETE 2>/dev/null; then
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
echo "Removed stale status/on-hold so the stale bot can age this PR (#9193)." >> "$GITHUB_STEP_SUMMARY"
else
echo "Failed to remove stale status/on-hold — leaving it in place (#9193)." >> "$GITHUB_STEP_SUMMARY"
fi
fi
fi
fi

if [ -n "$TRIGGER_COMMAND" ]; then
set -f
for token in $TRIGGER_COMMAND; do
Expand Down
143 changes: 143 additions & 0 deletions .qwen/skills/triage/references/pr-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,148 @@ If any file matches (the strongest triage-time signal — 10 of 31 reverted PRs

This signal is NOT a terminal gate — it does not stop the review or close the PR. It escalates review depth and flags risk so the reviewer knows where to focus. A PR that touches high-risk paths but passes full review with clean E2E verification can still be approved.

**1f. Non-functional change routing (triage-only outcome):**

Behavior-neutral maintenance PRs — internal renames, formatting, comment/JSDoc
fixes — burn the full multi-stage review while carrying no observable risk
(#7411). Route them to a lightweight triage-only outcome instead, so review
capacity goes to PRs that can break something. The classifier must be
conservative: **when in doubt, take the full path.** A false skip (a risky
change classified as neutral) silently loses review coverage — that is the
dangerous failure mode here.

**Applies only to unattended CI runs** (`GITHUB_EVENT_NAME=issues` or
`pull_request_target`). Explicit re-runs (`issue_comment`,
`workflow_dispatch`) and local invocations always take the full path — a
human asked for a review, so give them one.

**Preconditions — every one must hold before this route is even considered:**

- Stages 0 and 1a–1e pass without escalation, and 1e matched no high-risk path;
- the title is not a `fix` type — a fix claims a behaviour change by definition;
- no core module paths (Stage 0 list), no `.github/workflows/**`, no other
YAML (`*.yml`/`*.yaml` anywhere, e.g. `.yamllint.yml`,
`.github/dependabot.yml`, `.github/release.yml`), no `Makefile`,
`Dockerfile`, or `.github/scripts/**`, no package manifests or lockfiles,
no schema/generated files, no prompts/system instructions or other
model-visible text, no user-facing documentation or CLI help strings, no
broken-link changes;
- at most 100 production logic lines (Stage 0 size calculation).

**Classification — 100% certainty required.** Every hunk must be exactly one of:

1. a source-comment or JSDoc-only edit;
2. a whitespace/formatting change with no token-level semantic change — never
in files where whitespace IS syntax (YAML indentation, Makefile recipe
tabs, shell and Dockerfile line continuations); such edits send the whole
PR to the full path;
3. an internal identifier rename (variable, field, local function) where every
occurrence is updated consistently within the diff and no external contract
changes — no exported symbol, CLI flag, config key, serialized field, or
snapshot/test expectation.

A hunk that touches a string literal, a number, control flow, a dependency,
configuration, or anything you cannot certify as behaviour-neutral sends the
whole PR to the full path. Title, description, and line count are not
classification evidence — read the diff.

**Triage-only outcome (terminal — no review submitted):**

1. Apply the existing label (never create one) AND pin the skip to the
triaged head/base SHA pair with a marker comment — the review lane honours the
label only while this pin matches the live head and base, so a later push or retarget
re-enables review automatically (#9193):

```bash
BASE_SHA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json baseRefOid --jq '.baseRefOid') || exit 1
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
[ -n "$BASE_SHA" ] || { echo 'empty base SHA — fail closed'; exit 1; }
MARKER_FILE="$(mktemp "${RUNNER_TEMP:-/tmp}/qwen-triage-on-hold-marker.XXXXXX")"
printf '%s' '<!-- qwen-triage on-hold sha=<HEAD_SHA> base=<BASE_SHA> -->' > "$MARKER_FILE"
.github/scripts/upsert-bot-comment.sh "$REPO" "$PR_NUMBER" 'qwen-triage on-hold sha=' "$MARKER_FILE"
rm -f "$MARKER_FILE"
gh api --method POST "repos/$REPO/issues/$PR_NUMBER/labels" -f labels[]='status/on-hold'
```

Fail closed, in this order: post the marker FIRST. If the upsert fails
(retries exhausted), stop — do not apply the label and do not post the
triage-only outcome. If the label application fails AFTER the pin
posted, stop the same way — do not post the outcome: an orphaned pin
alone cannot skip (the gate requires the label too), and a re-run of
the triage upserts the same pin in place and retries the label; but an
outcome posted without the label would promise a skip the gate does
not grant. The review-lane gate requires the label AND a pin
matching the live head and base (a label without the pin runs the review lane),
so the harm of a partial state is not a silent skip — it is the inverse:
the outcome comment promises that no review verdict will post
automatically while the unpinned label leaves the automatic lane
running on every push, and the label alone reads as "parked" with no
pin to explain why. All three — pin, label, outcome — post together or
not at all.

`<HEAD_SHA>` is the head commit you triaged — the same SHA quoted in the
Stage 1 "Reviewed at" footer. Post the marker through the author-scoped
upsert helper (never a bare `gh pr comment`): a re-run of the same
triage then PATCHes the existing marker in place instead of minting a
byte-identical duplicate, and a later triage of a new head updates the
same comment to the new pin. The lookup marker is the SHA-less prefix
so the existing comment is found across heads.

2. Post the Stage 1 comment using the triage-only variant below.
3. Stop — no Stage 2, no Stage 3, no approval, no CHANGES_REQUESTED.
Comment thread
yiliang114 marked this conversation as resolved.
Resolved

The automated review workflow checks the live `status/on-hold` label AND the
marker pin before invoking the model and skips the automatic lane only while
the marker's SHA pair matches the live head and base (`qwen-code-pr-review.yml`,
'Resolve PR context'). Triage does not re-run on `synchronize` or base retarget,
so either change after the triage-only outcome invalidates the pin and the new
diff receives the full review — a benign v1 must not grant later content or
base changes a silent bypass. Maintainers pull a full review at any time with
`@qwen-code /review` — explicit triggers bypass the label check — and removing
the label re-enables the automatic lane on the next push. A manually applied
`status/on-hold` carries no marker and never skips. Nothing here blocks merging
or closes the PR.

```markdown
<!-- qwen-triage stage=1 -->

Thanks for the PR!

Template looks good ✓

This looks like a behaviour-neutral maintenance change (<one line of evidence,
e.g. "comment/JSDoc fixes only, across N files">), so it takes the lightweight
triage-only path instead of the full automated review: `status/on-hold` is
applied and no review verdict will post automatically<except that when the
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
triggering action is `ready_for_review` (the PR was just marked ready), a
review run may already be in flight — the two lanes cannot synchronize, so
say "a review verdict may still post from the run this pin raced; the pin
applies to later runs" instead of the absolute promise>. Nothing is blocked —
a maintainer can start a full review at any time with `@qwen-code /review`,
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
and removing the label re-enables the automatic lane. If this cleanup pairs
naturally with a substantive change, consider folding it in there.

<details>
<summary>中文说明</summary>

感谢贡献!

模板完整 ✓

这是一个行为中性的维护性改动(<一句话证据,如"仅 N 个文件的注释/JSDoc 修正">),
因此走轻量 triage-only 路径,不再自动进入完整评审:已添加 `status/on-hold`
标签,不会自动发布评审结论<例外:当触发动作是 `ready_for_review`(PR 刚被标记为就绪)时,
可能已有一个评审运行在路上——两条通道无法互相同步,此时改说"与该 pin 竞争的这次运行
仍可能发布评审结论;pin 对后续运行生效",不要给出绝对承诺>。合并不受阻塞——maintainer 可随时用
`@qwen-code /review` 发起完整评审;移除该标签后,下次推送会恢复自动评审。
如果这项清理适合随某个实质性改动一起提交,也可以考虑合并进去。

</details>

— _Qwen Code · qwen3.7-max_

<sub>Reviewed at `<HEAD_SHA>` · re-run with `@qwen-code /triage`</sub>
```

Post a single Stage 1 comment. Be direct — say what you actually think, not what's polite:

```markdown
Expand Down Expand Up @@ -305,6 +447,7 @@ Save this comment's ID. Terminal exits — stop here if any applies:
- Template failure (Stage 1a) → stopped.
- Problem does not exist (Stage 1b) → request changes, do not proceed to Stage 2.
- Direction escalated (Stage 1c) → stop here.
- Non-functional triage-only route (Stage 1f) → `status/on-hold` applied and triage summary posted, do not proceed to Stage 2.

### Stage 2: Review + Test

Expand Down
Loading
Loading