@@ -4,6 +4,7 @@ import * as cxapi from '@aws-cdk/cloud-assembly-api';
44import type { FeatureFlagReportProperties } from '@aws-cdk/cloud-assembly-schema' ;
55import { ArtifactType } from '@aws-cdk/cloud-assembly-schema' ;
66import type { TemplateDiff } from '@aws-cdk/cloudformation-diff' ;
7+ import type { DescribeChangeSetCommandOutput } from '@aws-sdk/client-cloudformation' ;
78import * as chalk from 'chalk' ;
89import * as chokidar from 'chokidar' ;
910import { type EventName , EVENTS } from 'chokidar/handler.js' ;
@@ -35,7 +36,7 @@ import type {
3536 EnvironmentBootstrapResult ,
3637} from '../actions/bootstrap' ;
3738import { BootstrapSource } from '../actions/bootstrap' ;
38- import { AssetBuildTime , type DeployOptions } from '../actions/deploy' ;
39+ import { AssetBuildTime , type DeploymentMethod , type DeployOptions } from '../actions/deploy' ;
3940import {
4041 buildParameterMap ,
4142 type PrivateDeployOptions ,
@@ -66,7 +67,7 @@ import type { StackAssembly } from '../api/cloud-assembly/private';
6667import { ALL_STACKS } from '../api/cloud-assembly/private' ;
6768import { CloudAssemblySourceBuilder } from '../api/cloud-assembly/source-builder' ;
6869import type { StackCollection } from '../api/cloud-assembly/stack-collection' ;
69- import { Deployments } from '../api/deployments' ;
70+ import { changeSetHasNoChanges , Deployments } from '../api/deployments' ;
7071import { DiffFormatter } from '../api/diff' ;
7172import { detectStackDrift } from '../api/drift' ;
7273import { DriftFormatter } from '../api/drift/drift-formatter' ;
@@ -614,32 +615,6 @@ export class Toolkit extends CloudAssemblySourceBuilder {
614615 return ;
615616 }
616617
617- const currentTemplate = await deployments . readCurrentTemplate ( stack ) ;
618-
619- const formatter = new DiffFormatter ( {
620- templateInfo : {
621- oldTemplate : currentTemplate ,
622- newTemplate : stack ,
623- } ,
624- } ) ;
625-
626- const securityDiff = formatter . formatSecurityDiff ( ) ;
627-
628- // Send a request response with the formatted security diff as part of the message,
629- // and the template diff as data
630- // (IoHost decides whether to print depending on permissionChangeType)
631- const deployMotivation = '"--require-approval" is enabled and stack includes security-sensitive updates.' ;
632- const deployQuestion = `${ securityDiff . formattedDiff } \n\n${ deployMotivation } \nDo you wish to deploy these changes` ;
633- const deployConfirmed = await ioHelper . requestResponse ( IO . CDK_TOOLKIT_I5060 . req ( deployQuestion , {
634- motivation : deployMotivation ,
635- concurrency,
636- permissionChangeType : securityDiff . permissionChangeType ,
637- templateDiffs : formatter . diffs ,
638- } ) ) ;
639- if ( ! deployConfirmed ) {
640- throw new ToolkitError ( 'Aborted by user' ) ;
641- }
642-
643618 // Following are the same semantics we apply with respect to Notification ARNs (dictated by the SDK)
644619 //
645620 // - undefined => cdk ignores it, as if it wasn't supported (allows external management).
@@ -655,6 +630,63 @@ export class Toolkit extends CloudAssemblySourceBuilder {
655630 }
656631 }
657632
633+ const tags = ( options . tags && options . tags . length > 0 ) ? options . tags : tagsForStack ( stack ) ;
634+
635+ let deploymentMethod : DeploymentMethod | undefined ;
636+ let changeSet : DescribeChangeSetCommandOutput | undefined ;
637+ if ( options . deploymentMethod ?. method === 'change-set' ) {
638+ // Create a CloudFormation change set
639+ const changeSetName = options . deploymentMethod ?. changeSetName || `cdk-deploy-change-set-${ Date . now ( ) } ` ;
640+ await deployments . deployStack ( {
641+ stack,
642+ deployName : stack . stackName ,
643+ roleArn : options . roleArn ,
644+ toolkitStackName : this . toolkitStackName ,
645+ reuseAssets : options . reuseAssets ,
646+ notificationArns,
647+ tags,
648+ deploymentMethod : { method : 'change-set' as const , changeSetName, execute : false } ,
649+ forceDeployment : options . forceDeployment ,
650+ parameters : Object . assign ( { } , parameterMap [ '*' ] , parameterMap [ stack . stackName ] ) ,
651+ usePreviousParameters : options . parameters ?. keepExistingParameters ,
652+ extraUserAgent : options . extraUserAgent ,
653+ assetParallelism : options . assetParallelism ,
654+ } ) ;
655+
656+ // Describe the change set to be presented to the user
657+ changeSet = await deployments . describeChangeSet ( stack , changeSetName ) ;
658+
659+ // Don't continue deploying the stack if there are no changes (unless forced)
660+ if ( ! options . forceDeployment && changeSetHasNoChanges ( changeSet ) ) {
661+ await deployments . deleteChangeSet ( stack , changeSet . ChangeSetName ! ) ;
662+ return ioHelper . notify ( IO . CDK_TOOLKIT_W5023 . msg ( `${ chalk . bold ( stack . displayName ) } : stack has no changes, skipping deployment.` ) ) ;
663+ }
664+
665+ // Adjust the deployment method for the subsequent deployment to execute the existing change set
666+ deploymentMethod = { ...options . deploymentMethod , changeSetName, executeExistingChangeSet : true } ;
667+ }
668+ // Present the diff to the user
669+ const oldTemplate = await deployments . readCurrentTemplate ( stack ) ;
670+ const formatter = new DiffFormatter ( { templateInfo : { oldTemplate, newTemplate : stack , changeSet } } ) ;
671+ const diff = formatter . formatStackDiff ( ) ;
672+
673+ // Send a request response with the formatted diff as part of the message, and the template diff as data
674+ // (IoHost decides whether to print depending on permissionChangeType)
675+ const deployMotivation = 'Approval required for stack deployment.' ;
676+ const deployQuestion = `${ diff . formattedDiff } \n\n${ deployMotivation } \nDo you wish to deploy these changes` ;
677+ const deployConfirmed = await ioHelper . requestResponse ( IO . CDK_TOOLKIT_I5060 . req ( deployQuestion , {
678+ motivation : deployMotivation ,
679+ concurrency,
680+ permissionChangeType : diff . permissionChangeType ,
681+ templateDiffs : formatter . diffs ,
682+ } ) ) ;
683+ if ( ! deployConfirmed ) {
684+ if ( changeSet ?. ChangeSetName ) {
685+ await deployments . deleteChangeSet ( stack , changeSet . ChangeSetName ) ;
686+ }
687+ throw new ToolkitError ( 'Aborted by user' ) ;
688+ }
689+
658690 const stackIndex = stacks . indexOf ( stack ) + 1 ;
659691 const deploySpan = await ioHelper . span ( SPAN . DEPLOY_STACK )
660692 . begin ( `${ chalk . bold ( stack . displayName ) } : deploying... [${ stackIndex } /${ stackCollection . stackCount } ]` , {
@@ -663,11 +695,6 @@ export class Toolkit extends CloudAssemblySourceBuilder {
663695 stack,
664696 } ) ;
665697
666- let tags = options . tags ;
667- if ( ! tags || tags . length === 0 ) {
668- tags = tagsForStack ( stack ) ;
669- }
670-
671698 let deployDuration ;
672699 try {
673700 let deployResult : SuccessfulDeployStackResult | undefined ;
@@ -687,7 +714,7 @@ export class Toolkit extends CloudAssemblySourceBuilder {
687714 reuseAssets : options . reuseAssets ,
688715 notificationArns,
689716 tags,
690- deploymentMethod : options . deploymentMethod ,
717+ deploymentMethod : deploymentMethod ?? options . deploymentMethod ,
691718 forceDeployment : options . forceDeployment ,
692719 parameters : Object . assign ( { } , parameterMap [ '*' ] , parameterMap [ stack . stackName ] ) ,
693720 usePreviousParameters : options . parameters ?. keepExistingParameters ,
0 commit comments