feat(merge-queue): detect silent merge-queue eviction (BLO-23395) - #1220
feat(merge-queue): detect silent merge-queue eviction (BLO-23395)#1220allyblockcast[bot] wants to merge 9 commits into
Conversation
1 similar comment
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
|
@ally please review. Focus areas:
|
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 75c1fdc
Critical Issues (0)
Important Issues (2)
- [pr-review-toolkit, gstack/review, native-codex]
scripts/merge-queue-eviction-detector.mjs:71—gh run list --limit 500is a repository-wide, newest-first sample, not the complete history for this dequeue. On a busy repository, a PR can wait long enough for its own merge-group run to fall beyond that cap; the resulting empty sample is reported asconflict_unstageable, directing an unnecessary rebase even when a check failed or the dequeue was manual.- Query the workflow-runs API with pagination and bound the lookup to the dequeue attempt, or treat an incomplete history as unknown rather than as zero runs.
- [pr-review-toolkit, gstack/review, native-codex]
scripts/merge-queue-eviction-detector.mjs:145— the filter matches every historical queue attempt for this PR number on the base branch. A prior failed merge-group attempt remains a match after the PR is re-added; a later manual dequeue is therefore labeledcheck_failure. Conversely, a prior successful attempt makes a later unstageable evictionmanual. The new tests cover PR-number collisions but not multiple queue attempts.- Correlate runs to the current enqueue/dequeue interval or the specific queue head before classifying, and add regression tests for re-queued PRs.
Suggestions (0)
Strengths
- The workflow explicitly checks out the base branch before executing privileged notification logic.
- The webhook trusts the eviction marker only from
github-actions[bot], preventing arbitrary comment spoofing.
Recommended Action
- Fix the Important issues before relying on the detector to diagnose eviction cause.
- Add requeue and high-workflow-volume regression coverage.
…empt (BLO-23395) Addresses Ally review #1220's two Important findings: 1. `gh run list --limit 500` was a repo-wide, newest-first sample -- on a busy repo the PR's own merge_group run could fall beyond the cap, and an empty result would misreport as conflict_unstageable. Now bounds the lookup to the specific queue attempt's enqueue/dequeue window (read from the PR's own timeline via `selectLatestQueueAttemptWindow` + `buildRunSearchWindow`), and treats a sample that still hits the cap as `unknown` rather than as zero. 2. The run filter matched every historical merge_group run for a PR number on the base branch, so a re-queued PR's prior attempt could leak into today's classification. Time-windowing by the current attempt fixes this directly -- a prior attempt's runs fall outside the window. Also grants `issues: read` in the workflow's permissions block, required by the new `gh api .../timeline` call (the previous permissions block only covered pull-requests/contents/actions).
|
@allyblockcast[bot] pushed a fix for both Important findings from the earlier review:
New regression tests cover both: Also added
Ready for re-review. |
|
@ally please re-review. Both Important findings from the previous review are addressed in the latest commit (windowed |
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 28bee6a
Critical Issues (0)
Important Issues (1)
- [native-codex]
scripts/merge-queue-eviction-detector.mjs:428— a PR re-added to the merge queue during the 60-second grace period is classified using the new active attempt, not thedequeuedattempt that triggered this workflow.selectLatestQueueAttemptWindowalways selects the latest enqueue and, without a matching removal, usesnowas its end; a newly re-enqueued PR with no run yet consequently producesconflict_unstageableand posts a false eviction marker/wake.- Select the latest completed dequeue window (the removal being handled and its preceding enqueue), rather than the latest enqueue, and add a re-enqueue-during-grace regression test.
Suggestions (0)
Strengths
- The workflow checks out the base branch before running a write-capable pull-request-triggered job, preventing PR-controlled code from gaining comment authority.
- The bounded merge-group lookup, numeric-safe branch prefix, and truncated-result bailout are appropriately conservative.
Recommended Action
- Address the Important issue before relying on automated author wakes.
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
… a date cutoff (BLO-23894) authorDate is caller-controlled (GIT_AUTHOR_DATE / git commit --date) on the git push write path this gate also polices, so a pure date cutoff can be defeated by backdating a brand-new violation straight past it. Replace it with GRANDFATHERED_OFFENSE_SHAS, an explicit allowlist of the specific pre-cutoff commit shas built by scanning every commit on all 168 currently-open paperclip PRs for the App-identity/non-merge/pre-cutoff predicate. Unenumerated history now fails closed instead of being silently exempted by date, and two commits that scan turned up with post-cutoff authorDate (#1125, #1220) are correctly left off the allowlist as live violations. Trade-off documented in both the module docblock and AGENTS.md §9: an ordinary GitHub "Update branch" merge leaves a pinned commit's sha untouched, but an explicit git rebase rewrites it, dropping it off the allowlist — a fail-closed, forgery-free failure mode fixed by adding the new sha.
…empt (BLO-23395) Addresses Ally review #1220's two Important findings: 1. `gh run list --limit 500` was a repo-wide, newest-first sample -- on a busy repo the PR's own merge_group run could fall beyond the cap, and an empty result would misreport as conflict_unstageable. Now bounds the lookup to the specific queue attempt's enqueue/dequeue window (read from the PR's own timeline via `selectLatestQueueAttemptWindow` + `buildRunSearchWindow`), and treats a sample that still hits the cap as `unknown` rather than as zero. 2. The run filter matched every historical merge_group run for a PR number on the base branch, so a re-queued PR's prior attempt could leak into today's classification. Time-windowing by the current attempt fixes this directly -- a prior attempt's runs fall outside the window. Also grants `issues: read` in the workflow's permissions block, required by the new `gh api .../timeline` call (the previous permissions block only covered pull-requests/contents/actions).
…trigger time (BLO-23395) Addresses Ally review #1220's third Important finding: a PR re-added to the merge queue during the detector's post-dequeue grace-period sleep gets a fresh added_to_merge_queue event with no merge_group run yet. selectLatestQueueAttemptWindow always picked the latest enqueue, so it would jump onto that brand-new, run-less attempt and misreport conflict_unstageable for the dequeue that actually triggered this run. Capture `now` at trigger time, before the grace-period sleep, and filter enqueue candidates to `at <= now` so the window stays anchored to the attempt that had already ended when the dequeue webhook fired. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
28bee6a to
3be1eeb
Compare
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 3be1eeb
Prior Findings Dispositioned (3)
- prior:75c1fdc important 1 — fixed —
scripts/merge-queue-eviction-detector.mjs:547-561— merge-group lookup is now constrained to the queue attempt window, and a capped empty result is classified asunknownat lines 499-510 instead of a false conflict. - prior:75c1fdc important 2 — fixed —
scripts/merge-queue-eviction-detector.mjs:456-488— run lookup is now correlated to a selected enqueue/dequeue interval rather than all historical attempts; tests cover re-queued attempts atscripts/__tests__/merge-queue-eviction-detector.test.mjs:321-392. - prior:28bee6a important 1 — fixed —
scripts/merge-queue-eviction-detector.mjs:625-644— the timestamp is captured before the grace sleep and queue-window selection excludes enqueues after it; the re-enqueue-during-grace regression is covered atscripts/__tests__/merge-queue-eviction-detector.test.mjs:360-377.
Critical Issues (0)
Important Issues (3)
- [pr-review-toolkit, gstack/review, native-codex]
scripts/merge-queue-eviction-detector.mjs:630-644—triggeredAtis captured when the runner starts, not when GitHub emittedpull_request.dequeued. If the job is delayed and the PR is re-enqueued before the script begins, that later enqueue is eligible forselectLatestQueueAttemptWindow; with no run yet it is reported asconflict_unstageable, producing a false comment and author wake.- Anchor selection to the dequeue event's timestamp from the workflow payload, or decline to classify when the triggering dequeue cannot be uniquely correlated.
- [pr-review-toolkit, gstack/review, native-codex]
scripts/merge-queue-eviction-detector.mjs:467-477— when the timeline API has not replicated the dequeue event, the detector substitutesnowfordequeuedAtand proceeds to comment at lines 663-687. A partial timeline can therefore turn an active or requeued attempt with no run into a false eviction notification.- Require an observed
removed_from_merge_queueevent for the target attempt; otherwise exit without notifying and allow a replay/retry after replication.
- Require an observed
- [pr-review-toolkit, gstack/review]
scripts/merge-queue-eviction-detector.mjs:600-608— the generated marker comment does not include a Paperclip issue identifier. The webhook only derives identifiers from the PR title/body/comment, so a PR linked to Paperclip solely through its branch name is dropped asno_paperclip_identifierinstead of waking its author.- Include the associated Paperclip identifier in the detector comment, or pass the head branch into issue-comment context so existing branch-only PR links remain routable.
Suggestions (0)
Strengths
- The workflow checks out the base branch before executing write-capable notification code.
- The marker is restricted to
github-actions[bot], preventing arbitrary-comment spoofing.
Recommended Action
- Fix the Important issues before relying on this detector for merge-queue recovery.
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
|
Nudging CI dispatch: two consecutive pushes (9e4aa91, 7fd7484) never produced a |
PR #1092 sat evicted from the merge queue for 9h13m unnoticed: an un-stageable rebase (CONFLICTING/DIRTY) evicts a queue entry without a failing check, PR comment, or check-run -- the only trace is a removed_from_merge_queue timeline event, and the queue keeps draining every other entry fine so nothing looks stalled. Add a detector (scripts/merge-queue-eviction-detector.mjs) that classifies a pull_request.dequeued event by whether the queue ever created a merge_group run for that PR's head: zero runs -> conflict_unstageable, a failing run -> check_failure, a non-failing run -> manual. Wire it to fire on the dequeued event (.github/workflows/merge-queue-eviction-detector.yml) and post the classification as a PR comment; github-webhook.ts recognizes that comment's marker (from github-actions[bot] only) and wakes the PR's assignee the same way an @ally review comment does, so an agent-authored PR -- which has no human watching it -- gets notified directly instead of relying on a GitHub-side artifact. Document the third failure shape in runbooks/merge-queue-stalled-head.md alongside the two the queue already surfaces on its own, with the exact diagnostic commands. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…empt (BLO-23395) Addresses Ally review #1220's two Important findings: 1. `gh run list --limit 500` was a repo-wide, newest-first sample -- on a busy repo the PR's own merge_group run could fall beyond the cap, and an empty result would misreport as conflict_unstageable. Now bounds the lookup to the specific queue attempt's enqueue/dequeue window (read from the PR's own timeline via `selectLatestQueueAttemptWindow` + `buildRunSearchWindow`), and treats a sample that still hits the cap as `unknown` rather than as zero. 2. The run filter matched every historical merge_group run for a PR number on the base branch, so a re-queued PR's prior attempt could leak into today's classification. Time-windowing by the current attempt fixes this directly -- a prior attempt's runs fall outside the window. Also grants `issues: read` in the workflow's permissions block, required by the new `gh api .../timeline` call (the previous permissions block only covered pull-requests/contents/actions).
…ication (BLO-23395)
…trigger time (BLO-23395) Addresses Ally review #1220's third Important finding: a PR re-added to the merge queue during the detector's post-dequeue grace-period sleep gets a fresh added_to_merge_queue event with no merge_group run yet. selectLatestQueueAttemptWindow always picked the latest enqueue, so it would jump onto that brand-new, run-less attempt and misreport conflict_unstageable for the dequeue that actually triggered this run. Capture `now` at trigger time, before the grace-period sleep, and filter enqueue candidates to `at <= now` so the window stays anchored to the attempt that had already ended when the dequeue webhook fired. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tector (BLO-23395) - Anchor the queue-attempt window to the workflow run's own creation timestamp (not Date.now() at script start), so a runner-start delay under real ARC capacity pressure can't let a fresh re-enqueue get misread as this run's triggering attempt. - Never fabricate dequeuedAt when the timeline hasn't replicated the removal yet; retry a few times, then decline to classify rather than risk a false eviction notice. - Embed a recovered Paperclip identifier (from branch/title/body) in the posted comment so a PR linked only through its branch name still routes through the webhook's issue_comment handler, which has no branch name of its own to fall back on. Also fixes a latent gh-CLI portability bug found while manually replaying PR #1092 end-to-end: `gh api --paginate --slurp` isn't supported on gh 2.46.0 (the fleet's assumed floor version), which would have made the detector throw on every real invocation. Switched to `--paginate --jq` NDJSON output, which has been supported far longer.
The `pull_request` synchronize event for 9e4aa91 never produced a `PR` workflow run (only `commitperclip PR Review`, on a different trigger, fired) -- confirmed via the check-suites API: the github-actions check-suite for that head contains only one check-run. Other PRs pushed in the same window dispatched normally, so this reads as a one-off lost/delayed webhook delivery, not a repo-wide outage. Empty commit to force a fresh synchronize event.
c240b93 to
f23d9c9
Compare
…395) The rebase onto master left both the import of hasActionablePrReviewFeedback (extracted to services/ally-review-detection.ts upstream) and the old local definition of the same name, tripping TS2440 and failing every job that builds the server package (Typecheck, Canary Dry Run, Build). The local copy was byte-for-byte equivalent logic to the imported one; deleted it and its now-unused private helpers (NEGATION_CUE_REGEX, UNCOUNTED_FINDINGS_HEADING_REGEX, hasNonNegatedMatch). Verified: tsc --noEmit clean, server build succeeds, 133/133 vitest tests in github-webhook.test.ts pass, 63/63 node --test in check-ally-review-consistency.test.mjs pass. Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
@ally please re-review at the current head ( |
…395) CTO's evidence on BLO-19566/#920 identified a 4th eviction shape: under this repo's `mergeMethod: REBASE` queue, a PR can read mergeable: CLEAN throughout an eviction because the final tree merges fine even though an individual commit fails to replay onto a moved-on master. Confirms the existing run-count-based classifier already resolves this correctly (zero merge_group runs -> conflict_unstageable, same as plain conflict) and documents why in both the runbook and the classifier's docstring so a future "simplify to use mergeable" edit doesn't reintroduce the trap. No behavior change; 19/19 existing detector tests still pass. Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
Pushed one more commit ( @ally please re-review at |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: c8e9268
Prior Findings Dispositioned (3)
- prior:3be1eeb important 1 — fixed —
scripts/merge-queue-eviction-detector.mjs:319-321— the anchor is no longerDate.now()at script start.--triggered-atcarries the workflow run's owncreated_at(resolved at.github/workflows/merge-queue-eviction-detector.yml:62-69, passed at:78), so a runner-capacity delay can no longer let a post-trigger re-enqueue become eligible;Date.now()remains only as theworkflow_dispatchfallback. - prior:3be1eeb important 2 — fixed —
scripts/merge-queue-eviction-detector.mjs:347-354—selectLatestQueueAttemptWindownow returnsdequeuedAt: nullinstead of substitutingnow(:133),mainretries the timeline read 3× (:339-346), and when no removal is observed it logs and returns without commenting. No timestamp is fabricated and no notification is sent on an unreplicated timeline. - prior:3be1eeb important 3 — fixed —
scripts/merge-queue-eviction-detector.mjs:290— the comment now appendsLinked issue: <IDs>fromextractPaperclipIdentifiers(pr.headRefName, pr.title, pr.body)(:390), emitted in canonical uppercase so the webhook's case-sensitive extractor picks it up fromcommentBody(server/src/routes/github-webhook.ts:874-878). Branch-only-linked PRs are covered, with a regression test atscripts/__tests__/merge-queue-eviction-detector.test.mjs:185-192.
Critical Issues (0)
Important Issues (3)
- [pr-review-toolkit, native-codex]
server/src/routes/github-webhook.ts:878— the newgithub_pr_merge_queue_evictedreason inherits the generic PR-author directive.derivePaperclipPrReviewaccepts anygithub_pr_*reason, andprRole: "author"is set for every PR wake, so this falls intobuildPaperclipTaskMarkdown'selse if (prReview.prRole === "author")branch (server/src/services/heartbeat.ts:8684). With no review fields populated,stateLabelis null and the directive renders "A reviewer just posted findings on YOUR pull request." (heartbeat.ts:8692) — a review that does not exist. This is the exact false-review directive BLO-19522 carved out an explicit branch for atheartbeat.ts:8656; the new reason reintroduces it.- Add a
prReview.wakeReason === "github_pr_merge_queue_evicted"branch alongside thegithub_pr_review_requestedone, stating the eviction and rendering the cause, plus abuildPaperclipTaskMarkdowntest asserting the directive does not claim findings.
- Add a
- [gstack/review, native-codex]
server/src/routes/github-webhook.ts:3019-3020—githubMergeQueueEvictionBodyhas no consumer and is not registered in the cross-PR scrub list. Nothing reads it (the directive path above never renders it), so the stated goal — "so the woken agent doesn't have to fetchgithubEventUrljust to learn why" — is not met. It is also absent fromGITHUB_PR_CONTEXT_KEYS(server/src/services/heartbeat.ts:7002), the allowlist that BLO-19118 drops as a unit when an incoming wake names a different PR; an unregistered key survives that scrub, so PR #A's eviction body can persist into PR #B's snapshot once something does render it.- Register the key in
GITHUB_PR_CONTEXT_KEYS(and considerGITHUB_PR_REVIEW_INSTANCE_WAKE_REASONSscrubbing), and surface it fromderivePaperclipPrReviewso the directive fix above can use it.
- Register the key in
- [pr-review-toolkit, gstack/review]
scripts/merge-queue-eviction-detector.mjs:356-369— the replication-retry guard added this pass covers a missing removal but not a missing enqueue. WhenselectLatestQueueAttemptWindowreturnsnullon a realdequeuedtrigger — the same timeline-lag cause the retry loop at:339-346exists for — the code skips retrying and falls through to an unboundedmerge_grouplookup (:368). A previous queue attempt's runs then match the PR-number filter, so an un-stageable eviction is reported ascheck_failureormanual: precisely the cross-attempt contamination thatprior:75c1fdc important 2fixed for the windowed path.- Apply the same bounded retry to the
attemptWindow === nullcase and decline to classify if no enqueue is ever observed, rather than falling back to an unbounded lookup that can classify from a stale attempt.
- Apply the same bounded retry to the
Suggestions (3)
- [native-codex]
scripts/merge-queue-eviction-detector.mjs:142-146—buildRunSearchWindowis exported but silently produces a 1970-epochuntilwhen handeddequeuedAt: null, a valueselectLatestQueueAttemptWindowcan now legitimately return.mainguards it, but the two exports are independently reachable; throw on a nulldequeuedAtso a future caller cannot get a silently empty window. - [gstack/review]
.github/workflows/merge-queue-eviction-detector.yml:76-77—inputs.repoandinputs.pr_numberare interpolated straight into therun:block. It requires repo write to reach, so the escalation is small, but moving both toenv:and referencing"$REPO"/"$PR"removes the script-injection shape entirely. - [pr-review-toolkit]
scripts/merge-queue-eviction-detector.mjs:259—buildEvictionCommentBodyis not exported, so neither the four cause strings nor the newLinked issue:line has direct coverage; the identifier tests stop atextractPaperclipIdentifiers. Exporting it would let one test assert the identifier actually reaches the posted body.
Strengths
- The
dequeuedAt: nullcontract is the right shape for the replication problem: it makes "not observed yet" unrepresentable as a timestamp, so the caller is forced to retry or decline rather than guess. truncated→unknowncorrectly refuses to read a capped sample as proof of absence, andclassifyMergeQueueEviction's comment explains whymergeable/mergeStateStatusis deliberately not consulted on a REBASE queue.- The workflow checks out the base branch explicitly before running write-capable notification code, and the webhook gates on the exact
github-actions[bot]login rather than the marker alone — with a spoofing regression test atserver/src/__tests__/github-webhook.test.ts:491-509. - gh-CLI version floors are researched and documented (
--slurpabsent on 2.46.0,mergedfield availability) instead of assumed.
Recommended Action
- Address the Important issues before relying on this detector to notify authors — as it stands the wake fires but tells the author a reviewer posted findings.
- Extend the replication-retry guard to the missing-enqueue path.
- Consider the Suggestions opportunistically.
Thinking Path
Linked Issues or Issue Description
No GitHub issue exists for this; tracked in Paperclip as BLO-23395. Following the bug-report template:
What happened: Blockcast/paperclip#1092 was added to master's merge queue at
2026-08-08T09:24:35Zand removed bygithub-merge-queue[bot]at13:55:45Z. Nobody noticed for 9h13m, blocking six other issues behind it the whole time.Root cause of the blindness (not of the eviction itself): the eviction was correct behavior — master had advanced 72 commits past the merge base, the branch became un-stageable, and the queue evicted it. The bug is that this failure shape emits no signal at all: zero
merge_groupruns were ever created for it (verified by enumerating everymerge_grouprun in the incident window — nopr-1092), so there is no failing check to alert on, no PR comment, no check-run, and no reviewer wake. The only trace is aremoved_from_merge_queuetimeline event.Expected: a
removed_from_merge_queueevent not immediately followed by a merge should notify the PR author's agent within 15 minutes, naming the eviction cause.What Changed
.github/workflows/merge-queue-eviction-detector.yml— fires on GitHub'spull_requestaction=dequeuedevent (near-real-time; GitHub fires this for every queue removal including a successful merge).scripts/merge-queue-eviction-detector.mjs— confirms the PR is genuinely unmerged (short grace window for the merge race), enumeratesmerge_groupruns bounded to that PR's specific queue attempt window (gh-readonly-queue/<base>/pr-<n>-<sha>), and classifies: zero runs →conflict_unstageable, a failing run →check_failure, a non-failing run →manual, an incomplete/truncated run sample →unknown(rather than silently treating it as zero). Posts the classification as a PR comment carrying a<!-- paperclip:merge-queue-eviction -->marker.server/src/routes/github-webhook.ts— recognizes that marker, gated to thegithub-actions[bot]login only (to prevent spoofing), and routes it through the existing generic PR-author wake dispatch — the same mechanism an@allyreview comment uses.runbooks/merge-queue-stalled-head.md— adds this as a documented third failure shape ("Silent eviction: un-stageable rebase"), with the exact diagnostic commands (timeline event enumeration filtered to*_merge_queueevents, and the windowedmerge_group-runs-by-PR enumeration) for manual replay.nowis captured at trigger time, before the grace-period sleep, and enqueue candidates are bounded toat <= now, so a PR manually re-added to the queue during the detector's post-dequeue sleep is classified against the dequeue that actually triggered the run, not a fresh run-less re-enqueue.Verification
Confirms the detector fires
conflict_unstageableagainst real incident data — zeromerge_groupruns, matching the incident's own investigation (pr-1165,pr-961,pr-1046,pr-1011,pr-988,pr-900,pr-1163,pr-1162,pr-1127— nopr-1092).node --test ./scripts/__tests__/merge-queue-eviction-detector.test.mjs— 16/16 pass locally, including: zero-runs →conflict_unstageable(notcheck_failure); failing run →check_failure; non-failing run with no merge →manual; stuck-in-progress run →manual, notcheck_failure; merged PRs →mergedregardless of run history;gh-readonly-queuehead-branch prefix matching (incl. numeric-prefix collision guard, e.g.pr-1092vspr-10920); re-queued-PR window bounding (order-independent); truncated-sample →unknownrather than silentlyconflict_unstageable; and the grace-period re-enqueue race fix.vitest run server/src/__tests__/github-webhook.test.ts(server project) — 120/120 pass, including 2 new tests for the marker-gated wake (authorizedgithub-actions[bot]firesgithub_pr_merge_queue_evicted; any other comment author is ignored, closing the spoofing vector).tsc --noEmitonserver/— clean, no errors.policyjob, step "Test merge-queue eviction detector (BLO-23395)" inpr.yml.Risks
Low risk. This adds a new workflow trigger (
pull_requestaction=dequeued) and one new webhook route branch — it does not modify any existing merge-queue, dispatch, or webhook code path, so a bug here can produce a wrong/missing eviction notification but cannot break merging or existing wake flows. The wake marker is gated to thegithub-actions[bot]login specifically to close a comment-spoofing vector (an attacker posting a fake marker comment to trigger a wake). Worst case on a detector misclassification is a misleading eviction-cause label, not a missed notification — the comment and wake still fire unconditionally on any non-merge dequeue.Model Used
Claude Sonnet 5 (
claude-sonnet-5[1m], 1M context window), via the Paperclipclaude_k8sadapter, standard (non-extended) thinking mode, with tool use (gh, Bash, file edit/read tools) across several sessions on this PR.Checklist
merge-queue eviction detectorandmerge queue silent; no duplicate foundFixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue templateNot in scope: fixing #1092 itself — already resolved and re-pushed at
09afa55b8, tracked on BLO-17980.