diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/toolkit-error.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/toolkit-error.ts index 8dfa5ac84..aed8ec4f7 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/toolkit-error.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/toolkit-error.ts @@ -1,7 +1,9 @@ -const TOOLKIT_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit.ToolkitError'); -const AUTHENTICATION_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit.AuthenticationError'); -const ASSEMBLY_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit.AssemblyError'); -const CONTEXT_PROVIDER_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit.ContextProviderError'); +import type * as cxapi from '@aws-cdk/cx-api'; + +const TOOLKIT_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.ToolkitError'); +const AUTHENTICATION_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.AuthenticationError'); +const ASSEMBLY_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.AssemblyError'); +const CONTEXT_PROVIDER_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.ContextProviderError'); /** * Represents a general toolkit error in the AWS CDK Toolkit. @@ -40,12 +42,18 @@ export class ToolkitError extends Error { */ public readonly type: string; + /** + * Denotes the source of the error as the toolkit. + */ + public readonly source: 'toolkit' | 'user'; + constructor(message: string, type: string = 'toolkit') { super(message); Object.setPrototypeOf(this, ToolkitError.prototype); Object.defineProperty(this, TOOLKIT_ERROR_SYMBOL, { value: true }); this.name = new.target.name; this.type = type; + this.source = 'toolkit'; } } @@ -53,6 +61,11 @@ export class ToolkitError extends Error { * Represents an authentication-specific error in the AWS CDK Toolkit. */ export class AuthenticationError extends ToolkitError { + /** + * Denotes the source of the error as user. + */ + public readonly source = 'user'; + constructor(message: string) { super(message, 'authentication'); Object.setPrototypeOf(this, AuthenticationError.prototype); @@ -61,13 +74,49 @@ export class AuthenticationError extends ToolkitError { } /** - * Represents an authentication-specific error in the AWS CDK Toolkit. + * Represents an error causes by cloud assembly synthesis + * + * This includes errors thrown during app execution, as well as failing annotations. */ export class AssemblyError extends ToolkitError { - constructor(message: string) { + /** + * An AssemblyError with an original error as cause + */ + public static withCause(message: string, error: unknown): AssemblyError { + return new AssemblyError(message, undefined, error); + } + + /** + * An AssemblyError with a list of stacks as cause + */ + public static withStacks(message: string, stacks?: cxapi.CloudFormationStackArtifact[]): AssemblyError { + return new AssemblyError(message, stacks); + } + + /** + * Denotes the source of the error as user. + */ + public readonly source = 'user'; + + /** + * The stacks that caused the error, if available + * + * The `messages` property of each `cxapi.CloudFormationStackArtifact` will contain the respective errors. + * Absence indicates synthesis didn't fully complete. + */ + public readonly stacks?: cxapi.CloudFormationStackArtifact[]; + + /** + * The specific original cause of the error, if available + */ + public readonly cause?: unknown; + + private constructor(message: string, stacks?: cxapi.CloudFormationStackArtifact[], cause?: unknown) { super(message, 'assembly'); Object.setPrototypeOf(this, AssemblyError.prototype); Object.defineProperty(this, ASSEMBLY_ERROR_SYMBOL, { value: true }); + this.stacks = stacks; + this.cause = cause; } } @@ -75,6 +124,11 @@ export class AssemblyError extends ToolkitError { * Represents an error originating from a Context Provider */ export class ContextProviderError extends ToolkitError { + /** + * Denotes the source of the error as user. + */ + public readonly source = 'user'; + constructor(message: string) { super(message, 'context-provider'); Object.setPrototypeOf(this, ContextProviderError.prototype); diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/test/api/toolkit-error.test.ts b/packages/@aws-cdk/tmp-toolkit-helpers/test/api/toolkit-error.test.ts index 6de217309..2e984eec6 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/test/api/toolkit-error.test.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/test/api/toolkit-error.test.ts @@ -3,35 +3,58 @@ import { AssemblyError, AuthenticationError, ContextProviderError, ToolkitError describe('toolkit error', () => { let toolkitError = new ToolkitError('Test toolkit error'); let authError = new AuthenticationError('Test authentication error'); - let assemblyError = new AssemblyError('Test authentication error'); let contextProviderError = new ContextProviderError('Test context provider error'); + let assemblyError = AssemblyError.withStacks('Test authentication error', []); + let assemblyCauseError = AssemblyError.withCause('Test authentication error', new Error('other error')); test('types are correctly assigned', async () => { expect(toolkitError.type).toBe('toolkit'); expect(authError.type).toBe('authentication'); expect(assemblyError.type).toBe('assembly'); + expect(assemblyCauseError.type).toBe('assembly'); expect(contextProviderError.type).toBe('context-provider'); }); test('isToolkitError works', () => { + expect(toolkitError.source).toBe('toolkit'); + expect(ToolkitError.isToolkitError(toolkitError)).toBe(true); expect(ToolkitError.isToolkitError(authError)).toBe(true); expect(ToolkitError.isToolkitError(assemblyError)).toBe(true); + expect(ToolkitError.isToolkitError(assemblyCauseError)).toBe(true); expect(ToolkitError.isToolkitError(contextProviderError)).toBe(true); }); test('isAuthenticationError works', () => { + expect(authError.source).toBe('user'); + expect(ToolkitError.isAuthenticationError(toolkitError)).toBe(false); expect(ToolkitError.isAuthenticationError(authError)).toBe(true); }); - test('isAssemblyError works', () => { - expect(ToolkitError.isAssemblyError(assemblyError)).toBe(true); - expect(ToolkitError.isAssemblyError(toolkitError)).toBe(false); - expect(ToolkitError.isAssemblyError(authError)).toBe(false); + describe('isAssemblyError works', () => { + test('AssemblyError.fromStacks', () => { + expect(assemblyError.source).toBe('user'); + expect(assemblyError.stacks).toStrictEqual([]); + + expect(ToolkitError.isAssemblyError(assemblyError)).toBe(true); + expect(ToolkitError.isAssemblyError(toolkitError)).toBe(false); + expect(ToolkitError.isAssemblyError(authError)).toBe(false); + }); + + test('AssemblyError.fromCause', () => { + expect(assemblyCauseError.source).toBe('user'); + expect((assemblyCauseError.cause as any)?.message).toBe('other error'); + + expect(ToolkitError.isAssemblyError(assemblyCauseError)).toBe(true); + expect(ToolkitError.isAssemblyError(toolkitError)).toBe(false); + expect(ToolkitError.isAssemblyError(authError)).toBe(false); + }); }); test('isContextProviderError works', () => { + expect(contextProviderError.source).toBe('user'); + expect(ToolkitError.isContextProviderError(contextProviderError)).toBe(true); expect(ToolkitError.isContextProviderError(toolkitError)).toBe(false); expect(ToolkitError.isContextProviderError(authError)).toBe(false); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts index 574e93af0..41dd48702 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts @@ -7,7 +7,7 @@ import { assemblyFromDirectory, changeDir, determineOutputDirectory, guessExecut import { ToolkitServices } from '../../../toolkit/private'; import { Context, ILock, RWLock, Settings } from '../../aws-cdk'; import { CODES } from '../../io/private'; -import { ToolkitError } from '../../shared-public'; +import { ToolkitError, AssemblyError } from '../../shared-public'; import { AssemblyBuilder } from '../source-builder'; export abstract class CloudAssemblySourceBuilder { @@ -42,10 +42,21 @@ export abstract class CloudAssemblySourceBuilder { const env = await prepareDefaultEnvironment(services, { outdir }); const assembly = await changeDir(async () => withContext(context.all, env, props.synthOptions ?? {}, async (envWithContext, ctx) => - withEnv(envWithContext, () => builder({ - outdir, - context: ctx, - })), + withEnv(envWithContext, () => { + try { + return builder({ + outdir, + context: ctx, + }); + } catch (error: unknown) { + // re-throw toolkit errors unchanged + if (ToolkitError.isToolkitError(error)) { + throw error; + } + // otherwise, wrap into an assembly error + throw AssemblyError.withCause('Assembly builder failed', error); + } + }), ), props.workingDirectory); if (cxapi.CloudAssembly.isCloudAssembly(assembly)) { diff --git a/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/source-builder.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/source-builder.test.ts index 8faaf301c..3790361a6 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/source-builder.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/source-builder.test.ts @@ -1,6 +1,10 @@ +import { ToolkitError } from '../../../lib'; import { Toolkit } from '../../../lib/toolkit'; import { appFixture, builderFixture, cdkOutFixture, TestIoHost } from '../../_helpers'; +// these tests often run a bit longer than the default +jest.setTimeout(10_000); + const ioHost = new TestIoHost(); const toolkit = new Toolkit({ ioHost }); @@ -30,6 +34,22 @@ describe('fromAssemblyBuilder', () => { // THEN expect(JSON.stringify(stack)).toContain('amzn-s3-demo-bucket'); }); + + test('errors are wrapped as AssemblyError', async () => { + // GIVEN + const cx = await toolkit.fromAssemblyBuilder(() => { + throw new Error('a wild error appeared'); + }); + + // WHEN + try { + await cx.produce(); + } catch (err: any) { + // THEN + expect(ToolkitError.isAssemblyError(err)).toBe(true); + expect(err.cause?.message).toContain('a wild error appeared'); + } + }); }); describe('fromCdkApp', () => { diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index 23e594790..c2c8aae65 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -336,11 +336,11 @@ export class StackCollection { } if (errors && failAt != 'none') { - throw new AssemblyError('Found errors'); + throw AssemblyError.withStacks('Found errors', this.stackArtifacts); } if (warnings && failAt === 'warn') { - throw new AssemblyError('Found warnings (--strict mode)'); + throw AssemblyError.withStacks('Found warnings (--strict mode)', this.stackArtifacts); } } }