feat(FN-4763): complete Step 1 — capture PR mergeable state
Fusion-Task-Id: FN-4763 Fusion-Task-Lineage: b3628e63-682b-4a39-9a7d-2c96278c5366
This commit is contained in:
committed by
gsxdsm
parent
1f8d995b82
commit
9d5d310fdc
@@ -1105,6 +1105,8 @@ describe("GitHubClient", () => {
|
||||
title: "Ready PR",
|
||||
state: "OPEN",
|
||||
reviewDecision: "APPROVED",
|
||||
mergeable: "MERGEABLE",
|
||||
mergeStateStatus: "CLEAN",
|
||||
baseRefName: "main",
|
||||
headRefName: "fusion/fn-093",
|
||||
})
|
||||
@@ -1116,6 +1118,8 @@ describe("GitHubClient", () => {
|
||||
const result = await client.getPrMergeStatus("owner", "repo", 42);
|
||||
|
||||
expect(result.mergeReady).toBe(true);
|
||||
expect(result.mergeable).toBe("clean");
|
||||
expect(result.prInfo.mergeable).toBe("clean");
|
||||
expect(result.blockingReasons).toEqual([]);
|
||||
expect(result.checks).toEqual([
|
||||
{
|
||||
@@ -1130,6 +1134,44 @@ describe("GitHubClient", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("maps DIRTY merge-state to conflicting even when mergeable is unknown", async () => {
|
||||
mockRunGhJsonAsync
|
||||
.mockResolvedValueOnce({
|
||||
number: 42,
|
||||
url: "https://github.com/owner/repo/pull/42",
|
||||
title: "Dirty PR",
|
||||
state: "OPEN",
|
||||
reviewDecision: "APPROVED",
|
||||
mergeable: "UNKNOWN",
|
||||
mergeStateStatus: "DIRTY",
|
||||
baseRefName: "main",
|
||||
headRefName: "fusion/fn-093",
|
||||
})
|
||||
.mockResolvedValueOnce([{ name: "ci", state: "SUCCESS" }]);
|
||||
|
||||
const result = await client.getPrMergeStatus("owner", "repo", 42);
|
||||
expect(result.mergeable).toBe("conflicting");
|
||||
expect(result.prInfo.mergeable).toBe("conflicting");
|
||||
});
|
||||
|
||||
it("maps missing mergeability fields to unknown", async () => {
|
||||
mockRunGhJsonAsync
|
||||
.mockResolvedValueOnce({
|
||||
number: 42,
|
||||
url: "https://github.com/owner/repo/pull/42",
|
||||
title: "Unknown PR",
|
||||
state: "OPEN",
|
||||
reviewDecision: "APPROVED",
|
||||
baseRefName: "main",
|
||||
headRefName: "fusion/fn-093",
|
||||
})
|
||||
.mockResolvedValueOnce([{ name: "ci", state: "SUCCESS" }]);
|
||||
|
||||
const result = await client.getPrMergeStatus("owner", "repo", 42);
|
||||
expect(result.mergeable).toBe("unknown");
|
||||
expect(result.prInfo.mergeable).toBe("unknown");
|
||||
});
|
||||
|
||||
it("falls back to GraphQL API when gh CLI merge-status lookup fails and token is available", async () => {
|
||||
mockRunGhJsonAsync.mockRejectedValue(new Error("gh failed"));
|
||||
const clientWithToken = new GitHubClient("ghp_token");
|
||||
@@ -1144,6 +1186,8 @@ describe("GitHubClient", () => {
|
||||
title: "Fallback PR",
|
||||
state: "OPEN",
|
||||
reviewDecision: null,
|
||||
mergeable: "CONFLICTING",
|
||||
mergeStateStatus: "DIRTY",
|
||||
baseRefName: "main",
|
||||
headRefName: "fusion/fn-093",
|
||||
comments: { totalCount: 0 },
|
||||
@@ -1196,6 +1240,8 @@ describe("GitHubClient", () => {
|
||||
const result = await clientWithToken.getPrMergeStatus("owner", "repo", 42);
|
||||
|
||||
expect(result.mergeReady).toBe(true);
|
||||
expect(result.mergeable).toBe("conflicting");
|
||||
expect(result.prInfo.mergeable).toBe("conflicting");
|
||||
expect(result.checks).toEqual([
|
||||
{
|
||||
name: "ci",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IssueInfo, PrInfo, TaskReviewData, TaskReviewItem, TaskReviewSummary } from "@fusion/core";
|
||||
import type { IssueInfo, PrConflictState, PrInfo, TaskReviewData, TaskReviewItem, TaskReviewSummary } from "@fusion/core";
|
||||
import {
|
||||
isGhAvailable,
|
||||
isGhAuthenticated,
|
||||
@@ -156,6 +156,7 @@ export interface PrMergeStatus {
|
||||
prInfo: PrInfo;
|
||||
reviewDecision: ReviewDecision;
|
||||
checks: PrCheckStatus[];
|
||||
mergeable: PrConflictState;
|
||||
mergeReady: boolean;
|
||||
blockingReasons: string[];
|
||||
}
|
||||
@@ -197,6 +198,9 @@ interface GhReviewJson {
|
||||
url?: string | null;
|
||||
}
|
||||
|
||||
type GhPrMergeable = "MERGEABLE" | "CONFLICTING" | "UNKNOWN";
|
||||
type GhPrMergeStateStatus = "CLEAN" | "DIRTY" | "BLOCKED" | "BEHIND" | "UNSTABLE" | "UNKNOWN" | "HAS_HOOKS";
|
||||
|
||||
interface GhPrViewJson {
|
||||
id?: string;
|
||||
number: number;
|
||||
@@ -205,6 +209,8 @@ interface GhPrViewJson {
|
||||
state: "OPEN" | "CLOSED" | "MERGED";
|
||||
isDraft?: boolean;
|
||||
reviewDecision?: ReviewDecision;
|
||||
mergeable?: GhPrMergeable;
|
||||
mergeStateStatus?: GhPrMergeStateStatus;
|
||||
baseRefName: string;
|
||||
headRefName: string;
|
||||
comments: Array<{
|
||||
@@ -337,6 +343,25 @@ function normalizeCheckState(state: string | null | undefined): PrCheckState {
|
||||
}
|
||||
}
|
||||
|
||||
function mapPrConflictState(
|
||||
mergeable?: GhPrMergeable,
|
||||
mergeStateStatus?: GhPrMergeStateStatus,
|
||||
): PrConflictState {
|
||||
if (mergeStateStatus === "DIRTY" || mergeable === "CONFLICTING") {
|
||||
return "conflicting";
|
||||
}
|
||||
if (mergeStateStatus === "BEHIND") {
|
||||
return "behind";
|
||||
}
|
||||
if (mergeStateStatus === "BLOCKED") {
|
||||
return "blocked";
|
||||
}
|
||||
if (mergeStateStatus === "CLEAN" || mergeable === "MERGEABLE") {
|
||||
return "clean";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function toPrInfo(input: {
|
||||
url: string;
|
||||
number: number;
|
||||
@@ -346,6 +371,7 @@ function toPrInfo(input: {
|
||||
baseBranch: string;
|
||||
isDraft?: boolean;
|
||||
commentCount?: number;
|
||||
mergeable?: PrConflictState;
|
||||
lastCommentAt?: string;
|
||||
lastCheckedAt?: string;
|
||||
}): PrInfo {
|
||||
@@ -359,6 +385,7 @@ function toPrInfo(input: {
|
||||
commentCount: input.commentCount ?? 0,
|
||||
isDraft: input.isDraft,
|
||||
draft: input.isDraft,
|
||||
mergeable: input.mergeable,
|
||||
lastCommentAt: input.lastCommentAt,
|
||||
lastCheckedAt: input.lastCheckedAt,
|
||||
};
|
||||
@@ -1017,8 +1044,9 @@ export class GitHubClient {
|
||||
const pr = await runGhJsonAsync<GhPrViewJson>([
|
||||
"pr", "view", String(number),
|
||||
"--repo", `${resolved.owner}/${resolved.repo}`,
|
||||
"--json", "number,url,title,state,isDraft,baseRefName,headRefName,reviewDecision",
|
||||
"--json", "number,url,title,state,isDraft,baseRefName,headRefName,reviewDecision,mergeable,mergeStateStatus",
|
||||
]);
|
||||
const mergeable = mapPrConflictState(pr.mergeable, pr.mergeStateStatus);
|
||||
const checks = await runGhJsonAsync<GhPrCheckJson[]>([
|
||||
"pr", "checks", String(number),
|
||||
"--repo", `${resolved.owner}/${resolved.repo}`,
|
||||
@@ -1035,6 +1063,7 @@ export class GitHubClient {
|
||||
baseBranch: pr.baseRefName,
|
||||
isDraft: pr.isDraft,
|
||||
commentCount: 0,
|
||||
mergeable,
|
||||
});
|
||||
const normalizedChecks = checks.map((check) => ({
|
||||
name: check.name,
|
||||
@@ -1054,6 +1083,7 @@ export class GitHubClient {
|
||||
prInfo,
|
||||
reviewDecision: pr.reviewDecision ?? null,
|
||||
checks: normalizedChecks,
|
||||
mergeable,
|
||||
mergeReady: readiness.ready,
|
||||
blockingReasons: readiness.blockingReasons,
|
||||
};
|
||||
@@ -1073,6 +1103,8 @@ export class GitHubClient {
|
||||
title
|
||||
state
|
||||
reviewDecision
|
||||
mergeable
|
||||
mergeStateStatus
|
||||
isDraft
|
||||
baseRefName
|
||||
headRefName
|
||||
@@ -1121,6 +1153,8 @@ export class GitHubClient {
|
||||
title: string;
|
||||
state: "OPEN" | "CLOSED" | "MERGED";
|
||||
reviewDecision: ReviewDecision;
|
||||
mergeable?: GhPrMergeable;
|
||||
mergeStateStatus?: GhPrMergeStateStatus;
|
||||
isDraft?: boolean;
|
||||
baseRefName: string;
|
||||
headRefName: string;
|
||||
@@ -1186,6 +1220,7 @@ export class GitHubClient {
|
||||
} satisfies PrCheckStatus];
|
||||
});
|
||||
|
||||
const mergeable = mapPrConflictState(pr.mergeable, pr.mergeStateStatus);
|
||||
const prInfo = toPrInfo({
|
||||
url: pr.url,
|
||||
number: pr.number,
|
||||
@@ -1195,6 +1230,7 @@ export class GitHubClient {
|
||||
baseBranch: pr.baseRefName,
|
||||
isDraft: pr.isDraft,
|
||||
commentCount: pr.comments.totalCount,
|
||||
mergeable,
|
||||
});
|
||||
const readiness = isPrMergeReady({
|
||||
status: prInfo.status,
|
||||
@@ -1206,6 +1242,7 @@ export class GitHubClient {
|
||||
prInfo,
|
||||
reviewDecision: pr.reviewDecision,
|
||||
checks,
|
||||
mergeable,
|
||||
mergeReady: readiness.ready,
|
||||
blockingReasons: readiness.blockingReasons,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user