From 5f7fe5a7c3b27c724f1293d9c3b194f99131e544 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Mon, 13 Apr 2026 14:07:52 -0400 Subject: [PATCH] Fix search cache wipe on installation source failures Move OAuth owner exclusion to after successful installation context creation so failed sources don't poison the fallback query. Remove blanket organization exclusion that hid PRs/issues from orgs without the app installed. Add merge strategy to list cache so partial refetch results preserve previously cached items instead of replacing them. --- apps/dashboard/src/lib/github-cache.ts | 14 ++++++- apps/dashboard/src/lib/github.functions.ts | 48 ++++++++-------------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/apps/dashboard/src/lib/github-cache.ts b/apps/dashboard/src/lib/github-cache.ts index de0e940..4c8a1bf 100644 --- a/apps/dashboard/src/lib/github-cache.ts +++ b/apps/dashboard/src/lib/github-cache.ts @@ -71,6 +71,7 @@ type GetOrRevalidateGitHubResourceOptions = { getNamespaceVersions?: ( namespaceKeys: string[], ) => Promise>; + merge?: (existing: TData, fresh: TData) => TData; now?: () => number; }; @@ -615,6 +616,7 @@ export async function getOrRevalidateGitHubResource({ cacheMode = "legacy", payloadRetentionSeconds = DEFAULT_GITHUB_PAYLOAD_RETENTION_SECONDS, fetcher, + merge, now = Date.now, store, payloadStore, @@ -755,6 +757,14 @@ export async function getOrRevalidateGitHubResource({ return parseCachedPayload(existingEntry.payloadJson); } + const mergedData = + merge && existingEntry + ? merge( + parseCachedPayload(existingEntry.payloadJson), + result.data, + ) + : result.data; + const nextEntry = { cacheKey, userId, @@ -762,7 +772,7 @@ export async function getOrRevalidateGitHubResource({ paramsJson, etag: result.metadata.etag, lastModified: result.metadata.lastModified, - payloadJson: JSON.stringify(result.data), + payloadJson: JSON.stringify(mergedData), fetchedAt: currentTime, freshUntil: currentTime + @@ -780,7 +790,7 @@ export async function getOrRevalidateGitHubResource({ payloadRetentionSeconds, }); - return result.data; + return mergedData; })(); resolvedInFlightCache?.set(cacheKey, task); diff --git a/apps/dashboard/src/lib/github.functions.ts b/apps/dashboard/src/lib/github.functions.ts index 1ee394f..6d29eb0 100644 --- a/apps/dashboard/src/lib/github.functions.ts +++ b/apps/dashboard/src/lib/github.functions.ts @@ -3787,20 +3787,13 @@ async function getMySearchSources( deadlineAt: number, ): Promise { let installations: GitHubAppInstallation[] = []; - let organizations: GitHubOrganization[] = []; try { - const [installationResult, organizationResult] = - await withGitHubOperationTimeout( - "github search source discovery", - getRemainingSearchTimeoutMs(deadlineAt, MY_SEARCH_SOURCE_TIMEOUT_MS), - () => - Promise.all([ - getGitHubAppUserInstallations(context.session.user.id), - getGitHubAuthenticatedOrganizations(context), - ]), - ); + const installationResult = await withGitHubOperationTimeout( + "github search source discovery", + getRemainingSearchTimeoutMs(deadlineAt, MY_SEARCH_SOURCE_TIMEOUT_MS), + () => getGitHubAppUserInstallations(context.session.user.id), + ); installations = installationResult.installations; - organizations = organizationResult; } catch (error) { console.error("[github-search] failed to discover search sources", error); } @@ -3808,13 +3801,6 @@ async function getMySearchSources( const sources: GitHubGraphQLSearchSource[] = []; const excludedOAuthOwners = new Map(); - for (const organization of organizations) { - addExcludedOwnerScope(excludedOAuthOwners, { - login: organization.login, - targetType: "Organization", - }); - } - for (const installation of installations) { const owner = toSearchOwnerScope(installation); if (!owner) { @@ -3832,17 +3818,6 @@ async function getMySearchSources( break; } - if (owner.targetType === "Organization") { - addExcludedOwnerScope(excludedOAuthOwners, owner); - } - if ( - owner.targetType === "User" && - installation.repositorySelection === "all" && - normalizeLogin(owner.login) === normalizeLogin(viewerLogin) - ) { - addExcludedOwnerScope(excludedOAuthOwners, owner); - } - const installationContext = await withGitHubOperationTimeout( `github installation context ${installation.id}`, contextTimeoutMs, @@ -3859,6 +3834,17 @@ async function getMySearchSources( continue; } + if (owner.targetType === "Organization") { + addExcludedOwnerScope(excludedOAuthOwners, owner); + } + if ( + owner.targetType === "User" && + installation.repositorySelection === "all" && + normalizeLogin(owner.login) === normalizeLogin(viewerLogin) + ) { + addExcludedOwnerScope(excludedOAuthOwners, owner); + } + sources.push({ label: `installation:${installation.id}:${owner.login}`, context: installationContext, @@ -3959,6 +3945,7 @@ async function getMyPullsResult({ signalKeys: [githubRevalidationSignalKeys.pullsMine], namespaceKeys: [githubRevalidationSignalKeys.pullsMine], cacheMode: "split", + merge: (existing, fresh) => mergeMyPullsResults([existing, fresh]), fetcher: async () => { const deadlineAt = Date.now() + MY_SEARCH_TOTAL_TIMEOUT_MS; const sources = await getMySearchSources(context, username, deadlineAt); @@ -4130,6 +4117,7 @@ async function getMyIssuesResult({ signalKeys: [githubRevalidationSignalKeys.issuesMine], namespaceKeys: [githubRevalidationSignalKeys.issuesMine], cacheMode: "split", + merge: (existing, fresh) => mergeMyIssuesResults([existing, fresh]), fetcher: async () => { const deadlineAt = Date.now() + MY_SEARCH_TOTAL_TIMEOUT_MS; const sources = await getMySearchSources(context, username, deadlineAt);