diff --git a/src/github/backfill.ts b/src/github/backfill.ts index 89ee5ddc61..24d29a128e 100644 --- a/src/github/backfill.ts +++ b/src/github/backfill.ts @@ -1980,6 +1980,19 @@ export async function fetchLiveCiAggregate(env: Env, repoFullName: string, headS return { ciState, failingDetails }; } +/** + * Fetch a PR's LIVE `mergeable_state` (clean / dirty / blocked / unstable / behind / has_hooks / unknown). The + * STORED value lags GitHub's async recompute — e.g. right after gittensory[bot]'s own APPROVE flips a `blocked` + * PR to `clean`, the stored row is still `blocked`, which stops an otherwise-eligible PR from auto-merging + * (observed: green+approved PRs stuck OPEN at `mergeState=CLEAN`). The auto-maintain planner uses this so the + * merge decision sees the CURRENT state. `unknown` (GitHub still computing) ⇒ caller treats as not-yet-clean and + * a later trigger / the sweep retries. Best-effort: a fetch error returns undefined (caller falls back to stored). + */ +export async function fetchLivePullRequestMergeState(env: Env, repoFullName: string, prNumber: number, token: string | undefined): Promise { + const result = await githubJsonWithHeaders<{ mergeable_state?: string | null }>(env, repoFullName, `/pulls/${prNumber}`, token).catch(() => undefined); + return result?.data.mergeable_state ?? undefined; +} + async function fetchPullRequestDetailsFromGraphQl( env: Env, repoFullName: string, diff --git a/src/queue/processors.ts b/src/queue/processors.ts index b41383fdf3..f5b9374896 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -65,6 +65,7 @@ import { enqueueRepositoryOpenDataBackfill, fetchAndStorePullRequestFilesForReview, fetchLiveCiAggregate, + fetchLivePullRequestMergeState, refreshContributorActivity, refreshInstallationHealth, refreshPullRequestDetails, @@ -605,10 +606,13 @@ async function maybeRunAgentMaintenance( // planner uses this to NEVER approve/merge a PR whose CI isn't green, to CLOSE a red-CI non-owner PR (citing // the failing checks) / HOLD the owner's, and to DEFER entirely while CI is still pending. const ciToken = await createInstallationToken(env, installationId).catch(() => undefined); - const [changedFiles, hardGuardrailGlobs, ciAggregate] = await Promise.all([ + const [changedFiles, hardGuardrailGlobs, ciAggregate, liveMergeState] = await Promise.all([ resolvePullRequestFilesForReview(env, { installationId, repoFullName, pullNumber: pr.number }), loadHardGuardrailGlobs(env, repoFullName), fetchLiveCiAggregate(env, repoFullName, pr.headSha, ciToken ?? env.GITHUB_PUBLIC_TOKEN), + // Live mergeable_state — the stored one lags GitHub's async recompute after the bot's own approve, which + // otherwise leaves a green+approved PR stuck OPEN at mergeState=CLEAN (never auto-merged). + fetchLivePullRequestMergeState(env, repoFullName, pr.number, ciToken ?? env.GITHUB_PUBLIC_TOKEN), ]); const changedPaths = changedFiles.map((file) => file.path).filter((path) => path.length > 0); const repoOwner = repoFullName.includes("/") ? repoFullName.slice(0, repoFullName.indexOf("/")) : ""; @@ -629,7 +633,7 @@ async function maybeRunAgentMaintenance( ciState: ciAggregate.ciState, failingCheckNames: ciAggregate.failingDetails.map((detail) => detail.name), pr: { - mergeableState: pr.mergeableState, + mergeableState: liveMergeState ?? pr.mergeableState, reviewDecision: pr.reviewDecision, slopRisk: pr.slopRisk, labels: pr.labels,