From ec0c1f115a4211457b88cc1d1f09745b60bb2da2 Mon Sep 17 00:00:00 2001 From: yi111 <153097222+Yi-111-a@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:43:32 +0800 Subject: [PATCH] fix(github): accept SSH certificate org remotes (org-@github.com) (#30659) GitHub orgs using an SSH certificate authority get an SCP remote whose user is org- rather than git. The three GitHub remote parsers only matched a literal git@ user, so those repos were classified not-github and lost PR, issue and check support. Co-authored-by: Yi-111-a --- .../github-remote-identity-parsing.test.ts | 34 +++++++++++++++++++ .../github/github-remote-identity-parsing.ts | 14 ++++++-- src/shared/project-host-setup-projection.ts | 3 +- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/main/github/github-remote-identity-parsing.test.ts b/src/main/github/github-remote-identity-parsing.test.ts index d2b9f1e629f..9d8ccc4abd3 100644 --- a/src/main/github/github-remote-identity-parsing.test.ts +++ b/src/main/github/github-remote-identity-parsing.test.ts @@ -6,6 +6,7 @@ import { parseGitHubOwnerRepo, parseGitHubOwnerRepoWithResolvedSshHostname, parseGitHubRemoteIdentity, + rawSshTransportHost, remoteUrlUsesSshTransport } from './github-remote-identity-parsing' @@ -188,3 +189,36 @@ describe('SSH Host alias identity (#10284)', () => { expect(effectiveGitHubRemoteHost('github.com', null)).toBe('github.com') }) }) + +describe('SSH certificate org remotes (#30601)', () => { + const certRemote = 'org-123456@github.com:Acme/app.git' + + it('parses an org-id SCP remote as a GitHub identity', () => { + expect(parseGitHubRemoteIdentity(certRemote)).toEqual({ + host: 'github.com', + owner: 'Acme', + repo: 'app' + }) + expect(parseGitHubOwnerRepo(certRemote)).toEqual({ owner: 'Acme', repo: 'app' }) + }) + + it('treats an org-id SCP remote as SSH transport', () => { + expect(remoteUrlUsesSshTransport(certRemote)).toBe(true) + }) + + it('reads the transport host from an org-id SCP remote', () => { + expect(rawSshTransportHost(certRemote)).toBe('github.com') + }) + + it('does not need an SSH config expansion for an org-id github.com remote', () => { + expect(gitHubSshConfigHostAlias(certRemote)).toBeNull() + expect( + parseGitHubOwnerRepoWithResolvedSshHostname(certRemote, null) + ).toEqual({ owner: 'Acme', repo: 'app' }) + }) + + it('still ignores non-GitHub hosts carrying an org-id user', () => { + expect(parseGitHubOwnerRepo('org-123456@github-work:Acme/app.git')).toBeNull() + expect(gitHubSshConfigHostAlias('org-123456@github-work:Acme/app.git')).toBe('github-work') + }) +}) diff --git a/src/main/github/github-remote-identity-parsing.ts b/src/main/github/github-remote-identity-parsing.ts index 32c640b8796..c9ec59575f1 100644 --- a/src/main/github/github-remote-identity-parsing.ts +++ b/src/main/github/github-remote-identity-parsing.ts @@ -27,14 +27,22 @@ function parseGitHubRemotePath(path: string): Pick@github.com:/`, + * so the SCP user is not always `git`. + */ +function isScpStyleRemote(remoteUrl: string): boolean { + return /^[^@\s/:]+@[^:\s/]+:[^\s]+$/.test(remoteUrl) +} + /** SSH transport host as written, preserving SCP alias case. */ export function rawSshTransportHost(remoteUrl: string): string | null { const trimmed = remoteUrl.trim() - const scpMatch = trimmed.match(/^git@([^:]+):/i) + const scpMatch = trimmed.match(/^[^@\s/:]+@([^:]+):/i) if (scpMatch) { return scpMatch[1] } @@ -63,7 +71,7 @@ export function gitHubSshConfigHostAlias(remoteUrl: string): string | null { export function parseGitHubRemoteIdentity(remoteUrl: string): GitHubRemoteIdentity | null { const trimmed = remoteUrl.trim() - const sshMatch = trimmed.match(/^git@([^:]+):([^/]+)\/([^/]+?)(?:\.git)?$/i) + const sshMatch = trimmed.match(/^[^@\s/:]+@([^:]+):([^/]+)\/([^/]+?)(?:\.git)?$/i) if (sshMatch) { return { host: normalizeGitHubRemoteHost(sshMatch[1]), owner: sshMatch[2], repo: sshMatch[3] } } diff --git a/src/shared/project-host-setup-projection.ts b/src/shared/project-host-setup-projection.ts index 1c4d6f06c4b..04b1c523f51 100644 --- a/src/shared/project-host-setup-projection.ts +++ b/src/shared/project-host-setup-projection.ts @@ -197,7 +197,8 @@ function parseGitHubRemoteUrl(remoteUrl: string | undefined): ProjectProviderIde if (!trimmed) { return null } - const sshMatch = trimmed.match(/^git@([^:]+):([^/]+)\/([^/]+?)(?:\.git)?$/i) + // Why: SSH certificate orgs use `org-@github.com:/`, so the user is not always `git`. + const sshMatch = trimmed.match(/^[^@\s/:]+@([^:]+):([^/]+)\/([^/]+?)(?:\.git)?$/i) if (sshMatch?.[1] && sshMatch[2] && sshMatch[3]) { return projectProviderIdentity(sshMatch[1], sshMatch[2], sshMatch[3]) }