GithubWebhooksController.handle (src/github/github-webhooks.controller.ts) is decorated @HttpCode(202) unconditionally, and always responds { received: true, eventId: event.id, status: event.status } — including when event.status is WebhookEventStatus.FAILED (set on e.g. a transient escrow-release/DB error while processing a merged PR). Because the HTTP response code is a 2xx regardless of the internal status, GitHub's own automatic webhook-redelivery-on-failure mechanism never triggers for a genuinely transient failure — from GitHub's point of view the delivery succeeded.
A repo-wide check for scheduled jobs (grep -rln "@Cron\|@Interval\|ScheduleModule" src) finds only bounty-expiry.scheduler.ts and idempotency-cleanup.service.ts — nothing touches WebhookEvent after it's written. So a FAILED event (e.g. a merged PR whose linked bounty's markMergedAndRelease throws due to a momentary Soroban/network blip) is recorded once, GitHub is told it succeeded, and the payout never gets a second attempt unless an operator manually notices the row in the webhook_events table and re-triggers it by hand — there's no operational tooling to even do that re-trigger.
Fix: either (a) return a 5xx when event.status === FAILED so GitHub's own retry mechanism handles it, or (b) add a scheduled job that requeues WebhookEvent rows with status = FAILED (bounded by retry count / age) through the same handlePullRequest/handleIssueEvent logic.
GithubWebhooksController.handle(src/github/github-webhooks.controller.ts) is decorated@HttpCode(202)unconditionally, and always responds{ received: true, eventId: event.id, status: event.status }— including whenevent.statusisWebhookEventStatus.FAILED(set on e.g. a transient escrow-release/DB error while processing a merged PR). Because the HTTP response code is a 2xx regardless of the internalstatus, GitHub's own automatic webhook-redelivery-on-failure mechanism never triggers for a genuinely transient failure — from GitHub's point of view the delivery succeeded.A repo-wide check for scheduled jobs (
grep -rln "@Cron\|@Interval\|ScheduleModule" src) finds onlybounty-expiry.scheduler.tsandidempotency-cleanup.service.ts— nothing touchesWebhookEventafter it's written. So aFAILEDevent (e.g. a merged PR whose linked bounty'smarkMergedAndReleasethrows due to a momentary Soroban/network blip) is recorded once, GitHub is told it succeeded, and the payout never gets a second attempt unless an operator manually notices the row in thewebhook_eventstable and re-triggers it by hand — there's no operational tooling to even do that re-trigger.Fix: either (a) return a 5xx when
event.status === FAILEDso GitHub's own retry mechanism handles it, or (b) add a scheduled job that requeuesWebhookEventrows withstatus = FAILED(bounded by retry count / age) through the samehandlePullRequest/handleIssueEventlogic.