From b5b36ab2c9b64408d46a1d8feda5ec59284a8d46 Mon Sep 17 00:00:00 2001 From: maphew <486200+maphew@users.noreply.github.com> Date: Sat, 5 Sep 2026 06:01:53 +0000 Subject: [PATCH] fix(cloud-agent): dedupe repositories across GitHub installations Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../github-integration-helpers.test.ts | 82 +++++++++++++++++++ .../cloud-agent/github-integration-helpers.ts | 36 +++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/apps/web/src/lib/cloud-agent/github-integration-helpers.test.ts b/apps/web/src/lib/cloud-agent/github-integration-helpers.test.ts index ae735ca83f..d829640df5 100644 --- a/apps/web/src/lib/cloud-agent/github-integration-helpers.test.ts +++ b/apps/web/src/lib/cloud-agent/github-integration-helpers.test.ts @@ -212,6 +212,88 @@ describe('github-integration-helpers', () => { ]); }); + it('deduplicates a repository reachable through multiple installations, preferring the owning account', async () => { + mockGetIntegrationsByOrganization.mockResolvedValue([ + buildIntegration({ + id: 'integration-1', + platform_installation_id: 'installation-1', + platform_account_login: 'alice', + repositories: [ + { id: 1, name: 'api', full_name: 'acme/api', private: true }, + { id: 2, name: 'docs', full_name: 'acme/docs', private: false }, + ], + }), + buildIntegration({ + id: 'integration-2', + platform_installation_id: 'installation-2', + platform_account_login: 'acme', + repositories: [ + { id: 1, name: 'api', full_name: 'acme/api', private: true }, + { id: 3, name: 'scanner', full_name: 'acme/scanner', private: true }, + ], + }), + ]); + + const { fetchAllGitHubRepositoriesForOrganization } = + await import('./github-integration-helpers'); + const result = await fetchAllGitHubRepositoriesForOrganization('org-123'); + + expect(result.repositories).toEqual([ + expect.objectContaining({ + fullName: 'acme/api', + platformIntegrationId: 'integration-2', + platformAccountLogin: 'acme', + }), + expect.objectContaining({ + fullName: 'acme/docs', + platformIntegrationId: 'integration-1', + platformAccountLogin: 'alice', + }), + expect.objectContaining({ + fullName: 'acme/scanner', + platformIntegrationId: 'integration-2', + platformAccountLogin: 'acme', + }), + ]); + }); + + it('deduplicates repositories when one account has multiple active installations', async () => { + mockGetIntegrationsByOrganization.mockResolvedValue([ + buildIntegration({ + id: 'integration-newest', + platform_installation_id: 'installation-2', + platform_account_login: 'acme', + repositories: [ + { id: 1, name: 'api', full_name: 'acme/api', private: true }, + { id: 2, name: 'docs', full_name: 'acme/docs', private: false }, + ], + }), + buildIntegration({ + id: 'integration-stale', + platform_installation_id: 'installation-1', + platform_account_login: 'acme', + repositories: [{ id: 1, name: 'api', full_name: 'acme/api', private: true }], + }), + ]); + + const { fetchAllGitHubRepositoriesForOrganization } = + await import('./github-integration-helpers'); + const result = await fetchAllGitHubRepositoriesForOrganization('org-123'); + + expect(result.repositories).toEqual([ + expect.objectContaining({ + fullName: 'acme/api', + platformIntegrationId: 'integration-newest', + platformAccountLogin: 'acme', + }), + expect.objectContaining({ + fullName: 'acme/docs', + platformIntegrationId: 'integration-newest', + platformAccountLogin: 'acme', + }), + ]); + }); + it('returns repositories from healthy installations when a sibling fetch fails', async () => { mockGetIntegrationsByOrganization.mockResolvedValue([ buildIntegration({ diff --git a/apps/web/src/lib/cloud-agent/github-integration-helpers.ts b/apps/web/src/lib/cloud-agent/github-integration-helpers.ts index 91329a3f82..dcd80f1960 100644 --- a/apps/web/src/lib/cloud-agent/github-integration-helpers.ts +++ b/apps/web/src/lib/cloud-agent/github-integration-helpers.ts @@ -197,6 +197,40 @@ export async function fetchAllGitHubRepositoriesForOrganization( return fetchRepositoriesForIntegrations(integrations, forceRefresh); } +const repositoryOwner = (fullName: string) => { + const slash = fullName.indexOf('/'); + return slash === -1 ? fullName : fullName.slice(0, slash); +}; + +/** + * Keeps a single entry per repository when the same repo is reachable through + * more than one GitHub installation (for example the app is installed on two + * accounts that share a repo, or a reinstall left two active installation rows + * for the same account). Prefer the installation whose GitHub account owns the + * repository so sessions use the canonical write identity; otherwise keep the + * first entry, which is the newest installation (created_at desc). + */ +function dedupeGitHubRepositories( + repositories: GitHubRepositoriesResult['repositories'] +): GitHubRepositoriesResult['repositories'] { + const byFullName = new Map(); + for (const repo of repositories) { + const key = repo.fullName.toLowerCase(); + const existing = byFullName.get(key); + if (!existing) { + byFullName.set(key, repo); + continue; + } + const owner = repositoryOwner(existing.fullName).toLowerCase(); + const existingIsOwner = (existing.platformAccountLogin ?? '').toLowerCase() === owner; + const candidateIsOwner = (repo.platformAccountLogin ?? '').toLowerCase() === owner; + if (candidateIsOwner && !existingIsOwner) { + byFullName.set(key, repo); + } + } + return [...byFullName.values()]; +} + async function fetchRepositoriesForIntegrations( integrations: Awaited>, forceRefresh: boolean @@ -235,7 +269,7 @@ async function fetchRepositoriesForIntegrations( } return { integrationInstalled: true, - repositories: results.flatMap(result => result.repositories), + repositories: dedupeGitHubRepositories(results.flatMap(result => result.repositories)), syncedAt: results .map(result => result.syncedAt) .filter((value): value is string => value !== null)