fix(github): accept SSH certificate org remotes (org-<id>@github.com) (#30659)

GitHub orgs using an SSH certificate authority get an SCP remote whose user is
org-<id> 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 <yi-111-a@users.noreply.github.com>
This commit is contained in:
yi111
2026-10-19 04:43:32 -07:00
committed by GitHub
co-authored by Yi-111-a
parent 346f6028a3
commit ec0c1f115a
3 changed files with 47 additions and 4 deletions
@@ -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')
})
})
@@ -27,14 +27,22 @@ function parseGitHubRemotePath(path: string): Pick<GitHubRemoteIdentity, 'owner'
export function remoteUrlUsesSshTransport(remoteUrl: string): boolean {
const trimmed = remoteUrl.trim().toLowerCase()
return (
trimmed.startsWith('git@') || trimmed.startsWith('ssh://') || trimmed.startsWith('git+ssh://')
isScpStyleRemote(trimmed) || trimmed.startsWith('ssh://') || trimmed.startsWith('git+ssh://')
)
}
/**
* Why: GitHub SSH certificate organizations hand out `org-<id>@github.com:<owner>/<repo>`,
* 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] }
}
+2 -1
View File
@@ -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-<id>@github.com:<owner>/<repo>`, 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])
}