fix: reconcile PR merge race
This commit is contained in:
5
.changeset/reconcile-pr-merge-race.md
Normal file
5
.changeset/reconcile-pr-merge-race.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Reconcile pull-request merge tasks when GitHub reports the PR merged after a merge command failure.
|
||||||
@@ -502,6 +502,80 @@ describe("processPullRequestMergeTask", () => {
|
|||||||
expect(store.moveTask).toHaveBeenCalledWith("FN-9004", "done");
|
expect(store.moveTask).toHaveBeenCalledWith("FN-9004", "done");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reconciles to done when PR merges after readiness check but before merge command completes", async () => {
|
||||||
|
const task: MockTask = {
|
||||||
|
id: "FN-9104",
|
||||||
|
title: "test",
|
||||||
|
description: "desc",
|
||||||
|
column: "in-review",
|
||||||
|
worktree: "/tmp/worktree-fn-9104",
|
||||||
|
prInfo: {
|
||||||
|
number: 124,
|
||||||
|
url: "https://github.com/x/y/pull/124",
|
||||||
|
status: "open",
|
||||||
|
headBranch: "fusion/fn-9104",
|
||||||
|
baseBranch: "main",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const store = makeStore(task);
|
||||||
|
execMock.mockImplementation(() => "");
|
||||||
|
|
||||||
|
const openPr = {
|
||||||
|
number: 124,
|
||||||
|
url: "https://github.com/x/y/pull/124",
|
||||||
|
status: "open" as const,
|
||||||
|
headBranch: "fusion/fn-9104",
|
||||||
|
baseBranch: "main",
|
||||||
|
};
|
||||||
|
const mergedPr = {
|
||||||
|
...openPr,
|
||||||
|
status: "merged" as const,
|
||||||
|
};
|
||||||
|
const github = {
|
||||||
|
findPrForBranch: vi.fn(),
|
||||||
|
createPr: vi.fn(),
|
||||||
|
getPrMergeStatus: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
prInfo: openPr,
|
||||||
|
reviewDecision: "APPROVED",
|
||||||
|
checks: [],
|
||||||
|
mergeReady: true,
|
||||||
|
blockingReasons: [],
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
prInfo: mergedPr,
|
||||||
|
reviewDecision: "APPROVED",
|
||||||
|
checks: [],
|
||||||
|
mergeReady: true,
|
||||||
|
blockingReasons: [],
|
||||||
|
}),
|
||||||
|
mergePr: vi.fn(async () => {
|
||||||
|
throw new Error("Pull request is not mergeable: the merge commit cannot be cleanly created");
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await processPullRequestMergeTask(
|
||||||
|
store as never,
|
||||||
|
"/repo",
|
||||||
|
task.id,
|
||||||
|
github as never,
|
||||||
|
() => undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe("merged");
|
||||||
|
expect(github.mergePr).toHaveBeenCalledWith({ number: 124, method: "squash" });
|
||||||
|
expect(github.getPrMergeStatus).toHaveBeenCalledTimes(2);
|
||||||
|
expect(store.updatePrInfo).toHaveBeenLastCalledWith("FN-9104", expect.objectContaining({ status: "merged" }));
|
||||||
|
expect(store.updateTask).toHaveBeenCalledWith("FN-9104", { status: null, mergeRetries: 0 });
|
||||||
|
expect(store.moveTask).toHaveBeenCalledWith("FN-9104", "done");
|
||||||
|
expect(store.logEntry).toHaveBeenCalledWith(
|
||||||
|
"FN-9104",
|
||||||
|
"Pull request already merged after merge command failed; reconciled task state from GitHub",
|
||||||
|
"PR #124: https://github.com/x/y/pull/124",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("preserves PR number/url through create, refresh, and merge completion", async () => {
|
it("preserves PR number/url through create, refresh, and merge completion", async () => {
|
||||||
const task: MockTask = {
|
const task: MockTask = {
|
||||||
id: "FN-9103",
|
id: "FN-9103",
|
||||||
|
|||||||
@@ -175,11 +175,12 @@ async function finalizePullRequestMerge(
|
|||||||
cwd: string,
|
cwd: string,
|
||||||
task: TaskDetail,
|
task: TaskDetail,
|
||||||
prInfo: PrInfo,
|
prInfo: PrInfo,
|
||||||
|
message = "Pull request merged",
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await cleanupMergedTaskArtifacts(cwd, task);
|
await cleanupMergedTaskArtifacts(cwd, task);
|
||||||
await store.updateTask(task.id, { status: null, mergeRetries: 0 });
|
await store.updateTask(task.id, { status: null, mergeRetries: 0 });
|
||||||
await store.moveTask(task.id, "done");
|
await store.moveTask(task.id, "done");
|
||||||
await store.logEntry(task.id, "Pull request merged", `PR #${prInfo.number}: ${prInfo.url}`);
|
await store.logEntry(task.id, message, `PR #${prInfo.number}: ${prInfo.url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -316,7 +317,36 @@ export async function processPullRequestMergeTask(
|
|||||||
return "waiting";
|
return "waiting";
|
||||||
}
|
}
|
||||||
await store.updateTask(task.id, { status: "merging-pr" });
|
await store.updateTask(task.id, { status: "merging-pr" });
|
||||||
const mergedPr = await github.mergePr({ number: prInfo.number, method: "squash" });
|
let mergedPr: PrInfo;
|
||||||
|
try {
|
||||||
|
mergedPr = await github.mergePr({ number: prInfo.number, method: "squash" });
|
||||||
|
} catch (err: unknown) {
|
||||||
|
let refreshedStatus: Awaited<ReturnType<GitHubOperations["getPrMergeStatus"]>>;
|
||||||
|
try {
|
||||||
|
refreshedStatus = await github.getPrMergeStatus(mergeTarget.branch, branch, prInfo.number);
|
||||||
|
} catch {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const refreshedAfterFailure: PrInfo = {
|
||||||
|
...prInfo,
|
||||||
|
...refreshedStatus.prInfo,
|
||||||
|
lastCheckedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
await store.updatePrInfo(task.id, refreshedAfterFailure);
|
||||||
|
|
||||||
|
if (refreshedAfterFailure.status === "merged") {
|
||||||
|
await finalizePullRequestMerge(
|
||||||
|
store,
|
||||||
|
cwd,
|
||||||
|
task,
|
||||||
|
refreshedAfterFailure,
|
||||||
|
"Pull request already merged after merge command failed; reconciled task state from GitHub",
|
||||||
|
);
|
||||||
|
return "merged";
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
await store.updatePrInfo(task.id, { ...mergedPr, lastCheckedAt: new Date().toISOString() });
|
await store.updatePrInfo(task.id, { ...mergedPr, lastCheckedAt: new Date().toISOString() });
|
||||||
await finalizePullRequestMerge(store, cwd, task, mergedPr);
|
await finalizePullRequestMerge(store, cwd, task, mergedPr);
|
||||||
return "merged";
|
return "merged";
|
||||||
|
|||||||
Reference in New Issue
Block a user