diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index d17a38e62f923..78e2177b186cd 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -285,6 +285,18 @@ When `cdk deploy` is executed, deployment events will include the complete histo The `progress` key can also be specified as a user setting (`~/.cdk.json`) +#### Externally Executable CloudFormation Change Sets + +For more control over when stack changes are deployed, the CDK can generate a +CloudFormation change set but not execute it. The name of the generated +change set is *cdk-deploy-change-set*, and a previous change set with that +name will be overwritten. The change set will always be created, even if it +is empty. + +```console +$ cdk deploy --no-execute +``` + ### `cdk destroy` Deletes a stack from it's environment. This will cause the resources in the stack to be destroyed (unless they were diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 1b52fb736e946..2dfb2f5119c71 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -174,6 +174,7 @@ export interface DeployStackOptions { } const LARGE_TEMPLATE_SIZE_KB = 50; +const CDK_CHANGE_SET_NAME = 'cdk-deploy-change-set'; /** @experimental */ export async function deployStack(options: DeployStackOptions): Promise { @@ -228,14 +229,20 @@ export async function deployStack(options: DeployStackOptions): Promise { executed = true; return {}; }), + deleteChangeSet: jest.fn(), getTemplate: jest.fn(() => { executed = true; return {}; diff --git a/packages/aws-cdk/test/api/deploy-stack.test.ts b/packages/aws-cdk/test/api/deploy-stack.test.ts index 1908b4716aac0..8fffb321ef995 100644 --- a/packages/aws-cdk/test/api/deploy-stack.test.ts +++ b/packages/aws-cdk/test/api/deploy-stack.test.ts @@ -459,6 +459,53 @@ test('not executed and no error if --no-execute is given', async () => { expect(cfnMocks.executeChangeSet).not.toHaveBeenCalled(); }); +test('empty change set is deleted if --execute is given', async () => { + cfnMocks.describeChangeSet?.mockImplementation(() => ({ + Status: 'FAILED', + StatusReason: 'No updates are to be performed.', + })); + + // GIVEN + givenStackExists(); + + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + execute: true, + force: true, // Necessary to bypass "skip deploy" + }); + + // THEN + expect(cfnMocks.createChangeSet).toHaveBeenCalled(); + expect(cfnMocks.executeChangeSet).not.toHaveBeenCalled(); + + //the first deletion is for any existing cdk change sets, the second is for the deleting the new empty change set + expect(cfnMocks.deleteChangeSet).toHaveBeenCalledTimes(2); +}); + +test('empty change set is not deleted if --no-execute is given', async () => { + cfnMocks.describeChangeSet?.mockImplementation(() => ({ + Status: 'FAILED', + StatusReason: 'No updates are to be performed.', + })); + + // GIVEN + givenStackExists(); + + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + execute: false, + }); + + // THEN + expect(cfnMocks.createChangeSet).toHaveBeenCalled(); + expect(cfnMocks.executeChangeSet).not.toHaveBeenCalled(); + + //the first deletion is for any existing cdk change sets + expect(cfnMocks.deleteChangeSet).toHaveBeenCalledTimes(1); +}); + test('use S3 url for stack deployment if present in Stack Artifact', async () => { // WHEN await deployStack({