Support GitHub SSH host aliases for PR actions

Resolves the case where a remote like `git@github-work:owner/repo.git`
points at github.com via `~/.ssh/config` but Paseo treats it as
non-GitHub.

Adds a centralized GitHub remote resolver that parses standard remotes
directly and resolves SSH aliases via `ssh -G` (with hostname
validation to keep the alias out of ssh's option parser). The resolver
is shared by workspace GitHub feature detection, PR status, PR
creation, and workspace git metadata.
This commit is contained in:
Jakub Skałecki
2026-04-27 20:28:10 +08:00
committed by GitHub
parent ec4f569f6e
commit ee5a23d962
10 changed files with 377 additions and 158 deletions
+8 -37
View File
@@ -1,6 +1,8 @@
import { z } from "zod";
import type { GitHubSearchKind } from "../shared/messages.js";
import { findExecutable } from "../utils/executable.js";
import { resolveGitHubRemote } from "../utils/github-remote.js";
import { runGitCommand } from "../utils/run-git-command.js";
import { execCommand } from "../utils/spawn.js";
const DEFAULT_GITHUB_CACHE_TTL_MS = 30_000;
@@ -1865,46 +1867,15 @@ function mapReviewDecision(value: unknown): PullRequestReviewDecision {
return null;
}
export interface GitHubRepoRemoteUrlResolver {
resolveRepoRemoteUrl(cwd: string, options?: GitHubReadOptions): Promise<string | null>;
}
export async function resolveGitHubRepo(
cwd: string,
options: { workspaceGitService: GitHubRepoRemoteUrlResolver; readOptions?: GitHubReadOptions },
): Promise<string | null> {
export async function resolveGitHubRepo(cwd: string): Promise<string | null> {
try {
const remoteUrl = await options.workspaceGitService.resolveRepoRemoteUrl(
const { stdout } = await runGitCommand(["config", "--get", "remote.origin.url"], {
cwd,
options.readOptions,
);
return parseGitHubRepoFromRemote(remoteUrl?.trim() ?? "");
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
});
const remote = await resolveGitHubRemote({ remoteUrl: stdout.trim() });
return remote?.repo ?? null;
} catch {
return null;
}
}
function parseGitHubRepoFromRemote(url: string): string | null {
if (!url) {
return null;
}
let cleaned = url;
if (cleaned.startsWith("git@github.com:")) {
cleaned = cleaned.slice("git@github.com:".length);
} else if (cleaned.startsWith("https://github.com/")) {
cleaned = cleaned.slice("https://github.com/".length);
} else if (cleaned.startsWith("http://github.com/")) {
cleaned = cleaned.slice("http://github.com/".length);
} else {
const marker = "github.com/";
const index = cleaned.indexOf(marker);
if (index === -1) {
return null;
}
cleaned = cleaned.slice(index + marker.length);
}
if (cleaned.endsWith(".git")) {
cleaned = cleaned.slice(0, -".git".length);
}
return cleaned.includes("/") ? cleaned : null;
}