qwen-code/.github
Shaojin Wen 1f9318f974
feat(triage): stop in-agent CI polling, finalize evidence and approval after CI completes (#7693)
* feat(triage): stop in-agent CI polling, finalize evidence and approval after CI completes

The triage agent's Stage 2b polled pending checks for up to 10 minutes, but
this repo's unit suite runs ~30 minutes, so the poll always burned its full
budget, gave up with 'CI still running', and Stage 3 could then approve before
the suite finished (observed on a PR approved 12 minutes before its Test job
completed).

Split the wait out of the agent entirely:

- pr-workflow.md Stage 2b now forbids polling: fetch check-runs once, report
  pending checks honestly, and wrap the CI table in qwen-triage-ci region
  markers keyed to the reviewed SHA.
- Stage 3 defers a clean-verdict approval when checks are still pending: the
  comment carries an approve-on-green marker instead of an immediate APPROVE.
- New qwen-triage-finalize.yml fires on workflow_run completion of 'Qwen Code
  CI' / 'E2E Tests' and, with plain bash over the API (no model, no checkout),
  rewrites the marked table region with the settled results and posts the
  commit-pinned approval only when every check landed green — failing closed
  on red checks, a moved head, or a closed/draft PR, and flipping the triage
  status comment to say which way it resolved.

Markers are honored only in comments authored by the bot identity itself, and
check names (attacker-influenced on fork PRs) go through the same HTML-escape
chain the skill mandates for file paths.

Stage comments now land ~10 minutes sooner and the approval, when deferred,
lands at CI completion with full evidence instead of before it.

* fix(triage): address finalize review — broken red gate, table truncation, dead trigger

Review findings on the finalize workflow, all reproduced before fixing:

- Blocker 1: the RED jq used the array-first membership form, where | rebinds
  . and .conclusion indexes an array — jq exits 5 every run, RED comes back
  empty, [ "" -gt 0 ] errors, and control falls through to the approve path:
  a red CI auto-approved. The gate now binds the conclusion before the
  membership test (IN(...)), and the counters are numeric-validated so any
  future jq failure reads as 'cannot attest', never 'approve'.
- Blocker 2: the table rendered raw check-runs — on a real PR (96 runs, 35
  names, 68 skipped) alphabetical sort + head -60 truncated away every actual
  test job. table_rows now dedups per name (latest run), drops skipped rows,
  and sorts running/non-green first so the cap can only cut green rows.
  Replayed against the same PR: 96 rows -> 16, unit suite present.
- The approval gate now reads workflow runs filtered to event=pull_request
  (deduped per workflow) instead of head-SHA check-runs, which also carry
  long-running bot orchestration jobs that would wedge PENDING above zero at
  the exact moment the last CI workflow fires — silently dropping the
  deferred approval forever. The skill's Stage 3 PENDING count matches.
- E2E Tests had no pull_request trigger (dead entry); the workflows list is
  now exactly the six pull_request-triggered workflows, so the last finisher
  always re-fires the job.
- Head/state re-check moved before the red/deferred verdicts so a
  cancel-in-progress firing on a stale SHA cannot stamp a red status over
  the new head's comment; the still-deferred branch now updates the status
  comment instead of staying invisible.
- replace_region fails closed when the end-marker text only precedes the
  begin marker (awk END guard) — previously that shape truncated the comment
  body, eating the signature and reviewed-commit footer.
- Region content is deterministic (no run URL) so the no-op cmp works;
  empty run list or unavailable gate skips approval; comment wording fixed
  (workflow_run jobs are attributed to the default branch, so the self-check
  exclusion is belt-and-braces, not load-bearing).

Tests now execute the decision logic, not just grep for it: gate_counts and
table_rows run against fixtures covering every conclusion class, non-PR
events, re-run dedup, skipped filtering, ordering, and both marker-order
failure shapes. 30/30 passing.

* fix(triage): keep a stale finalize firing from clobbering the newer review's status comment

The status comment is deliberately not SHA-scoped (the triage workflow
creates it unscoped; scoping only the finalize side would orphan the
pairing), so a finalize firing for an old SHA that loses the race against a
newer head's green approval would overwrite the  status with a stale
warning. Guard the stale path: when the current head already carries bot
sha= markers (a re-review owns the status comment), stay silent; when the
head moved with no re-review yet — triage does not auto-rerun on
synchronize — the stale note is accurate and still posts. Closed/draft PRs
now just log instead of flipping the status.

* fix(triage): close the guardrail bypass and align the finalize table with the gate

Second review round, all four findings reproduced or confirmed before fixing:

- The approve-on-green marker was emitted in Step 1 while the fork-refactor
  GUARD only ran in Step 2 — a marker that slipped out on a fork refactor
  would have been honored by the finalize job on green CI, bypassing the
  guardrail entirely. GUARD now computes in Step 1 and gates the marker's
  emission, and the finalize job re-asserts it structurally from the PR
  state it already fetched (null head.repo = deleted fork = blocked), with
  a 'guarded' status message instead of an approval.
- table_rows now restricts check-runs to the suites of the same deduped
  event=pull_request workflow runs the gate trusts. Without it, 5 of 8
  rendered rows on this PR's own head were bot plumbing presented as CI
  evidence; with it, 115 raw check-runs reduce to exactly the 3 CI rows.
- A firing that saw PENDING>0 after the approval landed flipped the status
  comment back to 'deferred' with nothing to ever right it; the
  already-approved branch now repairs the status.
- Zero surviving table rows (failed runs fetch, missing suite ids) skips
  the region rewrite instead of blanking the agent's table, and
  replace_region refuses an empty region file (an unchecked getline would
  have deleted the region and its markers unrecoverably).

Nits: the house github.repository guard on the job, the table header
matches the skill template, and the run-URL stays out of the region so the
no-op cmp keeps working.
2026-07-25 00:58:16 +00:00
..
actions/post-coverage-comment Upgrade GitHub Actions to latest versions (#3683) 2026-06-27 17:03:32 +00:00
assets/worktree-toggle feat(web-shell): surface worktree isolation in the new-session empty state (#7365) 2026-07-21 07:38:33 +00:00
ISSUE_TEMPLATE chore: re-organize labels for better triage results (#819) 2025-10-17 19:49:11 +08:00
scripts fix(ci): don't fail triage cleanup when there is nothing to clean (#7688) 2026-07-24 17:44:25 +00:00
workflows feat(triage): stop in-agent CI polling, finalize evidence and approval after CI completes (#7693) 2026-07-25 00:58:16 +00:00
actionlint.yaml ci: matrix ECS runner update + sudo install + repository_dispatch trigger (#7513) 2026-07-23 05:50:04 +00:00
CODEOWNERS chore: simplify CODEOWNERS to package-level rules (#7376) 2026-07-21 02:39:33 +00:00
dependabot.yml Limit dependabot PRs to security updates (#6657) 2025-08-20 22:24:43 +00:00
pull_request_template.md docs(agents,pr-template): add Working Principles and restructure PR template (#4496) 2026-05-25 19:15:35 +08:00
release.yml ci: auto-skip internal CI changes in release notes (#7251) 2026-07-20 11:10:55 +00:00