fix(github): align PR source and review head origin (#10677)

* fix(github): align PR source and review head origin

* fix(github): pin number-based work item open to the repo source preference

Open-by-number and details still ran the upstream-first multi-candidate PR
probe, so a fork and its upstream sharing a PR number opened different PRs
than the list and start-point paths did once #10677 pinned those to origin.

Thread repo.issueSourcePreference through dispatchWorkItem, getWorkItemDetails,
getRepoWorkItem, and getRepoWorkItemDetails. getWorkItemByOwnerRepo is left
alone: explicit owner/repo already pins identity. auto/upstream/undefined keep
the multi-candidate probe.

Co-authored-by: Orca <help@stably.ai>

* test(github): enforce origin preference in review head origin resolution

The explicit origin preference must short-circuit before any identity probe, so no remote queries should occur. Add validation to reject unexpected remotes and tighten the test assertion to verify no remote get-url calls happen at all.

* fix(github): enforce origin preference in issue open-by-number lookup

listWorkItems and getWorkItem must share preference so origin/upstream
toggles cannot disagree. Explicit origin preference now fail-closes when
origin identity is unresolved (no bare-lookup fallback), matching the
PR candidate resolution rule.

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinjing
2026-07-25 22:48:53 -07:00
committed by GitHub
co-authored by Orca
parent 8cb45318d7
commit 33bd676644
18 changed files with 654 additions and 69 deletions
+57 -16
View File
@@ -76,7 +76,6 @@ import { shouldHideNonOpenReviewOnDefaultBranch } from '../source-control/repo-d
import { readLocalGitConfigSignature } from './local-git-config-signature'
import {
getGitHubApiRepositoryForRemote,
getIssueGitHubApiRepository,
getOriginGitHubApiRepository,
githubHostExecOptions,
githubRepositorySlugArg,
@@ -283,16 +282,35 @@ export type PullRequestPushTarget = {
maintainerCanModify?: boolean
}
// Why: only an explicit `origin` preference is origin-only; `upstream`/`auto`/
// undefined keep the multi-candidate probe ordered upstream-first, matching
// resolvePrWorkItemSource list semantics.
async function resolvePullRequestLookupCandidates(
repoPath: string,
preference: IssueSourcePreference | undefined,
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
): Promise<GitHubApiRepository[]> {
if (preference === 'origin') {
const origin = await getOriginGitHubApiRepository(repoPath, connectionId, localGitOptions)
return origin ? [origin] : []
}
return (await resolveGitHubApiRepositoryCandidates(repoPath, connectionId, localGitOptions))
.candidates
}
export async function getPullRequestPushTarget(
repoPath: string,
prNumber: number,
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
localGitOptions: LocalGitExecOptions = {},
preference?: IssueSourcePreference
): Promise<PullRequestPushTarget | null> {
const context = githubRepoContext(repoPath, connectionId, localGitOptions)
const ghOptions = ghRepoExecOptions(context)
const { candidates } = await resolveGitHubApiRepositoryCandidates(
const candidates = await resolvePullRequestLookupCandidates(
repoPath,
preference,
connectionId,
localGitOptions
)
@@ -954,14 +972,19 @@ async function fetchPullRequestWorkItemFromCandidates(
repoPath: string,
number: number,
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
localGitOptions: LocalGitExecOptions = {},
preference?: IssueSourcePreference
): Promise<MainWorkItem | null> {
const { candidates } = await resolveGitHubApiRepositoryCandidates(
const candidates = await resolvePullRequestLookupCandidates(
repoPath,
preference,
connectionId,
localGitOptions
)
if (candidates.length === 0) {
if (preference === 'origin') {
return null
}
return fetchPullRequestWorkItem(repoPath, null, number, connectionId, localGitOptions)
}
for (const candidate of candidates) {
@@ -1953,38 +1976,55 @@ export async function getWorkItem(
number: number,
type?: 'issue' | 'pr',
connectionId?: string | null,
localGitOptions: LocalGitExecOptions = {}
localGitOptions: LocalGitExecOptions = {},
preference?: IssueSourcePreference
): Promise<MainWorkItem | null> {
await acquire()
try {
// Why: listWorkItems uses resolveIssueGitHubApiRepositorySource; open-by-number
// must share that preference so origin/upstream toggles cannot disagree.
if (type === 'issue') {
return await fetchIssueWorkItem(
const { source } = await resolveIssueGitHubApiRepositorySource(
repoPath,
await getIssueGitHubApiRepository(repoPath, connectionId, localGitOptions),
number,
preference,
connectionId,
localGitOptions
)
// Why: explicit origin with no origin identity must not bare-lookup ambient gh
// (same fail-closed rule as origin-pinned PR candidate resolution).
if (!source && preference === 'origin') {
return null
}
return await fetchIssueWorkItem(repoPath, source, number, connectionId, localGitOptions)
}
if (type === 'pr') {
return await fetchPullRequestWorkItemFromCandidates(
repoPath,
number,
connectionId,
localGitOptions
localGitOptions,
preference
)
}
try {
const issue = await fetchIssueWorkItem(
const { source } = await resolveIssueGitHubApiRepositorySource(
repoPath,
await getIssueGitHubApiRepository(repoPath, connectionId, localGitOptions),
number,
preference,
connectionId,
localGitOptions
)
if (issue) {
return issue
if (source || preference !== 'origin') {
const issue = await fetchIssueWorkItem(
repoPath,
source,
number,
connectionId,
localGitOptions
)
if (issue) {
return issue
}
}
} catch (err) {
// Why: only fall through to PR #N on a genuine 404; re-throw transient errors so a flake can't surface an unrelated PR.
@@ -1997,7 +2037,8 @@ export async function getWorkItem(
repoPath,
number,
connectionId,
localGitOptions
localGitOptions,
preference
)
} catch {
return null