Skip to content

Commit 2886b32

Browse files
tomiclaude
andauthored
fix(core): Forward activationMode in multi-main webhook/trigger setup (#25855)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 69e7cbf commit 2886b32

File tree

8 files changed

+165
-42
lines changed

8 files changed

+165
-42
lines changed

.github/workflows/test-e2e-reusable.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ jobs:
138138
# docker-build: build app + docker image locally
139139
# docker-pull: no build needed, image is pre-built
140140
# local: build app for local server
141-
build-command: ${{ inputs.test-mode == 'docker-build' && 'pnpm build:docker' || (inputs.test-mode == 'local' && 'pnpm build' || '') }}
141+
build-command: ${{ inputs.test-mode == 'docker-build' && 'pnpm build:docker' || 'pnpm build' }}
142142
enable-docker-cache: ${{ inputs.test-mode == 'docker-build' }}
143143
env:
144144
INCLUDE_TEST_CONTROLLER: ${{ inputs.test-mode == 'docker-build' && 'true' || '' }}

packages/cli/src/active-workflow-manager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ export class ActiveWorkflowManager {
610610

611611
void this.publisher.publishCommand({
612612
command: 'add-webhooks-triggers-and-pollers',
613-
payload: { workflowId, activeVersionId: dbWorkflow.activeVersionId },
613+
payload: { workflowId, activeVersionId: dbWorkflow.activeVersionId, activationMode },
614614
});
615615

616616
return added;
@@ -743,9 +743,10 @@ export class ActiveWorkflowManager {
743743
async handleAddWebhooksTriggersAndPollers({
744744
workflowId,
745745
activeVersionId,
746+
activationMode,
746747
}: PubSubCommandMap['add-webhooks-triggers-and-pollers']) {
747748
try {
748-
await this.add(workflowId, 'activate', undefined, {
749+
await this.add(workflowId, activationMode, undefined, {
749750
shouldPublish: false, // prevent leader from re-publishing message
750751
});
751752

packages/cli/src/scaling/pubsub/__tests__/pubsub.registry.test.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,17 @@ describe('PubSubRegistry', () => {
131131
);
132132
pubSubRegistry.init();
133133

134-
pubsubEventBus.emit('add-webhooks-triggers-and-pollers', { workflowId, activeVersionId });
134+
pubsubEventBus.emit('add-webhooks-triggers-and-pollers', {
135+
workflowId,
136+
activeVersionId,
137+
activationMode: 'activate',
138+
});
135139
expect(onLeaderInstanceSpy).toHaveBeenCalledTimes(1);
136-
expect(onLeaderInstanceSpy).toHaveBeenCalledWith({ workflowId, activeVersionId });
140+
expect(onLeaderInstanceSpy).toHaveBeenCalledWith({
141+
workflowId,
142+
activeVersionId,
143+
activationMode: 'activate',
144+
});
137145

138146
pubsubEventBus.emit('restart-event-bus');
139147
expect(onFollowerInstanceSpy).not.toHaveBeenCalled();
@@ -153,7 +161,11 @@ describe('PubSubRegistry', () => {
153161
);
154162
followerPubSubRegistry.init();
155163

156-
pubsubEventBus.emit('add-webhooks-triggers-and-pollers', { workflowId, activeVersionId });
164+
pubsubEventBus.emit('add-webhooks-triggers-and-pollers', {
165+
workflowId,
166+
activeVersionId,
167+
activationMode: 'activate',
168+
});
157169
expect(onLeaderInstanceSpy).not.toHaveBeenCalled();
158170

159171
pubsubEventBus.emit('restart-event-bus');
@@ -177,9 +189,17 @@ describe('PubSubRegistry', () => {
177189
);
178190
pubSubRegistry.init();
179191

180-
pubsubEventBus.emit('add-webhooks-triggers-and-pollers', { workflowId, activeVersionId });
192+
pubsubEventBus.emit('add-webhooks-triggers-and-pollers', {
193+
workflowId,
194+
activeVersionId,
195+
activationMode: 'activate',
196+
});
181197
expect(onLeaderInstanceSpy).toHaveBeenCalledTimes(1);
182-
expect(onLeaderInstanceSpy).toHaveBeenCalledWith({ workflowId, activeVersionId });
198+
expect(onLeaderInstanceSpy).toHaveBeenCalledWith({
199+
workflowId,
200+
activeVersionId,
201+
activationMode: 'activate',
202+
});
183203
});
184204

185205
it('should handle dynamic role changes at runtime', () => {
@@ -197,19 +217,35 @@ describe('PubSubRegistry', () => {
197217
pubSubRegistry.init();
198218

199219
// Initially as follower, event should be ignored
200-
pubsubEventBus.emit('add-webhooks-triggers-and-pollers', { workflowId, activeVersionId });
220+
pubsubEventBus.emit('add-webhooks-triggers-and-pollers', {
221+
workflowId,
222+
activeVersionId,
223+
activationMode: 'activate',
224+
});
201225
expect(onLeaderInstanceSpy).not.toHaveBeenCalled();
202226

203227
// Change role to leader
204228
instanceSettings.instanceRole = 'leader';
205-
pubsubEventBus.emit('add-webhooks-triggers-and-pollers', { workflowId, activeVersionId });
229+
pubsubEventBus.emit('add-webhooks-triggers-and-pollers', {
230+
workflowId,
231+
activeVersionId,
232+
activationMode: 'activate',
233+
});
206234
expect(onLeaderInstanceSpy).toHaveBeenCalledTimes(1);
207-
expect(onLeaderInstanceSpy).toHaveBeenCalledWith({ workflowId, activeVersionId });
235+
expect(onLeaderInstanceSpy).toHaveBeenCalledWith({
236+
workflowId,
237+
activeVersionId,
238+
activationMode: 'activate',
239+
});
208240

209241
// Change back to follower
210242
onLeaderInstanceSpy.mockClear();
211243
instanceSettings.instanceRole = 'follower';
212-
pubsubEventBus.emit('add-webhooks-triggers-and-pollers', { workflowId, activeVersionId });
244+
pubsubEventBus.emit('add-webhooks-triggers-and-pollers', {
245+
workflowId,
246+
activeVersionId,
247+
activationMode: 'activate',
248+
});
213249
expect(onLeaderInstanceSpy).not.toHaveBeenCalled();
214250
});
215251

packages/cli/src/scaling/pubsub/pubsub.event-map.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ChatHubMessageStatus, PushMessage, WorkerStatus } from '@n8n/api-types';
2-
import type { IWorkflowBase } from 'n8n-workflow';
2+
import type { IWorkflowBase, WorkflowActivateMode } from 'n8n-workflow';
33

44
export type PubSubCommandMap = {
55
// #region Lifecycle
@@ -61,6 +61,7 @@ export type PubSubCommandMap = {
6161
'add-webhooks-triggers-and-pollers': {
6262
workflowId: string;
6363
activeVersionId: string;
64+
activationMode: WorkflowActivateMode;
6465
};
6566

6667
'remove-triggers-and-pollers': {

packages/testing/playwright/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@n8n/api-types": "workspace:*",
3737
"@n8n/playwright-janitor": "workspace:*",
3838
"@n8n/constants": "workspace:*",
39+
"@n8n/workflow-sdk": "workspace:*",
3940
"@n8n/permissions": "workspace:*",
4041
"@n8n/db": "workspace:*",
4142
"@playwright/cli": "catalog:e2e",

packages/testing/playwright/services/workflow-api-helper.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,26 @@ export class WorkflowApiHelper {
8282
}
8383
}
8484

85+
async update(
86+
workflowId: string,
87+
versionId: string,
88+
data: Partial<IWorkflowBase>,
89+
): Promise<IWorkflowBase> {
90+
const response = await this.api.request.patch(`/rest/workflows/${workflowId}`, {
91+
data: {
92+
...data,
93+
versionId,
94+
},
95+
});
96+
97+
if (!response.ok()) {
98+
throw new TestError(`Failed to update workflow: ${await response.text()}`);
99+
}
100+
101+
const result = await response.json();
102+
return result.data ?? result;
103+
}
104+
85105
async deactivate(workflowId: string) {
86106
const response = await this.api.request.post(`/rest/workflows/${workflowId}/deactivate`);
87107

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { workflow, trigger, node } from '@n8n/workflow-sdk';
2+
import type { INode, IWorkflowBase } from 'n8n-workflow';
3+
import { nanoid } from 'nanoid';
4+
5+
import { test, expect } from '../../../fixtures/base';
6+
7+
type TriggerEventType = 'activate' | 'update';
8+
9+
const makeN8nTriggerWorkflow = (events: TriggerEventType[]) => {
10+
const n8nTrigger = trigger({
11+
type: 'n8n-nodes-base.n8nTrigger',
12+
version: 1,
13+
config: {
14+
name: 'n8n Trigger',
15+
parameters: { events },
16+
},
17+
});
18+
19+
const noOp = node({
20+
type: 'n8n-nodes-base.noOp',
21+
version: 1,
22+
config: {
23+
name: 'NoOp',
24+
},
25+
});
26+
27+
return workflow(nanoid(), `n8n Trigger Test ${nanoid()}`).add(n8nTrigger.to(noOp));
28+
};
29+
30+
test.describe(
31+
'n8n Trigger node',
32+
{
33+
annotation: [{ type: 'owner', description: 'Catalysts' }],
34+
},
35+
() => {
36+
test('should fire "activate" event when workflow is published', async ({ api }) => {
37+
const wf = makeN8nTriggerWorkflow(['activate']);
38+
const { workflowId, createdWorkflow } = await api.workflows.createWorkflowFromDefinition(
39+
wf.toJSON() as IWorkflowBase,
40+
);
41+
42+
// First activation — activationMode = 'activate'
43+
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
44+
45+
const execution = await api.workflows.waitForExecution(workflowId, 15_000, 'trigger');
46+
expect(execution.status).toBe('success');
47+
});
48+
49+
test('should fire "update" event when active workflow is re-published', async ({ api }) => {
50+
const wf = makeN8nTriggerWorkflow(['update']);
51+
const { workflowId, createdWorkflow } = await api.workflows.createWorkflowFromDefinition(
52+
wf.toJSON() as IWorkflowBase,
53+
);
54+
55+
// First activation — activationMode = 'activate', trigger should NOT fire
56+
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
57+
58+
// Update the workflow nodes to create a new version (simulates editing)
59+
const updatedNodes = wf.add(
60+
node({ type: 'n8n-nodes-base.noOp', version: 1, config: { name: 'NoOp2' } }),
61+
);
62+
const updatedWorkflow = await api.workflows.update(workflowId, createdWorkflow.versionId!, {
63+
nodes: updatedNodes.toJSON().nodes as INode[],
64+
});
65+
66+
// Re-activation with new version — activationMode = 'update', trigger should fire
67+
await api.workflows.activate(workflowId, updatedWorkflow.versionId!);
68+
69+
const execution = await api.workflows.waitForExecution(workflowId, 15_000, 'trigger');
70+
expect(execution.status).toBe('success');
71+
});
72+
},
73+
);

0 commit comments

Comments
 (0)