@@ -4,6 +4,7 @@ import type { FeatureFlagReportProperties } from '@aws-cdk/cloud-assembly-schema
44import { ArtifactType } from '@aws-cdk/cloud-assembly-schema' ;
55import type { TemplateDiff } from '@aws-cdk/cloudformation-diff' ;
66import * as cxapi from '@aws-cdk/cx-api' ;
7+ import type { DescribeChangeSetCommandOutput } from '@aws-sdk/client-cloudformation' ;
78import * as chalk from 'chalk' ;
89import * as chokidar from 'chokidar' ;
910import * as fs from 'fs-extra' ;
@@ -19,7 +20,7 @@ import type {
1920 EnvironmentBootstrapResult ,
2021} from '../actions/bootstrap' ;
2122import { BootstrapSource } from '../actions/bootstrap' ;
22- import { AssetBuildTime , type DeployOptions } from '../actions/deploy' ;
23+ import { AssetBuildTime , type DeploymentMethod , type DeployOptions } from '../actions/deploy' ;
2324import {
2425 buildParameterMap ,
2526 type PrivateDeployOptions ,
@@ -606,32 +607,6 @@ export class Toolkit extends CloudAssemblySourceBuilder {
606607 return ;
607608 }
608609
609- const currentTemplate = await deployments . readCurrentTemplate ( stack ) ;
610-
611- const formatter = new DiffFormatter ( {
612- templateInfo : {
613- oldTemplate : currentTemplate ,
614- newTemplate : stack ,
615- } ,
616- } ) ;
617-
618- const securityDiff = formatter . formatSecurityDiff ( ) ;
619-
620- // Send a request response with the formatted security diff as part of the message,
621- // and the template diff as data
622- // (IoHost decides whether to print depending on permissionChangeType)
623- const deployMotivation = '"--require-approval" is enabled and stack includes security-sensitive updates.' ;
624- const deployQuestion = `${ securityDiff . formattedDiff } \n\n${ deployMotivation } \nDo you wish to deploy these changes` ;
625- const deployConfirmed = await ioHelper . requestResponse ( IO . CDK_TOOLKIT_I5060 . req ( deployQuestion , {
626- motivation : deployMotivation ,
627- concurrency,
628- permissionChangeType : securityDiff . permissionChangeType ,
629- templateDiffs : formatter . diffs ,
630- } ) ) ;
631- if ( ! deployConfirmed ) {
632- throw new ToolkitError ( 'Aborted by user' ) ;
633- }
634-
635610 // Following are the same semantics we apply with respect to Notification ARNs (dictated by the SDK)
636611 //
637612 // - undefined => cdk ignores it, as if it wasn't supported (allows external management).
@@ -647,6 +622,67 @@ export class Toolkit extends CloudAssemblySourceBuilder {
647622 }
648623 }
649624
625+ const tags = ( options . tags && options . tags . length > 0 ) ? options . tags : tagsForStack ( stack ) ;
626+
627+ let deploymentMethod : DeploymentMethod | undefined ;
628+ let changeSet : DescribeChangeSetCommandOutput | undefined ;
629+ if ( options . deploymentMethod ?. method === 'change-set' ) {
630+ // Create a CloudFormation change set
631+ const changeSetName = options . deploymentMethod ?. changeSetName || `cdk-change-set-${ Date . now ( ) } ` ;
632+ const r = await deployments . deployStack ( {
633+ stack,
634+ deployName : stack . stackName ,
635+ roleArn : options . roleArn ,
636+ toolkitStackName : this . toolkitStackName ,
637+ reuseAssets : options . reuseAssets ,
638+ notificationArns,
639+ tags,
640+ deploymentMethod : { method : 'change-set' as const , changeSetName, execute : false } ,
641+ forceDeployment : options . forceDeployment ,
642+ parameters : Object . assign ( { } , parameterMap [ '*' ] , parameterMap [ stack . stackName ] ) ,
643+ usePreviousParameters : options . parameters ?. keepExistingParameters ,
644+ extraUserAgent : options . extraUserAgent ,
645+ assetParallelism : options . assetParallelism ,
646+ } ) ;
647+
648+ if ( r . type !== 'did-deploy-stack' ) {
649+ throw new ToolkitError ( `Unable to create CloudFormation change set for stack: '${ stack . stackName } ', ${ r . type } ` ) ;
650+ }
651+
652+ // Describe the change set to be presented to the user
653+ changeSet = await deployments . describeChangeSet ( stack , changeSetName ) ;
654+
655+ // Don't continue deploying the stack if there are no changes (unless forced)
656+ if ( ! options . forceDeployment && changeSet . ChangeSetName && ( changeSet . Changes === undefined || changeSet . Changes . length === 0 ) ) {
657+ await deployments . deleteChangeSet ( stack , changeSet . ChangeSetName ) ;
658+ return ioHelper . notify ( IO . CDK_TOOLKIT_W5023 . msg ( `${ chalk . bold ( stack . displayName ) } : stack has no changes, skipping deployment.` ) ) ;
659+ }
660+
661+ // Adjust the deployment method for the subsequent deployment to execute the existing change set
662+ deploymentMethod = { ...options . deploymentMethod , changeSetName, executeExistingChangeSet : true } ;
663+ }
664+ // Present the diff to the user
665+ const oldTemplate = await deployments . readCurrentTemplate ( stack ) ;
666+ const formatter = new DiffFormatter ( { templateInfo : { oldTemplate, newTemplate : stack , changeSet } } ) ;
667+ const diff = formatter . formatStackDiff ( ) ;
668+
669+ // Send a request response with the formatted security diff as part of the message, and the template diff as data
670+ // (IoHost decides whether to print depending on permissionChangeType)
671+ const deployMotivation = 'Approval required for stack deployment.' ;
672+ const deployQuestion = `${ diff . formattedDiff } \n\n${ deployMotivation } \nDo you wish to deploy these changes` ;
673+ const deployConfirmed = await ioHelper . requestResponse ( IO . CDK_TOOLKIT_I5060 . req ( deployQuestion , {
674+ motivation : deployMotivation ,
675+ concurrency,
676+ permissionChangeType : diff . permissionChangeType ,
677+ templateDiffs : formatter . diffs ,
678+ } ) ) ;
679+ if ( ! deployConfirmed ) {
680+ if ( changeSet ?. ChangeSetName ) {
681+ await deployments . deleteChangeSet ( stack , changeSet . ChangeSetName ) ;
682+ }
683+ throw new ToolkitError ( 'Aborted by user' ) ;
684+ }
685+
650686 const stackIndex = stacks . indexOf ( stack ) + 1 ;
651687 const deploySpan = await ioHelper . span ( SPAN . DEPLOY_STACK )
652688 . begin ( `${ chalk . bold ( stack . displayName ) } : deploying... [${ stackIndex } /${ stackCollection . stackCount } ]` , {
@@ -655,11 +691,6 @@ export class Toolkit extends CloudAssemblySourceBuilder {
655691 stack,
656692 } ) ;
657693
658- let tags = options . tags ;
659- if ( ! tags || tags . length === 0 ) {
660- tags = tagsForStack ( stack ) ;
661- }
662-
663694 let deployDuration ;
664695 try {
665696 let deployResult : SuccessfulDeployStackResult | undefined ;
@@ -679,7 +710,7 @@ export class Toolkit extends CloudAssemblySourceBuilder {
679710 reuseAssets : options . reuseAssets ,
680711 notificationArns,
681712 tags,
682- deploymentMethod : options . deploymentMethod ,
713+ deploymentMethod : deploymentMethod ?? options . deploymentMethod ,
683714 forceDeployment : options . forceDeployment ,
684715 parameters : Object . assign ( { } , parameterMap [ '*' ] , parameterMap [ stack . stackName ] ) ,
685716 usePreviousParameters : options . parameters ?. keepExistingParameters ,
0 commit comments