Hydrate task PR merge methods
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
e8b859118f
commit
1c1677eaad
|
|
@ -58,7 +58,12 @@ vi.mock('./rate-limit', () => ({
|
|||
noteRateLimitSpend: noteRateLimitSpendMock
|
||||
}))
|
||||
|
||||
import { countWorkItems, listWorkItems, _resetOwnerRepoCache } from './client'
|
||||
import {
|
||||
countWorkItems,
|
||||
listWorkItems,
|
||||
_resetMergeQueueCacheForTests,
|
||||
_resetOwnerRepoCache
|
||||
} from './client'
|
||||
|
||||
describe('listWorkItems', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -84,6 +89,7 @@ describe('listWorkItems', () => {
|
|||
}))
|
||||
getOwnerRepoForRemoteMock.mockResolvedValue(null)
|
||||
_resetOwnerRepoCache()
|
||||
_resetMergeQueueCacheForTests()
|
||||
})
|
||||
|
||||
it('runs both issue and PR GitHub searches for a mixed query and merges the results by recency', async () => {
|
||||
|
|
@ -221,6 +227,58 @@ describe('listWorkItems', () => {
|
|||
])
|
||||
})
|
||||
|
||||
it('hydrates PR list rows with repository merge method settings', async () => {
|
||||
getIssueOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
|
||||
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
|
||||
ghExecFileAsyncMock
|
||||
.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify([
|
||||
{
|
||||
number: 42,
|
||||
title: 'Add feature',
|
||||
state: 'OPEN',
|
||||
url: 'https://github.com/acme/widgets/pull/42',
|
||||
labels: [],
|
||||
updatedAt: '2026-03-28T00:00:00Z',
|
||||
author: { login: 'octocat' },
|
||||
isDraft: false,
|
||||
headRefName: 'feature/add-feature',
|
||||
headRefOid: 'head-42',
|
||||
baseRefName: 'main'
|
||||
}
|
||||
])
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
data: {
|
||||
repository: {
|
||||
viewerDefaultMergeMethod: 'REBASE',
|
||||
mergeCommitAllowed: false,
|
||||
rebaseMergeAllowed: true,
|
||||
squashMergeAllowed: true
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const { items } = await listWorkItems('/repo-root', 10, 'is:pr')
|
||||
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]?.mergeMethodSettings).toEqual({
|
||||
defaultMethod: 'rebase',
|
||||
allowedMethods: {
|
||||
squash: true,
|
||||
merge: false,
|
||||
rebase: true
|
||||
}
|
||||
})
|
||||
expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.arrayContaining(['api', 'graphql', '-f', 'owner=acme', '-f', 'repo=widgets']),
|
||||
{ cwd: '/repo-root' }
|
||||
)
|
||||
})
|
||||
|
||||
it('routes draft queries to PR search only', async () => {
|
||||
getIssueOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
|
||||
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
|
||||
|
|
@ -242,7 +300,7 @@ describe('listWorkItems', () => {
|
|||
])
|
||||
})
|
||||
const { items } = await listWorkItems('/repo-root', 10, 'is:pr is:draft')
|
||||
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
|
||||
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
|
||||
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
|
||||
[
|
||||
'pr',
|
||||
|
|
@ -301,7 +359,7 @@ describe('listWorkItems', () => {
|
|||
|
||||
const { items } = await listWorkItems('/repo-root', 10, 'is:merged')
|
||||
|
||||
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
|
||||
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
|
||||
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(
|
||||
[
|
||||
'pr',
|
||||
|
|
|
|||
|
|
@ -361,9 +361,9 @@ const WORK_ITEM_PR_LIST_JSON_FIELDS =
|
|||
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName,headRefOid,headRepositoryOwner,reviewRequests'
|
||||
|
||||
// Why: these fields are intentionally excluded from `gh pr list` because
|
||||
// statusCheckRollup/review decision/merge metadata fan out into expensive
|
||||
// GraphQL work across every row. Requested reviewers are kept in the list
|
||||
// payload because the Tasks table renders that column on first paint.
|
||||
// statusCheckRollup/review decision/PR-specific merge metadata fan out into
|
||||
// expensive GraphQL work across every row. Requested reviewers are kept in the
|
||||
// list payload because the Tasks table renders that column on first paint.
|
||||
const WORK_ITEM_PR_DETAIL_JSON_FIELDS =
|
||||
'number,title,state,url,labels,updatedAt,author,isDraft,headRefName,baseRefName,headRefOid,headRepositoryOwner,additions,deletions,changedFiles,reviewDecision,reviewRequests,latestReviews,assignees,statusCheckRollup,mergeable,mergeStateStatus,autoMergeRequest,maintainerCanModify'
|
||||
|
||||
|
|
@ -694,6 +694,26 @@ function mapPullRequestWorkItem(
|
|||
}
|
||||
}
|
||||
|
||||
async function hydrateWorkItemMergeMethodSettings(
|
||||
items: MainWorkItem[],
|
||||
ownerRepo: OwnerRepo | null,
|
||||
ghOptions: GhExecOptions
|
||||
): Promise<MainWorkItem[]> {
|
||||
const hasPullRequest = items.some((item) => item.type === 'pr')
|
||||
if (!ownerRepo || !hasPullRequest) {
|
||||
return items
|
||||
}
|
||||
// Why: merge method settings are repository-level, so one cached metadata
|
||||
// probe can keep Tasks rows accurate without per-PR GraphQL fan-out.
|
||||
const mergeMetadata = await detectRepositoryMergeMetadata(ownerRepo, undefined, ghOptions)
|
||||
if (!mergeMetadata.mergeMethodSettings) {
|
||||
return items
|
||||
}
|
||||
return items.map((item) =>
|
||||
item.type === 'pr' ? { ...item, mergeMethodSettings: mergeMetadata.mergeMethodSettings } : item
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchIssueWorkItem(
|
||||
repoPath: string,
|
||||
ownerRepo: OwnerRepo | null,
|
||||
|
|
@ -947,6 +967,7 @@ async function listRecentWorkItems(
|
|||
prs = (JSON.parse(prsSettled.value.stdout) as Record<string, unknown>[]).map((item) =>
|
||||
mapPullRequestWorkItem(item, prOwnerRepo)
|
||||
)
|
||||
prs = await hydrateWorkItemMergeMethodSettings(prs, prOwnerRepo, ghOptions)
|
||||
} else {
|
||||
// Why: PR-side failures must preserve the pre-diff behavior of
|
||||
// Promise.all by re-throwing so the rejection propagates up through
|
||||
|
|
@ -1088,10 +1109,11 @@ async function listQueriedWorkItems(
|
|||
const mapped = (JSON.parse(stdout) as Record<string, unknown>[]).map((item) =>
|
||||
mapPullRequestWorkItem(item, prOwnerRepo)
|
||||
)
|
||||
const hydrated = await hydrateWorkItemMergeMethodSettings(mapped, prOwnerRepo, ghOptions)
|
||||
if (query.state === 'closed') {
|
||||
return mapped.filter((item) => item.state !== 'merged')
|
||||
return hydrated.filter((item) => item.state !== 'merged')
|
||||
}
|
||||
return mapped
|
||||
return hydrated
|
||||
} catch (err) {
|
||||
console.warn('listQueriedWorkItems PRs partial failure:', err)
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -136,8 +136,7 @@ export default function HostedReviewActions({
|
|||
})
|
||||
}, [githubPR, isGitLab, review])
|
||||
const mergeMethods = useMemo(
|
||||
() =>
|
||||
resolveGitHubPRMergeMethods(isGitLab ? null : (githubPR?.mergeMethodSettings ?? null)),
|
||||
() => resolveGitHubPRMergeMethods(isGitLab ? null : (githubPR?.mergeMethodSettings ?? null)),
|
||||
[githubPR?.mergeMethodSettings, isGitLab]
|
||||
)
|
||||
const isUpdatingReviewState = stateUpdating !== null
|
||||
|
|
|
|||
Loading…
Reference in New Issue