diff --git a/packages/cli/src/public-api/types.ts b/packages/cli/src/public-api/types.ts index e3fee8a6c59..dd49efff012 100644 --- a/packages/cli/src/public-api/types.ts +++ b/packages/cli/src/public-api/types.ts @@ -148,7 +148,7 @@ export declare namespace WorkflowRequest { { id: string }, {}, WorkflowEntity & { parentFolderId?: string | null }, - {} + { publishIfActive?: boolean } >; type Activate = AuthenticatedRequest< { id: string }, diff --git a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.yml b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.yml index 1823e5f6b6e..9262bde1898 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.yml +++ b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.yml @@ -54,9 +54,22 @@ put: tags: - Workflow summary: Update a workflow - description: Update a workflow. If the workflow is published, the updated version will be automatically re-published. + description: >- + Update a workflow. If the workflow is published, the updated version will + be automatically re-published unless `publishIfActive` is set to `false`. parameters: - $ref: '../schemas/parameters/workflowId.yml' + - name: publishIfActive + in: query + required: false + description: >- + Whether to publish the update if the workflow is currently published. + Set to `false` to save the change as a draft on the existing published + version instead of releasing it. Has no effect on a workflow that + isn't currently published. + schema: + type: boolean + default: true requestBody: description: Updated workflow object. content: diff --git a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts index 47ab0290426..600f7679064 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts @@ -294,6 +294,10 @@ const workflowHandlers: WorkflowHandlers = { // null moves the workflow to the project root, (undefined) leaves the current folder untouched const resolvedParentFolderId = parentFolderId === null ? PROJECT_ROOT : parentFolderId; + // Defaults to true so existing integrations keep publishing on save; callers that want + // to stage a change on an already-published workflow can opt out explicitly. + const { publishIfActive = true } = req.query; + // binaryMode and credentialResolverId are derived, internal settings // rather than something users are expected to control programmatically; // strip them so the settings merge in WorkflowService.update preserves @@ -315,7 +319,7 @@ const workflowHandlers: WorkflowHandlers = { parentFolderId: resolvedParentFolderId, forceSave: true, // Skip version conflict check for public API publicApi: true, - publishIfActive: true, + publishIfActive, source: 'api', }, ); diff --git a/packages/cli/test/integration/public-api/workflows.test.ts b/packages/cli/test/integration/public-api/workflows.test.ts index 3501de5a925..d6789d3c577 100644 --- a/packages/cli/test/integration/public-api/workflows.test.ts +++ b/packages/cli/test/integration/public-api/workflows.test.ts @@ -2574,6 +2574,65 @@ describe('PUT /workflows/:id', () => { expect(versionInTheDb!.nodes).toEqual(updatedPayload.nodes); }); + test('should save as draft without republishing when publishIfActive=false', async () => { + const workflow = await createActiveWorkflow({}, member); + + const updatedPayload = { + name: 'Updated active workflow', + nodes: [ + { + id: 'uuid-updated', + parameters: { triggerTimes: { item: [{ mode: 'everyMinute' }] } }, + name: 'Updated Cron', + type: 'n8n-nodes-base.cron', + typeVersion: 1, + position: [300, 400], + }, + ], + connections: {}, + staticData: workflow.staticData, + settings: workflow.settings, + }; + + const updateResponse = await authMemberAgent + .put(`/workflows/${workflow.id}?publishIfActive=false`) + .send(updatedPayload); + + expect(updateResponse.statusCode).toBe(200); + expect(updateResponse.body.active).toBe(true); + expect(updateResponse.body.activeVersionId).toBe(workflow.versionId); + expect(updateResponse.body.nodes).toEqual(updatedPayload.nodes); + + const versionInTheDb = await Container.get(WorkflowHistoryRepository).findOne({ + where: { + workflowId: workflow.id, + versionId: Not(workflow.versionId), + }, + }); + + expect(versionInTheDb).not.toBeNull(); + expect(versionInTheDb!.nodes).toEqual(updatedPayload.nodes); + + const sharedWorkflow = await Container.get(SharedWorkflowRepository).findOne({ + where: { + projectId: memberPersonalProject.id, + workflowId: workflow.id, + }, + relations: ['workflow'], + }); + + // The published version itself is untouched: its history record still has the old content. + expect(sharedWorkflow?.workflow.activeVersionId).toBe(workflow.versionId); + const activeVersionInTheDb = await Container.get(WorkflowHistoryRepository).findOne({ + where: { + workflowId: workflow.id, + versionId: workflow.versionId, + }, + }); + expect(activeVersionInTheDb!.nodes).toEqual(workflow.nodes); + expect(activeVersionInTheDb!.nodes).not.toEqual(updatedPayload.nodes); + }); + test('should not allow updating active field', async () => { const workflow = await createWorkflowWithTriggerAndHistory({}, member);