diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 3007f92a2..5ea9cf232 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -757,13 +757,23 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab default: return 'CDK_ASSEMBLY_I9999'; } }; - await stacks.validateMetadata(this.props.assemblyFailureAt, async (level, msg) => ioHost.notify({ - time: new Date(), - level, - code: code(level), - message: `[${level} at ${msg.id}] ${msg.entry.data}`, - data: msg, - })); + + await stacks.validateMetadata(this.props.assemblyFailureAt, async (level, msg) => { + // Data comes from Annotations and the data can be of object type containing 'Fn::Join' or 'Ref' when tokens are included in Annotations. + // Therefore, we use JSON.stringify to convert it to a string when the data is of object type. + // see: https://github.com/aws/aws-cdk/issues/33527 + const data = msg.entry.data !== null && typeof msg.entry.data === 'object' + ? JSON.stringify(msg.entry.data) + : msg.entry.data; + + await ioHost.notify({ + time: new Date(), + level, + code: code(level), + message: `[${level} at ${msg.id}] ${data}`, + data: msg, + }); + }); } /** diff --git a/packages/@aws-cdk/toolkit-lib/test/toolkit/toolkit.test.ts b/packages/@aws-cdk/toolkit-lib/test/toolkit/toolkit.test.ts index 156b428b8..42154487a 100644 --- a/packages/@aws-cdk/toolkit-lib/test/toolkit/toolkit.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/toolkit/toolkit.test.ts @@ -7,7 +7,7 @@ import * as chalk from 'chalk'; import { Toolkit } from '../../lib'; -import { TestIoHost } from '../_helpers'; +import { TestCloudAssemblySource, TestIoHost } from '../_helpers'; describe('message formatting', () => { test('emojis can be stripped from message', async () => { @@ -70,3 +70,79 @@ describe('message formatting', () => { })); }); }); + +describe('metadata message formatting', () => { + test('converts object data for log message to string', async () => { + const ioHost = new TestIoHost(); + const toolkit = new Toolkit({ ioHost }); + + const source = new TestCloudAssemblySource({ + stacks: [{ + stackName: 'test-stack', + metadata: { + 'test-stack': [{ + type: 'aws:cdk:warning', + data: { + 'Fn::Join': [ + '', + [ + 'stackId: ', + { + Ref: 'AWS::StackId', + }, + ], + ], + } as any, + }], + }, + }], + }); + + await toolkit.synth(source); + + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + level: 'warn', + message: expect.stringContaining('{"Fn::Join":["",["stackId: ",{"Ref":"AWS::StackId"}]]}'), + data: { + entry: { + type: 'aws:cdk:warning', + data: { 'Fn::Join': ['', ['stackId: ', { Ref: 'AWS::StackId' }]] }, + }, + id: 'test-stack', + level: 'warning', + }, + })); + }); + + test('keeps non-object data for log message as-is', async () => { + const ioHost = new TestIoHost(); + const toolkit = new Toolkit({ ioHost }); + + const source = new TestCloudAssemblySource({ + stacks: [{ + stackName: 'test-stack', + metadata: { + 'test-stack': [{ + type: 'aws:cdk:info', + data: 'simple string message', + }], + }, + }], + }); + + await toolkit.synth(source); + + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + level: 'info', + message: expect.stringContaining('simple string message'), + data: { + entry: { + type: 'aws:cdk:info', + data: 'simple string message', + }, + id: 'test-stack', + level: 'info', + }, + })); + }); +});