Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ integTest(
ci: expect.anything(), // changes based on where this is called
validation: true,
quiet: false,
yes: false,
},
config: {
context: {},
Expand Down Expand Up @@ -84,6 +85,7 @@ integTest(
ci: expect.anything(), // changes based on where this is called
validation: true,
quiet: false,
yes: false,
},
config: {
context: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ integTest(
ci: expect.anything(), // changes based on where this is called
validation: true,
quiet: false,
yes: false,
},
config: {
context: {},
Expand Down Expand Up @@ -77,6 +78,7 @@ integTest(
ci: expect.anything(), // changes based on where this is called
validation: true,
quiet: false,
yes: false,
},
config: {
context: {},
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export async function makeConfig(): Promise<CliConfig> {
'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': {
Expand Down
6 changes: 6 additions & 0 deletions packages/aws-cdk/lib/cli/cli-type-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
isCI: Boolean(argv.ci),
currentAction: cmd,
stackProgress: argv.progress,
autoRespond: argv.yes,
}, true);
const ioHelper = asIoHelper(ioHost, ioHost.currentAction as any);

Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/cli/convert-to-user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function convertYargsToUserInput(args: any): UserInput {
ci: args.ci,
unstable: args.unstable,
telemetryFile: args.telemetryFile,
yes: args.yes,
};
let commandOptions;
switch (args._[0] as Command) {
Expand Down Expand Up @@ -344,6 +345,7 @@ export function convertConfigToUserInput(config: any): UserInput {
ci: config.ci,
unstable: config.unstable,
telemetryFile: config.telemetryFile,
yes: config.yes,
};
const listOptions = {
long: config.list?.long,
Expand Down
50 changes: 42 additions & 8 deletions packages/aws-cdk/lib/cli/io-host/cli-io-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ export interface CliIoHostProps {
* @default StackActivityProgress.BAR
*/
readonly stackProgress?: StackActivityProgress;

/**
* Whether the CLI should attempt to automatically respond to prompts.
*
* When true, operation will usually proceed without interactive confirmation.
* Confirmations are responded to with yes. Other prompts will respond with the default value.
*
* @default false
*/
readonly autoRespond?: boolean;
}

/**
Expand Down Expand Up @@ -160,6 +170,8 @@ export class CliIoHost implements IIoHost {
private corkedCounter = 0;
private readonly corkedLoggingBuffer: IoMessage<unknown>[] = [];

private readonly autoRespond: boolean;

public telemetry?: TelemetrySession;

private constructor(props: CliIoHostProps = {}) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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`);
Expand All @@ -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)) {
Expand Down
6 changes: 6 additions & 0 deletions packages/aws-cdk/lib/cli/parse-command-line-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ export function parseCommandLineArguments(args: Array<string>): 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', {
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/cli/user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/test/cli/cli-arguments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('yargs', () => {
unstable: [],
notices: undefined,
output: undefined,
yes: false,
},
deploy: {
STACKS: undefined,
Expand Down
32 changes: 32 additions & 0 deletions packages/aws-cdk/test/cli/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}),
}));
Expand Down Expand Up @@ -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();
});
});
51 changes: 51 additions & 0 deletions packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading