From e50a364355c8e84e3d4b549f3900ec535fe7b21a Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Wed, 4 Nov 2026 13:23:41 -0500 Subject: [PATCH 1/2] fix(deploy): show CloudFormation resource failure after rollback --- src/core/project/backends/cdk/toolkit.test.ts | 44 ++++++++++++++++++ src/core/project/backends/cdk/toolkit.ts | 46 +++++++++++++++++-- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/core/project/backends/cdk/toolkit.test.ts b/src/core/project/backends/cdk/toolkit.test.ts index 747bdf833b..956dd86972 100644 --- a/src/core/project/backends/cdk/toolkit.test.ts +++ b/src/core/project/backends/cdk/toolkit.test.ts @@ -307,6 +307,50 @@ describe("Toolkit loading", () => { expect(lines).toEqual(["CREATE_COMPLETE | AWS::IAM::Role"]); }); + + test("reports a resource failure when Toolkit only throws NoStack after rollback", async () => { + const { loaded } = loadedToolkit(); + const reason = "Agent with agentId: staging-agent not found"; + const runner = createCdkRunner(createSilentLogger(), async (ioHost) => { + loaded.toolkit.deploy = async () => { + for (const event of [ + { + ResourceType: "AWS::IAM::Role", + LogicalResourceId: "RuntimeRole", + ResourceStatus: "CREATE_FAILED", + ResourceStatusReason: "Resource creation cancelled", + }, + { + ResourceType: "AWS::BedrockAgentCore::RuntimeEndpoint", + LogicalResourceId: "RuntimeEndpointProd", + ResourceStatus: "CREATE_FAILED", + ResourceStatusReason: reason, + }, + { + ResourceType: "AWS::CloudFormation::Stack", + LogicalResourceId: "AgentCore-orders-staging", + ResourceStatus: "CREATE_FAILED", + ResourceStatusReason: "The following resource(s) failed", + }, + ]) { + await ioHost.notify({ + ...message("rollback progress"), + code: "CDK_TOOLKIT_I5502", + data: { event }, + }); + } + throw new Error("NoStack: CloudFormationStack object does not hold a stack"); + }; + return loaded; + }); + + await expect( + runner({ kind: "deploy", stackArtifactId: "AgentCore-orders-staging" }, runOptions()), + ).rejects.toThrow( + `NoStack: CloudFormationStack object does not hold a stack\n\n` + + `CloudFormation failure: RuntimeEndpointProd (CREATE_FAILED): ${reason}`, + ); + }); }); describe("bootstrap template loading", () => { diff --git a/src/core/project/backends/cdk/toolkit.ts b/src/core/project/backends/cdk/toolkit.ts index db567459c8..214e7d893e 100644 --- a/src/core/project/backends/cdk/toolkit.ts +++ b/src/core/project/backends/cdk/toolkit.ts @@ -1,6 +1,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; +import type { StackEvent } from "@aws-sdk/client-cloudformation"; import type { IActionAwareIoHost, IIoHost, @@ -120,7 +121,13 @@ export async function loadBootstrapTemplate( }; } -export function createCdkIoHost(logger: Logger, onLine?: (line: string) => void): IIoHost { +type ResourceFailure = { logicalId: string; status: string; reason: string }; + +export function createCdkIoHost( + logger: Logger, + onLine?: (line: string) => void, + onResourceFailure?: (failure: ResourceFailure) => void, +): IIoHost { const toolkitLogger = logger.child({ component: "cdk-toolkit" }); const notify = async (message: IoMessage): Promise => { toolkitLogger @@ -130,6 +137,22 @@ export function createCdkIoHost(logger: Logger, onLine?: (line: string) => void) ...(message.code && { code: message.code }), }) .debug(message.message); + if (message.code === "CDK_TOOLKIT_I5502" && onResourceFailure) { + const event = (message.data as { event?: StackEvent } | undefined)?.event; + if ( + event?.ResourceStatus?.endsWith("_FAILED") && + event.ResourceType !== "AWS::CloudFormation::Stack" && + event.LogicalResourceId && + event.ResourceStatusReason && + !event.ResourceStatusReason.includes("cancelled") + ) { + onResourceFailure({ + logicalId: event.LogicalResourceId, + status: event.ResourceStatus, + reason: event.ResourceStatusReason, + }); + } + } // Toolkit messages can span lines (diffs, stack traces); split so the sink // always receives displayable single lines. if (onLine) { @@ -245,8 +268,25 @@ export function createCdkRunner( return async (operation, options) => { // A fresh ioHost per operation, so each operation's messages can flow into // its own onOutput sink rather than only the construction-time logger. - const ioHost = createCdkIoHost(logger, options.onOutput); + let firstFailure: ResourceFailure | undefined; + const ioHost = createCdkIoHost(logger, options.onOutput, (failure) => { + firstFailure ??= failure; + }); const loaded = await load(ioHost, options.region, options.credentials); - return performCdkOperation(loaded, operation, options); + try { + return await performCdkOperation(loaded, operation, options); + } catch (error) { + if ( + operation.kind === "deploy" && + error instanceof Error && + firstFailure && + !error.message.includes(firstFailure.reason) + ) { + error.message += + `\n\nCloudFormation failure: ${firstFailure.logicalId} ` + + `(${firstFailure.status}): ${firstFailure.reason}`; + } + throw error; + } }; } From bddb182955b263f07e4fd28f1cf6fa2ba8e9fc3c Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Thu, 5 Nov 2026 15:09:43 -0500 Subject: [PATCH 2/2] fix(deploy): report resource failures for NoStack errors --- src/core/project/backends/cdk/toolkit.test.ts | 70 ++++++++++++++++++- src/core/project/backends/cdk/toolkit.ts | 11 ++- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/core/project/backends/cdk/toolkit.test.ts b/src/core/project/backends/cdk/toolkit.test.ts index 956dd86972..3575b9a4a5 100644 --- a/src/core/project/backends/cdk/toolkit.test.ts +++ b/src/core/project/backends/cdk/toolkit.test.ts @@ -339,7 +339,9 @@ describe("Toolkit loading", () => { data: { event }, }); } - throw new Error("NoStack: CloudFormationStack object does not hold a stack"); + const error = new Error("CloudFormationStack object does not hold a stack"); + error.name = "NoStack"; + throw error; }; return loaded; }); @@ -347,10 +349,74 @@ describe("Toolkit loading", () => { await expect( runner({ kind: "deploy", stackArtifactId: "AgentCore-orders-staging" }, runOptions()), ).rejects.toThrow( - `NoStack: CloudFormationStack object does not hold a stack\n\n` + + `CloudFormationStack object does not hold a stack\n\n` + `CloudFormation failure: RuntimeEndpointProd (CREATE_FAILED): ${reason}`, ); }); + + test("reports a bootstrap resource failure when Toolkit throws NoStack", async () => { + const { loaded } = loadedToolkit(); + const reason = "Bootstrap bucket could not be created"; + const runner = createCdkRunner(createSilentLogger(), async (ioHost) => { + loaded.toolkit.bootstrap = async () => { + await ioHost.notify({ + ...message("bootstrap rollback progress"), + action: "bootstrap", + code: "CDK_TOOLKIT_I5502", + data: { + event: { + ResourceType: "AWS::S3::Bucket", + LogicalResourceId: "StagingBucket", + ResourceStatus: "CREATE_FAILED", + ResourceStatusReason: reason, + }, + }, + }); + const error = new Error("CloudFormationStack object does not hold a stack"); + error.name = "NoStack"; + throw error; + }; + return loaded; + }); + + await expect( + runner({ kind: "bootstrap", environments: ["aws://111122223333/us-east-1"] }, runOptions()), + ).rejects.toThrow( + `CloudFormationStack object does not hold a stack\n\n` + + `CloudFormation failure: StagingBucket (CREATE_FAILED): ${reason}`, + ); + }); + + test("does not repeat a resource failure already diagnosed by Toolkit", async () => { + const { loaded } = loadedToolkit(); + const reason = `The resource failed: ${"validation error ".repeat(12)}`; + const original = new Error( + `The stack failed to deploy:\n ${reason.slice(0, 120)}\n ${reason.slice(120)}`, + ); + const runner = createCdkRunner(createSilentLogger(), async (ioHost) => { + loaded.toolkit.deploy = async () => { + await ioHost.notify({ + ...message("update failed"), + code: "CDK_TOOLKIT_I5502", + data: { + event: { + ResourceType: "AWS::BedrockAgentCore::RuntimeEndpoint", + LogicalResourceId: "RuntimeEndpointProd", + ResourceStatus: "UPDATE_FAILED", + ResourceStatusReason: reason, + }, + }, + }); + throw original; + }; + return loaded; + }); + + await expect( + runner({ kind: "deploy", stackArtifactId: "AgentCore-orders-staging" }, runOptions()), + ).rejects.toBe(original); + expect(original.message).not.toContain("CloudFormation failure:"); + }); }); describe("bootstrap template loading", () => { diff --git a/src/core/project/backends/cdk/toolkit.ts b/src/core/project/backends/cdk/toolkit.ts index 214e7d893e..3168567d7e 100644 --- a/src/core/project/backends/cdk/toolkit.ts +++ b/src/core/project/backends/cdk/toolkit.ts @@ -137,6 +137,9 @@ export function createCdkIoHost( ...(message.code && { code: message.code }), }) .debug(message.message); + // I5502 carries each stack activity event. Toolkit 1.38.2 loses its resource + // diagnosis on first-create rollback when it reads a missing stack before + // formatting the collected failures; use these events for that NoStack case. if (message.code === "CDK_TOOLKIT_I5502" && onResourceFailure) { const event = (message.data as { event?: StackEvent } | undefined)?.event; if ( @@ -144,6 +147,7 @@ export function createCdkIoHost( event.ResourceType !== "AWS::CloudFormation::Stack" && event.LogicalResourceId && event.ResourceStatusReason && + // Match Toolkit's cancellation filter so the initiating failure wins. !event.ResourceStatusReason.includes("cancelled") ) { onResourceFailure({ @@ -276,12 +280,7 @@ export function createCdkRunner( try { return await performCdkOperation(loaded, operation, options); } catch (error) { - if ( - operation.kind === "deploy" && - error instanceof Error && - firstFailure && - !error.message.includes(firstFailure.reason) - ) { + if (error instanceof Error && error.name === "NoStack" && firstFailure) { error.message += `\n\nCloudFormation failure: ${firstFailure.logicalId} ` + `(${firstFailure.status}): ${firstFailure.reason}`;