Skip to content

feat(toolkit-lib): simplify deploy approval message and surface auto-confirm#1585

Open
sai-ray wants to merge 8 commits into
mainfrom
sai/require-approval-notice-suppression
Open

feat(toolkit-lib): simplify deploy approval message and surface auto-confirm#1585
sai-ray wants to merge 8 commits into
mainfrom
sai/require-approval-notice-suppression

Conversation

@sai-ray
Copy link
Copy Markdown
Contributor

@sai-ray sai-ray commented Jun 1, 2026

Closes #828.

@aws-cdk/toolkit-lib's deploy() previously printed "--require-approval" is enabled and stack includes security-sensitive updates. Do you wish to deploy these changes and then silently auto-confirmed via the default NonInteractiveIoHost. The question-shaped text without a visible answer-shaped action confused programmatic users.

This PR keeps the I5060 confirmation request in _deploy (it's the IoHost's job to decide what to do with it), drops the --require-approval reference from the library's vocabulary, makes NonInteractiveIoHost's auto-response visible, and centralizes the CLI-side --require-approval framing inCliIoHost.

Changes

  • @aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts: motivation drops the --require-approval reference but keeps the existing security-sensitive vs non-security-sensitive branching:
    • Security-sensitive change: Stack includes security-sensitive updates
    • Non-security change: Stack includes updates
  • @aws-cdk/toolkit-lib/lib/toolkit/non-interactive-io-host.ts: requestResponse annotates the message based on the actual default response value:
    • defaultResponse: true: (auto-confirmed)
    • defaultResponse: false: (auto-denied)
    • non-boolean default: (auto-responded with default: <value>)
  • aws-cdk/lib/cli/io-host/cli-io-host.ts: new augmentDeployApprovalMessage helper. When CliIoHost receives an I5060, it reinjects the --require-approval framing based on requireDeployApproval:
    • BROADENING + broadening change → prepends "--require-approval" is enabled.
    • ANYCHANGE → prepends "--require-approval" is set to 'any-change'.
  • aws-cdk/lib/cli/cdk-toolkit.ts: emits the same bare-fact motivation as toolkit-lib; CliIoHost.augmentDeployApprovalMessage adds the framing. Single source of truth for the flag vocabulary.
  • @aws-cdk/toolkit-lib/test/actions/deploy.test.ts, aws-cdk/test/cli/cdk-toolkit.test.ts: motivation assertions updated for the new bare-fact text.

Example

Same stack, before vs after this PR:

Before

"--require-approval" is enabled and stack includes security-sensitive updates.
Do you wish to deploy these changes
Cdk828Probe: deploying... [1/1]

After (cdk deploy, default --require-approval=broadening, broadening change)

"--require-approval" is enabled. Stack includes security-sensitive updates: Do you wish to deploy these changes? (y/n)

After (cdk deploy --require-approval=any-change, broadening change)

"--require-approval" is set to 'any-change'. Stack includes security-sensitive updates: Do you wish to deploy these changes? (y/n)

After (await toolkit.deploy() programmatic, default NonInteractiveIoHost, security-sensitive change)

Stack includes security-sensitive updates. Do you wish to deploy these changes? (auto-confirmed)
Cdk828Probe: deploying... [1/1]

After (await toolkit.deploy() programmatic, default NonInteractiveIoHost, non-security change)

Stack includes updates. Do you wish to deploy these changes? (auto-confirmed)
Cdk828Probe: deploying... [1/1]

Integrators who want to suppress the message entirely can implement their own IIoHost and drop the CDK_TOOLKIT_I5060 request:

import { Toolkit, NonInteractiveIoHost } from '@aws-cdk/toolkit-lib';

class QuietIoHost extends NonInteractiveIoHost {
  async requestResponse(msg) {
    if (msg.code === 'CDK_TOOLKIT_I5060') return msg.defaultResponse;
    return super.requestResponse(msg);
  }
}

await new Toolkit({ ioHost: new QuietIoHost() }).deploy(source);

Test plan

  • Unit tests in test/actions/deploy.test.ts (27/27 pass; motivation assertions updated).
  • Unit tests in test/cli/io-host/cli-io-host.test.ts and test/cli/cdk-toolkit.test.ts pass with updated assertion.
  • Manually verified all four prompt branches against real deploys: cdk deploy --yes and await toolkit.deploy() against broadening (IAM role) and non-security (S3 bucket) stacks under --require-approval=broadening and --require-approval=any-change. Each produces a single, correctly-prefixed prompt.
  • Manually verified all four NonInteractiveIoHost annotation paths (true, false, string, number) via a probe script: each produces the expected annotation and returns the correct value.

Checklist

  • This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed
    • Release notes for the new version:

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@aws-cdk-automation aws-cdk-automation requested a review from a team June 1, 2026 22:16
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Jun 2, 2026

Codecov Report

❌ Patch coverage is 54.90196% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.06%. Comparing base (1bdaaaf) to head (01637af).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
packages/aws-cdk/lib/cli/io-host/cli-io-host.ts 53.19% 21 Missing and 1 partial ⚠️
packages/aws-cdk/lib/cli/cdk-toolkit.ts 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1585      +/-   ##
==========================================
- Coverage   88.16%   88.06%   -0.11%     
==========================================
  Files          76       76              
  Lines       10841    10909      +68     
  Branches     1494     1511      +17     
==========================================
+ Hits         9558     9607      +49     
- Misses       1255     1274      +19     
  Partials       28       28              
Flag Coverage Δ
suite.unit 88.06% <54.90%> (-0.11%) ⬇️

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.

@sai-ray sai-ray force-pushed the sai/require-approval-notice-suppression branch from d1786c1 to 41c4504 Compare June 2, 2026 02:44
@sai-ray sai-ray marked this pull request as ready for review June 2, 2026 02:44
Comment thread packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts Outdated
Copy link
Copy Markdown
Contributor

@mrgrain mrgrain left a comment

Choose a reason for hiding this comment

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

Need to look at this in more detail. It was by design that toolkit lib doesn't get interactions.

Copy link
Copy Markdown
Contributor

@rix0rrr rix0rrr left a comment

Choose a reason for hiding this comment

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

While wer'e doing this, can we rename it to require-confirmation ?

"Approval" is actually something else than approval.

Copy link
Copy Markdown
Contributor

@mrgrain mrgrain left a comment

Choose a reason for hiding this comment

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

Right, I don't think I want to encode this into toolkit-lib. We are already ask for confirmation, it is up to the implementor to decide how to proceed here.

For now, we should improve the message/motivation to simply be something like

Changes detected. Do you wish to deploy these changes?

Additionally, the NonInteractiveIoHost should be clearer about what's happening, e.g. print the auto-acceptance to the output like the --yes flag does.


In future, we should have an easy way to drop a specific message:

const ioHost = new NonInteractiveIoHost();
ioHost.on('I5060', (msg) => {
  // do nothing
});

@rix0rrr
Copy link
Copy Markdown
Contributor

rix0rrr commented Jun 2, 2026

I don't disagree @mrgrain, but then shouldn't we pull this all the way through and require the confirmation always, regardless of the change type? And give enough information for the client to correlate the change impact with the command line flag?

@sai-ray
Copy link
Copy Markdown
Contributor Author

sai-ray commented Jun 2, 2026

Thanks @mrgrain and @rix0rrr. Dropped the requireApproval option, so _deploy always emits I5060 on any change and the IoHost decides what to do with it. Simplified the motivation to Changes detected. so the --require-approval reference doesn't leak out of the library. And NonInteractiveIoHost now annotates auto-replies with (auto-confirmed) or (auto-responded with default: …), similar to --yes mode.

@rix0rrr on your second point : if I understand correctly, you're saying the I5060 payload should give the IoHost enough to make its own decision. permissionChangeType and templateDiffs are already on the payload, so the IoHost can correlate the change to whatever flag it exposes (that's what CliIoHost.skipApprovalStep uses today). The message itself still branches on security vs stack diff inside _deploy . should I move that out too, or is it fine where it is? Happy to do it here or as a follow-up.

// in the non-interactive IoHost, no requests are promptable
await this.notify(msg);
const annotation = typeof msg.defaultResponse === 'boolean'
? '(auto-confirmed)'
Copy link
Copy Markdown
Contributor

@rix0rrr rix0rrr Jun 3, 2026

Choose a reason for hiding this comment

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

You are assuming that all booleans are always true, apparently.

The defaultResponse boolean could have been false, in which case auto-confirmed is not correct. It could have been auto-denied as well, you don't have enough information to tell.

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.

Fixed. (auto-confirmed) for true, (auto-denied) for false, (auto-responded with default: <value>) for non-boolean.

const deployMotivation = hasSecurityChanges
? '"--require-approval" is enabled and stack includes security-sensitive updates.'
: '"--require-approval" is enabled and stack includes updates.';
const deployMotivation = 'Changes detected.';
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 I prefer the specificy of the original message. If the problem is that you cannot refer to --require-approval, then remove that part. But now you've removed all specificity.

Here is the question I want you to ask yourself, at all times: "What is better/clearer for the user?"

So, now answer that question: why is it better for the user to get a message like:

Changes detected. Do you wish to deploy these changes?

Put yourself in the shoes of the user. You've just made changes. You're now running cdk deploy to deploy your changes. Then the tool triumphantly tells you "there are changes!". What would you think at that point?

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 the point was: The original message was good and to the point. But the authority that comes up with that message is in the wrong place, because the toolkit-lib has no business talking about command-line arguments.

So:

  • You need to produce an accurate message here that does not refer to command line arguments.
  • You then need to produce an even more accurate message in the CLI, that does refer to command line arguments.

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.

Restored the security-vs-non-security branching, just dropped the --require-approval reference. Library now says Stack includes security-sensitive updates. / Stack includes updates..

Comment on lines 865 to 867
? 'Stack includes security-sensitive updates.'
: 'Stack includes updates.';
const diffOutput = hasSecurityChanges ? securityDiff.formattedDiff : stackDiff.formattedDiff;
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.

Please put the messaging about the flags back in the CliIoHost.

Copy link
Copy Markdown
Contributor

@rix0rrr rix0rrr left a comment

Choose a reason for hiding this comment

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

Almost there!

return msg;
}

const data = (msg.data ?? {}) as { motivation?: string; permissionChangeType?: string };
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.

Is there not an existing type definition that models the data field here? I feel like we require this.

In fact, there should be a CDK_TOOLKIT_I5060.is(msg) that will enforce a type guard that will automatically make msg.data have the right type.

let prefix: string;
switch (this.requireDeployApproval) {
case RequireApproval.ANYCHANGE:
prefix = `"--require-approval" is set to '${RequireApproval.ANYCHANGE}'. `;
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.

Because you want to reuse the original message, and both need to make sense in their own context, the final sentence here will be very staccato:

require-approval is set to anychange. Stack contains updates. Do you want to continue?

Instead, you can include hasSecurityChanges: true|false into the data object and format a complete sentence here, rather than scrapping about with fragments.

You might have noticed already, but I care quite a lot how CDK "feels". Not just: what the CLI says is technically accurate, but also that it speaks in a normal voice to a normal user. No formal IBM-speak, no vague Microsoft-speak.

// The library emits I5060 with a flag-free motivation. The CLI is the
// layer that knows about `--require-approval` and adds it to the
// user-facing message before any downstream rendering.
const promptMessage = this.augmentDeployApprovalMessage(msg).message;
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.

No need for this function to return a full IoRequest object if all we're going to do is pull out a single field from that object and throw the rest away. You can just have this return a string.

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.

(toolkit-lib): suppress deployment notice about --require-approval

4 participants