Fix GitHub PR auto-merge eligibility (#32075)

* Fix GitHub PR auto-merge eligibility handling

* Preserve GraphQL auto-merge state over REST fallback
This commit is contained in:
Jinjing
2026-10-22 21:23:08 -07:00
committed by GitHub
parent 26f677ee94
commit 075ed138bd
17 changed files with 535 additions and 231 deletions
@@ -1,6 +1,5 @@
import { describe, expect, it } from 'vitest'
import {
canEnableGitHubPRAutoMerge,
canShowGitHubPRAutoMergeControl,
type GitHubPRAutoMergeAvailabilityInput
} from './pull-request-auto-merge-availability'
@@ -18,34 +17,55 @@ function pr(
}
describe('github PR auto-merge availability', () => {
it.each([undefined, null, 'UNKNOWN', 'FUTURE_STATUS'])(
'does not infer an auto-merge requirement from unresolved status %s',
(mergeStateStatus) => {
const item = pr({ mergeable: 'UNKNOWN', mergeStateStatus })
expect(canShowGitHubPRAutoMergeControl(item)).toBe(false)
}
)
it('does not offer auto-merge for directly mergeable PRs with only optional checks pending', () => {
expect(canEnableGitHubPRAutoMerge(pr())).toBe(false)
expect(canShowGitHubPRAutoMergeControl(pr())).toBe(false)
})
it('offers auto-merge for requirement-blocked PRs', () => {
expect(
canEnableGitHubPRAutoMerge(
canShowGitHubPRAutoMergeControl(
pr({ mergeable: 'UNKNOWN', mergeStateStatus: 'BLOCKED', reviewDecision: 'REVIEW_REQUIRED' })
)
).toBe(true)
expect(canEnableGitHubPRAutoMerge(pr({ mergeStateStatus: 'BLOCKED' }))).toBe(true)
expect(canShowGitHubPRAutoMergeControl(pr({ mergeStateStatus: 'BLOCKED' }))).toBe(true)
expect(canShowGitHubPRAutoMergeControl(pr({ mergeStateStatus: 'BEHIND' }))).toBe(true)
expect(
canShowGitHubPRAutoMergeControl(pr({ mergeable: 'UNKNOWN', mergeStateStatus: 'BLOCKED' }))
).toBe(true)
})
it('keeps merge-queue branches available for merge-when-ready', () => {
expect(canEnableGitHubPRAutoMerge(pr({ mergeQueueRequired: true }))).toBe(false)
expect(canShowGitHubPRAutoMergeControl(pr({ mergeQueueRequired: true }))).toBe(true)
expect(
canShowGitHubPRAutoMergeControl(pr({ autoMergeAllowed: false, mergeQueueRequired: true }))
).toBe(true)
expect(
canShowGitHubPRAutoMergeControl(
pr({ mergeStateStatus: 'UNSTABLE', mergeQueueRequired: true })
)
).toBe(true)
})
it('keeps enabled auto-merge visible so users can disable it', () => {
expect(canEnableGitHubPRAutoMerge(pr({ autoMergeEnabled: true }))).toBe(false)
expect(canShowGitHubPRAutoMergeControl(pr({ autoMergeEnabled: true }))).toBe(true)
expect(
canShowGitHubPRAutoMergeControl(
pr({
autoMergeEnabled: true,
autoMergeAllowed: false,
mergeable: 'CONFLICTING',
mergeStateStatus: 'DIRTY'
})
)
).toBe(true)
})
it('suppresses closed, draft, disallowed, conflicting, and unstable PRs', () => {
@@ -63,9 +83,6 @@ describe('github PR auto-merge availability', () => {
expect(
canShowGitHubPRAutoMergeControl(pr({ mergeable: 'CONFLICTING', mergeStateStatus: 'DIRTY' }))
).toBe(false)
expect(
canEnableGitHubPRAutoMerge(pr({ mergeable: 'UNKNOWN', mergeStateStatus: 'UNSTABLE' }))
).toBe(false)
expect(
canShowGitHubPRAutoMergeControl(pr({ mergeable: 'UNKNOWN', mergeStateStatus: 'UNSTABLE' }))
).toBe(false)
@@ -1,7 +1,7 @@
import type { PRMergeableState, PRReviewDecision, PRState } from './pull-request-types'
export type GitHubPRAutoMergeAvailabilityInput = {
state: PRState | 'open' | 'closed' | 'merged' | 'draft'
state: PRState
mergeable?: PRMergeableState
mergeStateStatus?: string | null
reviewDecision?: PRReviewDecision | null
@@ -10,51 +10,26 @@ export type GitHubPRAutoMergeAvailabilityInput = {
mergeQueueRequired?: boolean | null
}
function isOpenPR(item: GitHubPRAutoMergeAvailabilityInput): boolean {
return item.state === 'open'
}
function isConflicting(item: GitHubPRAutoMergeAvailabilityInput): boolean {
return item.mergeable === 'CONFLICTING' || item.mergeStateStatus === 'DIRTY'
}
function isUnstable(item: GitHubPRAutoMergeAvailabilityInput): boolean {
return item.mergeStateStatus === 'UNSTABLE'
}
function hasReviewRequirement(item: GitHubPRAutoMergeAvailabilityInput): boolean {
return item.reviewDecision === 'REVIEW_REQUIRED' || item.reviewDecision === 'CHANGES_REQUESTED'
}
function canMergeImmediately(item: GitHubPRAutoMergeAvailabilityInput): boolean {
if (item.mergeStateStatus === 'BLOCKED' || item.mergeStateStatus === 'BEHIND') {
export function canShowGitHubPRAutoMergeControl(item: GitHubPRAutoMergeAvailabilityInput): boolean {
if (item.state !== 'open') {
return false
}
return item.mergeable === 'MERGEABLE' || item.mergeStateStatus === 'CLEAN'
}
function canRequestWhenReady(item: GitHubPRAutoMergeAvailabilityInput): boolean {
// Why: GitHub auto-merge waits on unmet requirements; UNSTABLE is rejected
// by the mutation rather than becoming a waitable auto-merge request.
if (!isOpenPR(item) || isConflicting(item) || isUnstable(item)) {
if (item.autoMergeEnabled === true) {
return true
}
if (item.mergeable === 'CONFLICTING' || item.mergeStateStatus === 'DIRTY') {
return false
}
if (item.mergeQueueRequired === true) {
return true
}
// Missing merge metadata is not evidence of an unmet branch requirement.
return (
item.autoMergeAllowed !== false && (hasReviewRequirement(item) || !canMergeImmediately(item))
item.mergeStateStatus !== 'UNSTABLE' &&
item.autoMergeAllowed !== false &&
(item.reviewDecision === 'REVIEW_REQUIRED' ||
item.reviewDecision === 'CHANGES_REQUESTED' ||
item.mergeStateStatus === 'BLOCKED' ||
item.mergeStateStatus === 'BEHIND')
)
}
export function canEnableGitHubPRAutoMerge(item: GitHubPRAutoMergeAvailabilityInput): boolean {
return (
item.autoMergeEnabled !== true && item.mergeQueueRequired !== true && canRequestWhenReady(item)
)
}
export function canShowGitHubPRAutoMergeControl(item: GitHubPRAutoMergeAvailabilityInput): boolean {
// Why: GitHub auto-merge waits for branch requirements, not arbitrary optional CI.
// Keep already-enabled PRs visible so users can disable the setting.
return isOpenPR(item) && (item.autoMergeEnabled === true || canRequestWhenReady(item))
}