diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-synth-telemetry-with-errors.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-synth-telemetry-with-errors.integtest.ts index 3f005dcf9..9b2cd082e 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-synth-telemetry-with-errors.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-synth-telemetry-with-errors.integtest.ts @@ -36,6 +36,7 @@ integTest( ci: expect.anything(), // changes based on where this is called validation: true, quiet: false, + yes: false, }, config: { context: {}, @@ -84,6 +85,7 @@ integTest( ci: expect.anything(), // changes based on where this is called validation: true, quiet: false, + yes: false, }, config: { context: {}, diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-synth-telemetry.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-synth-telemetry.integtest.ts index 577168125..c1c004641 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-synth-telemetry.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/synth/cdk-synth-telemetry.integtest.ts @@ -28,6 +28,7 @@ integTest( ci: expect.anything(), // changes based on where this is called validation: true, quiet: false, + yes: false, }, config: { context: {}, @@ -77,6 +78,7 @@ integTest( ci: expect.anything(), // changes based on where this is called validation: true, quiet: false, + yes: false, }, config: { context: {}, diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index fbe1aefcb..162b6eca5 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -44,6 +44,7 @@ export async function makeConfig(): Promise { 'ci': { type: 'boolean', desc: 'Force CI detection. If CI=true then logs will be sent to stdout instead of stderr', default: YARGS_HELPERS.isCI() }, 'unstable': { type: 'array', desc: 'Opt in to unstable features. The flag indicates that the scope and API of a feature might still change. Otherwise the feature is generally production ready and fully supported. Can be specified multiple times.', default: [] }, 'telemetry-file': { type: 'string', desc: 'Send telemetry data to a local file.', default: undefined }, + 'yes': { type: 'boolean', alias: 'y', desc: 'Automatically answer interactive prompts with the recommended response. This includes confirming actions.', default: false }, }, commands: { 'list': { diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index 3d7cbb488..db494fc7e 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -127,6 +127,12 @@ "telemetry-file": { "type": "string", "desc": "Send telemetry data to a local file." + }, + "yes": { + "type": "boolean", + "alias": "y", + "desc": "Automatically answer interactive prompts with the recommended response. This includes confirming actions.", + "default": false } }, "commands": { diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index b7d3a7bad..a29f56432 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -70,6 +70,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise[] = []; + private readonly autoRespond: boolean; + public telemetry?: TelemetrySession; private constructor(props: CliIoHostProps = {}) { @@ -170,6 +182,7 @@ export class CliIoHost implements IIoHost { this.requireDeployApproval = props.requireDeployApproval ?? RequireApproval.BROADENING; this.stackProgress = props.stackProgress ?? StackActivityProgress.BAR; + this.autoRespond = props.autoRespond ?? false; } public async startTelemetry(args: any, context: Context, _proxyAgent?: Agent) { @@ -413,6 +426,35 @@ export class CliIoHost implements IIoHost { const concurrency = data.concurrency ?? 0; const responseDescription = data.responseDescription; + // Special approval prompt + // Determine if the message needs approval. If it does, continue (it is a basic confirmation prompt) + // If it does not, return success (true). We only check messages with codes that we are aware + // are requires approval codes. + if (this.skipApprovalStep(msg)) { + return true; + } + + // In --yes mode, respond for the user if we can + if (this.autoRespond) { + // respond with yes to all confirmations + if (isConfirmationPrompt(msg)) { + await this.notify({ + ...msg, + message: `${chalk.cyan(msg.message)} (auto-confirmed)`, + }); + return true; + } + + // respond with the default for all other messages + if (msg.defaultResponse) { + await this.notify({ + ...msg, + message: `${chalk.cyan(msg.message)} (auto-responded with default: ${util.format(msg.defaultResponse)})`, + }); + return msg.defaultResponse; + } + } + // only talk to user if STDIN is a terminal (otherwise, fail) if (!this.isTTY) { throw new ToolkitError(`${motivation}, but terminal (TTY) is not attached so we are unable to get a confirmation from the user`); @@ -423,14 +465,6 @@ export class CliIoHost implements IIoHost { throw new ToolkitError(`${motivation}, but concurrency is greater than 1 so we are unable to get a confirmation from the user`); } - // Special approval prompt - // Determine if the message needs approval. If it does, continue (it is a basic confirmation prompt) - // If it does not, return success (true). We only check messages with codes that we are aware - // are requires approval codes. - if (this.skipApprovalStep(msg)) { - return true; - } - // Basic confirmation prompt // We treat all requests with a boolean response as confirmation prompts if (isConfirmationPrompt(msg)) { diff --git a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts index 1e9da6627..91c73b50f 100644 --- a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts @@ -161,6 +161,12 @@ export function parseCommandLineArguments(args: Array): any { type: 'string', desc: 'Send telemetry data to a local file.', }) + .option('yes', { + default: false, + type: 'boolean', + alias: 'y', + desc: 'Automatically answer interactive prompts with the recommended response. This includes confirming actions.', + }) .command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', (yargs: Argv) => yargs .option('long', { diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index 6a18b5873..735198270 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -327,6 +327,13 @@ export interface GlobalOptions { * @default - undefined */ readonly telemetryFile?: string; + + /** + * Automatically answer interactive prompts with the recommended response. This includes confirming actions. + * + * @default - false + */ + readonly yes?: boolean; } /** diff --git a/packages/aws-cdk/test/cli/cli-arguments.test.ts b/packages/aws-cdk/test/cli/cli-arguments.test.ts index 1a8adfdc3..3c3269361 100644 --- a/packages/aws-cdk/test/cli/cli-arguments.test.ts +++ b/packages/aws-cdk/test/cli/cli-arguments.test.ts @@ -35,6 +35,7 @@ describe('yargs', () => { unstable: [], notices: undefined, output: undefined, + yes: false, }, deploy: { STACKS: undefined, diff --git a/packages/aws-cdk/test/cli/cli.test.ts b/packages/aws-cdk/test/cli/cli.test.ts index ab4c38a05..7acbc533c 100644 --- a/packages/aws-cdk/test/cli/cli.test.ts +++ b/packages/aws-cdk/test/cli/cli.test.ts @@ -59,6 +59,12 @@ jest.mock('../../lib/cli/parse-command-line-arguments', () => ({ if (args.includes('--role-arn')) { result = { ...result, roleArn: 'arn:aws:iam::123456789012:role/TestRole' }; } + } else if (args.includes('deploy')) { + result = { + ...result, + _: ['deploy'], + parameters: [], + }; } // Handle notices flags @@ -79,6 +85,10 @@ jest.mock('../../lib/cli/parse-command-line-arguments', () => ({ result = { ...result, verbose: parseInt(args[verboseIndex + 1], 10) }; } + if (args.includes('--yes')) { + result = { ...result, yes: true }; + } + return Promise.resolve(result); }), })); @@ -481,3 +491,25 @@ describe('gc command tests', () => { expect(gcSpy).toHaveBeenCalled(); }); }); + +describe('--yes', () => { + test('when --yes option is provided, CliIoHost is using autoRespond', async () => { + // GIVEN + const migrateSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'deploy').mockResolvedValue(); + const execSpy = jest.spyOn(CliIoHost, 'instance'); + + // WHEN + await exec(['deploy', '--yes']); + + // THEN + expect(execSpy).toHaveBeenCalledWith( + expect.objectContaining({ + autoRespond: true, + }), + true, + ); + + migrateSpy.mockRestore(); + execSpy.mockRestore(); + }); +}); diff --git a/packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts b/packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts index 7af5cb581..c84ba98fb 100644 --- a/packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts +++ b/packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts @@ -494,6 +494,57 @@ describe('CliIoHost', () => { }); }); + describe('--yes mode', () => { + const autoRespondingIoHost = CliIoHost.instance({ + logLevel: 'trace', + autoRespond: true, + isCI: false, + isTTY: true, + }, true); + + test('it does not prompt the user and return true', async () => { + const notifySpy = jest.spyOn(autoRespondingIoHost, 'notify'); + + // WHEN + const response = await autoRespondingIoHost.requestResponse(plainMessage({ + time: new Date(), + level: 'info', + action: 'synth', + code: 'CDK_TOOLKIT_I0001', + message: 'test message', + defaultResponse: true, + })); + + // THEN + expect(mockStdout).not.toHaveBeenCalledWith(chalk.cyan('test message') + ' (y/n) '); + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: chalk.cyan('test message') + ' (auto-confirmed)', + })); + expect(response).toBe(true); + }); + + test('messages with default are skipped', async () => { + const notifySpy = jest.spyOn(autoRespondingIoHost, 'notify'); + + // WHEN + const response = await autoRespondingIoHost.requestResponse(plainMessage({ + time: new Date(), + level: 'info', + action: 'synth', + code: 'CDK_TOOLKIT_I5060', + message: 'test message', + defaultResponse: 'foobar', + })); + + // THEN + expect(mockStdout).not.toHaveBeenCalledWith(chalk.cyan('test message') + ' (y/n) '); + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: chalk.cyan('test message') + ' (auto-responded with default: foobar)', + })); + expect(response).toBe('foobar'); + }); + }); + describe('non-promptable data', () => { test('logs messages and returns default unchanged', async () => { const response = await ioHost.requestResponse(plainMessage({