From 6765cf46616882c691f5c6b4eaefa3e5145a348e Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 10 Jul 2026 11:17:45 -0700 Subject: [PATCH 1/3] feat: show not-found UI when an Ask GitHub repo doesn't exist Instead of throwing a ServiceErrorException (error boundary) when a repository doesn't exist on GitHub, the /askgh/[owner]/[repo] page now renders a dedicated "Repository not found" page. To distinguish "repo doesn't exist" from other failures, the signal is plumbed through the stack: - backend: experimental_addGithubRepo catches the Octokit 404 (new isNotFound helper) and returns a 404 instead of a generic 500. - web action: addGithubRepo maps a 404 response to a new repositoryNotFound service error (REPOSITORY_NOT_FOUND). - page: renders for REPOSITORY_NOT_FOUND, and only throws for other errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/backend/src/api.ts | 18 ++++++++--- packages/backend/src/errors.ts | 1 + .../[repo]/components/repoNotFound.tsx | 31 +++++++++++++++++++ .../app/(app)/askgh/[owner]/[repo]/page.tsx | 18 +++++++++-- .../web/src/features/workerApi/actions.ts | 5 ++- packages/web/src/lib/serviceError.ts | 8 +++++ 6 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 packages/web/src/app/(app)/askgh/[owner]/[repo]/components/repoNotFound.tsx diff --git a/packages/backend/src/api.ts b/packages/backend/src/api.ts index 3be070349..33f854ada 100644 --- a/packages/backend/src/api.ts +++ b/packages/backend/src/api.ts @@ -9,6 +9,7 @@ import { AccountPermissionSyncer } from './ee/accountPermissionSyncer.js'; import { PromClient } from './promClient.js'; import { RepoIndexManager } from './repoIndexManager.js'; import { createGitHubRepoRecord } from './repoCompileUtils.js'; +import { isNotFound } from './errors.js'; import { Octokit } from '@octokit/rest'; import { SINGLE_TENANT_ORG_ID } from './constants.js'; import z from 'zod'; @@ -150,10 +151,19 @@ export class Api { } const octokit = new Octokit(); - const response = await octokit.rest.repos.get({ - owner: parsed.data.owner, - repo: parsed.data.repo, - }); + let response; + try { + response = await octokit.rest.repos.get({ + owner: parsed.data.owner, + repo: parsed.data.repo, + }); + } catch (error) { + if (isNotFound(error)) { + res.status(404).json({ error: 'Repository not found on GitHub' }); + return; + } + throw error; + } const record = createGitHubRepoRecord({ repo: response.data, diff --git a/packages/backend/src/errors.ts b/packages/backend/src/errors.ts index d14760a49..e174f0b20 100644 --- a/packages/backend/src/errors.ts +++ b/packages/backend/src/errors.ts @@ -27,4 +27,5 @@ const getStatus = (err: unknown): number | null => { export const isUnauthorized = (err: unknown): boolean => getStatus(err) === 401; export const isForbidden = (err: unknown): boolean => getStatus(err) === 403; +export const isNotFound = (err: unknown): boolean => getStatus(err) === 404; export const isGone = (err: unknown): boolean => getStatus(err) === 410; diff --git a/packages/web/src/app/(app)/askgh/[owner]/[repo]/components/repoNotFound.tsx b/packages/web/src/app/(app)/askgh/[owner]/[repo]/components/repoNotFound.tsx new file mode 100644 index 000000000..070afa371 --- /dev/null +++ b/packages/web/src/app/(app)/askgh/[owner]/[repo]/components/repoNotFound.tsx @@ -0,0 +1,31 @@ +import { FileQuestion } from "lucide-react"; +import Link from "next/link"; +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +interface Props { + owner: string; + repo: string; +} + +export function RepoNotFound({ owner, repo }: Props) { + return ( +
+ +

+ Repository not found +

+

+ We couldn't find{" "} + {owner}/{repo}{" "} + on GitHub. It may not exist, or it might be private. +

+ + Go back home + +
+ ); +} diff --git a/packages/web/src/app/(app)/askgh/[owner]/[repo]/page.tsx b/packages/web/src/app/(app)/askgh/[owner]/[repo]/page.tsx index 37e49ce5a..caee69b70 100644 --- a/packages/web/src/app/(app)/askgh/[owner]/[repo]/page.tsx +++ b/packages/web/src/app/(app)/askgh/[owner]/[repo]/page.tsx @@ -1,10 +1,12 @@ import { addGithubRepo } from "@/features/workerApi/actions"; import { isServiceError } from "@/lib/utils"; -import { ServiceErrorException } from "@/lib/serviceError"; +import { ServiceError, ServiceErrorException } from "@/lib/serviceError"; +import { ErrorCode } from "@/lib/errorCodes"; import { __unsafePrisma } from "@/prisma"; import { getRepoInfo } from "./api"; import { CustomSlateEditor } from "@/features/chat/customSlateEditor"; import { RepoIndexedGuard } from "./components/repoIndexedGuard"; +import { RepoNotFound } from "./components/repoNotFound"; import { LandingPage } from "./components/landingPage"; import { getConfiguredLanguageModelsInfo } from "@/features/chat/utils.server"; import { auth } from "@/auth"; @@ -27,7 +29,7 @@ export default async function GitHubRepoPage(props: PageProps) { return ; } - const repoId = await (async () => { + const repoIdOrError = await (async (): Promise => { // 1. Look up repo by owner/repo const displayName = `${owner}/${repo}`; const existingRepo = await __unsafePrisma.repo.findFirst({ @@ -46,12 +48,22 @@ export default async function GitHubRepoPage(props: PageProps) { const response = await addGithubRepo(owner, repo); if (isServiceError(response)) { - throw new ServiceErrorException(response); + return response; } return response.repoId; })(); + if (isServiceError(repoIdOrError)) { + if (repoIdOrError.errorCode === ErrorCode.REPOSITORY_NOT_FOUND) { + return ; + } + + throw new ServiceErrorException(repoIdOrError); + } + + const repoId = repoIdOrError; + const repoInfo = await getRepoInfo(repoId) const languageModels = await getConfiguredLanguageModelsInfo() diff --git a/packages/web/src/features/workerApi/actions.ts b/packages/web/src/features/workerApi/actions.ts index 9e681d9de..e040ea1e0 100644 --- a/packages/web/src/features/workerApi/actions.ts +++ b/packages/web/src/features/workerApi/actions.ts @@ -1,7 +1,7 @@ 'use server'; import { sew } from "@/middleware/sew"; -import { unexpectedError } from "@/lib/serviceError"; +import { repositoryNotFound, unexpectedError } from "@/lib/serviceError"; import { withAuth, withOptionalAuth } from "@/middleware/withAuth"; import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole"; import { OrgRole } from "@sourcebot/db"; @@ -95,6 +95,9 @@ export const addGithubRepo = async (owner: string, repo: string) => sew(() => }); if (!response.ok) { + if (response.status === 404) { + return repositoryNotFound(`${owner}/${repo}`); + } return unexpectedError('Failed to add GitHub repo'); } diff --git a/packages/web/src/lib/serviceError.ts b/packages/web/src/lib/serviceError.ts index f2b447ced..d0b789f1d 100644 --- a/packages/web/src/lib/serviceError.ts +++ b/packages/web/src/lib/serviceError.ts @@ -103,6 +103,14 @@ export const notFound = (message?: string): ServiceError => { } } +export const repositoryNotFound = (repository: string): ServiceError => { + return { + statusCode: StatusCodes.NOT_FOUND, + errorCode: ErrorCode.REPOSITORY_NOT_FOUND, + message: `Repository "${repository}" was not found`, + } +} + export const userNotFound = (): ServiceError => { return { statusCode: StatusCodes.NOT_FOUND, From 87a28aa747a0aff00ea71552b7a9c884693ed544 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 10 Jul 2026 11:18:21 -0700 Subject: [PATCH 2/3] docs: add CHANGELOG entry for #1432 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b54760680..8383cb877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [EE] Added image attachments to Ask Sourcebot, letting users attach images to a chat message when the selected model supports image input. [#1375](https://github.com/sourcebot-dev/sourcebot/pull/1375) - Added deployment system resource stats (CPU cores + cgroup quota, host + container memory, disk, load average) to the service ping, so resource issues can be diagnosed more quickly. [#1424](https://github.com/sourcebot-dev/sourcebot/pull/1424) - Added a `robots.txt` that disallows crawlers, with an allowlist for link-preview bots so shared links keep their OpenGraph previews. [#1426](https://github.com/sourcebot-dev/sourcebot/pull/1426) +- [EE] Added a "repository not found" page to the experimental Ask GitHub repo flow (`/askgh`) shown when the repository doesn't exist on GitHub, instead of surfacing a generic error. [#1432](https://github.com/sourcebot-dev/sourcebot/pull/1432) ### Fixed - Send anonymous server-side PostHog events as personless so unauthenticated requests don't inflate person counts. [#1367](https://github.com/sourcebot-dev/sourcebot/pull/1367) From 8323ed6948f0bc9ff3d44f56bdd743267b5c61d1 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 10 Jul 2026 11:20:50 -0700 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8383cb877..b54760680 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [EE] Added image attachments to Ask Sourcebot, letting users attach images to a chat message when the selected model supports image input. [#1375](https://github.com/sourcebot-dev/sourcebot/pull/1375) - Added deployment system resource stats (CPU cores + cgroup quota, host + container memory, disk, load average) to the service ping, so resource issues can be diagnosed more quickly. [#1424](https://github.com/sourcebot-dev/sourcebot/pull/1424) - Added a `robots.txt` that disallows crawlers, with an allowlist for link-preview bots so shared links keep their OpenGraph previews. [#1426](https://github.com/sourcebot-dev/sourcebot/pull/1426) -- [EE] Added a "repository not found" page to the experimental Ask GitHub repo flow (`/askgh`) shown when the repository doesn't exist on GitHub, instead of surfacing a generic error. [#1432](https://github.com/sourcebot-dev/sourcebot/pull/1432) ### Fixed - Send anonymous server-side PostHog events as personless so unauthenticated requests don't inflate person counts. [#1367](https://github.com/sourcebot-dev/sourcebot/pull/1367)