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 <noreply@anthropic.com>

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
paseo-bot[bot]
2026-07-29 18:05:59 +02:00
committed by GitHub
co-authored by paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Mohamed Boudra Claude Opus 5
parent 891dd5a813
commit ee3a9b013b
5 changed files with 14 additions and 22 deletions
@@ -14,7 +14,6 @@ export interface GithubRepositoryChoice {
cloneUrl: string;
cloneProtocol?: "https" | "ssh";
description: string | null;
visibility: string | null;
updatedAt: string | null;
}
@@ -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,
}));
}
@@ -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: () =>
@@ -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",
+9 -14
View File
@@ -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<typeof PullRequestCheckoutTargetSchema>,
): PullRequestCheckoutTarget {