From ee3a9b013b3509769af0cb39e865d626e0ede822 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:05:59 +0200 Subject: [PATCH] Support repository search on older GitHub CLI versions (#2611) * fix(github): support repository lists from older gh versions GitHub CLI versions before 2.28 reject the visibility JSON field before returning repository data. Retry that specific failure with the older isPrivate field while preserving exact visibility on newer clients. * fix(github): stop requesting repository visibility Repository cloning does not use visibility. Leave the released response field optional and keep repository discovery on JSON fields supported by older GitHub CLI versions. * fix(github): keep the visibility enum on the wire, derive it from isPrivate Restores the required visibility enum in the search-repositories response schema. Omitting it broke old clients, which reject the whole response. The daemon still never asks gh for visibility -- that JSON field does not exist before gh 2.28 and Debian Bookworm ships 2.23. It asks for isPrivate, supported on every version, and maps it to public/private. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Mohamed Boudra Co-authored-by: Claude Opus 5 --- packages/app/src/add-project-flow/model.ts | 1 - packages/app/src/add-project-flow/options.ts | 2 -- .../app/src/components/add-project-flow.tsx | 2 +- .../services/github-repository-search.test.ts | 8 +++---- .../server/src/services/github-service.ts | 23 ++++++++----------- 5 files changed, 14 insertions(+), 22 deletions(-) diff --git a/packages/app/src/add-project-flow/model.ts b/packages/app/src/add-project-flow/model.ts index 16b80e9c4..3cfd7691a 100644 --- a/packages/app/src/add-project-flow/model.ts +++ b/packages/app/src/add-project-flow/model.ts @@ -14,7 +14,6 @@ export interface GithubRepositoryChoice { cloneUrl: string; cloneProtocol?: "https" | "ssh"; description: string | null; - visibility: string | null; updatedAt: string | null; } diff --git a/packages/app/src/add-project-flow/options.ts b/packages/app/src/add-project-flow/options.ts index 1c9fec5bf..c1d6ce9e4 100644 --- a/packages/app/src/add-project-flow/options.ts +++ b/packages/app/src/add-project-flow/options.ts @@ -101,7 +101,6 @@ export function buildManualGithubRepositoryChoices(query: string): GithubReposit nameWithOwner: identity?.repo ?? remoteName, cloneUrl: repo, description: "Clone this repository URL", - visibility: null, updatedAt: null, }, ]; @@ -116,7 +115,6 @@ export function buildManualGithubRepositoryChoices(query: string): GithubReposit cloneUrl: nameWithOwner, cloneProtocol, description: `Clone owner/repo via ${cloneProtocol.toUpperCase()}`, - visibility: null, updatedAt: null, })); } diff --git a/packages/app/src/components/add-project-flow.tsx b/packages/app/src/components/add-project-flow.tsx index 3078062a7..45425fde8 100644 --- a/packages/app/src/components/add-project-flow.tsx +++ b/packages/app/src/components/add-project-flow.tsx @@ -644,7 +644,7 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) { title: repository.cloneProtocol ? `${repository.nameWithOwner} via ${repository.cloneProtocol.toUpperCase()}` : repository.nameWithOwner, - subtitle: repository.description ?? repository.visibility, + subtitle: repository.description, icon: Github, testID: `add-project-flow-repository-${repository.id}`, select: () => diff --git a/packages/server/src/services/github-repository-search.test.ts b/packages/server/src/services/github-repository-search.test.ts index 94e2ee068..d324aab16 100644 --- a/packages/server/src/services/github-repository-search.test.ts +++ b/packages/server/src/services/github-repository-search.test.ts @@ -30,7 +30,7 @@ describe("GitHub repository search", () => { name: " paseo ", nameWithOwner: " getpaseo/paseo ", description: null, - visibility: "PUBLIC", + isPrivate: false, updatedAt: "2026-07-15T12:00:00Z", sshUrl: " git@github.com:getpaseo/paseo.git ", url: "https://github.com/getpaseo/paseo", @@ -62,7 +62,7 @@ describe("GitHub repository search", () => { "repo", "list", "--json", - "id,name,nameWithOwner,description,visibility,updatedAt,sshUrl,url", + "id,name,nameWithOwner,description,isPrivate,updatedAt,sshUrl,url", "--limit", "8", ], @@ -83,7 +83,7 @@ describe("GitHub repository search", () => { name: "private-repo", fullName: "octo/private-repo", description: "Private project", - visibility: "PRIVATE", + isPrivate: true, updatedAt: "2026-07-14T08:00:00Z", url: "https://github.com/octo/private-repo", }, @@ -115,7 +115,7 @@ describe("GitHub repository search", () => { "repos", "private project", "--json", - "id,name,fullName,description,visibility,updatedAt,url", + "id,name,fullName,description,isPrivate,updatedAt,url", "--sort", "updated", "--order", diff --git a/packages/server/src/services/github-service.ts b/packages/server/src/services/github-service.ts index e2d82db88..d43916cb6 100644 --- a/packages/server/src/services/github-service.ts +++ b/packages/server/src/services/github-service.ts @@ -147,7 +147,7 @@ const GitHubRepositoryListItemSchema = z.object({ name: z.string(), nameWithOwner: z.string(), description: z.string().nullable().optional(), - visibility: z.string(), + isPrivate: z.boolean(), updatedAt: z.string(), sshUrl: z.string(), url: z.string(), @@ -158,7 +158,7 @@ const GitHubRepositorySearchItemSchema = z.object({ name: z.string(), fullName: z.string(), description: z.string().nullable().optional(), - visibility: z.string(), + isPrivate: z.boolean(), updatedAt: z.string(), url: z.string(), }); @@ -1287,7 +1287,7 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G "repo", "list", "--json", - "id,name,nameWithOwner,description,visibility,updatedAt,sshUrl,url", + "id,name,nameWithOwner,description,isPrivate,updatedAt,sshUrl,url", "--limit", String(limit), ], @@ -1305,7 +1305,7 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G "repos", query, "--json", - "id,name,fullName,description,visibility,updatedAt,url", + "id,name,fullName,description,isPrivate,updatedAt,url", "--sort", "updated", "--order", @@ -2315,7 +2315,7 @@ function normalizeRepositorySummary(repository: { name: string; nameWithOwner: string; description?: string | null; - visibility: string; + isPrivate: boolean; updatedAt: string; cloneUrl: string; }): GitHubRepositorySummary { @@ -2328,20 +2328,15 @@ function normalizeRepositorySummary(repository: { name: repository.name.trim(), nameWithOwner, description: repository.description ?? null, - visibility: normalizeRepositoryVisibility(repository.visibility), + // We query isPrivate, not visibility: `gh repo list --json visibility` is unsupported before + // gh 2.28 (Debian Bookworm ships 2.23), while isPrivate works on every version. The wire schema + // requires the visibility enum, so old clients still get a value they can parse. + visibility: repository.isPrivate ? "private" : "public", updatedAt: repository.updatedAt, cloneUrl: repository.cloneUrl.trim(), }; } -function normalizeRepositoryVisibility(visibility: string): GitHubRepositorySummary["visibility"] { - const normalized = visibility.toLowerCase(); - if (normalized === "public" || normalized === "private" || normalized === "internal") { - return normalized; - } - throw new Error(`Unknown GitHub repository visibility: ${visibility}`); -} - function toPullRequestCheckoutTarget( parsed: z.infer, ): PullRequestCheckoutTarget {