Skip to content

feat(cli): warn of non-existent stacks in cdk destroy#984

Open
go-to-k wants to merge 17 commits into
aws:mainfrom
go-to-k:destroy
Open

feat(cli): warn of non-existent stacks in cdk destroy#984
go-to-k wants to merge 17 commits into
aws:mainfrom
go-to-k:destroy

Conversation

@go-to-k

@go-to-k go-to-k commented Dec 10, 2025

Copy link
Copy Markdown
Contributor

Fixes aws/aws-cdk#32836, aws/aws-cdk#32545, aws/aws-cdk#27179, aws/aws-cdk#22240

Reason for this change

This PR implements the feature that warns users when non-existent stacks are specified in cdk destroy.

  • It does not display the message Are you sure you want to delete: if there is no matching stack.
  • Even if the stack does not exist, cdk destroy will not fail, it will just print a warning.

For examples (that have Stacka, StackA, StackX):

destroy2
destroy3
destroy4

Difference from previous PR

The previous PR was reverted in aws/aws-cdk#32839 due to a regression with only nested stage stacks. So this version addresses that regression with comprehensive tests.

Description of changes

The original implementation added warnings for non-existent stacks in cdk destroy, but it failed when applications had only nested stage stacks (no top-level stacks). This happened because the code used allTopLevel: true, which only searched for stacks directly under the App, ignoring stacks within nested Stages.

Fixed the regression by changing suggestStacks method to use DefaultSelection.AllStacks instead of allTopLevel: true, ensuring the warning feature works for all stack configurations (top-level only, nested only, or both). Added regression tests to verify the fix for the nested stage scenario.

  private async suggestStacks(props: {
    selector: StackSelector;
    stacks: StackCollection;
    exclusively?: boolean;
  }) {
    const assembly = await this.assembly();
    const selectorWithoutPatterns: StackSelector = {
-      ...props.selector,
-      allTopLevel: true,
      patterns: [],
    };
    const stacksWithoutPatterns = await assembly.selectStacks(selectorWithoutPatterns, {
      extend: props.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream,
-      defaultBehavior: DefaultSelection.OnlySingle,
+      defaultBehavior: DefaultSelection.AllStacks,
    });

Additional Information

When running cdk destroy --all or cdk deploy --all against a configuration with no top-level stacks (nested stages only), the following error occurs.

However, this behavior existed before this PR and is unrelated to the changes made here. Since it is outside the scope of this PR, no fix has been implemented for this behavior.

FYI: I have submitted an issue and a PR about this behavior.

const app = new cdk.App();

new MyStage(app, 'MyStage'); // This has CdkSampleStack
> cdk deploy
Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`
Stacks: MyStage/CdkSampleStack

> cdk deploy --all
...
No stack found in the main cloud assembly. Use "list" to print manifest

> cdk destroy --all
...
No stack found in the main cloud assembly. Use "list" to print manifest

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@aws-cdk-automation aws-cdk-automation requested a review from a team December 10, 2025 12:11
@github-actions github-actions Bot added the p2 label Dec 10, 2025
auto-merge was automatically disabled December 10, 2025 12:18

Head branch was pushed to by a user without write access

@codecov-commenter

codecov-commenter commented Dec 10, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.59%. Comparing base (4086fdf) to head (653c0e6).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #984   +/-   ##
=======================================
  Coverage   89.59%   89.59%           
=======================================
  Files          77       77           
  Lines       11758    11758           
  Branches     1651     1651           
=======================================
  Hits        10534    10534           
  Misses       1195     1195           
  Partials       29       29           
Flag Coverage Δ
suite.unit 89.59% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The `refactor` command only operates on the stacks that are relevant for
the target CDK application. However, it gets all the templates of the
deployed stacks before filtering them, which is wasteful.

Invert the order, so that the filtering happens before getting the
templates.

---
By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache-2.0 license
@mrgrain

mrgrain commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@go-to-k I am looking to get this in soon (hence the rebase), but first want to change destroy to use toolkit-lib code internally, so we can avoid duplicate and complicated code.

@go-to-k

go-to-k commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

@mrgrain Also restored the confirmation prompt's stack-name color to blue: 4626130

It had turned red because toolkit-lib's destroy used chalk.red, whereas the CLI used chalk.blue before.

Before:
color-diff

After:
after

P.S. recommit: d666187

Align cdk destroy with the confirmation-prompt contract from aws#1667:
declining now throws AbortError('DestroyAborted', 'Deletion cancelled')
and exits non-zero instead of emitting CDK_TOOLKIT_E7010 and exiting 0.
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Comment thread packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts
@go-to-k

go-to-k commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Merged your PR and fixed conflicts.

Now:

cancel

…stroy

# Conflicts:
#	packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts
#	packages/aws-cdk/lib/cli/cdk-toolkit.ts
#	packages/aws-cdk/test/_helpers/io-recorder.ts
#	packages/aws-cdk/test/commands/__io_snapshots__/destroy/destroy_failure_emits_a_failure_message_and_rethrows_when_destroyStack_fails.ndjson
#	packages/aws-cdk/test/commands/__io_snapshots__/destroy/force_false_confirmation_prompt_aborts_with_an_AbortError_and_destroys_nothing_when_the_user_declines.ndjson
#	packages/aws-cdk/test/commands/__io_snapshots__/destroy/force_false_confirmation_prompt_asks_for_confirmation_and_proceeds_when_the_user_confirms.ndjson
#	packages/aws-cdk/test/commands/__io_snapshots__/destroy/force_false_confirmation_prompt_rethrows_an_unexpected_error_from_the_confirmation_prompt.ndjson
#	packages/aws-cdk/test/commands/__io_snapshots__/destroy/force_true_no_confirmation_prompt_destroys_a_single_nested_stack_fromDeploy_makes_it_say_deployed.ndjson
#	packages/aws-cdk/test/commands/__io_snapshots__/destroy/force_true_no_confirmation_prompt_destroys_all_top-level_stacks_with_concurrency.ndjson
#	packages/aws-cdk/test/commands/__io_snapshots__/destroy/force_true_no_confirmation_prompt_forwards_the_roleArn_to_destroyStack.ndjson
#	packages/aws-cdk/test/commands/__io_snapshots__/destroy/force_true_no_confirmation_prompt_respects_dependency_order_with_concurrency.ndjson
#	packages/aws-cdk/test/commands/__io_snapshots__/list/cdk_list_prints_machine-readable_JSON_without_the_synthesis-time_line.ndjson
#	packages/aws-cdk/test/commands/destroy.test.ts
… match

Re-applies the destroy warning feature on top of main's toolkit-lib destroy
migration (aws#1686):
- W7011: emit a warning and exit without prompting when no stacks match
- W7010: warn about each non-existent name (suggesting a close match when one
  exists, including stacks nested in a stage) while still destroying matches
- adds suggestStacks() to Toolkit and the supporting message codes/registry
- restores toolkit-lib unit tests, stage fixtures, and the destroy integ tests

The toolkit-lib migration plumbing from this branch is dropped in favor of
main's implementation; only the warning behavior is preserved.
@go-to-k

go-to-k commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

@mrgrain

Drops this branch's own toolkit-lib migration in favor of upstream #1686, keeps only the destroy warning feature.

The destroy confirmation prompt colored the stack names red (inherited from
main's _destroy). Use blue to match the color the CLI used before destroy was
routed through toolkit-lib, and assert it so the merge does not silently
re-introduce red.

Ref: aws#984 (comment)
@go-to-k

go-to-k commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@mrgrain Could you please take a look at my changes?

Comment on lines +1710 to +1731
private async suggestStacks(
ioHelper: IoHelper,
assembly: StackAssembly,
selector: StackSelector,
selected: StackCollection,
): Promise<void> {
const patterns = selector.patterns ?? [];
if (patterns.length === 0) {
return;
}

const allStacks = await assembly.selectStacksV2(ALL_STACKS);

for (const pattern of patterns) {
const matched = selected.stackArtifacts.some((stack) => picomatch.isMatch(stack.hierarchicalId, pattern));
if (matched) {
continue;
}

const closeMatches = allStacks.stackArtifacts
.filter((stack) => picomatch.isMatch(stack.hierarchicalId.toLowerCase(), pattern.toLowerCase()))
.map((stack) => stack.hierarchicalId);

@mrgrain mrgrain Jul 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function to match candidates (basically to this line) should live on StackAssembly class. It should not log any output but return a string[].

Comment on lines +1724 to +1727
const matched = selected.stackArtifacts.some((stack) => picomatch.isMatch(stack.hierarchicalId, pattern));
if (matched) {
continue;
}

@mrgrain mrgrain Jul 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain what purpose this is serving? I just don't understand it. We are excluding any of the stacks that already match the pattern themselves? But why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handles the case where several names are given and only some exist. E.g. cdk destroy StackA Typo: StackA is destroyed, Typo matches nothing. The matched check skips StackA and warns only about Typo ("Did you mean …?").

With "only warn when 0 stacks matched", a typo mixed in with valid names would slip through unnoticed, that's what I wanted to catch. That said, if you'd prefer a single message (only when nothing matched at all), I'm happy to fold it into one payload-carrying message. Which do you prefer?

@mrgrain mrgrain Jul 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, thank you. I understand now what this is trying to achieve. Let me think about this for a bit. Some of my comments are now obviously contingent on this decision here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding off until the direction above is settled, then I'll adjust accordingly.

const ioHelper = asIoHelper(this.ioHost, action);
const stacks = await assembly.selectStacksV2(selectStacks);

await this.suggestStacks(ioHelper, assembly, selectStacks, stacks);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should only call this if the original selector returned 0 results.

};

if (stacks.stackCount === 0) {
await ioHelper.notify(IO.CDK_TOOLKIT_W7011.msg(

@mrgrain mrgrain Jul 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think both messages can be folded into a single message that informs the user that no matching stacks were found. And if we have identified possible close matches ("candidates"), then we suggest them ("Did you mean...").

The Toolkit message should include a payload of the unmatched pattern and the identified matches as string[] | undefined.

Comment on lines +401 to +408
CDK_TOOLKIT_W7010: make.warn({
code: 'CDK_TOOLKIT_W7010',
description: 'A provided stack name does not match any stack',
}),
CDK_TOOLKIT_W7011: make.warn({
code: 'CDK_TOOLKIT_W7011',
description: 'No stacks match the provided names, nothing to destroy',
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like said elsewhere, I think a single message will be enough but it should carry a payload of the unmatched selector and idenfified matches.

@mrgrain mrgrain left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your continued work! FYI I want to let the changes to destroy bake a little bit longer to see if there's any issue from that.

But this PR is getting close to be ready now. Made some final comments on code organisation and messaging. We will also expect new tests in packages/aws-cdk/test/commands/destroy.test.ts and associated snapshots to record the exact output.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cdk destroy: No stack found in the main cloud assembly

5 participants