Skip to content
Open
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
@@ -1,3 +1,4 @@
import * as util from 'node:util';
import chalk from 'chalk';
import type { IIoHost, IoMessage, IoMessageLevel, IoRequest } from '../api/io';
import { isMessageRelevantForLevel } from '../api/io/private';
Expand Down Expand Up @@ -120,12 +121,20 @@ export class NonInteractiveIoHost implements IIoHost {
/**
* Notifies the host of a message that requires a response.
*
* If the host does not return a response the suggested
* default response from the input message will be used.
* The non-interactive IoHost cannot prompt the user, so it always returns
* the request's `defaultResponse`. To make this visible, the message is
* annotated to indicate the auto-response — same convention as the CLI's
* `--yes` mode.
*/
public async requestResponse<DataType, ResponseType>(msg: IoRequest<DataType, ResponseType>): Promise<ResponseType> {
// in the non-interactive IoHost, no requests are promptable
await this.notify(msg);
const annotation = typeof msg.defaultResponse === 'boolean'
? (msg.defaultResponse ? '(auto-confirmed)' : '(auto-denied)')
: `(auto-responded with default: ${util.format(msg.defaultResponse)})`;

await this.notify({
...msg,
message: `${msg.message} ${annotation}`,
});
return msg.defaultResponse;
}

Expand Down
10 changes: 5 additions & 5 deletions packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -926,14 +926,14 @@ export class Toolkit extends CloudAssemblySourceBuilder {
const stackDiff = formatter.formatStackDiff();

// Send a request response with the diff as part of the message,
// and the template diff as data
// (IoHost decides whether to print depending on permissionChangeType)
// and the template diff as data. The IoHost decides how to handle the
// request — interactively prompt the user, auto-confirm, or suppress.
const hasSecurityChanges = securityDiff.permissionChangeType !== PermissionChangeType.NONE;
const deployMotivation = hasSecurityChanges
? '"--require-approval" is enabled and stack includes security-sensitive updates.'
: '"--require-approval" is enabled and stack includes updates.';
? 'Stack includes security-sensitive updates'
: 'Stack includes updates';
const diffOutput = hasSecurityChanges ? securityDiff.formattedDiff : stackDiff.formattedDiff;
const deployQuestion = `${diffOutput}\n\n${deployMotivation}\nDo you wish to deploy these changes`;
const deployQuestion = `${diffOutput}\n\n${deployMotivation}. Do you wish to deploy these changes?`;
const deployConfirmed = await ioHelper.requestResponse(IO.CDK_TOOLKIT_I5060.req(deployQuestion, {
motivation: deployMotivation,
concurrency,
Expand Down
8 changes: 4 additions & 4 deletions packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ IAM Statement Changes
action: 'deploy',
level: 'info',
code: 'CDK_TOOLKIT_I5060',
message: expect.stringContaining('Do you wish to deploy these changes'),
message: expect.stringContaining('Do you wish to deploy these changes?'),
data: expect.objectContaining({
motivation: expect.stringContaining('stack includes security-sensitive updates.'),
motivation: 'Stack includes security-sensitive updates',
permissionChangeType: 'broadening',
templateDiffs: expect.objectContaining({
Stack1: expect.objectContaining({
Expand Down Expand Up @@ -115,12 +115,12 @@ IAM Statement Changes

// Message includes formatted stack diff (not just security diff)
expect(request).toContain('AWS::S3::Bucket');
expect(request).toContain('Do you wish to deploy these changes');
expect(request).toContain('Do you wish to deploy these changes?');

expect(ioHost.requestSpy).toHaveBeenCalledWith(expect.objectContaining({
code: 'CDK_TOOLKIT_I5060',
data: expect.objectContaining({
motivation: expect.stringContaining('stack includes updates.'),
motivation: 'Stack includes updates',
permissionChangeType: 'none',
}),
}));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { IoRequest } from '../../lib/api/io';
import { NonInteractiveIoHost } from '../../lib/toolkit/non-interactive-io-host';

// In non-CI mode the host writes non-error output to stderr.
let stderrMock: jest.SpyInstance;

beforeEach(() => {
stderrMock = jest.spyOn(process.stderr, 'write').mockReturnValue(true);
});

afterEach(() => {
stderrMock.mockRestore();
});

function request<U>(defaultResponse: U): IoRequest<undefined, U> {
return {
time: new Date(),
level: 'info',
action: 'deploy',
code: 'CDK_TOOLKIT_I5060',
message: 'Do you wish to deploy these changes?',
data: undefined,
defaultResponse,
};
}

describe('requestResponse auto-answers with the default and surfaces it', () => {
test('a true default is annotated as auto-confirmed', async () => {
const host = new NonInteractiveIoHost({ isCI: false });

const response = await host.requestResponse(request(true));

expect(response).toBe(true);
expect(stderrMock).toHaveBeenCalledWith(expect.stringContaining('Do you wish to deploy these changes? (auto-confirmed)'));
});

test('a false default is annotated as auto-denied', async () => {
const host = new NonInteractiveIoHost({ isCI: false });

const response = await host.requestResponse(request(false));

expect(response).toBe(false);
expect(stderrMock).toHaveBeenCalledWith(expect.stringContaining('Do you wish to deploy these changes? (auto-denied)'));
});

test('a non-boolean default reports the value it responded with', async () => {
const host = new NonInteractiveIoHost({ isCI: false });

const response = await host.requestResponse(request('the-default'));

expect(response).toBe('the-default');
expect(stderrMock).toHaveBeenCalledWith(expect.stringContaining('Do you wish to deploy these changes? (auto-responded with default: the-default)'));
});
});
Loading
Loading