diff --git a/src/app/api/pr-followup/sync/route.ts b/src/app/api/pr-followup/sync/route.ts index 8e58af1..fd8aa46 100644 --- a/src/app/api/pr-followup/sync/route.ts +++ b/src/app/api/pr-followup/sync/route.ts @@ -175,6 +175,8 @@ export async function POST(request: NextRequest) { id: String(review.id), state: review.state, linkedIssue, + prState: pr.state, + prMergedAt: pr.merged_at, }); } else { totalSkipped++; // APPROVED/COMMENTED don't trigger PR-fix work @@ -226,6 +228,8 @@ export async function POST(request: NextRequest) { mergeStateStatus: pr.mergeable_state, id: String(pr.id ?? Date.now()), linkedIssue, + prState: pr.state, + prMergedAt: pr.merged_at, }); } diff --git a/src/app/api/pr-followup/webhook/route.ts b/src/app/api/pr-followup/webhook/route.ts index f588f35..37bba83 100644 --- a/src/app/api/pr-followup/webhook/route.ts +++ b/src/app/api/pr-followup/webhook/route.ts @@ -72,6 +72,8 @@ function parseWebhookEvent(githubEvent: string, body: Record): id: String(prReview.review?.id), state: prReview.review?.state, linkedIssue: extractLinkedIssueFromPr(pr), + prState: pr.state, + prMergedAt: pr.merged_at, }); break; } @@ -173,6 +175,8 @@ function parseWebhookEvent(githubEvent: string, body: Record): mergeStateStatus: pr.mergeable_state, id: String(pr.id ?? Date.now()), linkedIssue: extractLinkedIssueFromPr(pr), + prState: pr.state, + prMergedAt: pr.merged_at, }); break; } diff --git a/src/lib/pr-followup-ingestion.test.ts b/src/lib/pr-followup-ingestion.test.ts index 84abe3d..7bb5974 100644 --- a/src/lib/pr-followup-ingestion.test.ts +++ b/src/lib/pr-followup-ingestion.test.ts @@ -275,6 +275,48 @@ describe("ingestReviewEvent", () => { expect(client.items).toHaveLength(0); }); + it("skips reviews for merged PRs (prMergedAt set)", async () => { + process.env.PR_FOLLOWUP_BOT_IDENTITIES = "itsmiso-ai"; + const client = makeClient(); + + const result = await ingestReviewEvent(client, { + repoFullName: "misospace/dispatch", + prNumber: 42, + branch: "fix/test", + url: "https://github.com/misospace/dispatch/pull/42", + title: "Fix test issue", + author: "itsmiso-ai", + reviewBody: "Change X to Y", + reviewId: "r4", + reviewState: "CHANGES_REQUESTED", + prMergedAt: "2026-06-01T00:00:00Z", + }); + + expect(result).toBeNull(); + expect(client.items).toHaveLength(0); + }); + + it("skips reviews for closed PRs (prState=closed)", async () => { + process.env.PR_FOLLOWUP_BOT_IDENTITIES = "itsmiso-ai"; + const client = makeClient(); + + const result = await ingestReviewEvent(client, { + repoFullName: "misospace/dispatch", + prNumber: 42, + branch: "fix/test", + url: "https://github.com/misospace/dispatch/pull/42", + title: "Fix test issue", + author: "itsmiso-ai", + reviewBody: "Change X to Y", + reviewId: "r5", + reviewState: "CHANGES_REQUESTED", + prState: "closed", + }); + + expect(result).toBeNull(); + expect(client.items).toHaveLength(0); + }); + afterEach(() => { delete process.env.PR_FOLLOWUP_BOT_IDENTITIES; }); @@ -383,6 +425,44 @@ describe("ingestMergeStateEvent", () => { expect(client.items).toHaveLength(0); }); + it("skips merge state events for merged PRs (prMergedAt set)", async () => { + process.env.PR_FOLLOWUP_BOT_IDENTITIES = "itsmiso-ai"; + const client = makeClient(); + + const result = await ingestMergeStateEvent(client, { + repoFullName: "misospace/dispatch", + prNumber: 42, + branch: "fix/test", + url: "https://github.com/misospace/dispatch/pull/42", + title: "Fix test issue", + author: "itsmiso-ai", + mergeStateStatus: "behind", + prMergedAt: "2026-06-01T00:00:00Z", + }); + + expect(result).toBeNull(); + expect(client.items).toHaveLength(0); + }); + + it("skips merge state events for closed PRs (prState=closed)", async () => { + process.env.PR_FOLLOWUP_BOT_IDENTITIES = "itsmiso-ai"; + const client = makeClient(); + + const result = await ingestMergeStateEvent(client, { + repoFullName: "misospace/dispatch", + prNumber: 42, + branch: "fix/test", + url: "https://github.com/misospace/dispatch/pull/42", + title: "Fix test issue", + author: "itsmiso-ai", + mergeStateStatus: "behind", + prState: "closed", + }); + + expect(result).toBeNull(); + expect(client.items).toHaveLength(0); + }); + afterEach(() => { delete process.env.PR_FOLLOWUP_BOT_IDENTITIES; }); diff --git a/src/lib/pr-followup-ingestion.ts b/src/lib/pr-followup-ingestion.ts index dd3d323..ad62657 100644 --- a/src/lib/pr-followup-ingestion.ts +++ b/src/lib/pr-followup-ingestion.ts @@ -231,12 +231,19 @@ export async function ingestReviewEvent( reviewBody: string; reviewId: string; reviewState: string; // "APPROVED", "CHANGES_REQUESTED", "COMMENTED" + prState?: string | null; + prMergedAt?: string | null; linkedIssue?: number | null; }, ): Promise { // Only CHANGES_REQUESTED triggers PR-fix work (not APPROVED or COMMENTED) if (opts.reviewState !== "CHANGES_REQUESTED") return null; + + // Filter merged/closed PRs — do not enqueue work for terminal PRs + if (opts.prMergedAt || opts.prState === "closed") { + return null; + } // Check author eligibility if (!isAllowedBotAuthor(opts.author)) return null; @@ -331,6 +338,8 @@ export async function ingestMergeStateEvent( title: string; author: string | null; mergeStateStatus: string; // "BEHIND", "DIRTY", "UNSTABLE", "HAS_HOOKS", etc. + prState?: string | null; + prMergedAt?: string | null; linkedIssue?: number | null; }, ): Promise { @@ -338,6 +347,11 @@ export async function ingestMergeStateEvent( const problematicStates = ["behind", "dirty", "unstable", "has_hooks"]; if (!problematicStates.includes(opts.mergeStateStatus.toLowerCase())) return null; + + // Filter merged/closed PRs — do not enqueue work for terminal PRs + if (opts.prMergedAt || opts.prState === "closed") { + return null; + } // Check author eligibility if (!isAllowedBotAuthor(opts.author)) return null; @@ -483,6 +497,8 @@ export interface PrFollowupEvent { conclusion?: string; checkName?: string; mergeStateStatus?: string; + prState?: string | null; + prMergedAt?: string | null; linkedIssue?: number | null; } @@ -537,6 +553,8 @@ export async function processPrFollowupEvents( reviewId: event.id, reviewState: event.state, linkedIssue: event.linkedIssue, + prState: event.prState, + prMergedAt: event.prMergedAt, }); if (key) enqueued++; else skipped++; } else { @@ -576,6 +594,8 @@ export async function processPrFollowupEvents( author: event.author, mergeStateStatus: event.mergeStateStatus, linkedIssue: event.linkedIssue, + prState: event.prState, + prMergedAt: event.prMergedAt, }); if (key) enqueued++; else skipped++; } else {