Skip to content

fix(mergeability): deferred re-check for async GitHub PR mergeability resolution#1267

Merged
zbigniewsobiecki merged 7 commits into
mainfrom
dev
May 8, 2026
Merged

fix(mergeability): deferred re-check for async GitHub PR mergeability resolution#1267
zbigniewsobiecki merged 7 commits into
mainfrom
dev

Conversation

@zbigniewsobiecki
Copy link
Copy Markdown
Member

Summary

  • Root cause: PRConflictDetectedTrigger had a 2×2s=4s synchronous retry budget waiting for GitHub to resolve PR.mergeable. GitHub frequently takes 10–30s for freshly-opened PRs, and never sends a follow-up webhook when mergeability resolves — so the trigger silently skipped forever (observed live on ucho/PR fix: remove non-functional checkbox syntax from prompt templates #329, 2026-05-07).
  • Fix: The trigger now returns TriggerResult.deferredRecheck (with agentType: null) when the synchronous budget exhausts while mergeable === null. The router schedules a bare BullMQ delayed re-check job ~45s later via scheduleCoalescedJob (deduped per PR via coalesceKey). The worker re-dispatches via the trigger registry to get fresh mergeability state.
  • Exhaustion guard: If mergeability is still null after the deferred re-check fires, processGitHubWebhook Sentry-captures under tag mergeability_recheck_exhausted and logs WARN instead of silently discarding.
  • Generic infrastructure: TriggerResult.deferredRecheck is adapter-agnostic — any trigger handler can use it to schedule deferred bare-job re-dispatch without touching shared router/worker infrastructure.

What changed

Types & queue (src/types/index.ts, src/router/queue.ts):

  • TriggerResult.deferredRecheck?: { delayMs: number; coalesceKey: string } — generic deferred re-check signal
  • GitHubJob.mergeabilityRecheckAttempt?: number — marks deferred re-check jobs so the worker can detect exhaustion

Router (src/router/webhook-processor.ts, src/router/adapters/github.ts):

  • New branch in processRouterWebhook after the skipReason check: calls scheduleCoalescedJob(job, coalesceKey, delayMs) when a trigger returns deferredRecheck with agentType: null; Sentry-captures on Redis failure without 500ing the webhook
  • GitHubRouterAdapter.buildJob omits triggerResult and sets mergeabilityRecheckAttempt: 1 when building a deferred re-check job (bare job → worker re-dispatches fresh)

Trigger (src/triggers/github/pr-conflict-detected.ts):

  • On null-timeout path: returns { agentType: null, deferredRecheck: { delayMs: 45_000, coalesceKey: '${projectId}:pr-conflict-recheck:${prNumber}' } } instead of skip()

Worker (src/triggers/github/webhook-handler.ts, src/worker-entry.ts):

  • processGitHubWebhook gains isRecheckJob?: boolean param; Sentry-captures mergeability_recheck_exhausted when re-dispatch returns deferredRecheck again
  • worker-entry.ts passes !!mergeabilityRecheckAttempt as isRecheckJob

Docs: CLAUDE.md "Deferred re-check" pattern note, CHANGELOG entry.

Test plan

  • tests/unit/router/deferred-recheck.test.ts — 11 tests: type fields, buildJob behavior, processRouterWebhook branch (scheduleCoalescedJob called, bare job, Sentry on throw, branch guards)
  • tests/unit/triggers/github-webhook-handler-recheck.test.ts — 4 tests: Sentry capture on re-check exhaustion (true/false/non-deferredRecheck/pre-resolved)
  • tests/unit/triggers/pr-conflict-detected.test.ts — updated 1 existing test + 3 new: deferredRecheck shape, coalesceKey, delayMs
  • tests/unit/worker-entry.test.ts — 1 new + 1 updated: isRecheckJob=true when mergeabilityRecheckAttempt set, existing routing test updated for 7th arg
  • 492 test files, 8945 tests passing locally
  • npm run typecheck passes
  • npm run lint passes

🤖 Generated with Claude Code

aaight and others added 7 commits May 8, 2026 16:22
Co-authored-by: Cascade Bot <bot@cascade.dev>
Co-authored-by: Cascade Bot <bot@cascade.dev>
Adds TriggerResult.deferredRecheck, GitHubJob.mergeabilityRecheckAttempt,
processRouterWebhook branch that schedules a bare BullMQ delayed job via
scheduleCoalescedJob, and GitHubRouterAdapter.buildJob changes to omit
triggerResult when deferring. CLAUDE.md documents the pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tection

PRConflictDetectedTrigger now returns deferredRecheck instead of a silent
skip when mergeable===null after the synchronous retry budget. processGitHubWebhook
gains an isRecheckJob param: when the re-check fires and the trigger still
returns deferredRecheck, Sentry captures mergeability_recheck_exhausted.
worker-entry.ts passes !!mergeabilityRecheckAttempt as isRecheckJob.
CHANGELOG entry added.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…all plans complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@zbigniewsobiecki zbigniewsobiecki merged commit 155ce7b into main May 8, 2026
15 checks passed
@codecov
Copy link
Copy Markdown

codecov Bot commented May 8, 2026

Codecov Report

❌ Patch coverage is 98.91304% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/router/webhook-processor.ts 96.55% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants