Skip to content

fix: isolate each linked issue's processing when a PR closes multiple bounties - #81

Merged
chonilius merged 5 commits into
MergeFi:mainfrom
boluwacodes:fix/pr-linked-issues-per-issue-isolation-47
Aug 17, 2026
Merged

fix: isolate each linked issue's processing when a PR closes multiple bounties#81
chonilius merged 5 commits into
MergeFi:mainfrom
boluwacodes:fix/pr-linked-issues-per-issue-isolation-47

Conversation

@boluwacodes

Copy link
Copy Markdown
Contributor

Summary

GithubWebhooksService.handlePullRequest processed every issue linked from a merged PR's body in one unguarded loop. If markMergedAndRelease threw for the first linked issue (an invalid state transition, an escrow release failure, a Soroban error), the exception propagated out of the loop and every subsequent linked issue in the same PR was never attempted — and the only error boundary (handleEvent's outer try/catch) marked the whole WebhookEvent FAILED with just the first error message, with no way to tell "only #12 failed" from "everything failed" from event.error alone.

There was also a more mundane trigger for the same failure mode: extractLinkedIssueNumbers didn't de-duplicate, so a PR body like "Fixes #12. This also resolves #12 as discussed." processed bounty #12 twice — the second call throws InvalidBountyTransitionError against the now-PAID bounty, marking an otherwise-fully-successful merge FAILED (confirmed safe from double-payment either way, but a real false-alarm cost for monitoring).

  • Linked issue numbers are de-duplicated before the loop.
  • Each linked issue's processing runs in its own try/catch, returning a succeeded/skipped/failed outcome per issue instead of throwing on the first failure.
  • Decision (documented on applyPullRequestOutcomes): event.status stays FAILED if any linked issue failed — FAILED already means "an operator should look at this," so it doesn't lose that signal — but event.error now lists every failure by issue number (#12: <message>; #56: <message>) instead of only whichever one happened to throw first. A PR where every linked issue succeeded or was skipped (no bounty) is PROCESSED, unchanged from before.

Closes #47

Test plan

  • npm run lint — clean (0 errors; only pre-existing unrelated warnings in test/*.e2e-spec.ts)
  • npx tsc --noEmit — clean
  • npm run build — succeeds
  • npm run test — 127/127 pass across 18 suites (7 pre-existing in this spec file + 2 new), including both scenarios from the issue's own reproduction plan:
    • a PR linking three issues where the middle one's bounty processing throws still releases the first and third bounties; event.status is FAILED but event.error names only the actual failure (#34), not the two successes
    • a PR body referencing the same issue twice only attempts that bounty once (de-duplicated) and stays PROCESSED, rather than falsely FAILED on the benign duplicate-transition error
  • npm run test:cov — passes

A PR body referencing the same issue twice (e.g. "Fixes MergeFi#12. Also
resolves MergeFi#12 as discussed.") previously attempted to process the same
bounty twice in one event — the second markMergedAndRelease call
throws InvalidBountyTransitionError against the state the first call
just left it in. Confirmed safe from double-payment either way
(assertTransition blocks it), but it's a benign, false-alarm failure
that shouldn't mark the webhook FAILED. Also adds the LinkedIssueOutcome
type used by the per-issue isolation in the next commit (MergeFi#47).
handlePullRequest previously ran every linked issue's bounty
processing in one unguarded loop — if markMergedAndRelease threw for
the first linked issue (an invalid state transition, an escrow
release failure, a Soroban error), the exception propagated straight
out of the loop and every subsequent linked issue in the same PR was
never attempted at all (MergeFi#47).

Now returns a LinkedIssueOutcome per issue number (succeeded/skipped/
failed) instead of throwing on the first failure, so one bounty's
problem no longer blocks the others linked from the same merged PR.
handleEvent's use of this return value follows in the next commit.
Wires handlePullRequest's per-issue outcomes into a deliberate policy
(applyPullRequestOutcomes, doc comment explains the choice among the
three the issue names): event.status stays FAILED if any linked issue
failed, but event.error now lists every failure by issue number
instead of only whichever one happened to throw first. A PR where
every linked issue succeeded or was skipped (no bounty) is PROCESSED,
unchanged from before (MergeFi#47).
The issue's reproduction plan: a PR linking three issues where the
middle one's markMergedAndRelease throws still releases the first
and third bounties. event.status is FAILED, but event.error names
only the failing issue (MergeFi#34) — the two successes aren't listed as
failures.
The issue's other reproduction scenario: "Fixes MergeFi#42. This also
resolves MergeFi#42 as discussed" only attempts bounty-42 once (de-duped),
and the event stays PROCESSED rather than FAILED on the benign
InvalidBountyTransitionError the second, redundant call would have
thrown against the now-PAID bounty.
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

@boluwacodes is attempting to deploy a commit to the chonilius' projects Team on Vercel.

A member of the Team first needs to authorize it.

@chonilius
chonilius merged commit 93f577b into MergeFi:main Aug 17, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment