From b64a69bc497d1e66cf957e27931780e6458d9feb Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:37:31 -0700 Subject: [PATCH] fix(github): host-qualify GHES gh commands and probe auth in the repo runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two correctness issues found in review of the #8312 fix. 1. GHES host was discarded before `gh pr create`. `--repo owner/repo` shorthand resolves against gh's default host (usually github.com), so for a user authed to both github.com and GHES it could target a same-named github.com repo or fail — deterministic for SSH repos, which run gh with no cwd. Now `createGitHubPullRequest` and the `findOpenPRByHeadBase` fallback pass a host-qualified `HOST/owner/repo` for GHES (github.com keeps the shorthand). Also generalize `parseCreatePRPayload`'s URL regex off github.com so a GHES PR URL parses directly instead of limping through the list fallback. 2. GHES auth was probed on the wrong gh runtime. `getAuthenticatedGitHubHosts` ran a global `gh auth status` with no cwd/WSL/SSH context and cached every runtime under one "local" key, so a GHES login present only in the repo's WSL distro was missed and the repo fell back to Gitea. Replaced with `isGitHubHostAuthenticated`, which runs `gh auth status --hostname ` with the repository's execution options (cwd/WSL distro, or SSH-local like the create path) and caches per runtime+host — mirroring GitLab's isGlabConfiguredForRemoteHost. This also honors GH_ENTERPRISE_TOKEN inferred from repo context. Spawn failures stay indeterminate (uncached). Adds createGitHubPullRequest-level tests asserting the actual gh `--repo` arguments (create + fallback) and the WSL/SSH runtime of the auth probe. --- src/main/github/client-create-pr.test.ts | 80 +++++++++++ src/main/github/client.ts | 27 +++- .../github-enterprise-repository.test.ts | 129 ++++++++++++------ .../github/github-enterprise-repository.ts | 117 ++++++++++------ 4 files changed, 257 insertions(+), 96 deletions(-) diff --git a/src/main/github/client-create-pr.test.ts b/src/main/github/client-create-pr.test.ts index 6c64e4b8066..79d614eb0d0 100644 --- a/src/main/github/client-create-pr.test.ts +++ b/src/main/github/client-create-pr.test.ts @@ -4,6 +4,7 @@ import { readFile } from 'node:fs/promises' const { ghExecFileAsyncMock, getOwnerRepoMock, + getEnterpriseGitHubRepoSlugMock, extractExecErrorMock, acquireMock, releaseMock, @@ -11,6 +12,7 @@ const { } = vi.hoisted(() => ({ ghExecFileAsyncMock: vi.fn(), getOwnerRepoMock: vi.fn(), + getEnterpriseGitHubRepoSlugMock: vi.fn(), extractExecErrorMock: vi.fn((error: unknown) => { const value = error as { stderr?: string; stdout?: string; message?: string } return { @@ -52,12 +54,18 @@ vi.mock('../git/runner', () => ({ gitExecFileAsync: vi.fn() })) +vi.mock('./github-enterprise-repository', () => ({ + getEnterpriseGitHubRepoSlug: getEnterpriseGitHubRepoSlugMock +})) + import { createGitHubPullRequest } from './client' describe('createGitHubPullRequest', () => { beforeEach(() => { ghExecFileAsyncMock.mockReset() getOwnerRepoMock.mockReset() + getEnterpriseGitHubRepoSlugMock.mockReset() + getEnterpriseGitHubRepoSlugMock.mockResolvedValue(null) extractExecErrorMock.mockClear() acquireMock.mockReset() releaseMock.mockReset() @@ -115,6 +123,78 @@ describe('createGitHubPullRequest', () => { expect(releaseMock).toHaveBeenCalledOnce() }) + it('host-qualifies --repo for a GHES remote so gh targets the Enterprise server (#8312)', async () => { + // github.com-only slug parsing misses GHES, so creation comes from the + // enterprise resolver, which carries the host. + getOwnerRepoMock.mockResolvedValueOnce(null) + getEnterpriseGitHubRepoSlugMock.mockResolvedValueOnce({ + owner: 'team', + repo: 'orca', + host: 'github.acme-corp.com' + }) + // gh prints the PR URL (not JSON); the GHES host must still parse directly. + ghExecFileAsyncMock.mockResolvedValueOnce({ + stdout: 'https://github.acme-corp.com/team/orca/pull/7\n' + }) + + await expect( + createGitHubPullRequest('/repo-root', { + provider: 'github', + base: 'main', + head: 'feature/create-pr', + title: 'GHES PR' + }) + ).resolves.toEqual({ + ok: true, + number: 7, + url: 'https://github.acme-corp.com/team/orca/pull/7' + }) + + const [args] = ghExecFileAsyncMock.mock.calls[0] + // Bare "team/orca" would resolve against gh's default host (github.com); + // the host prefix pins the command to the Enterprise server. + expect(args[args.indexOf('--repo') + 1]).toBe('github.acme-corp.com/team/orca') + }) + + it('host-qualifies --repo for the GHES existing-PR fallback lookup (#8312)', async () => { + getOwnerRepoMock.mockResolvedValue(null) + getEnterpriseGitHubRepoSlugMock.mockResolvedValue({ + owner: 'team', + repo: 'orca', + host: 'github.acme-corp.com' + }) + // Create reports "already exists", forcing the pr-list fallback. + ghExecFileAsyncMock + .mockRejectedValueOnce( + Object.assign(new Error('exists'), { + stderr: 'a pull request for branch "feature/create-pr" already exists', + stdout: '' + }) + ) + .mockResolvedValueOnce({ + stdout: JSON.stringify([ + { number: 9, url: 'https://github.acme-corp.com/team/orca/pull/9' } + ]) + }) + + await expect( + createGitHubPullRequest('/repo-root', { + provider: 'github', + base: 'main', + head: 'feature/create-pr', + title: 'GHES PR' + }) + ).resolves.toMatchObject({ + ok: false, + code: 'already_exists', + existingReview: { number: 9, url: 'https://github.acme-corp.com/team/orca/pull/9' } + }) + + const [listArgs] = ghExecFileAsyncMock.mock.calls[1] + expect(listArgs).toEqual(expect.arrayContaining(['pr', 'list'])) + expect(listArgs[listArgs.indexOf('--repo') + 1]).toBe('github.acme-corp.com/team/orca') + }) + it('runs local WSL project pull request creation through the selected distro', async () => { getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' }) ghExecFileAsyncMock.mockResolvedValueOnce({ diff --git a/src/main/github/client.ts b/src/main/github/client.ts index 31dbc519571..5848454e6dd 100644 --- a/src/main/github/client.ts +++ b/src/main/github/client.ts @@ -1751,16 +1751,29 @@ function parseCreatePRPayload(stdout: string): { number: number; url: string } | } catch { // Fall through to URL parsing for older gh versions without --json support. } - const urlMatch = trimmed.match(/https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/pull\/(\d+)/) + // Why: gh prints the PR URL (not JSON) here; match any host, not just + // github.com, so a GitHub Enterprise Server URL still parses directly (#8312). + const urlMatch = trimmed.match(/https?:\/\/[^\s/]+\/[^\s/]+\/[^\s/]+\/pull\/(\d+)/) if (!urlMatch) { return null } return { number: Number(urlMatch[1]), url: urlMatch[0] } } +// Why: `gh --repo OWNER/REPO` resolves the shorthand against gh's default host +// (usually github.com), not the repo's remote — so a GHES repo would target a +// same-named github.com repo, or fail. Qualify with the host for GHES so gh hits +// the Enterprise server; this is the only host signal for SSH repos, which run +// gh with no cwd context (#8312). github.com keeps the bare shorthand. +function ghRepoArg(slug: { owner: string; repo: string; host?: string }): string { + return slug.host && slug.host.toLowerCase() !== 'github.com' + ? `${slug.host}/${slug.owner}/${slug.repo}` + : `${slug.owner}/${slug.repo}` +} + async function findOpenPRByHeadBase(args: { repoPath: string - ownerRepo: OwnerRepo + repoArg: string head: string base: string connectionId?: string | null @@ -1772,7 +1785,7 @@ async function findOpenPRByHeadBase(args: { 'pr', 'list', '--repo', - `${args.ownerRepo.owner}/${args.ownerRepo.repo}`, + args.repoArg, '--head', args.head, '--base', @@ -1857,6 +1870,8 @@ export async function createGitHubPullRequest( error: 'Creating pull requests requires a GitHub remote.' } } + // Host-qualified for GHES so gh targets the Enterprise server, not github.com. + const repoArg = ghRepoArg(ownerRepo) const base = normalizeHostedReviewBaseRef(input.base) const head = input.head ? normalizeHostedReviewHeadRef(input.head) || undefined : undefined @@ -1889,7 +1904,7 @@ export async function createGitHubPullRequest( 'pr', 'create', '--repo', - `${ownerRepo.owner}/${ownerRepo.repo}`, + repoArg, '--base', base, '--title', @@ -1918,7 +1933,7 @@ export async function createGitHubPullRequest( const found = head ? await findOpenPRByHeadBase({ repoPath, - ownerRepo, + repoArg, head, base, connectionId, @@ -1942,7 +1957,7 @@ export async function createGitHubPullRequest( ) { const existing = await findOpenPRByHeadBase({ repoPath, - ownerRepo, + repoArg, head, base, connectionId, diff --git a/src/main/github/github-enterprise-repository.test.ts b/src/main/github/github-enterprise-repository.test.ts index cdc409f6322..73de02ddae9 100644 --- a/src/main/github/github-enterprise-repository.test.ts +++ b/src/main/github/github-enterprise-repository.test.ts @@ -5,17 +5,17 @@ const { ghExecFileAsyncMock, gitExecFileAsyncMock } = vi.hoisted(() => ({ gitExecFileAsyncMock: vi.fn() })) -// Mock only the exec boundary so the real remote-identity parsing and -// `gh auth status` parsing run against controlled command output. +// Mock only the exec boundary so the real remote-identity parsing, runtime +// option resolution, and `gh auth status` parsing run against controlled output. vi.mock('../git/runner', () => ({ ghExecFileAsync: ghExecFileAsyncMock, gitExecFileAsync: gitExecFileAsyncMock })) import { - _resetAuthenticatedGitHubHostsCache, - getAuthenticatedGitHubHosts, - getEnterpriseGitHubRepoSlug + _resetGitHubHostAuthCache, + getEnterpriseGitHubRepoSlug, + isGitHubHostAuthenticated } from './github-enterprise-repository' function mockOriginRemote(url: string): void { @@ -27,26 +27,50 @@ function mockOriginRemote(url: string): void { }) } -function mockGhAuthStatus(hosts: string[]): void { - const text = hosts - .map( - (host) => - `${host}\n ✓ Logged in to ${host} account kelora (keyring)\n - Active account: true\n - Token scopes: 'repo', 'read:org'` - ) - .join('\n') - ghExecFileAsyncMock.mockResolvedValue({ stdout: text, stderr: '' }) +// gh exit 0 for `auth status --hostname ` means logged in to that host. +function mockHostAuthenticated(host = 'github.acme-corp.com'): void { + ghExecFileAsyncMock.mockResolvedValue({ + stdout: `${host}\n ✓ Logged in to ${host} account kelora (keyring)`, + stderr: '' + }) +} + +// gh exits non-zero and reports no matching host when not logged in. +function mockHostNotAuthenticated(): void { + ghExecFileAsyncMock.mockRejectedValue( + Object.assign(new Error('exit 1'), { + stdout: '', + stderr: 'You are not logged into any GitHub hosts. To log in, run: gh auth login' + }) + ) } describe('getEnterpriseGitHubRepoSlug', () => { beforeEach(() => { ghExecFileAsyncMock.mockReset() gitExecFileAsyncMock.mockReset() - _resetAuthenticatedGitHubHostsCache() + _resetGitHubHostAuthCache() }) it('resolves a GHES remote whose host the user is gh-authenticated to (#8312)', async () => { mockOriginRemote('https://github.acme-corp.com/team/orca.git') - mockGhAuthStatus(['github.acme-corp.com']) + mockHostAuthenticated() + + await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({ + owner: 'team', + repo: 'orca', + host: 'github.acme-corp.com' + }) + // The auth probe targets the remote's host, not a hardcoded github.com. + expect(ghExecFileAsyncMock).toHaveBeenCalledWith( + ['auth', 'status', '--hostname', 'github.acme-corp.com'], + { cwd: '/repo' } + ) + }) + + it('resolves a GHES SCP-style SSH remote', async () => { + mockOriginRemote('git@github.acme-corp.com:team/orca.git') + mockHostAuthenticated() await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({ owner: 'team', @@ -55,15 +79,18 @@ describe('getEnterpriseGitHubRepoSlug', () => { }) }) - it('resolves a GHES SCP-style SSH remote', async () => { - mockOriginRemote('git@github.acme-corp.com:team/orca.git') - mockGhAuthStatus(['github.acme-corp.com']) + it('probes gh in the repository WSL runtime, not the host/default distro', async () => { + mockOriginRemote('https://github.acme-corp.com/team/orca.git') + mockHostAuthenticated() - await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toEqual({ - owner: 'team', - repo: 'orca', - host: 'github.acme-corp.com' + await getEnterpriseGitHubRepoSlug('/repo', null, { + localGitExecOptions: { wslDistro: 'Ubuntu' } }) + + expect(ghExecFileAsyncMock).toHaveBeenCalledWith( + ['auth', 'status', '--hostname', 'github.acme-corp.com'], + { cwd: '/repo', wslDistro: 'Ubuntu' } + ) }) it('leaves github.com to getOwnerRepo without probing gh auth', async () => { @@ -75,16 +102,17 @@ describe('getEnterpriseGitHubRepoSlug', () => { it('declines a custom host the user is not gh-authenticated to (leaves it for Gitea)', async () => { mockOriginRemote('https://gitea.example.com/team/orca.git') - mockGhAuthStatus(['github.com']) + mockHostNotAuthenticated() await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull() }) it('returns null for an unparseable remote', async () => { mockOriginRemote('not-a-remote-url') - mockGhAuthStatus(['github.acme-corp.com']) + mockHostAuthenticated() await expect(getEnterpriseGitHubRepoSlug('/repo')).resolves.toBeNull() + expect(ghExecFileAsyncMock).not.toHaveBeenCalled() }) it('returns null when the origin remote lookup fails', async () => { @@ -94,49 +122,60 @@ describe('getEnterpriseGitHubRepoSlug', () => { }) }) -describe('getAuthenticatedGitHubHosts', () => { +describe('isGitHubHostAuthenticated', () => { beforeEach(() => { ghExecFileAsyncMock.mockReset() gitExecFileAsyncMock.mockReset() - _resetAuthenticatedGitHubHostsCache() + _resetGitHubHostAuthCache() }) - it('parses and lowercases the logged-in hosts', async () => { - mockGhAuthStatus(['github.com', 'GitHub.Acme-Corp.com']) + it('runs gh in the SSH-local runtime (no cwd) for connection-backed repos', async () => { + mockHostAuthenticated() - const hosts = await getAuthenticatedGitHubHosts() - expect(hosts.has('github.com')).toBe(true) - expect(hosts.has('github.acme-corp.com')).toBe(true) + await expect( + isGitHubHostAuthenticated('github.acme-corp.com', '/remote/repo', 'ssh-1') + ).resolves.toBe(true) + expect(ghExecFileAsyncMock).toHaveBeenCalledWith( + ['auth', 'status', '--hostname', 'github.acme-corp.com'], + {} + ) }) - it('caches a non-empty result to avoid re-spawning gh per detection poll', async () => { - mockGhAuthStatus(['github.acme-corp.com']) + it('caches per runtime+host so detection polling does not re-spawn gh', async () => { + mockHostAuthenticated() - await getAuthenticatedGitHubHosts() - await getAuthenticatedGitHubHosts() + await isGitHubHostAuthenticated('github.acme-corp.com', '/repo') + await isGitHubHostAuthenticated('github.acme-corp.com', '/repo') expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1) }) - it('recovers hosts from gh output even when gh exits non-zero', async () => { + it('does not share cache state across WSL distros', async () => { + mockHostAuthenticated() + + await isGitHubHostAuthenticated('github.acme-corp.com', '/repo', null, { wslDistro: 'Ubuntu' }) + await isGitHubHostAuthenticated('github.acme-corp.com', '/repo', null, { wslDistro: 'Debian' }) + expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2) + }) + + it('treats a listed host as authenticated even when gh exits non-zero', async () => { ghExecFileAsyncMock.mockRejectedValue( Object.assign(new Error('exit 1'), { - stdout: - 'github.acme-corp.com\n ✓ Logged in to github.acme-corp.com account kelora (keyring)', - stderr: '' + stdout: '', + stderr: + 'github.acme-corp.com\n ✓ Logged in to github.acme-corp.com account kelora (keyring)\n X github.com: token expired' }) ) - const hosts = await getAuthenticatedGitHubHosts() - expect(hosts.has('github.acme-corp.com')).toBe(true) + await expect(isGitHubHostAuthenticated('github.acme-corp.com', '/repo')).resolves.toBe(true) }) - it('does not cache an empty result so a later gh login is discovered', async () => { + it('does not cache a hard gh failure so a later probe can recover', async () => { ghExecFileAsyncMock.mockRejectedValueOnce( Object.assign(new Error('not installed'), { stdout: '', stderr: '' }) ) - expect((await getAuthenticatedGitHubHosts()).size).toBe(0) + expect(await isGitHubHostAuthenticated('github.acme-corp.com', '/repo')).toBe(false) - mockGhAuthStatus(['github.acme-corp.com']) - expect((await getAuthenticatedGitHubHosts()).has('github.acme-corp.com')).toBe(true) + mockHostAuthenticated() + expect(await isGitHubHostAuthenticated('github.acme-corp.com', '/repo')).toBe(true) }) }) diff --git a/src/main/github/github-enterprise-repository.ts b/src/main/github/github-enterprise-repository.ts index 8542343ef0e..94cffe725fa 100644 --- a/src/main/github/github-enterprise-repository.ts +++ b/src/main/github/github-enterprise-repository.ts @@ -6,71 +6,94 @@ import { } from '../source-control/hosted-review-git-options' import { parseAuthStatus } from './auth-diagnose' import { + ghRepoExecOptions, getRemoteUrlForRepo, githubRepoContext, - parseGitHubRemoteIdentity + parseGitHubRemoteIdentity, + type LocalGitExecOptions } from './github-repository-identity' export type GitHubEnterpriseRepoSlug = GitHubOwnerRepo & { host: string } // Why: `gh` only ever manages github.com / GitHub Enterprise credentials, so a -// host that `gh auth status` reports as logged-in is definitively a GitHub host. -// This is the same signal `glab auth status` provides for GitLab self-hosted -// detection, mirrored here so GHES is not left to fall through to Gitea (#8312). -const AUTHENTICATED_HOSTS_TTL_MS = 60_000 +// host `gh auth status` reports as logged-in is definitively a GitHub host. This +// mirrors the `glab auth status` signal GitLab self-hosted detection uses, so a +// GHES remote is not left to fall through to Gitea (#8312). +const HOST_AUTH_TTL_MS = 60_000 -type AuthenticatedHostsCacheEntry = { - hosts: ReadonlySet +type HostAuthCacheEntry = { + authenticated: boolean expiresAt: number } -const authenticatedHostsCache = new Map() +const hostAuthCache = new Map() -function connectionCacheKey(connectionId?: string | null): string { - return connectionId ?? 'local' +// Why: gh's authenticated hosts live in per-runtime config — a WSL distro and an +// SSH host each carry their own `hosts.yml` — so cache state must be keyed by the +// runtime that executes gh, not shared under one "local" bucket. Mirrors the +// runtime scoping used by owner/repo resolution. +function runtimeCacheKey(connectionId?: string | null, wslDistro?: string): string { + return connectionId ?? `local:${wslDistro ?? 'host'}` } /** @internal - exposed for tests only */ -export function _resetAuthenticatedGitHubHostsCache(): void { - authenticatedHostsCache.clear() +export function _resetGitHubHostAuthCache(): void { + hostAuthCache.clear() +} + +// Only gh's own stdout/stderr — not the Error.message — counts as an +// authoritative answer. A spawn failure (gh missing, ENOENT) carries just a +// message and no command output, and must stay indeterminate rather than be +// read as "host not authenticated". +function ghCommandOutput(error: unknown): string { + const execErr = error as { stdout?: unknown; stderr?: unknown } + return [execErr?.stdout, execErr?.stderr] + .filter((value): value is string => typeof value === 'string' && value.trim().length > 0) + .join('\n') } /** - * Hosts the local `gh` CLI is authenticated to, lowercased. Cached briefly so a - * `gh auth login --hostname ` performed after startup is picked up without - * spawning `gh auth status` on every provider-detection poll. Failures are not - * cached so a later probe (gh installed, tunnel ready) can still discover hosts. + * Whether `gh` is authenticated to `host` from the repository's own runtime. + * + * The probe runs `gh auth status --hostname ` with the repo's execution + * options (cwd / WSL distro, or SSH-local like the create path), so a GHES login + * stored only in that runtime's gh config — or a `GH_ENTERPRISE_TOKEN` inferred + * from it — is honored instead of the host/default-distro gh. Cached briefly per + * runtime+host so provider-detection polling does not re-spawn gh each time. */ -export async function getAuthenticatedGitHubHosts( - connectionId?: string | null -): Promise> { - const key = connectionCacheKey(connectionId) +export async function isGitHubHostAuthenticated( + host: string, + repoPath: string, + connectionId?: string | null, + localGitOptions: LocalGitExecOptions = {} +): Promise { + const normalizedHost = host.toLowerCase() + const cacheKey = `${runtimeCacheKey(connectionId, localGitOptions.wslDistro)}\0${normalizedHost}` const now = Date.now() - const cached = authenticatedHostsCache.get(key) + const cached = hostAuthCache.get(cacheKey) if (cached && cached.expiresAt > now) { - return cached.hosts + return cached.authenticated } - let text = '' + const execOptions = ghRepoExecOptions(githubRepoContext(repoPath, connectionId, localGitOptions)) + let authenticated: boolean try { - // Why: `gh auth status` reads gh's own config, so it is host-scoped rather - // than cwd-scoped — no repo context is needed to enumerate logged-in hosts. - const { stdout, stderr } = await ghExecFileAsync(['auth', 'status']) - text = `${stdout}\n${stderr}` - } catch (err) { - // gh exits non-zero when any host has a token problem but still prints the - // per-host status we parse; recover its output before giving up. - const execErr = err as { stdout?: unknown; stderr?: unknown } - text = `${String(execErr?.stdout ?? '')}\n${String(execErr?.stderr ?? '')}`.trim() - if (!text) { - return new Set() + await ghExecFileAsync(['auth', 'status', '--hostname', normalizedHost], execOptions) + authenticated = true + } catch (error) { + const output = ghCommandOutput(error) + if (!output) { + // Indeterminate (gh missing / spawn failure) — do not cache so a later + // probe (gh installed, tunnel ready, token added) can recover. + return false } + // gh exits non-zero when a host has a token problem but still prints the + // per-host status; treat the host as GitHub only when it is actually listed. + authenticated = parseAuthStatus(output).some( + (account) => account.host.toLowerCase() === normalizedHost + ) } - const hosts = new Set(parseAuthStatus(text).map((account) => account.host.toLowerCase())) - if (hosts.size === 0) { - return hosts - } - authenticatedHostsCache.set(key, { hosts, expiresAt: now + AUTHENTICATED_HOSTS_TTL_MS }) - return hosts + hostAuthCache.set(cacheKey, { authenticated, expiresAt: now + HOST_AUTH_TTL_MS }) + return authenticated } /** @@ -85,7 +108,8 @@ export async function getEnterpriseGitHubRepoSlug( connectionId?: string | null, options: HostedReviewExecutionOptions = {} ): Promise { - const context = githubRepoContext(repoPath, connectionId, getHostedReviewLocalGitOptions(options)) + const localGitOptions = getHostedReviewLocalGitOptions(options) + const context = githubRepoContext(repoPath, connectionId, localGitOptions) let remoteUrl: string | null try { remoteUrl = await getRemoteUrlForRepo(context, 'origin') @@ -96,8 +120,11 @@ export async function getEnterpriseGitHubRepoSlug( if (!identity || identity.host === 'github.com') { return null } - const authenticatedHosts = await getAuthenticatedGitHubHosts(connectionId) - return authenticatedHosts.has(identity.host) - ? { owner: identity.owner, repo: identity.repo, host: identity.host } - : null + const authenticated = await isGitHubHostAuthenticated( + identity.host, + repoPath, + connectionId, + localGitOptions + ) + return authenticated ? { owner: identity.owner, repo: identity.repo, host: identity.host } : null }