From ffe97a82e7b7c6e3beb60d562ebf0abcda534c35 Mon Sep 17 00:00:00 2001 From: "n8n-cat-bot[bot]" <283985454+n8n-cat-bot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 07:02:52 +0000 Subject: [PATCH] chore: Extract the duplicated per-node failure message formatter in the workflow publication service (#37673) Co-authored-by: n8n-cat-bot[bot] Co-authored-by: Claude Opus 5 --- .../__tests__/format-node-failures.test.ts | 22 +++++++++++++++++++ .../publication/format-node-failures.ts | 4 ++++ .../publication-status-reporter.ts | 13 ++++++----- .../workflow-publication-applier.ts | 7 +++--- 4 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 packages/cli/src/workflows/publication/__tests__/format-node-failures.test.ts create mode 100644 packages/cli/src/workflows/publication/format-node-failures.ts diff --git a/packages/cli/src/workflows/publication/__tests__/format-node-failures.test.ts b/packages/cli/src/workflows/publication/__tests__/format-node-failures.test.ts new file mode 100644 index 00000000000..c82fb7ac37d --- /dev/null +++ b/packages/cli/src/workflows/publication/__tests__/format-node-failures.test.ts @@ -0,0 +1,22 @@ +import { formatNodeFailures } from '@/workflows/publication/format-node-failures'; + +describe('formatNodeFailures', () => { + test('returns an empty string for no failures', () => { + expect(formatNodeFailures([])).toBe(''); + }); + + test('quotes the node name and appends the message', () => { + expect(formatNodeFailures([{ nodeName: 'Webhook', message: 'port in use' }])).toBe( + '"Webhook": port in use', + ); + }); + + test('joins multiple failures with a semicolon', () => { + expect( + formatNodeFailures([ + { nodeName: 'Webhook', message: 'port in use' }, + { nodeName: 'Schedule Trigger', message: 'bad cron' }, + ]), + ).toBe('"Webhook": port in use; "Schedule Trigger": bad cron'); + }); +}); diff --git a/packages/cli/src/workflows/publication/format-node-failures.ts b/packages/cli/src/workflows/publication/format-node-failures.ts new file mode 100644 index 00000000000..0e63a1f8f9f --- /dev/null +++ b/packages/cli/src/workflows/publication/format-node-failures.ts @@ -0,0 +1,4 @@ +/** Formats per-node failures as `"NodeName": message`, joined with `; `. Callers add the prefix. */ +export function formatNodeFailures(failures: Array<{ nodeName: string; message: string }>): string { + return failures.map(({ nodeName, message }) => `"${nodeName}": ${message}`).join('; '); +} diff --git a/packages/cli/src/workflows/publication/publication-status-reporter.ts b/packages/cli/src/workflows/publication/publication-status-reporter.ts index 6352a6de4ed..f4f99fe294c 100644 --- a/packages/cli/src/workflows/publication/publication-status-reporter.ts +++ b/packages/cli/src/workflows/publication/publication-status-reporter.ts @@ -14,6 +14,7 @@ import { OperationalError } from 'n8n-workflow'; import { ActivationErrorsService } from '@/activation-errors.service'; import { isPolicyRefusal } from '@/policy/policy-violation.error'; import { Publisher } from '@/scaling/pubsub/publisher.service'; +import { formatNodeFailures } from '@/workflows/publication/format-node-failures'; import type { FailedTriggerPublicationStatus, PublicationResult, @@ -183,9 +184,9 @@ export class PublicationStatusReporter { /** Builds a human-readable message naming each failed node and its error. */ private formatActivationError(failures: FailedTriggerPublicationStatus[]): string { - const detail = failures - .map((status) => `"${status.nodeName}": ${status.errorMessage}`) - .join('; '); + const detail = formatNodeFailures( + failures.map(({ nodeName, errorMessage }) => ({ nodeName, message: errorMessage })), + ); return `Some triggers failed to activate: ${detail}`; } @@ -236,9 +237,9 @@ export class PublicationStatusReporter { ): string | undefined { if (!teardownFailures?.length) return undefined; - const detail = teardownFailures - .map((failure) => `"${failure.nodeName}": ${failure.error.message}`) - .join('; '); + const detail = formatNodeFailures( + teardownFailures.map(({ nodeName, error }) => ({ nodeName, message: error.message })), + ); const message = `External webhook deregistration failed for: ${detail}`; this.errorReporter.error( diff --git a/packages/cli/src/workflows/publication/workflow-publication-applier.ts b/packages/cli/src/workflows/publication/workflow-publication-applier.ts index f9dd5770102..dcaef852487 100644 --- a/packages/cli/src/workflows/publication/workflow-publication-applier.ts +++ b/packages/cli/src/workflows/publication/workflow-publication-applier.ts @@ -21,6 +21,7 @@ import { DurableJobProvisioner } from '@/scheduling/durable-job-provisioner'; import { WorkflowScheduledJobOwner } from '@/scheduling/workflow-scheduled-job-owner'; import { OwnershipService } from '@/services/ownership.service'; import { Telemetry } from '@/telemetry'; +import { formatNodeFailures } from '@/workflows/publication/format-node-failures'; import { healNodeIds } from '@/workflows/publication/heal-node-ids'; import type { PublicationResult, @@ -440,9 +441,9 @@ export class WorkflowPublicationApplier { private toActivationError(failures: TriggerActivationFailure[]): Error { if (failures.length === 1) return failures[0].error; - const detail = failures - .map((failure) => `"${failure.nodeName}": ${failure.error.message}`) - .join('; '); + const detail = formatNodeFailures( + failures.map(({ nodeName, error }) => ({ nodeName, message: error.message })), + ); return new Error(`Triggers failed to activate: ${detail}`); }