From 7643c1f62b9f359e7f73f379f6c4e85148741445 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Thu, 11 Dec 2025 02:49:33 +0900 Subject: [PATCH 1/2] fix(cli): deploy and destroy with --all option fails on apps with no top-level stacks fix --- packages/aws-cdk/lib/cxapp/cloud-assembly.ts | 3 + packages/aws-cdk/test/cli/cdk-toolkit.test.ts | 56 ++++++++++++++++--- .../aws-cdk/test/cxapp/cloud-assembly.test.ts | 38 +++++++++++++ 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/packages/aws-cdk/lib/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/cxapp/cloud-assembly.ts index 824cd6aaf..5a5feb38e 100644 --- a/packages/aws-cdk/lib/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/cxapp/cloud-assembly.ts @@ -119,6 +119,9 @@ export class CloudAssembly extends BaseStackAssembly { ): Promise { if (topLevelStacks.length > 0) { return this.extendStacks(topLevelStacks, stacks, extend); + } else if (stacks.length > 0) { + // Fallback to all stacks if no top-level stacks (e.g., app including stages only in top-level assembly) + return this.extendStacks(stacks, stacks, extend); } else { throw new ToolkitError('No stack found in the main cloud assembly. Use "list" to print manifest'); } diff --git a/packages/aws-cdk/test/cli/cdk-toolkit.test.ts b/packages/aws-cdk/test/cli/cdk-toolkit.test.ts index 45c611918..459e4ca63 100644 --- a/packages/aws-cdk/test/cli/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cli/cdk-toolkit.test.ts @@ -150,6 +150,26 @@ function defaultToolkitSetup() { }); } +// only stacks within stages (no top-level stacks) +async function stageOnlyToolkitSetup() { + const stageOnlyExecutable = await MockCloudExecutable.create({ + stacks: [], + nestedAssemblies: [{ + stacks: [MockStack.MOCK_STACK_C], + }], + }); + + return new CdkToolkit({ + ioHost, + cloudExecutable: stageOnlyExecutable, + configuration: stageOnlyExecutable.configuration, + sdkProvider: stageOnlyExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-C': { Baz: 'Zinga!' }, + }), + }); +} + const mockSdk = new MockSdk(); describe('bootstrap', () => { @@ -284,6 +304,17 @@ describe('deploy', () => { }); }); + test('deploy all stacks in stage-only configuration with --all option', async () => { + // GIVEN + const toolkit = await stageOnlyToolkitSetup(); + + // WHEN & THEN + await expect(toolkit.deploy({ + selector: { patterns: [], allTopLevel: true }, + deploymentMethod: { method: 'change-set' }, + })).resolves.not.toThrow(); + }); + test('uses display names to reference assets', async () => { // GIVEN cloudExecutable = await MockCloudExecutable.create({ @@ -1080,14 +1111,23 @@ describe('destroy', () => { test('destroy correct stack', async () => { const toolkit = defaultToolkitSetup(); - expect(() => { - return toolkit.destroy({ - selector: { patterns: ['Test-Stack-A/Test-Stack-C'] }, - exclusively: true, - force: true, - fromDeploy: true, - }); - }).resolves; + await expect(toolkit.destroy({ + selector: { patterns: ['Test-Stack-A/Test-Stack-C'] }, + exclusively: true, + force: true, + fromDeploy: true, + })).resolves.not.toThrow(); + }); + + test('destroy all stacks in stage-only configuration with --all option', async () => { + const toolkit = await stageOnlyToolkitSetup(); + + await expect(toolkit.destroy({ + selector: { patterns: [], allTopLevel: true }, + exclusively: true, + force: true, + fromDeploy: true, + })).resolves.not.toThrow(); }); }); diff --git a/packages/aws-cdk/test/cxapp/cloud-assembly.test.ts b/packages/aws-cdk/test/cxapp/cloud-assembly.test.ts index 60cb8da5f..5c3537dd1 100644 --- a/packages/aws-cdk/test/cxapp/cloud-assembly.test.ts +++ b/packages/aws-cdk/test/cxapp/cloud-assembly.test.ts @@ -17,6 +17,19 @@ test('select all top level stacks in the presence of nested assemblies', async ( expect(x.stackIds).toContain('withouterrors'); }); +test('select all stacks when only nested assemblies exist (Stage-only apps)', async () => { + // GIVEN + const cxasm = await testStageOnlyCloudAssembly(); + + // WHEN + const x = await cxasm.selectStacks({ allTopLevel: true, patterns: [] }, { defaultBehavior: DefaultSelection.AllStacks }); + + // THEN + expect(x.stackCount).toBe(2); + expect(x.stackIds).toContain('stack1'); + expect(x.stackIds).toContain('stack2'); +}); + test('select stacks by glob pattern', async () => { // GIVEN const cxasm = await testCloudAssembly(); @@ -337,3 +350,28 @@ async function testNestedCloudAssembly({ env }: { env?: string; versionReporting const asm = await cloudExec.synthesize(); return cliAssemblyWithForcedVersion(asm, '30.0.0'); } + +async function testStageOnlyCloudAssembly({ env }: { env?: string; versionReporting?: boolean } = {}) { + const cloudExec = await MockCloudExecutable.create({ + stacks: [], // No top-level stacks + nestedAssemblies: [{ + stacks: [{ + stackName: 'stack1', + displayName: 'stage1/stack1', + env, + template: { resource: 'resource1' }, + }], + }, + { + stacks: [{ + stackName: 'stack2', + displayName: 'stage2/stack2', + env, + template: { resource: 'resource2' }, + }], + }], + }); + + const asm = await cloudExec.synthesize(); + return cliAssemblyWithForcedVersion(asm, '30.0.0'); +} From 9550003818655c1969837969461711683b33aafa Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Thu, 11 Dec 2025 03:25:02 +0900 Subject: [PATCH 2/2] integ --- .../cli-integ/resources/cdk-apps/app/app.js | 4 +++ ...oy-stage-only-with-all-option.integtest.ts | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/destroy/cdk-destroy-stage-only-with-all-option.integtest.ts diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js index 8735e727c..8e4ef8fd6 100755 --- a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js @@ -993,6 +993,10 @@ switch (stackSet) { case 'stage-with-no-stacks': break; + case 'stage-only': + new SomeStage(app, `${stackPrefix}-stage`); + break; + default: throw new Error(`Unrecognized INTEG_STACK_SET: '${stackSet}'`); } diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/destroy/cdk-destroy-stage-only-with-all-option.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/destroy/cdk-destroy-stage-only-with-all-option.integtest.ts new file mode 100644 index 000000000..47568533a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/destroy/cdk-destroy-stage-only-with-all-option.integtest.ts @@ -0,0 +1,27 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../../lib'; + +integTest('cdk destroy can destroy stacks in stage-only configuration with --all option', withDefaultFixture(async (fixture) => { + const integStackSet = 'stage-only'; + + await fixture.cdkDeploy([], { + options: ['--all'], + modEnv: { + INTEG_STACK_SET: integStackSet, + }, + }); + + const stackName = `${fixture.fullStackName('stage')}-StackInStage`; + const stack = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: stackName })); + expect(stack.Stacks?.length ?? 0).toEqual(1); + + await fixture.cdkDestroy([], { + options: ['--all'], + modEnv: { + INTEG_STACK_SET: integStackSet, + }, + }); + + await expect(fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: stackName }))) + .rejects.toThrow(/does not exist/); +}));