From 1ddb6344a5dbddd6385f1d68d94314fa5ab72cb1 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Mon, 1 Dec 2025 11:31:32 +0200 Subject: [PATCH] fix: Detect workflow calling itself expression in migration report (#22516) --- .../wait-node-subworkflow.rule.test.ts | 35 +++++++++++++++++-- .../rules/v2/wait-node-subworkflow.rule.ts | 14 +++++--- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/modules/breaking-changes/rules/v2/__tests__/wait-node-subworkflow.rule.test.ts b/packages/cli/src/modules/breaking-changes/rules/v2/__tests__/wait-node-subworkflow.rule.test.ts index 7819bb8538f..e72cc604f45 100644 --- a/packages/cli/src/modules/breaking-changes/rules/v2/__tests__/wait-node-subworkflow.rule.test.ts +++ b/packages/cli/src/modules/breaking-changes/rules/v2/__tests__/wait-node-subworkflow.rule.test.ts @@ -1,13 +1,21 @@ +import type { INode } from 'n8n-workflow'; + import { createNode, createWorkflow } from '../../../__tests__/test-helpers'; import { BreakingChangeCategory } from '../../../types'; import { WaitNodeSubworkflowRule } from '../wait-node-subworkflow.rule'; +class TestRule extends WaitNodeSubworkflowRule { + testExtractCalledWorkflowId(node: INode, callerWorkflowId: string): string | undefined { + return super.extractCalledWorkflowId(node, callerWorkflowId); + } +} + describe('WaitNodeSubworkflowRule', () => { - let rule: WaitNodeSubworkflowRule; + let rule: TestRule; beforeEach(() => { jest.clearAllMocks(); - rule = new WaitNodeSubworkflowRule(); + rule = new TestRule(); rule.reset(); }); @@ -418,4 +426,27 @@ describe('WaitNodeSubworkflowRule', () => { expect(report2.affectedWorkflows).toHaveLength(0); }); }); + + describe('extractCalledWorkflowId()', () => { + it('should extract the called workflow ID', () => { + const node = createNode('ExecuteWorkflow', 'n8n-nodes-base.executeWorkflow', { + source: 'database', + workflowId: { value: 'sub-wf-1' }, + }); + + const calledWorkflowId = rule.testExtractCalledWorkflowId(node, 'caller-wf-1'); + expect(calledWorkflowId).toBe('sub-wf-1'); + }); + + it('should return the caller workflow ID if it is an expression', () => { + const node = createNode('ExecuteWorkflow', 'n8n-nodes-base.executeWorkflow', { + workflowId: '={{ $workflow.id }}', + mode: 'each', + options: {}, + }); + + const calledWorkflowId = rule.testExtractCalledWorkflowId(node, 'caller-wf-1'); + expect(calledWorkflowId).toBe('caller-wf-1'); + }); + }); }); diff --git a/packages/cli/src/modules/breaking-changes/rules/v2/wait-node-subworkflow.rule.ts b/packages/cli/src/modules/breaking-changes/rules/v2/wait-node-subworkflow.rule.ts index 5bee49c6066..3f664c06df2 100644 --- a/packages/cli/src/modules/breaking-changes/rules/v2/wait-node-subworkflow.rule.ts +++ b/packages/cli/src/modules/breaking-changes/rules/v2/wait-node-subworkflow.rule.ts @@ -123,7 +123,7 @@ export class WaitNodeSubworkflowRule implements IBreakingChangeBatchWorkflowRule continue; // Skip if not waiting for sub-workflow completion } - const calledWorkflowId = this.extractCalledWorkflowId(node); + const calledWorkflowId = this.extractCalledWorkflowId(node, workflow.id); this.parentWorkflowsCalling.push({ parentWorkflowId: workflow.id, @@ -199,7 +199,7 @@ export class WaitNodeSubworkflowRule implements IBreakingChangeBatchWorkflowRule return waitingNodes; } - private extractCalledWorkflowId(node: INode): string | undefined { + protected extractCalledWorkflowId(node: INode, callerWorkflowId: string): string | undefined { const source = node.parameters.source as string | undefined; // Only handle database source - for other sources we can't determine the workflow ID statically @@ -213,7 +213,8 @@ export class WaitNodeSubworkflowRule implements IBreakingChangeBatchWorkflowRule if (typeof workflowId === 'string') { // Check if it's an expression (starts with =) if (workflowId.startsWith('=')) { - return undefined; // Can't evaluate expressions statically + // Can't evaluate expressions statically + return this.isWorkflowItselfExpression(workflowId) ? callerWorkflowId : undefined; } return workflowId; } @@ -223,7 +224,8 @@ export class WaitNodeSubworkflowRule implements IBreakingChangeBatchWorkflowRule if (typeof value === 'string') { // Check if it's an expression (starts with =) if (value.startsWith('=')) { - return undefined; // Can't evaluate expressions statically + // Can't evaluate expressions statically + return this.isWorkflowItselfExpression(value) ? callerWorkflowId : undefined; } return value; } @@ -231,4 +233,8 @@ export class WaitNodeSubworkflowRule implements IBreakingChangeBatchWorkflowRule return undefined; } + + private isWorkflowItselfExpression(workflowIdExpression: string): boolean { + return workflowIdExpression.replace('{{ ', '{{').replace(' }}', '}}') === '={{$workflow.id}}'; + } }