FN-6137: convert PR lifecycle built-in to fragment
Convert the built-in PR lifecycle into a reusable fragment instead of a selectable task workflow. - mark builtin:pr-workflow as a fragment and exclude fragments from default-enabled built-ins - hide built-in fragments from the General settings workflow toggles - update core and dashboard tests to assert fragment behavior and reject task selection of the PR lifecycle fragment - add a changeset for the published CLI package Files changed: .changeset/pr-lifecycle-fragment.md | 5 +++++ .../core/src/__tests__/builtin-workflows.test.ts | 24 ++++++++++++++++------ packages/core/src/builtin-workflows.ts | 7 ++++--- packages/core/src/store.ts | 2 +- .../__tests__/WorkflowNodeEditor.test.tsx | 3 ++- .../settings/sections/GeneralSection.tsx | 4 +++- 6 files changed, 33 insertions(+), 12 deletions(-) Fusion-Task-Id: FN-6137 Fusion-Task-Lineage: d959ba3b-2e50-4c9b-a1fa-161776939403
This commit is contained in:
5
.changeset/pr-lifecycle-fragment.md
Normal file
5
.changeset/pr-lifecycle-fragment.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Convert the built-in PR lifecycle from a selectable task workflow into a reusable workflow-editor fragment template.
|
||||||
@@ -16,17 +16,16 @@ import { createTaskStoreTestHarness } from "./store-test-helpers.js";
|
|||||||
const EXECUTE_NODE_MAX_RETRIES = 2;
|
const EXECUTE_NODE_MAX_RETRIES = 2;
|
||||||
|
|
||||||
describe("built-in workflows", () => {
|
describe("built-in workflows", () => {
|
||||||
// Graph-only built-ins (step inversion, KTD-9) model branching/foreach/rework
|
// Non-compiler built-ins model graph-only node kinds or reusable fragments the
|
||||||
// structure the linear compiler cannot lower to a step list — they run only
|
// linear compiler cannot lower to a step list. They still must parse as valid IR.
|
||||||
// under the workflow graph executor. They still must parse as valid IR.
|
const NON_COMPILABLE_BUILTIN_IDS = new Set(["builtin:stepwise-coding", "builtin:pr-workflow"]);
|
||||||
const GRAPH_ONLY_BUILTIN_IDS = new Set(["builtin:stepwise-coding", "builtin:pr-workflow"]);
|
|
||||||
|
|
||||||
it("every built-in has a valid IR; linear built-ins compile without error", () => {
|
it("every built-in has a valid IR; linear built-ins compile without error", () => {
|
||||||
expect(BUILTIN_WORKFLOWS.length).toBeGreaterThanOrEqual(4);
|
expect(BUILTIN_WORKFLOWS.length).toBeGreaterThanOrEqual(4);
|
||||||
for (const wf of BUILTIN_WORKFLOWS) {
|
for (const wf of BUILTIN_WORKFLOWS) {
|
||||||
expect(isBuiltinWorkflowId(wf.id)).toBe(true);
|
expect(isBuiltinWorkflowId(wf.id)).toBe(true);
|
||||||
expect(() => parseWorkflowIr(wf.ir)).not.toThrow();
|
expect(() => parseWorkflowIr(wf.ir)).not.toThrow();
|
||||||
if (!GRAPH_ONLY_BUILTIN_IDS.has(wf.id)) {
|
if (!NON_COMPILABLE_BUILTIN_IDS.has(wf.id)) {
|
||||||
expect(() => compileWorkflowToSteps(wf.ir)).not.toThrow();
|
expect(() => compileWorkflowToSteps(wf.ir)).not.toThrow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +50,8 @@ describe("built-in workflows", () => {
|
|||||||
it("includes the PR lifecycle built-in wiring the PR nodes end to end (U9)", () => {
|
it("includes the PR lifecycle built-in wiring the PR nodes end to end (U9)", () => {
|
||||||
const pr = getBuiltinWorkflow("builtin:pr-workflow");
|
const pr = getBuiltinWorkflow("builtin:pr-workflow");
|
||||||
expect(pr).toBeDefined();
|
expect(pr).toBeDefined();
|
||||||
|
expect(pr!.kind).toBe("fragment");
|
||||||
|
expect(BUILTIN_WORKFLOWS.some((workflow) => workflow.id === "builtin:pr-workflow")).toBe(true);
|
||||||
const ir = parseWorkflowIr(pr!.ir);
|
const ir = parseWorkflowIr(pr!.ir);
|
||||||
if (ir.version !== "v2") throw new Error("expected v2");
|
if (ir.version !== "v2") throw new Error("expected v2");
|
||||||
|
|
||||||
@@ -143,7 +144,11 @@ describe("built-in workflows", () => {
|
|||||||
expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR);
|
expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR);
|
||||||
expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR);
|
expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR);
|
||||||
expect(BUILTIN_WORKFLOWS.find((workflow) => workflow.id === "builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR);
|
expect(BUILTIN_WORKFLOWS.find((workflow) => workflow.id === "builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR);
|
||||||
expect(defaultEnabledBuiltinWorkflowIds()).toEqual(BUILTIN_WORKFLOWS.map((workflow) => workflow.id));
|
expect(defaultEnabledBuiltinWorkflowIds()).toEqual(
|
||||||
|
BUILTIN_WORKFLOWS.filter((workflow) => workflow.kind !== "fragment").map((workflow) => workflow.id),
|
||||||
|
);
|
||||||
|
expect(defaultEnabledBuiltinWorkflowIds()).not.toContain("builtin:pr-workflow");
|
||||||
|
expect(getBuiltinWorkflow("builtin:pr-workflow")!.kind).toBe("fragment");
|
||||||
expect(defaultEnabledBuiltinWorkflowIds().slice(0, 5)).toEqual([
|
expect(defaultEnabledBuiltinWorkflowIds().slice(0, 5)).toEqual([
|
||||||
"builtin:coding",
|
"builtin:coding",
|
||||||
"builtin:quick-fix",
|
"builtin:quick-fix",
|
||||||
@@ -295,5 +300,12 @@ describe("built-in workflows", () => {
|
|||||||
await store.selectTaskWorkflow(task.id, "builtin:compound-engineering");
|
await store.selectTaskWorkflow(task.id, "builtin:compound-engineering");
|
||||||
expect(store.getTaskWorkflowSelection(task.id)?.workflowId).toBe("builtin:compound-engineering");
|
expect(store.getTaskWorkflowSelection(task.id)?.workflowId).toBe("builtin:compound-engineering");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects selecting the PR lifecycle fragment for a task", async () => {
|
||||||
|
const task = await store.createTask({ description: "T", enabledWorkflowSteps: [] });
|
||||||
|
await expect(store.selectTaskWorkflow(task.id, "builtin:pr-workflow")).rejects.toThrow(
|
||||||
|
"is a fragment and cannot be selected for a task",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function isBuiltinWorkflowId(id: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function defaultEnabledBuiltinWorkflowIds(): string[] {
|
export function defaultEnabledBuiltinWorkflowIds(): string[] {
|
||||||
return BUILTIN_WORKFLOWS.map((workflow) => workflow.id);
|
return BUILTIN_WORKFLOWS.filter((workflow) => workflow.kind !== "fragment").map((workflow) => workflow.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBuiltinWorkflowEnabled(id: string, enabledIds?: readonly string[]): boolean {
|
export function isBuiltinWorkflowEnabled(id: string, enabledIds?: readonly string[]): boolean {
|
||||||
@@ -69,7 +69,8 @@ function linear(spec: BuiltinSpec): WorkflowDefinition {
|
|||||||
id: spec.id,
|
id: spec.id,
|
||||||
name: spec.name,
|
name: spec.name,
|
||||||
description: spec.description,
|
description: spec.description,
|
||||||
// Built-ins are always selectable workflows, never fragments (KTD-1).
|
// Linear built-ins remain selectable workflows; catalog entries authored
|
||||||
|
// directly below may opt into fragment kind when they are palette templates.
|
||||||
kind: "workflow",
|
kind: "workflow",
|
||||||
ir,
|
ir,
|
||||||
layout,
|
layout,
|
||||||
@@ -215,7 +216,7 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [
|
|||||||
name: "PR lifecycle (built-in)",
|
name: "PR lifecycle (built-in)",
|
||||||
description:
|
description:
|
||||||
"The unified PR lifecycle as graph nodes: create the PR, await review, respond to changes (bounded rework loop), gate on auto-merge, then merge — with GitHub reconciliation advancing the await holds. Requires the workflow graph executor.",
|
"The unified PR lifecycle as graph nodes: create the PR, await review, respond to changes (bounded rework loop), gate on auto-merge, then merge — with GitHub reconciliation advancing the await holds. Requires the workflow graph executor.",
|
||||||
kind: "workflow",
|
kind: "fragment",
|
||||||
ir: BUILTIN_PR_WORKFLOW_IR,
|
ir: BUILTIN_PR_WORKFLOW_IR,
|
||||||
layout: {
|
layout: {
|
||||||
start: { x: 60, y: 160 },
|
start: { x: 60, y: 160 },
|
||||||
|
|||||||
@@ -13299,7 +13299,7 @@ ${stepsSection}`;
|
|||||||
|
|
||||||
/** Read (and cache) the full merged workflow-definition set, oldest first.
|
/** Read (and cache) the full merged workflow-definition set, oldest first.
|
||||||
* Built-in templates lead the list and cannot be edited/deleted; built-ins
|
* Built-in templates lead the list and cannot be edited/deleted; built-ins
|
||||||
* are always kind "workflow". */
|
* may be selectable workflows or reusable fragments. */
|
||||||
private async readAllWorkflowDefinitions(): Promise<WorkflowDefinition[]> {
|
private async readAllWorkflowDefinitions(): Promise<WorkflowDefinition[]> {
|
||||||
if (this.workflowDefinitionsCache) return this.workflowDefinitionsCache;
|
if (this.workflowDefinitionsCache) return this.workflowDefinitionsCache;
|
||||||
const rows = this.db.prepare("SELECT * FROM workflows ORDER BY createdAt ASC").all() as Array<{
|
const rows = this.db.prepare("SELECT * FROM workflows ORDER BY createdAt ASC").all() as Array<{
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ function builtinDef(): WorkflowDefinition {
|
|||||||
function builtinPrDef(): WorkflowDefinition {
|
function builtinPrDef(): WorkflowDefinition {
|
||||||
return {
|
return {
|
||||||
id: "builtin:pr-workflow",
|
id: "builtin:pr-workflow",
|
||||||
kind: "workflow",
|
kind: "fragment",
|
||||||
name: "PR lifecycle (built-in)",
|
name: "PR lifecycle (built-in)",
|
||||||
description: "Ships with Fusion",
|
description: "Ships with Fusion",
|
||||||
ir: BUILTIN_PR_WORKFLOW_IR,
|
ir: BUILTIN_PR_WORKFLOW_IR,
|
||||||
@@ -271,6 +271,7 @@ describe("workflow-flow-mapping", () => {
|
|||||||
expect(BUILTIN_WORKFLOWS.map((workflow) => workflow.id).sort()).toEqual(
|
expect(BUILTIN_WORKFLOWS.map((workflow) => workflow.id).sort()).toEqual(
|
||||||
expect.arrayContaining(["builtin:coding", "builtin:stepwise-coding", "builtin:pr-workflow"]),
|
expect.arrayContaining(["builtin:coding", "builtin:stepwise-coding", "builtin:pr-workflow"]),
|
||||||
);
|
);
|
||||||
|
expect(BUILTIN_WORKFLOWS.find((workflow) => workflow.id === "builtin:pr-workflow")?.kind).toBe("fragment");
|
||||||
|
|
||||||
for (const workflow of BUILTIN_WORKFLOWS) {
|
for (const workflow of BUILTIN_WORKFLOWS) {
|
||||||
edgeRenderableAssertion(workflow);
|
edgeRenderableAssertion(workflow);
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ export function GeneralSection({
|
|||||||
fetchWorkflows(projectId, { includeDisabledBuiltins: true })
|
fetchWorkflows(projectId, { includeDisabledBuiltins: true })
|
||||||
.then((workflows) => {
|
.then((workflows) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setBuiltinWorkflows(workflows.filter((workflow) => workflow.id.startsWith("builtin:")));
|
setBuiltinWorkflows(
|
||||||
|
workflows.filter((workflow) => workflow.id.startsWith("builtin:") && workflow.kind !== "fragment"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user