Skip to content

Commit b670116

Browse files
ren-yamanashimrgrain
authored andcommitted
feat(cli): add non-interactive mode support to CLI
1 parent 779352d commit b670116

File tree

10 files changed

+105
-0
lines changed

10 files changed

+105
-0
lines changed

packages/aws-cdk/lib/cli/cli-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export async function makeConfig(): Promise<CliConfig> {
4444
'ci': { type: 'boolean', desc: 'Force CI detection. If CI=true then logs will be sent to stdout instead of stderr', default: YARGS_HELPERS.isCI() },
4545
'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: [] },
4646
'telemetry-file': { type: 'string', desc: 'Send telemetry data to a local file.', default: undefined },
47+
'yes': { type: 'boolean', alias: 'y', desc: 'Skip interactive prompts. If yes=true then the operation will proceed without confirmation.', default: false },
4748
},
4849
commands: {
4950
'list': {

packages/aws-cdk/lib/cli/cli-type-registry.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@
127127
"telemetry-file": {
128128
"type": "string",
129129
"desc": "Send telemetry data to a local file."
130+
},
131+
"yes": {
132+
"type": "boolean",
133+
"alias": "y",
134+
"desc": "Skip interactive prompts. If yes=true then the operation will proceed without confirmation.",
135+
"default": false
130136
}
131137
},
132138
"commands": {

packages/aws-cdk/lib/cli/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
7070
isCI: Boolean(argv.ci),
7171
currentAction: cmd,
7272
stackProgress: argv.progress,
73+
nonInteractive: argv.yes,
7374
}, true);
7475
const ioHelper = asIoHelper(ioHost, ioHost.currentAction as any);
7576

packages/aws-cdk/lib/cli/convert-to-user-input.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function convertYargsToUserInput(args: any): UserInput {
3535
ci: args.ci,
3636
unstable: args.unstable,
3737
telemetryFile: args.telemetryFile,
38+
yes: args.yes,
3839
};
3940
let commandOptions;
4041
switch (args._[0] as Command) {
@@ -344,6 +345,7 @@ export function convertConfigToUserInput(config: any): UserInput {
344345
ci: config.ci,
345346
unstable: config.unstable,
346347
telemetryFile: config.telemetryFile,
348+
yes: config.yes,
347349
};
348350
const listOptions = {
349351
long: config.list?.long,

packages/aws-cdk/lib/cli/io-host/cli-io-host.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ export interface CliIoHostProps {
8181
* @default StackActivityProgress.BAR
8282
*/
8383
readonly stackProgress?: StackActivityProgress;
84+
85+
/**
86+
* Whether the CLI is running in non-interactive mode.
87+
* When true, operation will proceed without confirmation.
88+
*
89+
* @default false
90+
*/
91+
readonly nonInteractive?: boolean;
8492
}
8593

8694
/**
@@ -160,6 +168,8 @@ export class CliIoHost implements IIoHost {
160168
private corkedCounter = 0;
161169
private readonly corkedLoggingBuffer: IoMessage<unknown>[] = [];
162170

171+
private readonly nonInteractive: boolean;
172+
163173
public telemetry?: TelemetrySession;
164174

165175
private constructor(props: CliIoHostProps = {}) {
@@ -170,6 +180,7 @@ export class CliIoHost implements IIoHost {
170180
this.requireDeployApproval = props.requireDeployApproval ?? RequireApproval.BROADENING;
171181

172182
this.stackProgress = props.stackProgress ?? StackActivityProgress.BAR;
183+
this.nonInteractive = props.nonInteractive ?? false;
173184
}
174185

175186
public async startTelemetry(args: any, context: Context, _proxyAgent?: Agent) {
@@ -423,6 +434,11 @@ export class CliIoHost implements IIoHost {
423434
throw new ToolkitError(`${motivation}, but concurrency is greater than 1 so we are unable to get a confirmation from the user`);
424435
}
425436

437+
// In non-interactive mode, always return success (true).
438+
if (this.nonInteractive) {
439+
return true;
440+
}
441+
426442
// Special approval prompt
427443
// Determine if the message needs approval. If it does, continue (it is a basic confirmation prompt)
428444
// If it does not, return success (true). We only check messages with codes that we are aware

packages/aws-cdk/lib/cli/parse-command-line-arguments.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ export function parseCommandLineArguments(args: Array<string>): any {
161161
type: 'string',
162162
desc: 'Send telemetry data to a local file.',
163163
})
164+
.option('yes', {
165+
default: false,
166+
type: 'boolean',
167+
alias: 'y',
168+
desc: 'Skip interactive prompts. If yes=true then the operation will proceed without confirmation.',
169+
})
164170
.command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', (yargs: Argv) =>
165171
yargs
166172
.option('long', {

packages/aws-cdk/lib/cli/user-input.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,13 @@ export interface GlobalOptions {
327327
* @default - undefined
328328
*/
329329
readonly telemetryFile?: string;
330+
331+
/**
332+
* Skip interactive prompts. If yes=true then the operation will proceed without confirmation.
333+
*
334+
* @default - false
335+
*/
336+
readonly yes?: boolean;
330337
}
331338

332339
/**

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ describe('yargs', () => {
3535
unstable: [],
3636
notices: undefined,
3737
output: undefined,
38+
yes: false,
3839
},
3940
deploy: {
4041
STACKS: undefined,

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ jest.mock('../../lib/cli/parse-command-line-arguments', () => ({
7979
result = { ...result, verbose: parseInt(args[verboseIndex + 1], 10) };
8080
}
8181

82+
if (args.includes('--yes')) {
83+
result = { ...result, yes: true };
84+
}
85+
8286
return Promise.resolve(result);
8387
}),
8488
}));
@@ -481,3 +485,23 @@ describe('gc command tests', () => {
481485
expect(gcSpy).toHaveBeenCalled();
482486
});
483487
});
488+
489+
test('when --yes option is provided, CliIoHost is in non-interactive mode', async () => {
490+
// GIVEN
491+
const migrateSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'deploy').mockResolvedValue();
492+
const execSpy = jest.spyOn(CliIoHost, 'instance');
493+
494+
// WHEN
495+
await exec(['deploy', '--yes']);
496+
497+
// THEN
498+
expect(execSpy).toHaveBeenCalledWith(
499+
expect.objectContaining({
500+
nonInteractive: true,
501+
}),
502+
true,
503+
);
504+
505+
migrateSpy.mockRestore();
506+
execSpy.mockRestore();
507+
});

packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,47 @@ describe('CliIoHost', () => {
494494
});
495495
});
496496

497+
describe('non-interactive mode', () => {
498+
const nonInteractiveIoHost = CliIoHost.instance({
499+
logLevel: 'trace',
500+
nonInteractive: true,
501+
isCI: false,
502+
isTTY: true,
503+
}, true);
504+
505+
test('it does not prompt the user and return true', async () => {
506+
// WHEN
507+
const response = await nonInteractiveIoHost.requestResponse(plainMessage({
508+
time: new Date(),
509+
level: 'info',
510+
action: 'synth',
511+
code: 'CDK_TOOLKIT_I0001',
512+
message: 'test message',
513+
defaultResponse: true,
514+
}));
515+
516+
// THEN
517+
expect(mockStdout).not.toHaveBeenCalledWith(chalk.cyan('test message') + ' (y/n) ');
518+
expect(response).toBe(true);
519+
});
520+
521+
test('approvalToolkitCodes also skip', async () => {
522+
// WHEN
523+
const response = await nonInteractiveIoHost.requestResponse(plainMessage({
524+
time: new Date(),
525+
level: 'info',
526+
action: 'synth',
527+
code: 'CDK_TOOLKIT_I5060',
528+
message: 'test message',
529+
defaultResponse: true,
530+
}));
531+
532+
// THEN
533+
expect(mockStdout).not.toHaveBeenCalledWith(chalk.cyan('test message') + ' (y/n) ');
534+
expect(response).toBe(true);
535+
});
536+
});
537+
497538
describe('non-promptable data', () => {
498539
test('logs messages and returns default unchanged', async () => {
499540
const response = await ioHost.requestResponse(plainMessage({

0 commit comments

Comments
 (0)