FN-5785: add group-level pull request support for branch_groups
Add a single group-level pull request path for branch_groups in task lifecycle workflows. - add CLI task lifecycle logic to create one pull request per branch group - update task lifecycle tests to cover grouped pull request behavior and edge cases - document the pull/merge dashboard behavior update - add a changeset for @runfusion/fusion patch release Files changed: .changeset/fn-5785-group-pr.md | 7 + docs/dashboard-guide.md | 1 + .../src/commands/__tests__/task-lifecycle.test.ts | 294 ++++++++++++++++++++- packages/cli/src/commands/task-lifecycle.ts | 156 ++++++++++- 4 files changed, 443 insertions(+), 15 deletions(-) Fusion-Task-Id: FN-5785 Fusion-Task-Lineage: 3100dedd-5b41-4cde-a56c-291410563568
This commit is contained in:
@@ -68,6 +68,8 @@ function makeStore(task: MockTask, settings: Record<string, unknown> = {}) {
|
||||
logEntry: vi.fn().mockResolvedValue(undefined),
|
||||
getActiveMergingTask: vi.fn().mockReturnValue(null),
|
||||
getBranchGroup: vi.fn().mockReturnValue(null),
|
||||
updateBranchGroup: vi.fn(),
|
||||
listTasksByBranchGroup: vi.fn().mockResolvedValue([]),
|
||||
_updates: updates,
|
||||
});
|
||||
}
|
||||
@@ -92,6 +94,8 @@ function makeStatefulStore(task: MockTask, settings: Record<string, unknown> = {
|
||||
logEntry: vi.fn().mockResolvedValue(undefined),
|
||||
getActiveMergingTask: vi.fn().mockReturnValue(null),
|
||||
getBranchGroup: vi.fn().mockReturnValue(null),
|
||||
updateBranchGroup: vi.fn(),
|
||||
listTasksByBranchGroup: vi.fn().mockResolvedValue([]),
|
||||
_getState: () => state,
|
||||
});
|
||||
}
|
||||
@@ -162,7 +166,7 @@ describe("processPullRequestMergeTask", () => {
|
||||
expect(pushIdx).toBeLessThan(createIdx);
|
||||
});
|
||||
|
||||
it("uses inherited branch-context merge target when creating a PR", async () => {
|
||||
it("creates shared-group PR from integration branch into default branch", async () => {
|
||||
const task: MockTask = {
|
||||
id: "FN-9002",
|
||||
title: "test",
|
||||
@@ -175,7 +179,6 @@ describe("processPullRequestMergeTask", () => {
|
||||
inheritedBaseBranch: "develop",
|
||||
},
|
||||
};
|
||||
const branch = getTaskBranchName(task.id);
|
||||
const store = makeStore(task, { baseBranch: "main" });
|
||||
(store.getBranchGroup as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
id: "BG-1",
|
||||
@@ -188,7 +191,11 @@ describe("processPullRequestMergeTask", () => {
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
execMock.mockImplementation(() => "");
|
||||
(store.listTasksByBranchGroup as ReturnType<typeof vi.fn>).mockResolvedValue([task]);
|
||||
execMock.mockImplementation((cmd: string) => {
|
||||
if (cmd.includes("rev-list --count")) return "1\n";
|
||||
return "";
|
||||
});
|
||||
|
||||
const github = {
|
||||
findPrForBranch: vi.fn(async () => null),
|
||||
@@ -196,8 +203,6 @@ describe("processPullRequestMergeTask", () => {
|
||||
number: 7,
|
||||
url: "https://github.com/x/y/pull/7",
|
||||
status: "open" as const,
|
||||
headBranch: branch,
|
||||
baseBranch: "fusion/groups/planning-abc",
|
||||
})),
|
||||
getPrMergeStatus: vi.fn(async () => ({
|
||||
prInfo: { number: 7, status: "open" as const, url: "https://github.com/x/y/pull/7" },
|
||||
@@ -209,16 +214,279 @@ describe("processPullRequestMergeTask", () => {
|
||||
mergePr: vi.fn(),
|
||||
};
|
||||
|
||||
await processPullRequestMergeTask(
|
||||
store as never,
|
||||
"/repo",
|
||||
task.id,
|
||||
github as never,
|
||||
() => undefined,
|
||||
);
|
||||
await processPullRequestMergeTask(store as never, "/repo", task.id, github as never, () => undefined);
|
||||
|
||||
expect(github.createPr).toHaveBeenCalledWith(expect.objectContaining({
|
||||
base: "fusion/groups/planning-abc",
|
||||
head: "fusion/groups/planning-abc",
|
||||
base: "main",
|
||||
}));
|
||||
expect(store.updateBranchGroup).toHaveBeenCalledWith("BG-1", expect.objectContaining({
|
||||
prNumber: 7,
|
||||
prUrl: "https://github.com/x/y/pull/7",
|
||||
prState: "open",
|
||||
}));
|
||||
});
|
||||
|
||||
it("routes shared branch-group members through group PR flow", async () => {
|
||||
const task: MockTask = {
|
||||
id: "FN-9010",
|
||||
title: "group member",
|
||||
description: "desc",
|
||||
column: "in-review",
|
||||
branchContext: {
|
||||
groupId: "BG-1",
|
||||
source: "planning",
|
||||
assignmentMode: "shared",
|
||||
},
|
||||
};
|
||||
const store = makeStore(task);
|
||||
(store.getBranchGroup as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
id: "BG-1",
|
||||
sourceType: "planning",
|
||||
sourceId: "P-1",
|
||||
branchName: "fusion/groups/p-1",
|
||||
autoMerge: false,
|
||||
prState: "none",
|
||||
status: "open",
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
(store.listTasksByBranchGroup as ReturnType<typeof vi.fn>).mockResolvedValue([task]);
|
||||
execMock.mockImplementation((cmd: string) => {
|
||||
if (cmd.includes("rev-list --count")) return "1\n";
|
||||
return "";
|
||||
});
|
||||
|
||||
const github = {
|
||||
findPrForBranch: vi.fn(async () => null),
|
||||
createPr: vi.fn(async () => ({ number: 13, url: "https://github.com/x/y/pull/13", status: "open" as const })),
|
||||
getPrMergeStatus: vi.fn(async () => ({
|
||||
prInfo: { number: 13, status: "open" as const, url: "https://github.com/x/y/pull/13" },
|
||||
reviewDecision: null,
|
||||
checks: [],
|
||||
mergeReady: false,
|
||||
blockingReasons: [],
|
||||
})),
|
||||
mergePr: vi.fn(),
|
||||
};
|
||||
|
||||
await processPullRequestMergeTask(store as never, "/repo", task.id, github as never, () => undefined);
|
||||
|
||||
expect(github.createPr).toHaveBeenCalledWith(expect.objectContaining({ head: "fusion/groups/p-1" }));
|
||||
expect(store.listTasksByBranchGroup).toHaveBeenCalledWith("BG-1");
|
||||
});
|
||||
|
||||
it("falls back to per-task path when shared group row is missing", async () => {
|
||||
const task: MockTask = {
|
||||
id: "FN-9011",
|
||||
title: "group member",
|
||||
description: "desc",
|
||||
column: "in-review",
|
||||
branchContext: {
|
||||
groupId: "BG-missing",
|
||||
source: "planning",
|
||||
assignmentMode: "shared",
|
||||
},
|
||||
};
|
||||
const store = makeStore(task);
|
||||
execMock.mockImplementation(() => "");
|
||||
|
||||
const github = {
|
||||
findPrForBranch: vi.fn(async () => null),
|
||||
createPr: vi.fn(async () => ({
|
||||
number: 14,
|
||||
url: "https://github.com/x/y/pull/14",
|
||||
status: "open" as const,
|
||||
headBranch: getTaskBranchName(task.id),
|
||||
})),
|
||||
getPrMergeStatus: vi.fn(async () => ({
|
||||
prInfo: { number: 14, status: "open" as const, url: "https://github.com/x/y/pull/14" },
|
||||
reviewDecision: null,
|
||||
checks: [],
|
||||
mergeReady: false,
|
||||
blockingReasons: [],
|
||||
})),
|
||||
mergePr: vi.fn(),
|
||||
};
|
||||
|
||||
await processPullRequestMergeTask(store as never, "/repo", task.id, github as never, () => undefined);
|
||||
|
||||
expect(github.createPr).toHaveBeenCalledWith(expect.objectContaining({ head: getTaskBranchName(task.id) }));
|
||||
});
|
||||
|
||||
it("does not create duplicate group PR when branch-group PR already exists", async () => {
|
||||
const task: MockTask = {
|
||||
id: "FN-9012",
|
||||
title: "group member",
|
||||
description: "desc",
|
||||
column: "in-review",
|
||||
branchContext: {
|
||||
groupId: "BG-2",
|
||||
source: "planning",
|
||||
assignmentMode: "shared",
|
||||
},
|
||||
};
|
||||
const store = makeStore(task);
|
||||
(store.getBranchGroup as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
id: "BG-2",
|
||||
sourceType: "planning",
|
||||
sourceId: "P-2",
|
||||
branchName: "fusion/groups/p-2",
|
||||
autoMerge: false,
|
||||
prState: "open",
|
||||
prNumber: 22,
|
||||
prUrl: "https://github.com/x/y/pull/22",
|
||||
status: "open",
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
(store.listTasksByBranchGroup as ReturnType<typeof vi.fn>).mockResolvedValue([task]);
|
||||
execMock.mockImplementation((cmd: string) => {
|
||||
if (cmd.includes("rev-list --count")) return "1\n";
|
||||
return "";
|
||||
});
|
||||
|
||||
const github = {
|
||||
findPrForBranch: vi.fn(async () => null),
|
||||
createPr: vi.fn(),
|
||||
getPrMergeStatus: vi.fn(async () => ({
|
||||
prInfo: { number: 22, status: "open" as const, url: "https://github.com/x/y/pull/22" },
|
||||
reviewDecision: null,
|
||||
checks: [],
|
||||
mergeReady: false,
|
||||
blockingReasons: [],
|
||||
})),
|
||||
mergePr: vi.fn(),
|
||||
};
|
||||
|
||||
await processPullRequestMergeTask(store as never, "/repo", task.id, github as never, () => undefined);
|
||||
|
||||
expect(github.createPr).not.toHaveBeenCalled();
|
||||
expect(github.getPrMergeStatus).toHaveBeenCalledWith("main", "fusion/groups/p-2", 22);
|
||||
expect(store.updateBranchGroup).toHaveBeenCalledWith("BG-2", expect.objectContaining({
|
||||
prNumber: 22,
|
||||
prUrl: "https://github.com/x/y/pull/22",
|
||||
prState: "open",
|
||||
}));
|
||||
});
|
||||
|
||||
it("finalizes branch group and member tasks when shared group PR is already merged", async () => {
|
||||
const taskA: MockTask = {
|
||||
id: "FN-9015",
|
||||
title: "A",
|
||||
description: "desc A",
|
||||
column: "in-review",
|
||||
branchContext: { groupId: "BG-4", source: "planning", assignmentMode: "shared" },
|
||||
worktree: "/tmp/a",
|
||||
};
|
||||
const taskB: MockTask = {
|
||||
id: "FN-9016",
|
||||
title: "B",
|
||||
description: "desc B",
|
||||
column: "in-review",
|
||||
branchContext: { groupId: "BG-4", source: "planning", assignmentMode: "shared" },
|
||||
worktree: "/tmp/b",
|
||||
};
|
||||
const store = makeStore(taskA);
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockImplementation(async (id: string) => (id === taskB.id ? taskB : taskA));
|
||||
(store.getBranchGroup as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
id: "BG-4",
|
||||
sourceType: "planning",
|
||||
sourceId: "P-4",
|
||||
branchName: "fusion/groups/p-4",
|
||||
autoMerge: false,
|
||||
prState: "open",
|
||||
prNumber: 24,
|
||||
prUrl: "https://github.com/x/y/pull/24",
|
||||
status: "open",
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
(store.listTasksByBranchGroup as ReturnType<typeof vi.fn>).mockResolvedValue([taskA, taskB]);
|
||||
execMock.mockImplementation((cmd: string) => {
|
||||
if (cmd.includes("rev-list --count")) return "1\n";
|
||||
return "";
|
||||
});
|
||||
|
||||
const github = {
|
||||
findPrForBranch: vi.fn(async () => null),
|
||||
createPr: vi.fn(),
|
||||
getPrMergeStatus: vi.fn(async () => ({
|
||||
prInfo: { number: 24, status: "merged" as const, url: "https://github.com/x/y/pull/24" },
|
||||
reviewDecision: "APPROVED" as const,
|
||||
checks: [],
|
||||
mergeReady: true,
|
||||
blockingReasons: [],
|
||||
})),
|
||||
mergePr: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await processPullRequestMergeTask(store as never, "/repo", taskA.id, github as never, () => undefined);
|
||||
|
||||
expect(result).toBe("merged");
|
||||
expect(store.moveTask).toHaveBeenCalledWith(taskA.id, "done");
|
||||
expect(store.moveTask).toHaveBeenCalledWith(taskB.id, "done");
|
||||
expect(store.updateBranchGroup).toHaveBeenCalledWith("BG-4", expect.objectContaining({
|
||||
status: "finalized",
|
||||
prState: "merged",
|
||||
}));
|
||||
});
|
||||
|
||||
it("excludes empty member branches from group PR body", async () => {
|
||||
const taskA: MockTask = {
|
||||
id: "FN-9013",
|
||||
title: "A",
|
||||
description: "desc A",
|
||||
column: "in-review",
|
||||
branchContext: { groupId: "BG-3", source: "planning", assignmentMode: "shared" },
|
||||
};
|
||||
const taskB: MockTask = {
|
||||
id: "FN-9014",
|
||||
title: "B",
|
||||
description: "desc B",
|
||||
column: "in-review",
|
||||
branchContext: { groupId: "BG-3", source: "planning", assignmentMode: "shared" },
|
||||
};
|
||||
const store = makeStore(taskA);
|
||||
(store.getBranchGroup as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
id: "BG-3",
|
||||
sourceType: "planning",
|
||||
sourceId: "P-3",
|
||||
branchName: "fusion/groups/p-3",
|
||||
autoMerge: false,
|
||||
prState: "none",
|
||||
status: "open",
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
(store.listTasksByBranchGroup as ReturnType<typeof vi.fn>).mockResolvedValue([taskA, taskB]);
|
||||
execMock.mockImplementation((cmd: string) => {
|
||||
if (cmd.includes("rev-list --count") && cmd.includes("fn-9014")) return "0\n";
|
||||
if (cmd.includes("rev-list --count")) return "1\n";
|
||||
return "";
|
||||
});
|
||||
|
||||
const github = {
|
||||
findPrForBranch: vi.fn(async () => null),
|
||||
createPr: vi.fn(async () => ({ number: 23, url: "https://github.com/x/y/pull/23", status: "open" as const })),
|
||||
getPrMergeStatus: vi.fn(async () => ({
|
||||
prInfo: { number: 23, status: "open" as const, url: "https://github.com/x/y/pull/23" },
|
||||
reviewDecision: null,
|
||||
checks: [],
|
||||
mergeReady: false,
|
||||
blockingReasons: [],
|
||||
})),
|
||||
mergePr: vi.fn(),
|
||||
};
|
||||
|
||||
await processPullRequestMergeTask(store as never, "/repo", taskA.id, github as never, () => undefined);
|
||||
|
||||
expect(github.createPr).toHaveBeenCalledTimes(1);
|
||||
expect(github.createPr).toHaveBeenCalledWith(expect.objectContaining({
|
||||
body: expect.stringContaining("FN-9013"),
|
||||
}));
|
||||
expect(github.createPr).toHaveBeenCalledWith(expect.objectContaining({
|
||||
body: expect.not.stringContaining("FN-9014"),
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { promisify } from "node:util";
|
||||
const execAsync = promisify(exec);
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import { resolveTaskMergeTarget } from "@fusion/core";
|
||||
import type { Settings, TaskDetail, PrInfo, MergeResult } from "@fusion/core";
|
||||
import type { Settings, TaskDetail, PrInfo, MergeResult, BranchGroup, BranchGroupPrState, Task } from "@fusion/core";
|
||||
import { resolveIntegrationBranch } from "@fusion/engine";
|
||||
import type { WorktreePool } from "@fusion/engine";
|
||||
|
||||
@@ -137,6 +137,41 @@ function buildPullRequestBody(task: Pick<TaskDetail, "id" | "description">): str
|
||||
return [`Automated PR for ${task.id}.`, "", task.description].join("\n");
|
||||
}
|
||||
|
||||
function buildGroupPullRequestTitle(group: Pick<BranchGroup, "id" | "sourceType" | "sourceId">, members: Task[]): string {
|
||||
return `${group.id}: ${group.sourceType}/${group.sourceId} (${members.length} tasks)`;
|
||||
}
|
||||
|
||||
function buildGroupPullRequestBody(
|
||||
group: Pick<BranchGroup, "id" | "branchName" | "sourceType" | "sourceId">,
|
||||
members: Array<Pick<Task, "id" | "title"> & { branchName: string }>,
|
||||
): string {
|
||||
const lines = members.map((member) => `- ${member.id}: ${member.title || "(untitled)"} — \`${member.branchName}\``);
|
||||
return [
|
||||
`Automated group PR for ${group.id}.`,
|
||||
`Source: ${group.sourceType}/${group.sourceId}`,
|
||||
`Integration branch: \`${group.branchName}\``,
|
||||
"",
|
||||
"Included tasks:",
|
||||
...(lines.length > 0 ? lines : ["- (none)"]),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function toBranchGroupPrState(prInfo: PrInfo | null): BranchGroupPrState {
|
||||
if (!prInfo) return "none";
|
||||
if (prInfo.status === "merged") return "merged";
|
||||
if (prInfo.status === "closed") return "closed";
|
||||
return "open";
|
||||
}
|
||||
|
||||
async function hasCommitsRelativeToBranch(cwd: string, branch: string, baseBranch: string): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync(`git rev-list --count "${baseBranch}..${branch}"`, { cwd, timeout: 30_000 });
|
||||
return Number.parseInt(stdout.trim(), 10) > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up worktree and branch artifacts after a successful merge.
|
||||
* Both operations are best-effort; errors are logged but don't propagate.
|
||||
@@ -269,9 +304,126 @@ export async function processPullRequestMergeTask(
|
||||
const settings = await store.getSettings();
|
||||
const resolvedIntegrationBranch = await resolveIntegrationBranch(cwd, settings);
|
||||
const projectDefaultBranch = resolvedIntegrationBranch;
|
||||
const branchGroup = task.branchContext?.assignmentMode === "shared"
|
||||
|
||||
// FN-5782 contract: shared group members promote via branch_groups.branchName
|
||||
// integration branch, while non-shared tasks keep per-task PR behavior.
|
||||
const isSharedBranchGroupMember = task.branchContext?.assignmentMode === "shared";
|
||||
const branchGroup = isSharedBranchGroupMember
|
||||
? store.getBranchGroup(task.branchContext.groupId)
|
||||
: null;
|
||||
|
||||
if (isSharedBranchGroupMember && branchGroup) {
|
||||
const members = await store.listTasksByBranchGroup(branchGroup.id);
|
||||
const membersWithCommits: Array<Pick<Task, "id" | "title"> & { branchName: string }> = [];
|
||||
for (const member of members) {
|
||||
const memberBranch = getTaskBranchName(member.id);
|
||||
const hasCommits = await hasCommitsRelativeToBranch(cwd, memberBranch, branchGroup.branchName);
|
||||
if (hasCommits || member.id === task.id) {
|
||||
membersWithCommits.push({ id: member.id, title: member.title, branchName: memberBranch });
|
||||
}
|
||||
}
|
||||
|
||||
await store.updateTask(task.id, { status: "creating-pr" });
|
||||
let groupPrInfo: PrInfo | null = null;
|
||||
if (branchGroup.prNumber) {
|
||||
groupPrInfo = {
|
||||
number: branchGroup.prNumber,
|
||||
url: branchGroup.prUrl ?? "",
|
||||
status: branchGroup.prState === "merged" ? "merged" : branchGroup.prState === "closed" ? "closed" : "open",
|
||||
};
|
||||
} else {
|
||||
groupPrInfo = await github.findPrForBranch({ head: branchGroup.branchName, state: "all" });
|
||||
if (!groupPrInfo) {
|
||||
await pushTaskBranchToOrigin(cwd, branchGroup.branchName);
|
||||
try {
|
||||
groupPrInfo = await github.createPr({
|
||||
title: buildGroupPullRequestTitle(branchGroup, members),
|
||||
body: buildGroupPullRequestBody(branchGroup, membersWithCommits),
|
||||
head: branchGroup.branchName,
|
||||
base: projectDefaultBranch,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
if (message.includes("No commits between")) {
|
||||
await store.updateBranchGroup(branchGroup.id, { prState: "none", prNumber: null, prUrl: null });
|
||||
await store.updateTask(task.id, { status: "failed", error: `No pull request created for ${branchGroup.branchName}: no commits relative to ${projectDefaultBranch}.` });
|
||||
await store.logEntry(task.id, "No group pull request created", message);
|
||||
return "skipped";
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
await store.logEntry(task.id, "Created group PR", `PR #${groupPrInfo.number}: ${groupPrInfo.url}`);
|
||||
} else {
|
||||
await store.logEntry(task.id, "Linked existing group PR", `PR #${groupPrInfo.number}: ${groupPrInfo.url}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!groupPrInfo) {
|
||||
throw new Error(`Failed to create or resolve pull request for branch group ${branchGroup.id}`);
|
||||
}
|
||||
|
||||
await store.updateBranchGroup(branchGroup.id, {
|
||||
prNumber: groupPrInfo.number,
|
||||
prUrl: groupPrInfo.url,
|
||||
prState: toBranchGroupPrState(groupPrInfo),
|
||||
});
|
||||
|
||||
const mergeStatus = await github.getPrMergeStatus(projectDefaultBranch, branchGroup.branchName, groupPrInfo.number);
|
||||
const refreshedPrInfo: PrInfo = {
|
||||
...groupPrInfo,
|
||||
...mergeStatus.prInfo,
|
||||
lastCheckedAt: new Date().toISOString(),
|
||||
};
|
||||
await store.updateBranchGroup(branchGroup.id, {
|
||||
prNumber: refreshedPrInfo.number,
|
||||
prUrl: refreshedPrInfo.url,
|
||||
prState: toBranchGroupPrState(refreshedPrInfo),
|
||||
});
|
||||
|
||||
if (mergeStatus.prInfo.status === "merged") {
|
||||
for (const member of members) {
|
||||
const memberDetail = await store.getTask(member.id);
|
||||
await finalizePullRequestMerge(store, cwd, memberDetail, refreshedPrInfo, "Group pull request merged", pool);
|
||||
}
|
||||
await store.updateBranchGroup(branchGroup.id, { status: "finalized", prState: "merged" });
|
||||
return "merged";
|
||||
}
|
||||
|
||||
if (settings.requirePrApproval && mergeStatus.reviewDecision !== "APPROVED") {
|
||||
await store.updateTask(task.id, { status: "awaiting-pr-checks" });
|
||||
return "waiting";
|
||||
}
|
||||
|
||||
if (!mergeStatus.mergeReady) {
|
||||
await store.updateTask(task.id, { status: mergeStatus.prInfo.status === "open" ? "awaiting-pr-checks" : null });
|
||||
return "waiting";
|
||||
}
|
||||
|
||||
const activeMerge = store.getActiveMergingTask(task.id);
|
||||
if (activeMerge) {
|
||||
await store.updateTask(task.id, { status: "awaiting-pr-checks" });
|
||||
return "waiting";
|
||||
}
|
||||
|
||||
await store.updateTask(task.id, { status: "merging-pr" });
|
||||
const mergedPr = await github.mergePr({ number: refreshedPrInfo.number, method: "squash" });
|
||||
await store.updateBranchGroup(branchGroup.id, {
|
||||
prNumber: mergedPr.number,
|
||||
prUrl: mergedPr.url,
|
||||
prState: toBranchGroupPrState(mergedPr),
|
||||
});
|
||||
for (const member of members) {
|
||||
const memberDetail = await store.getTask(member.id);
|
||||
await finalizePullRequestMerge(store, cwd, memberDetail, mergedPr, "Group pull request merged", pool);
|
||||
}
|
||||
await store.updateBranchGroup(branchGroup.id, { status: "finalized", prState: "merged" });
|
||||
return "merged";
|
||||
}
|
||||
|
||||
if (isSharedBranchGroupMember && !branchGroup) {
|
||||
await store.logEntry(task.id, "Branch group missing; falling back to per-task PR path", task.branchContext?.groupId);
|
||||
}
|
||||
|
||||
const mergeTarget = resolveTaskMergeTarget(task, {
|
||||
projectDefaultBranch,
|
||||
branchGroup,
|
||||
|
||||
Reference in New Issue
Block a user