fix(review): evict a manual-review-held sibling from the merge-train gate - #9217
Merged
Conversation
…gate (#9039) The merge-train FIFO gate already scopes blocking to overlapping siblings and caps the wait at 24h, so a head-of-line PR held for manual review was still treated as "still viable to eventually clear on its own" -- exactly what let #8735 stall 5 overlapping PRs for 4 hours (57 denials) until a human merged it by hand. A manual-review hold does not self-clear on any timer, unlike an actively-reviewing PR, so shouldWaitForOlderSiblings now evicts a sibling carrying the repo's configured manual-review label the same way it already evicts a git-conflicted one, instead of waiting for it. Also wires a bounded, best-effort wedge alert: N consecutive merge-train denials against the SAME blocking sibling (regardless of mode) now page via the existing PagerDuty helper once a repo opts in, mirroring how ops-wire.ts already alerts on a sustained condition from audit-event counts -- so a wedged train is visible immediately instead of only via audit rows a human happens to notice.
Contributor
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
❌ 4 Tests Failed:
View the top 3 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
src/review/merge-train.ts,shouldWaitForOlderSiblings) already hasoverlap-scoping (a sibling only blocks when it shares a linked issue or a meaningful changed file)
and a 24h staleness cap. Neither is the actual gap the confirmed incident (fix(test): add required default field to queue-3 pause commandAuthorization fixture #8735: 57 denials, 5 PRs
blocked, 4 hours) exposed — overlap-scoping worked exactly as designed (those 5 PRs genuinely
overlapped fix(test): add required default field to queue-3 pause commandAuthorization fixture #8735), and fix(test): add required default field to queue-3 pause commandAuthorization fixture #8735 was created only ~4h before a human intervened, nowhere near the 24h
cap. The real gap: the gate has no concept of "this sibling is held for a reason that will never
self-resolve without a human" — it only ever asked "is this sibling older, overlapping, and not git-
conflicted," and a PR wearing the
manual-reviewlabel passed all three, so the gate waited for itexactly like an ordinary in-review PR that's still making forward progress.
MergeTrainSiblinggained aheldForManualReviewfield, resolved by the caller(
agent-action-executor.ts) the SAME way the existing live approve/merge guard already resolves"does this PR carry the manual-review label" (
ctx.manualReviewLabel—nulldisables it,absent/undefined falls back to
AGENT_LABEL_NEEDS_REVIEW). A manual-review-held sibling is nowevicted from the train exactly like a git-conflicted (
dirty) one, instead of counting as "stillviable to eventually clear on its own."
the only siblings left for the 24h cap to bound are ones genuinely still in CI/AI-review/human-review
— i.e. actively making progress toward a natural resolution, not administratively frozen. That's the
same case the 24h cap was already designed for. A shorter cap on top would mostly just race an
actively-progressing review, trading a hypothetical extra-fast escape hatch for spurious out-of-order
merges on PRs that were about to clear on their own. Happy to add one if you disagree, but I don't see
a concrete failure mode it would close that the eviction fix + existing cap don't already cover.
denials land against the SAME blocking sibling within an hour — keyed to the blocking PR number
(
owner/repo#merge-train-blocked-by-<N>), not the waiting PR, since the incident denied 5 differentPR numbers behind the one stuck sibling. Reuses the existing
isPagerDutyEnabled/triggerPagerDutyIncidenthelpers verbatim (same min-severity floor + cooldown-via-dedup_keyops-wire.ts already relies on for its own "count recent audit rows, page past a threshold" alerting),
rather than inventing a second alerting mechanism. No-op unless
LOOPOVER_ENABLE_PAGERDUTYis set.Judgment calls (flagging explicitly, not asking mid-task)
extend it to
mergeBlockedSha/pending-closure (orb(disposition): merge_blocked_sha never clears and mergeAttemptCount never resets — one token blip permanently strands green approved PRs, invisibly #9012/orb(sweep): a PR that goes green during a pause is never re-evaluated after unpause #9018/orb(gate): pending-closure flag can strand a PR when the delayed verify job is lost #9031). Both label checks(manual-review, pending-closure) are equally cheap on
MergeTrainSibling.labels, but I kept this PRto the one confirmed incident to keep the diff, and the coverage surface, tight; a pending-closure
eviction (also just a label check) looks like a clean, low-risk follow-up if wanted.
mergeBlockedShaneeds real new plumbing (the sibling's live head SHA, not currently fetched/passed)and interacts with
mergeBlockedUntil's infra-scoped-expiry logic (merge-failure.ts), so it's abigger, separate change.
denials / 4h) so a real wedge pages inside its first hour, but above 1 so a single denial that clears
in seconds (the other observed train-wait episode, behind fix(miner): reject a portfolio-queue identifier containing the '::' composite-id separator (#8857) #8925, cleared in 20s) never pages.
same call site (
agent-action-executor.ts's step 8b) and splitting them after the fact risked aninconsistent intermediate state; both are fully tested together.
Scope
type(scope): short summaryConventional Commit format.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run actionlint(not run — no workflow files touched)npm run typecheck(npx tsc --noEmit -p tsconfig.json --incremental false, clean)npm run test:coveragelocally, scoped:test/unit/merge-train.test.ts(31/31) +test/unit/agent-action-executor.test.ts(211/211) = 242/242 passing; v8 coverage on the twotouched
src/**files is 100% branch/line onmerge-train.tsand every new line/branch inagent-action-executor.tsis covered (the file's only uncovered lines are pre-existing, in anunrelated function this PR does not touch).
npm run test:workers(not run — no Worker-specific surface touched)npm run build:mcp/npm run test:mcp-pack(not run — no MCP surface touched)npm run ui:openapi:check(not run — no route/schema change)npm run ui:lint/npm run ui:typecheck/npm run ui:build(not run — no UI change)npm audit --audit-level=moderatewas not re-run in this pass (no dependency changes)If any required check was skipped, explain why:
npm run test:ci) intentionally not run per this task's own instructions — scopedto typecheck + the touched test files instead. This PR is backend-only (
src/review,src/services,test/unit), so the UI/MCP/worker/OpenAPI checks above are not applicable to this diff.Safety
rankings, or private maintainer evidence are exposed.
optimization tactics.
CHANGELOG.md).UI Evidence
N/A — backend-only change, no visible UI/frontend/docs surface.
Notes
test/unit/agent-action-executor.test.ts, describe block "manual-review eviction (orb(merge-train): a blocked head-of-line PR stalls every PR behind it — 5 PRs blocked 4h behind one manual-review label #9039 — confirmedfix(test): add required default field to queue-3 pause commandAuthorization fixture #8735 incident: 57 denials, 5 PRs blocked 4h)": an older, overlapping, manual-review-labeled sibling
no longer blocks — the newer PR merges normally in
enforcemode.merge-train.ts, which explicitly documented the OLD (nowincorrect) behavior — "An overlapping older sibling DOES still count as a blocker even while held for
manual review" — so a future reader isn't misled by a stale rationale.
Closes #9039