chore: Extract the duplicated per-node failure message formatter in the workflow publication service (#37673)

Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
n8n-cat-bot[bot] 2026-09-03 07:02:52 +00:00 committed by GitHub
parent 11289ced77
commit ffe97a82e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 37 additions and 9 deletions

View File

@ -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');
});
});

View File

@ -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('; ');
}

View File

@ -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(

View File

@ -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}`);
}