qwen-code/.github
Shaojin Wen 68c9032cd5
feat(autofix): direct takeover of maintainer-fork PRs (#7213)
* feat(autofix): direct takeover of maintainer-fork PRs

Maintainer-approved v2: many maintainers work from personal forks, and
adoption-snapshotting breaks their local workflow. A fork PR is now
directly manageable when three live conditions hold — the takeover
label, 'Allow edits from maintainers' (org-owned forks cannot enable
it; adoption remains their path), and a fork author who holds write+
RIGHT NOW (the same live-privilege rule as the comment command, so an
ex-member's fork can never summon secret-bearing runs).

Plumbing:
- Scan: fork takeover candidates are admitted per candidate (allow-
  edits + no-skip filtered in jq; the author's live write+ gate is one
  permission call each — a rare set); every matrix target now carries
  its head repo.
- Address: prepare fetches the fork branch (origin has no copy) and
  checks out FETCH_HEAD with hooks already severed; the eligibility
  gate re-verifies takeover + allow-edits + author write+ live; the
  report step pushes back to the fork via the allow-edits grant.
- Triggers: fork pull_request label events carry NO secrets, so the
  route notes them and the next scheduled scan engages (≤10m); the
  comment command now toggles fork PRs too (write+ senders only — fork
  authors stay silently dropped) and refuses only when allow-edits is
  missing, with the actionable ask. The scan posts a first-pickup
  engage ack (identity-verified, deduped on any existing ack, ic.json
  re-fetched so the same scan counts under the fresh window key) —
  closing the fork/manual-label ack gap and anchoring the round
  window.

Behavioral coverage: fork-candidate admission jq (allow-edits, skip,
in-repo exclusion, tsv rows), eligibility across
fork+takeover+allow-edits+write / no-allow-edits / read-author, the
toggle's fork split (refusal vs managed), plus plumbing pins (fork
fetch/push forms, head_repo threading, first-pickup ack dedup).
60/60 + 12/12.

* fix(autofix): strip stray patch-artifact quotes after two fi keywords

Two inserted blocks ended 'fi"' — the quotes balanced against each
other inside the same script, so bash -n stayed green while runtime
would have lexed 'fi' as a command word and swallowed the span between
them (the fork head-repo resolution tail and the engage-ack block)
into one string. Removed both, and pinned the artifact class in the
suite: a lone fi/done/esac followed by a quote now fails the tests.
61/61 + 12/12.

* fix(autofix): author-filtered, re-armable first-pickup engage ack

Reverse-audit findings on the scan-side ack:

- Dedup was a raw grep over ic.json — a forged human comment carrying
  the engaged marker would have suppressed the real ack (and with it
  the window anchor). Dedup now selects bot-authored engaged acks via
  jq, same author rule as the window key itself.
- Fork PRs get NO ack job (label events carry no secrets), so the
  documented re-arm gesture — remove and re-add the label to reset the
  round window — silently kept the old window: any historical ack
  blocked a fresh one. When a bot ack already exists, the scan now
  compares it against the takeover label's latest application time
  (issue events, fetched only in that rare case); a newer application
  posts a fresh ack, resetting window and cap as documented.

Coverage: verbatim jq replays for both selections (forged-marker and
released-marker exclusion, label-name filter, sort|last) plus the
lexicographic re-arm gate pin. 61/61 + 12/12.

* fix(autofix): review round 1 — ack ordering, dry-run, ghost-engage gate

Addresses the maintainer review on #7213 (all findings confirmed):

- Critical: the first-pickup ack read ic.json BEFORE the per-PR fetch —
  the first takeover candidate killed the whole scan step (missing file
  under -eo pipefail; every in-repo label-forced scan regressed), and
  later candidates dedup'd against the PREVIOUS PR's comments (bot PR
  ahead → fresh ack every 10min → window reset → cap never binds). The
  block now sits directly AFTER the fetch; its post-ack re-fetch keeps
  the downstream MARKERS/window-key reads fresh. A contract pin asserts
  the fetch precedes the first ack-timestamp read.
- Medium: the ack now honors DRY_RUN (log only, window key untouched).
- Medium: the command refused forks only for missing allow-edits — a
  below-write fork author was a silent ghost engagement (label sticks,
  no ack, nothing ever manages it). The command now mirrors the scan's
  author write+ gate with an actionable bilingual refusal. Found while
  fixing it: PR_INFO never fetched maintainerCanModify (or author), so
  EVERY fork toggle refused regardless of allow-edits — the test stub
  carried the field and masked the gap. Both engage-side fork gates are
  now also scoped to 'add': release is never blocked.
- Low: the two new paginated jq reads are slurped (add-merged) so >100
  comments/events cannot scramble the timestamp comparisons; replays
  now feed two concatenated page-documents. Fork fetch pins
  refs/heads/ (tag shadowing); HEAD_REPO_FULL guards each component
  (deleted fork = owner XOR name empty); fork-rotation caveat
  documented; forced-path refusal mentions the scheduled fork path.
- Security caveat adopted: prepare proves fork push access with a
  --dry-run push right after checkout (allow-edits rides the
  classic-PAT grant only) and discards gracefully instead of 403ing
  after a full agent round.

61/61 + 12/12; YAML parses; every run block passes bash -n.

* fix(autofix): fork targets keep base/branch invariants + 3 hardening follow-ups

Blocking (yiliang114): the last fork elif ends the eligibility ladder
for every eligible fork, so the LIVE_BASE/LIVE_BRANCH re-checks were
unreachable for exactly the PR class the loop fetches and pushes — a
labeled fork retargeted off main (or head-renamed) between scan and
address would have had conflicts resolved against the wrong base. The
base/branch invariants now sit ABOVE the fork chain (comment explains
why the order is load-bearing), with replay cases pinning a
retargeted and a renamed fork to the discard path.

Follow-ups from the same review, all adopted:
- PR_LIVE re-reads headRepositoryOwner/headRepository; a fork renamed
  or transferred since the scan discards at the live re-check (moved
  or unresolved, fail-closed) instead of fetching and token-pushing a
  stale path. The replay's fork fixture now carries its head repo and
  the harness provides the matrix HEAD_REPO to compare against.
- The fork fetch failure (force-push/rename race) discards through the
  standard no-action path instead of a red run.
- The first-pickup engage ack defers to the in-repo label event's
  DEDICATED ack job within a 3-minute grace after the label lands, so
  a concurrent ack job is never double-posted (which would shift the
  round-window anchor); a failed ack job is still healed by the next
  scan, and forks (no ack job) keep immediate pickup. Events are read
  once, before the branch split.

Both hooks-order regex windows widened to span the new fork-arm guards
(the assertions are about order; one hooksPath site genuinely covers
both checkout arms). 61/61 + 12/12.

* test: raise timeout ceiling for I/O-bound tests flaky under CI contention

The self-hosted CI runners are heavily oversubscribed (core runs
maxThreads: 16), and a recurring class of tests blows vitest's 5s
default timeout purely under that contention — not from any logic
fault. Observed repeatedly across unrelated PRs (#7213, #7219, and
noted in prior sessions):

- packages/core/src/utils/shell-ast-parser-lazy.test.ts — fully
  mocked, but the dynamic import + async coordination exceeds 5s when
  16 threads contend.
- packages/cli/src/serve/workspace-registration-store.test.ts —
  tempdir round-trip.
- packages/core/src/extension/github.test.ts > extractFile — its
  waitForFileData helper polled a FIXED 1_000 setImmediate turns, which
  elapse in <100ms while the tar extraction I/O is still catching up,
  throwing 'Timed out waiting for extracted data'.

Fixes:
- testTimeout: 15000 in the core and cli vitest configs — 3x the
  default. Assertions still fail instantly; only the timeout ceiling
  grows, so this masks no logic bug (a real hang still fails, just
  later, and the job timeout still bounds it).
- waitForFileData now polls a real ~10s wall-clock budget
  (2_000 x 5ms) instead of a fixed iteration count, so a slow
  extraction is awaited rather than raced. Stays under the 15s ceiling.

These are the deterministic root-cause fixes for the flake class the
autofix loop and CI Failure Patrol were papering over with reruns.

---------

Co-authored-by: wenshao <wenshao@example.com>
2026-07-19 12:49:55 +00:00
..
actions/post-coverage-comment Upgrade GitHub Actions to latest versions (#3683) 2026-06-27 17:03:32 +00:00
ISSUE_TEMPLATE chore: re-organize labels for better triage results (#819) 2025-10-17 19:49:11 +08:00
scripts ci(web-shell): denoise cross-job font-AA so visual previews stop false-flagging (#7210) 2026-07-19 11:27:08 +00:00
workflows feat(autofix): direct takeover of maintainer-fork PRs (#7213) 2026-07-19 12:49:55 +00:00
actionlint.yaml feat(ci): on-demand tmux real-user testing for PRs (#5203) 2026-06-21 11:56:29 +08: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 chore: add .github/release.yml to support skip-changelog label (#4327) 2026-05-20 22:30:52 +08:00