mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* feat(autofix): escalate stopped takeover PRs and age out unanswered pauses Takeover PRs that hit the round cap (or a circuit breaker) went silent: no label, no dashboard entry, no escalation — five PRs had been paused for days. The fleet shepherd only tracked bot-authored PRs, so the whole 35-PR human takeover pool was invisible. The autofix scan now applies an autofix/needs-human label whenever a PR reaches its cap (the write rides every cap detection, so already-paused PRs backfill on the regular scan rotation), and removes it wherever management resumes or a human releases the PR. The fleet shepherd enumerates the takeover pool onto its dashboard (state, stop reason, pause age, plus an awaiting-human section for released PRs) and gains a single bounded lever: a takeover whose pause went unanswered for AUTO_RELEASE_DAYS days gets its takeover label removed with a bilingual summary, keeping the needs-human label as the filterable TODO. Resume evidence newer than the pause notice — bot markers, trusted re-arm commands, fresh labeled events — vetoes the release; every read fails closed and a per-tick cap bounds blast radius. Co-authored-by: Qwen-Coder <[email protected]> * fix(autofix): harden the takeover auto-release against review round 1 Addresses the PR review's two Criticals and eleven Suggestions: - Command-comment resume evidence now counts only while FRESH (2h grace) and UNSUPERSEDED by a refusal ack (fork-refused/base-refused/ skip-blocked) — an ignored command expires instead of vetoing the release forever, and no permission logic is mirrored from the route. - The release lever's population comes from the needs-human enumeration (needs-human ∩ takeover), never the display window; both enumerations cap at 100 with saturation warnings, and a failed enumeration degrades to an error row so the dashboard write (and its liveness watermark) always runs. - The auto-release summary posts before the label DELETE, dedup'd by its own marker — neither half can strand the other on a transient failure. - Awaiting-human rows use neutral wording (capped bot PRs land there too) and a shepherd-side heal clears stale needs-human labels left by manual UI releases on fork PRs (human unlabeled event, budgeted, skip-vetoed). - Fail-closed deferrals now still render a dashboard row (the row append moved outside the evaluation arms); tick summary and dashboard header report the same counters; days_since() replaces pasted epoch math. - Tests: command-evidence gate replays (fresh/refused/expired/acked), refusal-variant and command-string cross-file pins, DELETE-target and fallback-assignment pins, heal jq replays, unified-row-render pin. Co-authored-by: Qwen-Coder <[email protected]> * fix(autofix): close review round 2 — cycle-scoped heal, retryable summary - The stale-label heal now only counts a human unlabel NEWER than the latest label-apply, so an unlabel from an earlier takeover cycle can no longer heal the current cycle's needs-human after an auto-release (R2-1). - The summary dedup marker is scoped to the current pause cycle (markers older than the latest cap notice are ignored), so a re-armed and re-capped PR still gets its second release summary (R2-4). - The two DELETE levers no longer redirect act()'s stdout, keeping the DRY-RUN preview and failure warning visible (R2-5). - AUTO_RELEASE_DAYS is base-10 normalized after the numeric guard, so a zero-padded repo variable can't silently kill the lever (R2-6). Co-authored-by: Qwen-Coder <[email protected]> * fix(autofix): close review round 2 — cycle-scoped markers and mutation-tested pins - Heal is cycle-correlated: only a human unlabel NEWER than the latest label-apply counts (an earlier cycle's unlabel can't heal this cycle). - The release summary dedup marker is scoped to the current pause cycle, so a re-armed and re-capped PR still gets its second summary. - act() stdout is no longer redirected on the two DELETE levers (DRY-RUN preview and failure warning stay visible). - AUTO_RELEASE_DAYS is base-10 normalized so a zero-padded repo variable cannot silently kill the lever. - Doc/workflow-header text corrected to the implemented order (summary first, marker-dedup'd) and to the idle-backoff backfill timing. - Mutation-tested test pins for every gap the reviewer probed: days_since replay, NH_PREFIX interpolation + truth map, loop-1 deferral, full cross-file marker/refusal-set equality, label-constant cross-pin, EVENT_TS merge + promotion ordering, CLEANUPS increment, unclassified headline classification, filter byte-identity, exit-spelling ban, @uri encoding, sort/field-list attribution, paginate shapes, scope --arg bindings, LIVE_LABELS_JSON wiring, positional append pin, label-create idempotence + POST guard, and per-branch removal attribution in the toggle replay. Co-authored-by: Qwen-Coder <[email protected]> * fix(autofix): close review round 3 — release-ack label gate and shared classifiers - R3-1 (Critical): the every-scan cap-branch label POST is now suppressed when a release ack (takeover-ack released) is newer than the last re-arm, so a released bot PR is not re-labeled each scan (which would fight every release-side removal and ping-pong with the shepherd cleanup). A re-arm advances the window past the release ack, re-enabling the label. - R3-7: the /retry re-arm's needs-human removal now honors autofix/skip, mirroring the takeover-command guard — a frozen PR keeps its only filterable escalation state. - R3-2 (Critical): the takeover-enum error row no longer claims 'no release evaluation ran' — the lever is fed by the needs-human enumeration. - R3-8: the conflict-dispatch lever refuses a paused (needs-human) PR instead of spending a dispatch slot the scan would refuse. - R1-10: extracted pending_checks()/failed_test_url() helpers so both dashboard loops share one CI-status classifier (the round-1 reply was wrong that the restructure removed this duplication — it did not). - Hardened the mutation-tested pins: exact terminal-headline count (5), full rearm DELETE line + single-API-write, AUTO_RELEASE_DAYS guard order, runRearm env/stub/assertion for the /retry DELETE + skip guard, and the scope-guard comparison operator. Co-authored-by: Qwen-Coder <[email protected]> * fix(autofix): close review round 4 — label-lifecycle hardening - R4-C1: the conflict-dispatch lever reads needs-human from the LIVE label payload (after live_skip), not the tick-start snapshot, so a label applied after enumeration is still honored. - R4-C2: a re-armed PR that still carries needs-human (a resume-side removal failed) now gets a bounded, skip-vetoed cleanup retry instead of staying pinned in the paused population forever. - R4-C3: the per-tick release budget is consumed before the first external write — a DELETE outage can no longer mutate many PRs while RELEASES=0. - R4-C4: dashboard row routing follows post-action label state — a released PR moves to Awaiting human, a healed one drops off entirely. - R4-C5: the AUTO_RELEASE_DAYS guard also rejects over-long digit strings before any arithmetic (Bash-int overflow would wrap negative and pass -ge). - R4-32: takeover-command stop only removes needs-human when the takeover release actually landed (REMOVED_OK; 404 counts) — a failed release no longer strands the escalation label while latching RELEASE_ACKED. - R4-2: the /retry skip guard fails closed — an unreadable label state keeps the label (mirrors takeover-ack's exit-1 convention). - R4-3: the takeover-ack released arm and the stop branch both honor autofix/skip when removing needs-human. - R4-S1: producer headlines must be explicitly classified terminal or transient — an unclassified headline now fails the cross-file test. - Pins updated/added for every behavior above. Co-authored-by: Qwen-Coder <[email protected]> * fix(autofix): close review round 4 — robust release detection and marker-true gates - R4-1/R4-5: release detection now uses the takeover unlabeled EVENT (recorded on every removal path, unlike the tolerated-lost ack comment), and the suppression only applies to human-authored PRs — a bot PR released from takeover returns to standard management and keeps the cap notice + escalation label. - R4-6: the conflict-dispatch lever requires marker truth (conflict_paused) — an armed PR with a stale needs-human label is dispatched normally. - R4-C1: the pause check reads needs-human from the live label payload. - R4-C2: re-armed PRs with a stale label get a bounded cleanup retry. - R4-C3: the release budget is consumed before the first external write. - R4-C4: dashboard rows route on post-action label state. - R4-C5: AUTO_RELEASE_DAYS rejects over-long digit strings before arithmetic. - R4-32/R4-2/R4-3: stop/ack/retry removal paths gate on REMOVED_OK and skip. - R4-9/R4-10/R4-13: membership check, STATE escaping, HM_OK-branched error row. - R4-14: command evidence requires a write/maintain/admin commenter. - R4-11/R4-15/R4-24: behavioral replays for the classifiers, the release jq, and the gate nesting. Co-authored-by: Qwen-Coder <[email protected]> * fix(autofix): keep the label-DELETE idiom byte-identical across workflows R4-32's REMOVED_OK tracking reworked the takeover-command stop branch's 404-tolerance block, breaking the pr-self-report-label ↔ qwen-autofix contract test that pins the two workflows' label-DELETE idiom byte-identical. Keep the canonical idiom and derive REMOVED_OK from REMOVE_ERR's content afterward (empty = landed, 404 = already off, anything else = release did not land) — same behavior, contract intact. Co-authored-by: Qwen-Coder <[email protected]> * test(autofix): close review round 4b — mutation-tested harness hardening - R4-4: re-bound the conflict-lever regex spans and anchor on conflict_paused so the pin can't resolve live_skip against the sync lever's call site. - R4-16: runAck records gh calls and asserts per-branch needs-human DELETE counts (engaged/released=1; base-refused/skip=0). - R4-17: pin the first-pickup scan DELETE inside the engage-ack success branch. - R4-18/R4-19: ordering pins — takeover POST before needs-human DELETE (engage), marker comment before cleanup DELETE (/retry). - R4-20: deleteFail stub branch replays non-404 (warns, status 0) and 404 (silent) DELETE outcomes. - R4-21: identity-failure paths assert no DELETE ran. - R4-22: runRearm stub serves labels only when --json labels is requested. - R4-23: full api-write census pinned (exactly api user + one DELETE). - R4-25: skip fixture uses the production multi-label shape. - R4-28: loop-2 fetch pins include the jq -s 'add // []' merge program. - R4-29: cmdGate scenario where a refusal is OLDER than the fresh command. - R4-30: takeoverEnum asserts its own sort:updated-asc qualifier. - R4-31: multi-entry fixtures pin the max/last/length aggregation operators on CMD_TS, EVENT_TS, REASON, SUMMARY_POSTED, and the heal lever's LATEST_LABEL_TS/UNLABEL_ACTOR programs. Co-authored-by: Qwen-Coder <[email protected]> * fix(autofix): close review round 5 — trust boundaries and evidence freshness - R4-5 residual: PR_META now fetches author so IS_BOT_AUTHOR actually resolves (the exemption was dead on arrival), with a behavioral replay. - R5-1: REMOVED_OK derives from the captured stream ('HTTP ' non-404 = not landed) instead of output emptiness — GitHub returns a body on success. - R5-2: /retry only drops needs-human when management actually resumes (takeover label present or bot-authored) — an auto-released human PR keeps its escalation label. - R5-3: conflict_paused requires a real cap notice AND a newer resume marker — label-present/notice-absent now fails closed toward paused. - R5-4: a failed permission read defers the release (PERM_READ_FAILED), never counts as no-permission — at both evaluation points. - R5-5: compute_resume_ts scans in-grace commands newest-first and permission-checks each (≤2 reads), so a stranger's echo can't shadow a maintainer's command. - R5-6: the release branch re-fetches evidence and recomputes resume state immediately before the first write. - R5-8: the heal re-checks the takeover label from the live payload before clearing needs-human. - R5-9: same-second ties resolve toward resume/release suppression in both files (RESUME>=TERM; RELEASE_ACKED >= window). - R5-10: the heal anchors to the current pause boundary (latest needs-human apply event); an absent anchor skips the cleanup, fail closed. - Tests: whole-function compute_resume_ts replay (permission/shadow/tie/ grace/refusal cases), heal anchor fixtures, toggle stub models the real DELETE body, runRearm orphan case. Co-authored-by: Qwen-Coder <[email protected]> * fix(autofix): close review round 6 — contract-safe gates and subshell flag fix - B5: the bot-fleet enumeration failure now degrades to a loud error row and falls through (FLEET_OK gate) instead of exiting before the independently-fed takeover/needs-human processing and the dashboard's liveness-watermark write. - B12: the cap-branch LIVE_LABELS consent re-read fails closed on an unreadable gh pr view (a collapse to '' ignored a concurrently added skip for standard bot PRs). - R6-1/R6-19: the takeover-release landed flag is keyed on the DELETE exit status (LBL_DEL_FAILED set inside the pinned idiom's failure branch) — never on output text, which lies in both directions. The pr-self-report-label idiom evolves identically to keep the cross-workflow contract green (and its own 'removed' log line no longer lies either). - R5-4 residual: compute_resume_ts now returns via globals (RESUME_OUT/PERM_READ_FAILED) and both call sites invoke it directly — the previous subshell silently dropped PERM_READ_FAILED, leaving the fail-closed defer branches dead. - R6-3: command candidates are deduped by author before permission reads, so a stranger posting N commands can't burn the 2-read budget and shadow a maintainer's command. - R6-4: an unreadable release history is reported as such, not as 'released'. * fix(autofix): close review round 7 — lever starvation, re-arm anchoring, permission shadows - R5-7: the release lever gets its OWN enumeration of the paused population (takeover+needs-human, stale-first) instead of the long-lived needs-human display window — released-awaiting PRs aging back into that window could truncate exactly the fresh pauses that become release-eligible, starving the lever and making the zombie state permanent and self-feeding. - R6-3: the 2-read permission budget now sets PERM_READ_FAILED on exhaustion (it was failing open), and the candidate walk sorts newest-first per author (group_by+max_by+sort) instead of unique_by's alphabetical order, so two read-only strangers can't shadow a maintainer's newer command. - R7-1: the stale-label cleanup anchors on the current pause boundary (latest needs-human apply) and is marker-confirmed only — not keyed on TERM_TS, and never on command/label evidence — so a re-paused PR with a lost cycle-2 notice isn't read as re-armed on stale cycle-1 evidence. - R7-7: the /takeover stop success echo is gated on REMOVED_OK — a failed DELETE no longer logs 'removed'. - R7-2: TAKEOVER_COMMAND/RETRY_COMMAND mirrored into the shepherd env and passed via --arg, so the resume matcher can't drift from the route. - Tests: conflict_paused + re-arm guard behavioral replays, mirrored-command cross-file pin, engaged/released-with-skip ack matrix cells, LBL_DEL_FAILED branching, gnuDateShim hoisted to module scope, R4-24 nesting indices. Co-authored-by: Qwen-Coder <[email protected]> * test(autofix): complete the R4-16 ack-matrix DELETE-count coverage Add fork-refused and skip-blocked ack cases to the takeover-ack harness — management never resumed on either, so zero needs-human DELETEs, each asserted by total DELETE count (not just toContain). Co-authored-by: Qwen-Coder <[email protected]> * fix(autofix): close review round 8 — reachable re-arm cleanup, release race guards, pin census * fix(autofix): close review round 9 — honest release-failed ack, cleanup attempt budgets, dashboard single-owner routing - /takeover stop whose label DELETE failed no longer posts a 'Takeover released' ack: a release-failed variant names the retry (R9-4), and the R7-7 echo pair gains symmetric log pins (R9-3) - stale-label cleanups count ATTEMPTS like the release budget, so a DELETE outage trips the cap instead of leaving it inert (R9-5) - dashboard renders each both-label PR exactly once: loop 1 defers by paused membership, loop 3 is the render of last resort (R9-1/R9-13) - a 404 from the collaborators-permission endpoint classifies the author read-only instead of renewably deferring the release (R9-10) - cap-branch release evidence reuses the per-iteration events fetch under a success flag (R9-18); release-clock comment corrected (R9-11) - harness gates end-anchor the --json field list (R9-14/R9-15); the escalation POST and the ack-body census gain count pins (R9-16); the date shim answers only the +%s shape it emulates (R9-9) * fix(autofix): close review round 10 Criticals — exact HTTP 404 release classification, isolated replay fixtures Co-authored-by: Qwen-Coder <[email protected]> * fix(autofix): close review round 11 Criticals — engaged stale-ack guard, exact HTTP 404 permission classification --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]>
120 lines
6.3 KiB
YAML
120 lines
6.3 KiB
YAML
name: 'PR self-report label'
|
|
|
|
# When a PR closes an issue that its OWN author opened (self-reported and
|
|
# self-fixed), apply a label so a reviewer can see at a glance that the problem
|
|
# itself was not independently reported — worth a look that the issue is real,
|
|
# not only that the fix is correct. Metadata only: it reads the PR's linked
|
|
# issues and applies a label, and NEVER checks out the PR's code, so running
|
|
# under pull_request_target (needed for write access on fork PRs) is safe.
|
|
|
|
on:
|
|
pull_request_target:
|
|
types: ['opened', 'edited', 'reopened', 'synchronize']
|
|
|
|
permissions:
|
|
contents: 'read'
|
|
issues: 'write'
|
|
pull-requests: 'write'
|
|
|
|
concurrency:
|
|
group: 'pr-self-report-${{ github.event.pull_request.number }}'
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
label:
|
|
if: |-
|
|
${{ github.repository == 'QwenLM/qwen-code' }}
|
|
# Checks out nothing and runs no repository code (pull_request_target
|
|
# events use base-repo YAML), so the persistent ECS pool is safe and
|
|
# skips the saturated hosted queue. Kill-switch: MAINTAINER_ECS_RUNNER_DISABLED.
|
|
runs-on: '${{ (github.repository == ''QwenLM/qwen-code'' && vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true'') && fromJSON(''["self-hosted", "linux", "x64", "ecs-qwen"]'') || fromJSON(''["ubuntu-latest"]'') }}'
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: 'Label a self-reported PR'
|
|
# PR-controlled values reach the script ONLY through env, never
|
|
# interpolated into the run body — the injection-safe pattern for
|
|
# pull_request_target.
|
|
env:
|
|
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
REPO: '${{ github.repository }}'
|
|
PR: '${{ github.event.pull_request.number }}'
|
|
PR_AUTHOR: '${{ github.event.pull_request.user.login }}'
|
|
LABEL: 'review/self-reported'
|
|
run: |
|
|
set -euo pipefail
|
|
# Issues this PR closes — "Fixes/Closes #N" keywords AND Development
|
|
# sidebar links — and the login that OPENED each. Track whether the
|
|
# query succeeded: an API blip must never strip a correct label, so a
|
|
# failed query sets API_OK=false and blocks removal (fail-open) rather
|
|
# than masquerading as "no linked issues".
|
|
API_OK='false'
|
|
if ISSUE_AUTHORS="$(gh api graphql \
|
|
-f owner="${REPO%/*}" -f name="${REPO#*/}" -F pr="${PR}" -f query='
|
|
query($owner: String!, $name: String!, $pr: Int!) {
|
|
repository(owner: $owner, name: $name) {
|
|
pullRequest(number: $pr) {
|
|
closingIssuesReferences(first: 50) { nodes { author { login } } }
|
|
}
|
|
}
|
|
}' --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[]
|
|
| .author.login // empty' 2> /dev/null)"; then
|
|
API_OK='true'
|
|
fi
|
|
|
|
# Self-reported iff SOME closed issue was opened by the PR author.
|
|
SELF='false'
|
|
while IFS= read -r a; do
|
|
if [[ -n "${a}" && "${a}" == "${PR_AUTHOR}" ]]; then
|
|
SELF='true'
|
|
break
|
|
fi
|
|
done <<< "${ISSUE_AUTHORS}"
|
|
|
|
HAS_LABEL="$(gh pr view "${PR}" --repo "${REPO}" --json labels \
|
|
--jq "[.labels[].name] | index(\"${LABEL}\") != null" 2> /dev/null || echo 'false')"
|
|
|
|
# Label mutations go through REST, NOT `gh pr edit`: that command's
|
|
# GraphQL lookup requests repository.pullRequest.projectCards, and
|
|
# GitHub answers the Projects (classic) deprecation as an ERROR on
|
|
# the gh builds that still send that query — this job's runner
|
|
# image does, so `gh pr edit` exits 1 before mutating anything.
|
|
# Every add/remove path of this job failed that way from
|
|
# 2026-08-04 on (43 runs); the green runs were all the no-change
|
|
# arm. REST never touches that query.
|
|
if [[ "${SELF}" == 'true' && "${HAS_LABEL}" != 'true' ]]; then
|
|
# Create the label on first use (idempotent), then apply it.
|
|
gh label create "${LABEL}" --repo "${REPO}" --color 'BFD4F2' \
|
|
--description 'The linked issue was opened by the PR author (self-reported)' \
|
|
2> /dev/null || true
|
|
gh api -X POST "repos/${REPO}/issues/${PR}/labels" -f "labels[]=${LABEL}" > /dev/null
|
|
echo "🏷️ #${PR}: added ${LABEL} — a closed issue was opened by ${PR_AUTHOR}"
|
|
elif [[ "${SELF}" != 'true' && "${HAS_LABEL}" == 'true' && "${API_OK}" == 'true' ]]; then
|
|
# The link was removed or re-pointed to someone else's issue — clear
|
|
# the now-stale label so it never lies. Only when the query itself
|
|
# succeeded: an API failure must not look like "no self-reported link".
|
|
# The label name is a PATH SEGMENT here, and it contains a slash —
|
|
# unencoded, the request hits …/labels/review/self-reported (404).
|
|
# The presence check above is not atomic with this DELETE: if the
|
|
# label was removed in the gap, the 404 only says the desired end
|
|
# state already holds and must not fail the step. Any other
|
|
# failure (403 rate limit, 5xx, network) also keeps the step
|
|
# green — the job only re-runs on the next PR event — but must
|
|
# not disappear: without the warning, the log claims "removed"
|
|
# while the label stays on the PR. LBL_DEL_FAILED is keyed on the
|
|
# exit status, with the exact "HTTP 404" token as the only
|
|
# text-derived exception (a bare 404 substring also rides
|
|
# transport failures' embedded URLs), and mirrors the identical
|
|
# idiom in qwen-autofix.yml (a contract test pins them in sync).
|
|
LBL_DEL_FAILED=false
|
|
if ! REMOVE_ERR="$(gh api -X DELETE "repos/${REPO}/issues/${PR}/labels/$(jq -rn --arg l "${LABEL}" '$l|@uri')" 2>&1)"; then
|
|
[[ "${REMOVE_ERR}" == *"HTTP 404"* ]] || { LBL_DEL_FAILED=true; echo "::warning::#${PR}: ${LABEL} removal failed — ${REMOVE_ERR}"; }
|
|
fi
|
|
if [[ "${LBL_DEL_FAILED}" == "true" ]]; then
|
|
echo "#${PR}: ${LABEL} removal did not land — will retry on the next PR event"
|
|
else
|
|
echo "🏷️ #${PR}: removed ${LABEL} — no self-reported linked issue"
|
|
fi
|
|
else
|
|
echo "#${PR}: no change (self=${SELF}, labeled=${HAS_LABEL}, api=${API_OK})"
|
|
fi
|