From cf9037118b9b49c4b669577e18832118b35fec9b Mon Sep 17 00:00:00 2001 From: AbhishekMauryaGEEK Date: Fri, 5 Jun 2026 23:26:37 +0530 Subject: [PATCH] fix: make installation repository events idempotent --- .../process-installation-event.test.ts | 34 ++++++++++++++++++- .../functions/process-installation-event.ts | 5 ++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/inngest/functions/process-installation-event.test.ts b/src/inngest/functions/process-installation-event.test.ts index c0ef45e2..7bbead43 100644 --- a/src/inngest/functions/process-installation-event.test.ts +++ b/src/inngest/functions/process-installation-event.test.ts @@ -1,5 +1,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { processInstallationEvent } from './process-installation-event'; +import { + processInstallationEvent, + processInstallationReposEvent, +} from './process-installation-event'; import { sb, wire, step } from './__tests__/test-helpers'; // Mock external dependencies. @@ -13,6 +16,11 @@ vi.mock('../client', () => ({ }, })); +const reposRun = processInstallationReposEvent as unknown as (ctx: { + event: { data: { payload: Record } }; + step: typeof step; +}) => Promise; + // Handler references. const installRun = processInstallationEvent as unknown as (ctx: { event: { data: { payload: Record } }; @@ -121,4 +129,28 @@ describe('processInstallationEvent', () => { }), ); }); + it('repositories_added uses upsert to support webhook replays', async () => { + const repos = sb({ + upsert: vi.fn().mockResolvedValue({ error: null }), + }); + + wire({ + installation_repositories: repos, + }); + + await reposRun({ + event: { + data: { + payload: { + action: 'added', + installation: { id: 100 }, + repositories_added: [{ full_name: 'myorg/repo-a' }], + }, + }, + }, + step, + }); + + expect(repos.upsert).toHaveBeenCalled(); + }); }); diff --git a/src/inngest/functions/process-installation-event.ts b/src/inngest/functions/process-installation-event.ts index 7a2c7152..089c99be 100644 --- a/src/inngest/functions/process-installation-event.ts +++ b/src/inngest/functions/process-installation-event.ts @@ -220,11 +220,14 @@ export const processInstallationReposEvent = inngest.createFunction( if (!sb) throw new Error('service role missing'); if (payload.repositories_added?.length) { - await sb.from('installation_repositories').insert( + await sb.from('installation_repositories').upsert( payload.repositories_added.map((r) => ({ installation_id: payload.installation.id, repo_full_name: r.full_name, })), + { + onConflict: 'installation_id,repo_full_name', + }, ); // Fan-out a per-repo backfill for each new repo so the maintainer // queue picks them up without waiting for the next cron tick.