Skip to content

Commit 7643c1f

Browse files
committed
fix(cli): deploy and destroy with --all option fails on apps with no top-level stacks
fix
1 parent 2a6f8d3 commit 7643c1f

File tree

3 files changed

+89
-8
lines changed

3 files changed

+89
-8
lines changed

packages/aws-cdk/lib/cxapp/cloud-assembly.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ export class CloudAssembly extends BaseStackAssembly {
119119
): Promise<StackCollection> {
120120
if (topLevelStacks.length > 0) {
121121
return this.extendStacks(topLevelStacks, stacks, extend);
122+
} else if (stacks.length > 0) {
123+
// Fallback to all stacks if no top-level stacks (e.g., app including stages only in top-level assembly)
124+
return this.extendStacks(stacks, stacks, extend);
122125
} else {
123126
throw new ToolkitError('No stack found in the main cloud assembly. Use "list" to print manifest');
124127
}

packages/aws-cdk/test/cli/cdk-toolkit.test.ts

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,26 @@ function defaultToolkitSetup() {
150150
});
151151
}
152152

153+
// only stacks within stages (no top-level stacks)
154+
async function stageOnlyToolkitSetup() {
155+
const stageOnlyExecutable = await MockCloudExecutable.create({
156+
stacks: [],
157+
nestedAssemblies: [{
158+
stacks: [MockStack.MOCK_STACK_C],
159+
}],
160+
});
161+
162+
return new CdkToolkit({
163+
ioHost,
164+
cloudExecutable: stageOnlyExecutable,
165+
configuration: stageOnlyExecutable.configuration,
166+
sdkProvider: stageOnlyExecutable.sdkProvider,
167+
deployments: new FakeCloudFormation({
168+
'Test-Stack-C': { Baz: 'Zinga!' },
169+
}),
170+
});
171+
}
172+
153173
const mockSdk = new MockSdk();
154174

155175
describe('bootstrap', () => {
@@ -284,6 +304,17 @@ describe('deploy', () => {
284304
});
285305
});
286306

307+
test('deploy all stacks in stage-only configuration with --all option', async () => {
308+
// GIVEN
309+
const toolkit = await stageOnlyToolkitSetup();
310+
311+
// WHEN & THEN
312+
await expect(toolkit.deploy({
313+
selector: { patterns: [], allTopLevel: true },
314+
deploymentMethod: { method: 'change-set' },
315+
})).resolves.not.toThrow();
316+
});
317+
287318
test('uses display names to reference assets', async () => {
288319
// GIVEN
289320
cloudExecutable = await MockCloudExecutable.create({
@@ -1080,14 +1111,23 @@ describe('destroy', () => {
10801111
test('destroy correct stack', async () => {
10811112
const toolkit = defaultToolkitSetup();
10821113

1083-
expect(() => {
1084-
return toolkit.destroy({
1085-
selector: { patterns: ['Test-Stack-A/Test-Stack-C'] },
1086-
exclusively: true,
1087-
force: true,
1088-
fromDeploy: true,
1089-
});
1090-
}).resolves;
1114+
await expect(toolkit.destroy({
1115+
selector: { patterns: ['Test-Stack-A/Test-Stack-C'] },
1116+
exclusively: true,
1117+
force: true,
1118+
fromDeploy: true,
1119+
})).resolves.not.toThrow();
1120+
});
1121+
1122+
test('destroy all stacks in stage-only configuration with --all option', async () => {
1123+
const toolkit = await stageOnlyToolkitSetup();
1124+
1125+
await expect(toolkit.destroy({
1126+
selector: { patterns: [], allTopLevel: true },
1127+
exclusively: true,
1128+
force: true,
1129+
fromDeploy: true,
1130+
})).resolves.not.toThrow();
10911131
});
10921132
});
10931133

packages/aws-cdk/test/cxapp/cloud-assembly.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ test('select all top level stacks in the presence of nested assemblies', async (
1717
expect(x.stackIds).toContain('withouterrors');
1818
});
1919

20+
test('select all stacks when only nested assemblies exist (Stage-only apps)', async () => {
21+
// GIVEN
22+
const cxasm = await testStageOnlyCloudAssembly();
23+
24+
// WHEN
25+
const x = await cxasm.selectStacks({ allTopLevel: true, patterns: [] }, { defaultBehavior: DefaultSelection.AllStacks });
26+
27+
// THEN
28+
expect(x.stackCount).toBe(2);
29+
expect(x.stackIds).toContain('stack1');
30+
expect(x.stackIds).toContain('stack2');
31+
});
32+
2033
test('select stacks by glob pattern', async () => {
2134
// GIVEN
2235
const cxasm = await testCloudAssembly();
@@ -337,3 +350,28 @@ async function testNestedCloudAssembly({ env }: { env?: string; versionReporting
337350
const asm = await cloudExec.synthesize();
338351
return cliAssemblyWithForcedVersion(asm, '30.0.0');
339352
}
353+
354+
async function testStageOnlyCloudAssembly({ env }: { env?: string; versionReporting?: boolean } = {}) {
355+
const cloudExec = await MockCloudExecutable.create({
356+
stacks: [], // No top-level stacks
357+
nestedAssemblies: [{
358+
stacks: [{
359+
stackName: 'stack1',
360+
displayName: 'stage1/stack1',
361+
env,
362+
template: { resource: 'resource1' },
363+
}],
364+
},
365+
{
366+
stacks: [{
367+
stackName: 'stack2',
368+
displayName: 'stage2/stack2',
369+
env,
370+
template: { resource: 'resource2' },
371+
}],
372+
}],
373+
});
374+
375+
const asm = await cloudExec.synthesize();
376+
return cliAssemblyWithForcedVersion(asm, '30.0.0');
377+
}

0 commit comments

Comments
 (0)