fix(github): honor the saved project account for PR requests (#35023)

* fix(github): resolve project accounts at the request boundary

* fix(github): retain account identity through request settlement

* fix: isolate GitHub leaf caches by held execution identity

* fix: use canonical options type for merged review membership
This commit is contained in:
Brennan Benson
2026-10-30 12:39:59 -07:00
committed by GitHub
parent bd82832a53
commit eca40bc3bf
32 changed files with 2748 additions and 1316 deletions
+150 -134
View File
@@ -1,3 +1,4 @@
import { withGitHubRepoExecution } from './github-repository-identity'
import type { GitHubOwnerRepo } from '../../shared/github/pull-request-types'
import type { IssueSourcePreference } from '../../shared/repo-types'
import {
@@ -44,26 +45,33 @@ export async function getIssueGitHubApiRepository(
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
): Promise<GitHubApiRepository | null> {
const originPromise = getGitHubApiRepositoryForRemote(
repoPath,
'origin',
connectionId,
localGitOptions
).then(
(value) => ({ status: 'fulfilled' as const, value }),
(reason: unknown) => ({ status: 'rejected' as const, reason })
)
const upstream = (await shouldProbeGitRemote(repoPath, 'upstream', connectionId, localGitOptions))
? await getGitHubApiRepositoryForRemote(repoPath, 'upstream', connectionId, localGitOptions)
: null
if (upstream) {
return upstream
}
const origin = await originPromise
if (origin.status === 'rejected') {
throw origin.reason
}
return origin.value
return withGitHubRepoExecution(repoPath, connectionId, localGitOptions, async () => {
const originPromise = getGitHubApiRepositoryForRemote(
repoPath,
'origin',
connectionId,
localGitOptions
).then(
(value) => ({ status: 'fulfilled' as const, value }),
(reason: unknown) => ({ status: 'rejected' as const, reason })
)
const upstream = (await shouldProbeGitRemote(
repoPath,
'upstream',
connectionId,
localGitOptions
))
? await getGitHubApiRepositoryForRemote(repoPath, 'upstream', connectionId, localGitOptions)
: null
if (upstream) {
return upstream
}
const origin = await originPromise
if (origin.status === 'rejected') {
throw origin.reason
}
return origin.value
})
}
export type GitHubApiRepositoryCandidates = {
@@ -77,50 +85,52 @@ export async function resolveGitHubApiRepositoryCandidates(
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
): Promise<GitHubApiRepositoryCandidates> {
const originPromise = getGitHubApiRepositoryForRemote(
repoPath,
'origin',
connectionId,
localGitOptions,
{
requireVerifiedSshProbe: true
return withGitHubRepoExecution(repoPath, connectionId, localGitOptions, async () => {
const originPromise = getGitHubApiRepositoryForRemote(
repoPath,
'origin',
connectionId,
localGitOptions,
{
requireVerifiedSshProbe: true
}
).then(
(value) => ({ status: 'fulfilled' as const, value }),
(reason: unknown) => ({ status: 'rejected' as const, reason })
)
const probeUpstream = await shouldProbeGitRemote(
repoPath,
'upstream',
connectionId,
localGitOptions
)
const [upstream, originResult] = await Promise.all([
probeUpstream
? getGitHubApiRepositoryForRemote(repoPath, 'upstream', connectionId, localGitOptions, {
requireVerifiedSshProbe: true
})
: null,
originPromise
])
if (originResult.status === 'rejected') {
throw originResult.reason
}
).then(
(value) => ({ status: 'fulfilled' as const, value }),
(reason: unknown) => ({ status: 'rejected' as const, reason })
)
const probeUpstream = await shouldProbeGitRemote(
repoPath,
'upstream',
connectionId,
localGitOptions
)
const [upstream, originResult] = await Promise.all([
probeUpstream
? getGitHubApiRepositoryForRemote(repoPath, 'upstream', connectionId, localGitOptions, {
requireVerifiedSshProbe: true
})
: null,
originPromise
])
if (originResult.status === 'rejected') {
throw originResult.reason
}
const origin = originResult.value
const seen = new Set<string>()
const candidates: GitHubApiRepository[] = []
for (const candidate of [upstream, origin]) {
if (!candidate) {
continue
const origin = originResult.value
const seen = new Set<string>()
const candidates: GitHubApiRepository[] = []
for (const candidate of [upstream, origin]) {
if (!candidate) {
continue
}
const key = githubRepoIdentityKey(candidate)
if (seen.has(key)) {
continue
}
seen.add(key)
candidates.push(candidate)
}
const key = githubRepoIdentityKey(candidate)
if (seen.has(key)) {
continue
}
seen.add(key)
candidates.push(candidate)
}
return { candidates, headRepo: origin }
return { candidates, headRepo: origin }
})
}
export type ResolvedGitHubApiRepositorySource = {
@@ -135,39 +145,41 @@ export async function resolveIssueGitHubApiRepositorySource(
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
): Promise<ResolvedGitHubApiRepositorySource> {
if (preference === 'upstream') {
const upstream = await getGitHubApiRepositoryForRemote(
repoPath,
'upstream',
connectionId,
localGitOptions
)
if (upstream) {
return { source: upstream, fellBack: false }
}
const origin = await getGitHubApiRepositoryForRemote(
repoPath,
'origin',
connectionId,
localGitOptions
)
return { source: origin, fellBack: origin !== null }
}
if (preference === 'origin') {
return {
source: await getGitHubApiRepositoryForRemote(
return withGitHubRepoExecution(repoPath, connectionId, localGitOptions, async () => {
if (preference === 'upstream') {
const upstream = await getGitHubApiRepositoryForRemote(
repoPath,
'upstream',
connectionId,
localGitOptions
)
if (upstream) {
return { source: upstream, fellBack: false }
}
const origin = await getGitHubApiRepositoryForRemote(
repoPath,
'origin',
connectionId,
localGitOptions
),
)
return { source: origin, fellBack: origin !== null }
}
if (preference === 'origin') {
return {
source: await getGitHubApiRepositoryForRemote(
repoPath,
'origin',
connectionId,
localGitOptions
),
fellBack: false
}
}
return {
source: await getIssueGitHubApiRepository(repoPath, connectionId, localGitOptions),
fellBack: false
}
}
return {
source: await getIssueGitHubApiRepository(repoPath, connectionId, localGitOptions),
fellBack: false
}
})
}
export async function resolveGitHubApiRepository(
@@ -176,40 +188,42 @@ export async function resolveGitHubApiRepository(
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
): Promise<GitHubApiRepository | null> {
if (repository && !isValidGitHubApiRepository(repository)) {
return null
}
if (repository?.host) {
const host = repository.host.trim().toLowerCase()
if (!host) {
return withGitHubRepoExecution(repoPath, connectionId, localGitOptions, async () => {
if (repository && !isValidGitHubApiRepository(repository)) {
return null
}
if (isDefaultGitHubHost(host)) {
return { ...repository, host }
if (repository?.host) {
const host = repository.host.trim().toLowerCase()
if (!host) {
return null
}
if (isDefaultGitHubHost(host)) {
return { ...repository, host }
}
// Verify supplied hosts against local auth inventory before giving them ambient Enterprise credentials.
const authenticated = await isGitHubHostAuthenticated(
host,
repoPath,
connectionId,
localGitOptions
)
return authenticated ? { ...repository, host } : null
}
// Verify supplied hosts against local auth inventory before giving them ambient Enterprise credentials.
const authenticated = await isGitHubHostAuthenticated(
host,
const originRepository = await getOriginGitHubApiRepository(
repoPath,
connectionId,
localGitOptions
)
return authenticated ? { ...repository, host } : null
}
const originRepository = await getOriginGitHubApiRepository(
repoPath,
connectionId,
localGitOptions
)
if (!repository) {
return originRepository
}
// Older clients omit host; resolved origin supplies it for fork-base slugs on the same server.
if (originRepository?.host) {
return { ...repository, host: originRepository.host }
}
// Host-less calls could follow ambient GH_HOST; only resolved origin can supply the legacy host.
return null
if (!repository) {
return originRepository
}
// Older clients omit host; resolved origin supplies it for fork-base slugs on the same server.
if (originRepository?.host) {
return { ...repository, host: originRepository.host }
}
// Host-less calls could follow ambient GH_HOST; only resolved origin can supply the legacy host.
return null
})
}
export async function resolveGitHubRepoExecution(
@@ -218,23 +232,25 @@ export async function resolveGitHubRepoExecution(
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
): Promise<GitHubRepoExecution> {
// Retain upstream-first issue resolution with the same host-pinned execution options.
const requestedRepository = typeof repository === 'function' ? await repository() : repository
// Preserve authoritative nulls instead of replacing them with origin.
const ownerRepo =
typeof repository === 'function' && !requestedRepository
? null
: await resolveGitHubApiRepository(
repoPath,
requestedRepository,
connectionId,
localGitOptions
)
return {
ownerRepo,
ghOptions: {
...ghRepoExecOptions(githubRepoContext(repoPath, connectionId, localGitOptions)),
...githubHostExecOptions(ownerRepo)
return withGitHubRepoExecution(repoPath, connectionId, localGitOptions, async () => {
// Retain upstream-first issue resolution with the same host-pinned execution options.
const requestedRepository = typeof repository === 'function' ? await repository() : repository
// Preserve authoritative nulls instead of replacing them with origin.
const ownerRepo =
typeof repository === 'function' && !requestedRepository
? null
: await resolveGitHubApiRepository(
repoPath,
requestedRepository,
connectionId,
localGitOptions
)
return {
ownerRepo,
ghOptions: {
...ghRepoExecOptions(githubRepoContext(repoPath, connectionId, localGitOptions)),
...githubHostExecOptions(ownerRepo)
}
}
}
})
}