mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
fix(github): verify cached integration PRs after tracking is removed
Use the existing REST PR mapping to read the head repository's default branch when no tracked remote or origin can identify it. Require matching repository and head metadata, and preserve the PR if the lookup fails. This clears both closed and merged integration PRs cached by a fork checkout after its upstream tracking is removed. Co-authored-by: petersindex <287557875+petersindex@users.noreply.github.com>
This commit is contained in:
committed by
Neil
co-authored by
petersindex
parent
82c5484f66
commit
884575a797
@@ -117,9 +117,20 @@ function primeGh(
|
||||
)
|
||||
return { stdout: JSON.stringify(list) }
|
||||
}
|
||||
const exactRestMatch = args[1]?.match(/^repos\/([^/]+\/[^/]+)\/pulls\/(\d+)$/)
|
||||
if (args[0] === 'api' && exactRestMatch) {
|
||||
const pr = prsByRepositoryAndNumber.get(`${host}/${exactRestMatch[1]}#${exactRestMatch[2]}`)
|
||||
if (pr) {
|
||||
return { stdout: JSON.stringify(pr) }
|
||||
}
|
||||
throw new Error('HTTP 404 Not Found')
|
||||
}
|
||||
const repoIndex = args.indexOf('--repo')
|
||||
const exact = prsByRepositoryAndNumber.get(`${host}/${args[repoIndex + 1]}#${args[2]}`)
|
||||
if (args[0] === 'pr' && args[1] === 'view' && exact) {
|
||||
if (args[0] === 'pr' && args[1] === 'view') {
|
||||
if (!exact) {
|
||||
throw new Error('HTTP 404 Not Found')
|
||||
}
|
||||
return {
|
||||
stdout: JSON.stringify({
|
||||
number: exact.number,
|
||||
@@ -264,6 +275,92 @@ describe('issue #26948: a branch tracking the default branch', () => {
|
||||
).toBe('no-pr')
|
||||
})
|
||||
|
||||
it.each(['closed', 'merged'] as const)(
|
||||
'clears a cached %s integration PR in a fork after upstream tracking is removed',
|
||||
async (state) => {
|
||||
primeForkWithUpstream()
|
||||
primeGit('', { origin: 'main' })
|
||||
const integrationPR = restPR('develop', UPSTREAM, UPSTREAM)
|
||||
primeGh(
|
||||
{},
|
||||
{
|
||||
...integrationPR,
|
||||
merged_at: state === 'merged' ? '2025-01-02T00:00:00Z' : null,
|
||||
head: {
|
||||
...integrationPR.head,
|
||||
sha: 'feature-head-oid',
|
||||
repo: { name: 'widgets', owner: { login: 'stablyai' }, default_branch: 'develop' }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(
|
||||
await getPRForBranchOutcome('/repo-root', 'feature/my-change', null, null, 7, {
|
||||
currentHeadOid: 'feature-head-oid',
|
||||
acceptMergedFallbackPR: true
|
||||
})
|
||||
).toMatchObject({
|
||||
kind: 'no-pr',
|
||||
rejectedPRUrls: ['https://github.com/stablyai/widgets/pull/7']
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('preserves a cached renamed feature PR using its own repository default metadata', async () => {
|
||||
primeForkWithUpstream()
|
||||
primeGit('', { origin: 'develop' })
|
||||
const featurePR = restPR('develop', UPSTREAM, UPSTREAM)
|
||||
primeGh(
|
||||
{},
|
||||
{
|
||||
...featurePR,
|
||||
state: 'open',
|
||||
head: {
|
||||
...featurePR.head,
|
||||
repo: { name: 'widgets', owner: { login: 'stablyai' }, default_branch: 'main' }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(await getPRForBranch('/repo-root', 'feature/my-change', null, null, 7)).toMatchObject({
|
||||
number: 7,
|
||||
headRefName: 'develop'
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['unavailable', 'changed-head'] as const)(
|
||||
'preserves an untracked cached PR when REST metadata is %s',
|
||||
async (metadataState) => {
|
||||
primeForkWithUpstream()
|
||||
primeGit('', { origin: 'develop' })
|
||||
primeGh({}, { ...restPR('develop', UPSTREAM, UPSTREAM), state: 'open' })
|
||||
const ghImplementation = ghExecFileAsyncMock.getMockImplementation()
|
||||
ghExecFileAsyncMock.mockImplementation(async (args: string[], options) => {
|
||||
if (args[0] === 'api' && args[1] === 'repos/stablyai/widgets/pulls/7') {
|
||||
if (metadataState === 'unavailable') {
|
||||
throw new Error('network unavailable')
|
||||
}
|
||||
const pr = restPR('develop', ACME, UPSTREAM)
|
||||
return {
|
||||
stdout: JSON.stringify({
|
||||
...pr,
|
||||
head: {
|
||||
...pr.head,
|
||||
repo: { name: 'widgets', owner: { login: 'acme' }, default_branch: 'develop' }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return ghImplementation?.(args, options)
|
||||
})
|
||||
|
||||
expect(await getPRForBranch('/repo-root', 'feature/my-change', null, null, 7)).toMatchObject({
|
||||
number: 7,
|
||||
headRepo: UPSTREAM
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('tries the cached feature PR after rejecting an unrelated upstream result', async () => {
|
||||
primeGit('refs/remotes/origin/develop')
|
||||
primeGh(
|
||||
|
||||
@@ -87,7 +87,8 @@ export async function resolvePRForBranchOutcome(input: {
|
||||
linkedPRNumber,
|
||||
repoPath,
|
||||
connectionId,
|
||||
localGitOptions
|
||||
localGitOptions,
|
||||
ghOptions
|
||||
}
|
||||
const implicitPRPolicy = createImplicitDefaultBranchPRPolicy(defaultBranchPRContext)
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ import {
|
||||
getRemoteHeadBranchName,
|
||||
getRepoDefaultBranchName
|
||||
} from '../../../source-control/repo-default-branch'
|
||||
import type { HostedReviewLocalGitOptions } from './../github-exec-scope'
|
||||
import type { GhExecOptions, HostedReviewLocalGitOptions } from './../github-exec-scope'
|
||||
import type { TrackedUpstreamBranch } from './tracked-upstream-cache'
|
||||
import {
|
||||
pullRequestHeadRepository,
|
||||
type PullRequestLookupData,
|
||||
type PullRequestLookupPolicy
|
||||
} from './pull-request-lookup-data'
|
||||
import { getRestPRByNumber } from './pr-number-lookup'
|
||||
|
||||
export type ImplicitDefaultBranchPRContext = {
|
||||
originHeadRepo: OwnerRepo | null
|
||||
@@ -20,6 +21,7 @@ export type ImplicitDefaultBranchPRContext = {
|
||||
repoPath: string
|
||||
connectionId?: string | null
|
||||
localGitOptions: HostedReviewLocalGitOptions
|
||||
ghOptions: GhExecOptions
|
||||
}
|
||||
|
||||
export function createImplicitDefaultBranchPRPolicy(
|
||||
@@ -73,7 +75,19 @@ async function shouldHideImplicitDefaultBranchPR(
|
||||
? 'origin'
|
||||
: null
|
||||
if (!remoteName) {
|
||||
return false
|
||||
try {
|
||||
// Removed tracking can leave a cached integration PR without a matching remote.
|
||||
const metadata = await getRestPRByNumber(prRepo, data.number, input.ghOptions)
|
||||
const metadataHeadRepo = pullRequestHeadRepository(metadata, prRepo)
|
||||
return (
|
||||
metadataHeadRepo !== null &&
|
||||
githubRepoIdentityKey(metadataHeadRepo) === headRepoKey &&
|
||||
metadata.headRefName === data.headRefName &&
|
||||
metadata.headDefaultBranchName === data.headRefName
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// An unknown upstream HEAD cannot borrow a different repository's origin default.
|
||||
const defaultBranchName =
|
||||
|
||||
Reference in New Issue
Block a user