diff --git a/.vscode/launch.json b/.vscode/launch.json index 66f6db80dcd14..936bf55717b05 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,7 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { // Has convenient settings for attaching to a NodeJS process for debugging purposes // that are NOT the default and otherwise every developers has to configure for diff --git a/packages/@aws-cdk-testing/framework-integ/test/pipelines/test/integ.newpipeline.ts b/packages/@aws-cdk-testing/framework-integ/test/pipelines/test/integ.newpipeline.ts index 258b3e80d1bdc..7e1a2b53d53b2 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/pipelines/test/integ.newpipeline.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/pipelines/test/integ.newpipeline.ts @@ -57,4 +57,4 @@ const app = new App({ }, }); new PipelineStack(app, 'PipelineStack'); -app.synth(); \ No newline at end of file +app.synth(); diff --git a/packages/@aws-cdk-testing/framework-integ/test/pipelines/test/integ.newpipeline_with_allPrepareNodesFirst.ts b/packages/@aws-cdk-testing/framework-integ/test/pipelines/test/integ.newpipeline_with_allPrepareNodesFirst.ts new file mode 100644 index 0000000000000..391166fd12628 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/pipelines/test/integ.newpipeline_with_allPrepareNodesFirst.ts @@ -0,0 +1,75 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +/// !cdk-integ PipelineStack pragma:set-context:@aws-cdk/core:newStyleStackSynthesis=true +import { App, Stack, StackProps, Stage, StageProps } from 'aws-cdk-lib'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; +import * as pipelines from 'aws-cdk-lib/pipelines'; +import { Construct } from 'constructs'; + +class PipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.gitHub( + 'rix0rrr/cdk-pipelines-demo', + 'main', + ), + commands: ['npm ci', 'npm run build', 'npx cdk synth'], + }), + allPrepareNodesFirst: true, + }); + + pipeline.addStage(new AppStage(this, 'Beta'), { + }); + + const group = pipeline.addWave('Wave1'); + group.addStage(new AppStage(this, 'Prod1')); + group.addStage(new AppStage(this, 'Prod2')); + + const group2 = pipeline.addWave('Wave2'); + group2.addStage(new AppStage2(this, 'Prod3')); + group2.addStage(new AppStage3(this, 'Prod4')); + } +} + +class AppStage extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const stack1 = new Stack(this, 'Stack1'); + const queue1 = new sqs.Queue(stack1, 'Queue'); + + const stack2 = new Stack(this, 'Stack2'); + new sqs.Queue(stack2, 'OtherQueue', { + deadLetterQueue: { + queue: queue1, + maxReceiveCount: 5, + }, + }); + } +} + +class AppStage2 extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + new Stack(this, 'Stack1'); + } +} + +class AppStage3 extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + new Stack(this, 'Stack2'); + } +} + +const app = new App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': '1', + }, +}); +new PipelineStack(app, 'PipelineWithAllPrepareNodesFirstStack'); +app.synth(); diff --git a/packages/@aws-cdk-testing/framework-integ/test/pipelines/test/integ.newpipeline_with_postPrepare.ts b/packages/@aws-cdk-testing/framework-integ/test/pipelines/test/integ.newpipeline_with_postPrepare.ts new file mode 100644 index 0000000000000..72ca8de2d3843 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/pipelines/test/integ.newpipeline_with_postPrepare.ts @@ -0,0 +1,79 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +/// !cdk-integ PipelineStack pragma:set-context:@aws-cdk/core:newStyleStackSynthesis=true +import { App, Stack, StackProps, Stage, StageProps } from 'aws-cdk-lib'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; +import * as pipelines from 'aws-cdk-lib/pipelines'; +import { Construct } from 'constructs'; + +class PipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const pipeline = new pipelines.CodePipeline(this, 'PipelineWithPostPrepare', { + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.gitHub( + 'rix0rrr/cdk-pipelines-demo', + 'main', + ), + commands: ['npm ci', 'npm run build', 'npx cdk synth'], + }), + allPrepareNodesFirst: true, + }); + + pipeline.addStage(new AppStage(this, 'Beta'), { + postPrepare: [new pipelines.ManualApprovalStep('Approval0')], + }); + + const group = pipeline.addWave('Wave1', { + + postPrepare: [new pipelines.ManualApprovalStep('Approval1')], + }); + group.addStage(new AppStage(this, 'Prod1')); + group.addStage(new AppStage(this, 'Prod2')); + + const group2 = pipeline.addWave('Wave2', { postPrepare: [new pipelines.ManualApprovalStep('Approval2')] }); + group2.addStage(new AppStage2(this, 'Prod3')); + group2.addStage(new AppStage3(this, 'Prod4')); + } +} + +class AppStage extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const stack1 = new Stack(this, 'Stack1'); + const queue1 = new sqs.Queue(stack1, 'Queue'); + + const stack2 = new Stack(this, 'Stack2'); + new sqs.Queue(stack2, 'OtherQueue', { + deadLetterQueue: { + queue: queue1, + maxReceiveCount: 5, + }, + }); + } +} + +class AppStage2 extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + new Stack(this, 'Stack1'); + } +} + +class AppStage3 extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + new Stack(this, 'Stack2'); + } +} + +const app = new App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': '1', + }, +}); +new PipelineStack(app, 'PipelineWithPostPrepareStack'); +app.synth(); diff --git a/packages/aws-cdk-lib/pipelines/README.md b/packages/aws-cdk-lib/pipelines/README.md index 769e0714edffc..dccd121cd6774 100644 --- a/packages/aws-cdk-lib/pipelines/README.md +++ b/packages/aws-cdk-lib/pipelines/README.md @@ -1,6 +1,5 @@ # CDK Pipelines - A construct library for painless Continuous Delivery of CDK applications. CDK Pipelines is an *opinionated construct library*. It is purpose-built to @@ -162,9 +161,9 @@ has been bootstrapped (see below), and then execute deploying the Run the following commands to get the pipeline going: ```console -$ git commit -a -$ git push -$ cdk deploy PipelineStack +git commit -a +git push +cdk deploy PipelineStack ``` Administrative permissions to the account are only necessary up until @@ -565,6 +564,80 @@ class PipelineStack extends Stack { } ``` +#### Deploying with all change sets at first + +Deployment is done by default with `CodePipeline` engine using change sets, +i.e. to first create a change set and then execute it. This allows you to inject +steps that inspect the change set and approve or reject it, but failed deployments +are not retryable and creation of the change set costs time. The change sets tough are +being sorted within the pipeline by its dependencies. This means that some of the change set +might not be at the top level of a stage. Therefore there is the possibility to define, that +every change set is set as the first action (all in parallel) + +The creation of change sets at the top level can be switched on by setting `allPrepareNodesFirst: true`. +`useChangeSets` needs to be activated in order to use this feature. + +```ts +declare const synth: pipelines.ShellStep; + +class PipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth, + + allPrepareNodesFirst: true, + }); + } +} +``` + +It is further possible to add Steps in between the change sets and the deploy nodes (e.g. a manual approval step). This allows inspecting all change sets before deploying the stacks in the desired order. +```ts +declare const synth: pipelines.ShellStep; + +class PipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth, + + allPrepareNodesFirst: true, + }); + //example for a stage + pipeline.addStage(new AppStage(this, 'Beta'), { + postPrepare: [new pipelines.ManualApprovalStep('Approval0')], + }); + + // example for a wave + const group = pipeline.addWave('Wave1', { + + postPrepare: [new pipelines.ManualApprovalStep('Approval1')], + }); + group.addStage(new AppStage(this, 'Prod1')); + group.addStage(new AppStage(this, 'Prod2')); + } +} +class AppStage extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + + const stack1 = new Stack(this, 'Stack1'); + const queue1 = new sqs.Queue(stack1, 'Queue'); + + const stack2 = new Stack(this, 'Stack2'); + new sqs.Queue(stack2, 'OtherQueue', { + deadLetterQueue: { + queue: queue1, + maxReceiveCount: 5, + }, + }); + } +} +``` + ### Validation Every `addStage()` and `addWave()` command takes additional options. As part of these options, @@ -1608,7 +1681,7 @@ $ env CDK_NEW_BOOTSTRAP=1 npx cdk bootstrap \ ``` - Update all impacted stacks in the pipeline to use this new qualifier. -See https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html for more info. +See for more info. ```ts new Stack(this, 'MyStack', { diff --git a/packages/aws-cdk-lib/pipelines/lib/blueprint/stack-deployment.ts b/packages/aws-cdk-lib/pipelines/lib/blueprint/stack-deployment.ts index aa4f7e6c25ffa..65c3d1a8f835c 100644 --- a/packages/aws-cdk-lib/pipelines/lib/blueprint/stack-deployment.ts +++ b/packages/aws-cdk-lib/pipelines/lib/blueprint/stack-deployment.ts @@ -1,9 +1,9 @@ import * as path from 'path'; import * as cxapi from '../../../cx-api'; -import { AssetType } from './asset-type'; -import { Step } from './step'; import { AssetManifestReader, DockerImageManifestEntry, FileManifestEntry } from '../private/asset-manifest'; import { isAssetManifest } from '../private/cloud-assembly-internals'; +import { AssetType } from './asset-type'; +import { Step } from './step'; /** * Properties for a `StackDeployment` @@ -91,11 +91,14 @@ export class StackDeployment { /** * Build a `StackDeployment` from a Stack Artifact in a Cloud Assembly. */ - public static fromArtifact(stackArtifact: cxapi.CloudFormationStackArtifact): StackDeployment { + public static fromArtifact( + stackArtifact: cxapi.CloudFormationStackArtifact, + ): StackDeployment { const artRegion = stackArtifact.environment.region; const region = artRegion === cxapi.UNKNOWN_REGION ? undefined : artRegion; const artAccount = stackArtifact.environment.account; - const account = artAccount === cxapi.UNKNOWN_ACCOUNT ? undefined : artAccount; + const account = + artAccount === cxapi.UNKNOWN_ACCOUNT ? undefined : artAccount; return new StackDeployment({ account, @@ -104,7 +107,10 @@ export class StackDeployment { stackArtifactId: stackArtifact.id, constructPath: stackArtifact.hierarchicalId, stackName: stackArtifact.stackName, - absoluteTemplatePath: path.join(stackArtifact.assembly.directory, stackArtifact.templateFile), + absoluteTemplatePath: path.join( + stackArtifact.assembly.directory, + stackArtifact.templateFile, + ), assumeRoleArn: stackArtifact.assumeRoleArn, executionRoleArn: stackArtifact.cloudFormationExecutionRoleArn, assets: extractStackAssets(stackArtifact), @@ -206,6 +212,12 @@ export class StackDeployment { */ public readonly post: Step[] = []; + /** + * Additional steps to run after all of the prepare-nodes in the stage + */ + + public readonly postPrepare: Step[] = []; + private constructor(props: StackDeploymentProps) { this.stackArtifactId = props.stackArtifactId; this.constructPath = props.constructPath; @@ -216,7 +228,9 @@ export class StackDeployment { this.executionRoleArn = props.executionRoleArn; this.stackName = props.stackName; this.absoluteTemplatePath = props.absoluteTemplatePath; - this.templateUrl = props.templateS3Uri ? s3UrlFromUri(props.templateS3Uri, props.region) : undefined; + this.templateUrl = props.templateS3Uri + ? s3UrlFromUri(props.templateS3Uri, props.region) + : undefined; this.assets = new Array(); @@ -242,10 +256,16 @@ export class StackDeployment { * @param changeSet steps executed after stack.prepare and before stack.deploy * @param post steps executed after stack.deploy */ - public addStackSteps(pre: Step[], changeSet: Step[], post: Step[]) { + public addStackSteps( + pre: Step[], + changeSet: Step[], + post: Step[], + postPrepare: Step[], + ) { this.pre.push(...pre); this.changeSet.push(...changeSet); this.post.push(...post); + this.postPrepare.push(...postPrepare); } } diff --git a/packages/aws-cdk-lib/pipelines/lib/blueprint/stage-deployment.ts b/packages/aws-cdk-lib/pipelines/lib/blueprint/stage-deployment.ts index b2fedfcc0dd9f..78fa4f79d3daa 100644 --- a/packages/aws-cdk-lib/pipelines/lib/blueprint/stage-deployment.ts +++ b/packages/aws-cdk-lib/pipelines/lib/blueprint/stage-deployment.ts @@ -1,9 +1,9 @@ import * as cdk from '../../../core'; import { CloudFormationStackArtifact } from '../../../cx-api'; -import { StackDeployment } from './stack-deployment'; -import { StackSteps, Step } from './step'; import { isStackArtifact } from '../private/cloud-assembly-internals'; import { pipelineSynth } from '../private/construct-internals'; +import { StackDeployment } from './stack-deployment'; +import { StackSteps, Step } from './step'; /** * Properties for a `StageDeployment` @@ -30,6 +30,13 @@ export interface StageDeploymentProps { */ readonly post?: Step[]; + /** + * Additional steps to run after all of the prepare-nodes in the stage. If this property is set allPrepareNodesFirst has to be set to true also. This is the case, because dependency cycle will occour otherwise. + * + * @default - No additional steps + */ + readonly postPrepare?: Step[]; + /** * Instructions for additional steps that are run at the stack level * @@ -56,36 +63,54 @@ export class StageDeployment { if (assembly.stacks.length === 0) { // If we don't check here, a more puzzling "stage contains no actions" // error will be thrown come deployment time. - throw new Error(`The given Stage construct ('${stage.node.path}') should contain at least one Stack`); + throw new Error( + `The given Stage construct ('${stage.node.path}') should contain at least one Stack`, + ); } - const stepFromArtifact = new Map(); + const stepFromArtifact = new Map< + CloudFormationStackArtifact, + StackDeployment + >(); for (const artifact of assembly.stacks) { const step = StackDeployment.fromArtifact(artifact); stepFromArtifact.set(artifact, step); } if (props.stackSteps) { for (const stackstep of props.stackSteps) { - const stackArtifact = assembly.getStackArtifact(stackstep.stack.artifactId); + const stackArtifact = assembly.getStackArtifact( + stackstep.stack.artifactId, + ); const thisStep = stepFromArtifact.get(stackArtifact); if (!thisStep) { - throw new Error('Logic error: we just added a step for this artifact but it disappeared.'); + throw new Error( + 'Logic error: we just added a step for this artifact but it disappeared.', + ); } - thisStep.addStackSteps(stackstep.pre ?? [], stackstep.changeSet ?? [], stackstep.post ?? []); + thisStep.addStackSteps( + stackstep.pre ?? [], + stackstep.changeSet ?? [], + stackstep.post ?? [], + stackstep.postPrepare ?? [], + ); } } for (const artifact of assembly.stacks) { const thisStep = stepFromArtifact.get(artifact); if (!thisStep) { - throw new Error('Logic error: we just added a step for this artifact but it disappeared.'); + throw new Error( + 'Logic error: we just added a step for this artifact but it disappeared.', + ); } const stackDependencies = artifact.dependencies.filter(isStackArtifact); for (const dep of stackDependencies) { const depStep = stepFromArtifact.get(dep); if (!depStep) { - throw new Error(`Stack '${artifact.id}' depends on stack not found in same Stage: '${dep.id}'`); + throw new Error( + `Stack '${artifact.id}' depends on stack not found in same Stage: '${dep.id}'`, + ); } thisStep.addStackDependency(depStep); } @@ -112,6 +137,13 @@ export class StageDeployment { */ public readonly post: Step[]; + /** + * Additional steps to run after all of the prepare-nodes in the stage. If this property is set allPrepareNodesFirst has to be set to true also. This is the case, because dependency cycle will occour otherwise. + * + * @default - No additional steps + */ + public readonly postPrepare: Step[]; + /** * Instructions for additional steps that are run at stack level */ @@ -123,12 +155,16 @@ export class StageDeployment { */ public readonly prepareStep?: boolean; + private constructor( /** The stacks deployed in this stage */ - public readonly stacks: StackDeployment[], props: StageDeploymentProps = {}) { + public readonly stacks: StackDeployment[], + props: StageDeploymentProps = {}, + ) { this.stageName = props.stageName ?? ''; this.pre = props.pre ?? []; this.post = props.post ?? []; + this.postPrepare = props.postPrepare ?? []; this.stackSteps = props.stackSteps ?? []; } @@ -145,4 +181,11 @@ export class StageDeployment { public addPost(...steps: Step[]) { this.post.push(...steps); } + + /** + * Add an additional step to run after all of the stacks in this stage + */ + public addPostPrepare(...steps: Step[]) { + this.postPrepare.push(...steps); + } } \ No newline at end of file diff --git a/packages/aws-cdk-lib/pipelines/lib/blueprint/step.ts b/packages/aws-cdk-lib/pipelines/lib/blueprint/step.ts index 3c61e7df5d309..6fc586fb68ed5 100644 --- a/packages/aws-cdk-lib/pipelines/lib/blueprint/step.ts +++ b/packages/aws-cdk-lib/pipelines/lib/blueprint/step.ts @@ -1,7 +1,7 @@ import { Stack, Token } from '../../../core'; +import { StepOutput } from '../helpers-internal/step-output'; import { FileSet, IFileSetProducer } from './file-set'; import { StackOutputReference } from './shell-step'; -import { StepOutput } from '../helpers-internal/step-output'; /** * A generic Step which can be added to a Pipeline @@ -142,6 +142,13 @@ export interface StackSteps { */ readonly pre?: Step[]; + /** + * Additional steps to run after all of the prepare-nodes in the stage. If this property is set allPrepareNodesFirst has to be set to true also. This is the case, because dependency cycle will occour otherwise. + * + * @default - No additional steps + */ + readonly postPrepare?: Step[]; + /** * Steps that execute after stack is prepared but before stack is deployed * diff --git a/packages/aws-cdk-lib/pipelines/lib/blueprint/wave.ts b/packages/aws-cdk-lib/pipelines/lib/blueprint/wave.ts index 5234ae18c8aec..a0b3a5e3df72b 100644 --- a/packages/aws-cdk-lib/pipelines/lib/blueprint/wave.ts +++ b/packages/aws-cdk-lib/pipelines/lib/blueprint/wave.ts @@ -19,6 +19,14 @@ export interface WaveProps { * @default - No additional steps */ readonly post?: Step[]; + + /** + * Additional steps to run after all of the prepare-nodes in the stage. If this property is set allPrepareNodesFirst has to be set to true also. This is the case, because dependency cycle will occour otherwise. + * + * @default - No additional steps + */ + readonly postPrepare?: Step[]; + } /** @@ -35,16 +43,27 @@ export class Wave { */ public readonly post: Step[]; + /** + * Additional steps to run after all of the prepare-nodes in the stage. If this property is set allPrepareNodesFirst has to be set to true also. This is the case, because dependency cycle will occour otherwise. + * + */ + public readonly postPrepare: Step[]; + /** * The stages that are deployed in this wave */ public readonly stages: StageDeployment[] = []; + constructor( /** Identifier for this Wave */ - public readonly id: string, props: WaveProps = {}) { + public readonly id: string, + props: WaveProps = {}, + ) { this.pre = props.pre ?? []; this.post = props.post ?? []; + this.postPrepare = props.postPrepare ?? []; + } /** @@ -72,6 +91,13 @@ export class Wave { public addPost(...steps: Step[]) { this.post.push(...steps); } + + /** + * Add an additional step to run after all of the stacks in this stage + */ + public addPostPrepare(...steps: Step[]) { + this.postPrepare.push(...steps); + } } /** @@ -92,12 +118,21 @@ export interface AddStageOpts { */ readonly post?: Step[]; + /** + * Additional steps to run after all of the prepare-nodes in the stage. If this property is set allPrepareNodesFirst has to be set to true also. This is the case, because dependency cycle will occour otherwise. + * + * @default - No additional steps + */ + readonly postPrepare?: Step[]; + /** * Instructions for stack level steps * * @default - No additional instructions */ readonly stackSteps?: StackSteps[]; + + } /** @@ -117,4 +152,12 @@ export interface WaveOptions { * @default - No additional steps */ readonly post?: Step[]; + + /** + * Additional steps to run after all of the prepare-nodes in the stage + * + * @default - No additional steps + */ + readonly postPrepare?: Step[]; + } \ No newline at end of file diff --git a/packages/aws-cdk-lib/pipelines/lib/codepipeline/codepipeline.ts b/packages/aws-cdk-lib/pipelines/lib/codepipeline/codepipeline.ts index 3971a0e108a4c..63d8e08f4df1d 100644 --- a/packages/aws-cdk-lib/pipelines/lib/codepipeline/codepipeline.ts +++ b/packages/aws-cdk-lib/pipelines/lib/codepipeline/codepipeline.ts @@ -1,3 +1,4 @@ +import { Construct } from 'constructs'; import * as fs from 'fs'; import * as path from 'path'; import * as cb from '../../../aws-codebuild'; @@ -6,18 +7,11 @@ import * as cpa from '../../../aws-codepipeline-actions'; import * as ec2 from '../../../aws-ec2'; import * as iam from '../../../aws-iam'; import * as s3 from '../../../aws-s3'; -import { Aws, CfnCapabilities, Duration, PhysicalName, Stack, Names } from '../../../core'; +import { Aws, CfnCapabilities, Duration, Names, PhysicalName, Stack } from '../../../core'; import * as cxapi from '../../../cx-api'; -import { Construct } from 'constructs'; -import { ArtifactMap } from './artifact-map'; -import { CodeBuildStep } from './codebuild-step'; -import { CodePipelineActionFactoryResult, ICodePipelineActionFactory } from './codepipeline-action-factory'; -import { CodeBuildFactory, mergeCodeBuildOptions } from './private/codebuild-factory'; -import { namespaceStepOutputs } from './private/outputs'; -import { StackOutputsMap } from './stack-outputs-map'; import { AssetType, FileSet, IFileSetProducer, ManualApprovalStep, ShellStep, StackAsset, StackDeployment, Step } from '../blueprint'; -import { DockerCredential, dockerCredentialsInstallCommands, DockerCredentialUsage } from '../docker-credentials'; -import { GraphNodeCollection, isGraph, AGraphNode, PipelineGraph } from '../helpers-internal'; +import { DockerCredential, DockerCredentialUsage, dockerCredentialsInstallCommands } from '../docker-credentials'; +import { AGraphNode, GraphNodeCollection, PipelineGraph, isGraph } from '../helpers-internal'; import { PipelineBase } from '../main'; import { AssetSingletonRole } from '../private/asset-singleton-role'; import { CachedFnSub } from '../private/cached-fnsub'; @@ -28,6 +22,12 @@ import { toPosixPath } from '../private/fs'; import { actionName, stackVariableNamespace } from '../private/identifiers'; import { enumerate, flatten, maybeSuffix, noUndefined } from '../private/javascript'; import { writeTemplateConfiguration } from '../private/template-configuration'; +import { ArtifactMap } from './artifact-map'; +import { CodeBuildStep } from './codebuild-step'; +import { CodePipelineActionFactoryResult, ICodePipelineActionFactory } from './codepipeline-action-factory'; +import { CodeBuildFactory, mergeCodeBuildOptions } from './private/codebuild-factory'; +import { namespaceStepOutputs } from './private/outputs'; +import { StackOutputsMap } from './stack-outputs-map'; /** @@ -245,6 +245,12 @@ export interface CodePipelineProps { * @default - A new S3 bucket will be created. */ readonly artifactBucket?: s3.IBucket; + + /** + * If all "prepare" step should be placed all together as the first actions within a stage/wave + */ + + readonly allPrepareNodesFirst?: boolean; } /** @@ -358,11 +364,12 @@ export class CodePipeline extends PipelineBase { private readonly dockerCredentials: DockerCredential[]; private readonly cachedFnSub = new CachedFnSub(); private stackOutputs: StackOutputsMap; - + private readonly allPrepareNodesFirst: boolean; /** * Asset roles shared for publishing */ - private readonly assetCodeBuildRoles: Map = new Map(); + private readonly assetCodeBuildRoles: Map = + new Map(); /** * This is set to the very first artifact produced in the pipeline @@ -374,7 +381,11 @@ export class CodePipeline extends PipelineBase { private readonly singlePublisherPerAssetType: boolean; private readonly cliVersion?: string; - constructor(scope: Construct, id: string, private readonly props: CodePipelineProps) { + constructor( + scope: Construct, + id: string, + private readonly props: CodePipelineProps, + ) { super(scope, id, props); this.selfMutationEnabled = props.selfMutation ?? true; @@ -383,6 +394,7 @@ export class CodePipeline extends PipelineBase { this.cliVersion = props.cliVersion ?? preferredCliVersion(); this.useChangeSets = props.useChangeSets ?? true; this.stackOutputs = new StackOutputsMap(this); + this.allPrepareNodesFirst = props.allPrepareNodesFirst ?? false; } /** @@ -392,7 +404,9 @@ export class CodePipeline extends PipelineBase { */ public get synthProject(): cb.IProject { if (!this._synthProject) { - throw new Error('Call pipeline.buildPipeline() before reading this property'); + throw new Error( + 'Call pipeline.buildPipeline() before reading this property', + ); } return this._synthProject; } @@ -405,10 +419,14 @@ export class CodePipeline extends PipelineBase { */ public get selfMutationProject(): cb.IProject { if (!this._pipeline) { - throw new Error('Call pipeline.buildPipeline() before reading this property'); + throw new Error( + 'Call pipeline.buildPipeline() before reading this property', + ); } if (!this._selfMutationProject) { - throw new Error('No selfMutationProject since the selfMutation property was set to false'); + throw new Error( + 'No selfMutationProject since the selfMutation property was set to false', + ); } return this._selfMutationProject; } @@ -425,7 +443,6 @@ export class CodePipeline extends PipelineBase { return this._pipeline; } - protected doBuildPipeline(): void { if (this._pipeline) { throw new Error('Pipeline already created'); @@ -435,26 +452,45 @@ export class CodePipeline extends PipelineBase { if (this.props.codePipeline) { if (this.props.pipelineName) { - throw new Error('Cannot set \'pipelineName\' if an existing CodePipeline is given using \'codePipeline\''); + throw new Error( + "Cannot set 'pipelineName' if an existing CodePipeline is given using 'codePipeline'", + ); } if (this.props.crossAccountKeys !== undefined) { - throw new Error('Cannot set \'crossAccountKeys\' if an existing CodePipeline is given using \'codePipeline\''); + throw new Error( + "Cannot set 'crossAccountKeys' if an existing CodePipeline is given using 'codePipeline'", + ); } if (this.props.enableKeyRotation !== undefined) { - throw new Error('Cannot set \'enableKeyRotation\' if an existing CodePipeline is given using \'codePipeline\''); + throw new Error( + "Cannot set 'enableKeyRotation' if an existing CodePipeline is given using 'codePipeline'", + ); } if (this.props.reuseCrossRegionSupportStacks !== undefined) { - throw new Error('Cannot set \'reuseCrossRegionSupportStacks\' if an existing CodePipeline is given using \'codePipeline\''); + throw new Error( + "Cannot set 'reuseCrossRegionSupportStacks' if an existing CodePipeline is given using 'codePipeline'", + ); } if (this.props.role !== undefined) { - throw new Error('Cannot set \'role\' if an existing CodePipeline is given using \'codePipeline\''); + throw new Error( + "Cannot set 'role' if an existing CodePipeline is given using 'codePipeline'", + ); } if (this.props.artifactBucket !== undefined) { - throw new Error('Cannot set \'artifactBucket\' if an existing CodePipeline is given using \'codePipeline\''); + throw new Error( + "Cannot set 'artifactBucket' if an existing CodePipeline is given using 'codePipeline'", + ); } this._pipeline = this.props.codePipeline; } else { + + if (this.props.allPrepareNodesFirst && this.props.useChangeSets === false) { + throw new Error( + "Cannot set 'allPrepareNodesFirst: true' if 'useChangeSets' is set to false.", + ); + } + this._pipeline = new cp.Pipeline(this, 'Pipeline', { pipelineName: this.props.pipelineName, crossAccountKeys: this.props.crossAccountKeys ?? false, @@ -472,6 +508,7 @@ export class CodePipeline extends PipelineBase { selfMutation: this.selfMutationEnabled, singlePublisherPerAssetType: this.singlePublisherPerAssetType, prepareStep: this.useChangeSets, + allPrepareNodesFirst: this.allPrepareNodesFirst, }); this._cloudAssemblyFileSet = graphFromBp.cloudAssemblyFileSet; @@ -479,12 +516,18 @@ export class CodePipeline extends PipelineBase { // Write a dotfile for the pipeline layout const dotFile = `${Names.uniqueId(this)}.dot`; - fs.writeFileSync(path.join(this.myCxAsmRoot, dotFile), graphFromBp.graph.renderDot().replace(/input\.dot/, dotFile), { encoding: 'utf-8' }); + fs.writeFileSync( + path.join(this.myCxAsmRoot, dotFile), + graphFromBp.graph.renderDot().replace(/input\.dot/, dotFile), + { encoding: 'utf-8' }, + ); } private get myCxAsmRoot(): string { if (!this._myCxAsmRoot) { - throw new Error('Can\'t read \'myCxAsmRoot\' if build deployment not called yet'); + throw new Error( + "Can't read 'myCxAsmRoot' if build deployment not called yet", + ); } return this._myCxAsmRoot; } @@ -503,7 +546,9 @@ export class CodePipeline extends PipelineBase { let beforeSelfMutation = this.selfMutationEnabled; for (const stageNode of flatten(structure.graph.sortedChildren())) { if (!isGraph(stageNode)) { - throw new Error(`Top-level children must be graphs, got '${stageNode}'`); + throw new Error( + `Top-level children must be graphs, got '${stageNode}'`, + ); } // Group our ordered tranches into blocks of 50. @@ -511,10 +556,14 @@ export class CodePipeline extends PipelineBase { const chunks = chunkTranches(50, stageNode.sortedLeaves()); const actionsOverflowStage = chunks.length > 1; for (const [i, tranches] of enumerate(chunks)) { - const stageName = actionsOverflowStage ? `${stageNode.id}.${i + 1}` : stageNode.id; + const stageName = actionsOverflowStage + ? `${stageNode.id}.${i + 1}` + : stageNode.id; const pipelineStage = this.pipeline.addStage({ stageName }); - const sharedParent = new GraphNodeCollection(flatten(tranches)).commonAncestor(); + const sharedParent = new GraphNodeCollection( + flatten(tranches), + ).commonAncestor(); let runOrder = 1; for (const tranche of tranches) { @@ -526,9 +575,10 @@ export class CodePipeline extends PipelineBase { const nodeType = this.nodeTypeFromNode(node); const name = actionName(node, sharedParent); - const variablesNamespace = node.data?.type === 'step' - ? namespaceStepOutputs(node.data.step, pipelineStage, name) - : undefined; + const variablesNamespace = + node.data?.type === 'step' + ? namespaceStepOutputs(node.data.step, pipelineStage, name) + : undefined; const result = factory.produceAction(pipelineStage, { actionName: name, @@ -538,7 +588,9 @@ export class CodePipeline extends PipelineBase { fallbackArtifact: this._fallbackArtifact, pipeline: this, // If this step happens to produce a CodeBuild job, set the default options - codeBuildDefaults: nodeType ? this.codeBuildDefaultsFor(nodeType) : undefined, + codeBuildDefaults: nodeType + ? this.codeBuildDefaultsFor(nodeType) + : undefined, beforeSelfMutation, variablesNamespace, stackOutputsMap: this.stackOutputs, @@ -565,11 +617,16 @@ export class CodePipeline extends PipelineBase { * Some minor state manipulation of CodeBuild projects and pipeline * artifacts. */ - private postProcessNode(node: AGraphNode, result: CodePipelineActionFactoryResult) { + private postProcessNode( + node: AGraphNode, + result: CodePipelineActionFactoryResult, + ) { const nodeType = this.nodeTypeFromNode(node); if (result.project) { - const dockerUsage = dockerUsageFromCodeBuild(nodeType ?? CodeBuildProjectType.STEP); + const dockerUsage = dockerUsageFromCodeBuild( + nodeType ?? CodeBuildProjectType.STEP, + ); if (dockerUsage) { for (const c of this.dockerCredentials) { c.grantRead(result.project, dockerUsage); @@ -584,8 +641,14 @@ export class CodePipeline extends PipelineBase { } } - if (node.data?.type === 'step' && node.data.step.primaryOutput?.primaryOutput && !this._fallbackArtifact) { - this._fallbackArtifact = this.artifacts.toCodePipeline(node.data.step.primaryOutput?.primaryOutput); + if ( + node.data?.type === 'step' && + node.data.step.primaryOutput?.primaryOutput && + !this._fallbackArtifact + ) { + this._fallbackArtifact = this.artifacts.toCodePipeline( + node.data.step.primaryOutput?.primaryOutput, + ); } } @@ -598,7 +661,9 @@ export class CodePipeline extends PipelineBase { case 'group': case 'stack-group': case undefined: - throw new Error(`actionFromNode: did not expect to get group nodes: ${node.data?.type}`); + throw new Error( + `actionFromNode: did not expect to get group nodes: ${node.data?.type}`, + ); case 'self-update': return this.selfMutateAction(); @@ -611,14 +676,22 @@ export class CodePipeline extends PipelineBase { case 'execute': return node.data.withoutChangeSet - ? this.executeDeploymentAction(node.data.stack, node.data.captureOutputs) - : this.executeChangeSetAction(node.data.stack, node.data.captureOutputs); + ? this.executeDeploymentAction( + node.data.stack, + node.data.captureOutputs, + ) + : this.executeChangeSetAction( + node.data.stack, + node.data.captureOutputs, + ); case 'step': return this.actionFromStep(node, node.data.step); default: - throw new Error(`CodePipeline does not support graph nodes of type '${node.data?.type}'. You are probably using a feature this CDK Pipelines implementation does not support.`); + throw new Error( + `CodePipeline does not support graph nodes of type '${node.data?.type}'. You are probably using a feature this CDK Pipelines implementation does not support.`, + ); } } @@ -634,7 +707,10 @@ export class CodePipeline extends PipelineBase { * The rest is expressed in terms of these 3, or in terms of graph nodes * which are handled elsewhere. */ - private actionFromStep(node: AGraphNode, step: Step): ICodePipelineActionFactory { + private actionFromStep( + node: AGraphNode, + step: Step, + ): ICodePipelineActionFactory { const nodeType = this.nodeTypeFromNode(node); // CodePipeline-specific steps first -- this includes Sources @@ -645,9 +721,8 @@ export class CodePipeline extends PipelineBase { // Now built-in steps if (step instanceof ShellStep || step instanceof CodeBuildStep) { // The 'CdkBuildProject' will be the construct ID of the CodeBuild project, necessary for backwards compat - let constructId = nodeType === CodeBuildProjectType.SYNTH - ? 'CdkBuildProject' - : step.id; + let constructId = + nodeType === CodeBuildProjectType.SYNTH ? 'CdkBuildProject' : step.id; return step instanceof CodeBuildStep ? CodeBuildFactory.fromCodeBuildStep(constructId, step) @@ -657,101 +732,174 @@ export class CodePipeline extends PipelineBase { if (step instanceof ManualApprovalStep) { return { produceAction: (stage, options) => { - stage.addAction(new cpa.ManualApprovalAction({ - actionName: options.actionName, - runOrder: options.runOrder, - additionalInformation: step.comment, - })); + stage.addAction( + new cpa.ManualApprovalAction({ + actionName: options.actionName, + runOrder: options.runOrder, + additionalInformation: step.comment, + }), + ); return { runOrdersConsumed: 1 }; }, }; } - throw new Error(`Deployment step '${step}' is not supported for CodePipeline-backed pipelines`); + throw new Error( + `Deployment step '${step}' is not supported for CodePipeline-backed pipelines`, + ); } - private createChangeSetAction(stack: StackDeployment): ICodePipelineActionFactory { + private createChangeSetAction( + stack: StackDeployment, + ): ICodePipelineActionFactory { const changeSetName = 'PipelineChange'; - const templateArtifact = this.artifacts.toCodePipeline(this._cloudAssemblyFileSet!); + const templateArtifact = this.artifacts.toCodePipeline( + this._cloudAssemblyFileSet!, + ); const templateConfigurationPath = this.writeTemplateConfiguration(stack); - const region = stack.region !== Stack.of(this).region ? stack.region : undefined; - const account = stack.account !== Stack.of(this).account ? stack.account : undefined; + const region = + stack.region !== Stack.of(this).region ? stack.region : undefined; + const account = + stack.account !== Stack.of(this).account ? stack.account : undefined; - const relativeTemplatePath = path.relative(this.myCxAsmRoot, stack.absoluteTemplatePath); + const relativeTemplatePath = path.relative( + this.myCxAsmRoot, + stack.absoluteTemplatePath, + ); return { produceAction: (stage, options) => { - stage.addAction(new cpa.CloudFormationCreateReplaceChangeSetAction({ - actionName: options.actionName, - runOrder: options.runOrder, - changeSetName, - stackName: stack.stackName, - templatePath: templateArtifact.atPath(toPosixPath(relativeTemplatePath)), - adminPermissions: true, - role: this.roleFromPlaceholderArn(this.pipeline, region, account, stack.assumeRoleArn), - deploymentRole: this.roleFromPlaceholderArn(this.pipeline, region, account, stack.executionRoleArn), - region: region, - templateConfiguration: templateConfigurationPath - ? templateArtifact.atPath(toPosixPath(templateConfigurationPath)) - : undefined, - cfnCapabilities: [CfnCapabilities.NAMED_IAM, CfnCapabilities.AUTO_EXPAND], - })); + stage.addAction( + new cpa.CloudFormationCreateReplaceChangeSetAction({ + actionName: options.actionName, + runOrder: options.runOrder, + changeSetName, + stackName: stack.stackName, + templatePath: templateArtifact.atPath( + toPosixPath(relativeTemplatePath), + ), + adminPermissions: true, + role: this.roleFromPlaceholderArn( + this.pipeline, + region, + account, + stack.assumeRoleArn, + ), + deploymentRole: this.roleFromPlaceholderArn( + this.pipeline, + region, + account, + stack.executionRoleArn, + ), + region: region, + templateConfiguration: templateConfigurationPath + ? templateArtifact.atPath(toPosixPath(templateConfigurationPath)) + : undefined, + cfnCapabilities: [ + CfnCapabilities.NAMED_IAM, + CfnCapabilities.AUTO_EXPAND, + ], + }), + ); return { runOrdersConsumed: 1 }; }, }; } - private executeChangeSetAction(stack: StackDeployment, captureOutputs: boolean): ICodePipelineActionFactory { + private executeChangeSetAction( + stack: StackDeployment, + captureOutputs: boolean, + ): ICodePipelineActionFactory { const changeSetName = 'PipelineChange'; - const region = stack.region !== Stack.of(this).region ? stack.region : undefined; - const account = stack.account !== Stack.of(this).account ? stack.account : undefined; + const region = + stack.region !== Stack.of(this).region ? stack.region : undefined; + const account = + stack.account !== Stack.of(this).account ? stack.account : undefined; return { produceAction: (stage, options) => { - stage.addAction(new cpa.CloudFormationExecuteChangeSetAction({ - actionName: options.actionName, - runOrder: options.runOrder, - changeSetName, - stackName: stack.stackName, - role: this.roleFromPlaceholderArn(this.pipeline, region, account, stack.assumeRoleArn), - region: region, - variablesNamespace: captureOutputs ? stackVariableNamespace(stack) : undefined, - })); + stage.addAction( + new cpa.CloudFormationExecuteChangeSetAction({ + actionName: options.actionName, + runOrder: options.runOrder, + changeSetName, + stackName: stack.stackName, + role: this.roleFromPlaceholderArn( + this.pipeline, + region, + account, + stack.assumeRoleArn, + ), + region: region, + variablesNamespace: captureOutputs + ? stackVariableNamespace(stack) + : undefined, + }), + ); return { runOrdersConsumed: 1 }; }, }; } - private executeDeploymentAction(stack: StackDeployment, captureOutputs: boolean): ICodePipelineActionFactory { - const templateArtifact = this.artifacts.toCodePipeline(this._cloudAssemblyFileSet!); + private executeDeploymentAction( + stack: StackDeployment, + captureOutputs: boolean, + ): ICodePipelineActionFactory { + const templateArtifact = this.artifacts.toCodePipeline( + this._cloudAssemblyFileSet!, + ); const templateConfigurationPath = this.writeTemplateConfiguration(stack); - const region = stack.region !== Stack.of(this).region ? stack.region : undefined; - const account = stack.account !== Stack.of(this).account ? stack.account : undefined; + const region = + stack.region !== Stack.of(this).region ? stack.region : undefined; + const account = + stack.account !== Stack.of(this).account ? stack.account : undefined; - const relativeTemplatePath = path.relative(this.myCxAsmRoot, stack.absoluteTemplatePath); + const relativeTemplatePath = path.relative( + this.myCxAsmRoot, + stack.absoluteTemplatePath, + ); return { produceAction: (stage, options) => { - stage.addAction(new cpa.CloudFormationCreateUpdateStackAction({ - actionName: options.actionName, - runOrder: options.runOrder, - stackName: stack.stackName, - templatePath: templateArtifact.atPath(toPosixPath(relativeTemplatePath)), - adminPermissions: true, - role: this.roleFromPlaceholderArn(this.pipeline, region, account, stack.assumeRoleArn), - deploymentRole: this.roleFromPlaceholderArn(this.pipeline, region, account, stack.executionRoleArn), - region: region, - templateConfiguration: templateConfigurationPath - ? templateArtifact.atPath(toPosixPath(templateConfigurationPath)) - : undefined, - cfnCapabilities: [CfnCapabilities.NAMED_IAM, CfnCapabilities.AUTO_EXPAND], - variablesNamespace: captureOutputs ? stackVariableNamespace(stack) : undefined, - })); + stage.addAction( + new cpa.CloudFormationCreateUpdateStackAction({ + actionName: options.actionName, + runOrder: options.runOrder, + stackName: stack.stackName, + templatePath: templateArtifact.atPath( + toPosixPath(relativeTemplatePath), + ), + adminPermissions: true, + role: this.roleFromPlaceholderArn( + this.pipeline, + region, + account, + stack.assumeRoleArn, + ), + deploymentRole: this.roleFromPlaceholderArn( + this.pipeline, + region, + account, + stack.executionRoleArn, + ), + region: region, + templateConfiguration: templateConfigurationPath + ? templateArtifact.atPath(toPosixPath(templateConfigurationPath)) + : undefined, + cfnCapabilities: [ + CfnCapabilities.NAMED_IAM, + CfnCapabilities.AUTO_EXPAND, + ], + variablesNamespace: captureOutputs + ? stackVariableNamespace(stack) + : undefined, + }), + ); return { runOrdersConsumed: 1 }; }, @@ -762,16 +910,17 @@ export class CodePipeline extends PipelineBase { const installSuffix = this.cliVersion ? `@${this.cliVersion}` : ''; const pipelineStack = Stack.of(this.pipeline); - const pipelineStackIdentifier = pipelineStack.node.path ?? pipelineStack.stackName; + const pipelineStackIdentifier = + pipelineStack.node.path ?? pipelineStack.stackName; const step = new CodeBuildStep('SelfMutate', { projectName: maybeSuffix(this.props.pipelineName, '-selfupdate'), input: this._cloudAssemblyFileSet, - installCommands: [ - `npm install -g aws-cdk${installSuffix}`, - ], + installCommands: [`npm install -g aws-cdk${installSuffix}`], commands: [ - `cdk -a ${toPosixPath(embeddedAsmPath(this.pipeline))} deploy ${pipelineStackIdentifier} --require-approval=never --verbose`, + `cdk -a ${toPosixPath( + embeddedAsmPath(this.pipeline), + )} deploy ${pipelineStackIdentifier} --require-approval=never --verbose`, ], rolePolicyStatements: [ @@ -781,7 +930,11 @@ export class CodePipeline extends PipelineBase { resources: [`arn:*:iam::${Stack.of(this.pipeline).account}:role/*`], conditions: { 'ForAnyValue:StringEquals': { - 'iam:ResourceTag/aws-cdk:bootstrap-role': ['image-publishing', 'file-publishing', 'deploy'], + 'iam:ResourceTag/aws-cdk:bootstrap-role': [ + 'image-publishing', + 'file-publishing', + 'deploy', + ], }, }, }), @@ -804,38 +957,47 @@ export class CodePipeline extends PipelineBase { }); } - private publishAssetsAction(node: AGraphNode, assets: StackAsset[]): ICodePipelineActionFactory { + private publishAssetsAction( + node: AGraphNode, + assets: StackAsset[], + ): ICodePipelineActionFactory { const installSuffix = this.cliVersion ? `@${this.cliVersion}` : ''; - const commands = assets.map(asset => { - const relativeAssetManifestPath = path.relative(this.myCxAsmRoot, asset.assetManifestPath); - return `cdk-assets --path "${toPosixPath(relativeAssetManifestPath)}" --verbose publish "${asset.assetSelector}"`; + const commands = assets.map((asset) => { + const relativeAssetManifestPath = path.relative( + this.myCxAsmRoot, + asset.assetManifestPath, + ); + return `cdk-assets --path "${toPosixPath( + relativeAssetManifestPath, + )}" --verbose publish "${asset.assetSelector}"`; }); const assetType = assets[0].assetType; - if (assets.some(a => a.assetType !== assetType)) { - throw new Error('All assets in a single publishing step must be of the same type'); + if (assets.some((a) => a.assetType !== assetType)) { + throw new Error( + 'All assets in a single publishing step must be of the same type', + ); } const role = this.obtainAssetCodeBuildRole(assets[0].assetType); - for (const roleArn of assets.flatMap(a => a.assetPublishingRoleArn ? [a.assetPublishingRoleArn] : [])) { + for (const roleArn of assets.flatMap((a) => + a.assetPublishingRoleArn ? [a.assetPublishingRoleArn] : [], + )) { // The ARNs include raw AWS pseudo parameters (e.g., ${AWS::Partition}), which need to be substituted. role.addAssumeRole(this.cachedFnSub.fnSub(roleArn)); - }; + } // The base commands that need to be run const script = new CodeBuildStep(node.id, { commands, - installCommands: [ - `npm install -g cdk-assets${installSuffix}`, - ], + installCommands: [`npm install -g cdk-assets${installSuffix}`], input: this._cloudAssemblyFileSet, buildEnvironment: { - privileged: ( - assets.some(asset => asset.assetType === AssetType.DOCKER_IMAGE) || - this.props.codeBuildDefaults?.buildEnvironment?.privileged - ), + privileged: + assets.some((asset) => asset.assetType === AssetType.DOCKER_IMAGE) || + this.props.codeBuildDefaults?.buildEnvironment?.privileged, }, role, }); @@ -853,7 +1015,9 @@ export class CodePipeline extends PipelineBase { private nodeTypeFromNode(node: AGraphNode) { if (node.data?.type === 'step') { - return !!node.data?.isBuildStep ? CodeBuildProjectType.SYNTH : CodeBuildProjectType.STEP; + return !!node.data?.isBuildStep + ? CodeBuildProjectType.SYNTH + : CodeBuildProjectType.STEP; } if (node.data?.type === 'publish-assets') { return CodeBuildProjectType.ASSETS; @@ -864,7 +1028,9 @@ export class CodePipeline extends PipelineBase { return undefined; } - private codeBuildDefaultsFor(nodeType: CodeBuildProjectType): CodeBuildOptions | undefined { + private codeBuildDefaultsFor( + nodeType: CodeBuildProjectType, + ): CodeBuildOptions | undefined { const defaultOptions: CodeBuildOptions = { buildEnvironment: { buildImage: CDKP_DEFAULT_CODEBUILD_IMAGE, @@ -874,32 +1040,46 @@ export class CodePipeline extends PipelineBase { const typeBasedCustomizations = { [CodeBuildProjectType.SYNTH]: this.props.dockerEnabledForSynth - ? mergeCodeBuildOptions(this.props.synthCodeBuildDefaults, { buildEnvironment: { privileged: true } }) + ? mergeCodeBuildOptions(this.props.synthCodeBuildDefaults, { + buildEnvironment: { privileged: true }, + }) : this.props.synthCodeBuildDefaults, - [CodeBuildProjectType.ASSETS]: this.props.assetPublishingCodeBuildDefaults, + [CodeBuildProjectType.ASSETS]: + this.props.assetPublishingCodeBuildDefaults, - [CodeBuildProjectType.SELF_MUTATE]: this.props.dockerEnabledForSelfMutation - ? mergeCodeBuildOptions(this.props.selfMutationCodeBuildDefaults, { buildEnvironment: { privileged: true } }) + [CodeBuildProjectType.SELF_MUTATE]: this.props + .dockerEnabledForSelfMutation + ? mergeCodeBuildOptions(this.props.selfMutationCodeBuildDefaults, { + buildEnvironment: { privileged: true }, + }) : this.props.selfMutationCodeBuildDefaults, [CodeBuildProjectType.STEP]: {}, }; const dockerUsage = dockerUsageFromCodeBuild(nodeType); - const dockerCommands = dockerUsage !== undefined - ? dockerCredentialsInstallCommands(dockerUsage, this.dockerCredentials, 'both') - : []; - const typeBasedDockerCommands = dockerCommands.length > 0 ? { - partialBuildSpec: cb.BuildSpec.fromObject({ - version: '0.2', - phases: { - pre_build: { - commands: dockerCommands, - }, - }, - }), - } : {}; + const dockerCommands = + dockerUsage !== undefined + ? dockerCredentialsInstallCommands( + dockerUsage, + this.dockerCredentials, + 'both', + ) + : []; + const typeBasedDockerCommands = + dockerCommands.length > 0 + ? { + partialBuildSpec: cb.BuildSpec.fromObject({ + version: '0.2', + phases: { + pre_build: { + commands: dockerCommands, + }, + }, + }), + } + : {}; return mergeCodeBuildOptions( defaultOptions, @@ -909,31 +1089,53 @@ export class CodePipeline extends PipelineBase { ); } - private roleFromPlaceholderArn(scope: Construct, region: string | undefined, - account: string | undefined, arn: string): iam.IRole; - private roleFromPlaceholderArn(scope: Construct, region: string | undefined, - account: string | undefined, arn: string | undefined): iam.IRole | undefined; - private roleFromPlaceholderArn(scope: Construct, region: string | undefined, - account: string | undefined, arn: string | undefined): iam.IRole | undefined { - - if (!arn) { return undefined; } + private roleFromPlaceholderArn( + scope: Construct, + region: string | undefined, + account: string | undefined, + arn: string + ): iam.IRole; + private roleFromPlaceholderArn( + scope: Construct, + region: string | undefined, + account: string | undefined, + arn: string | undefined + ): iam.IRole | undefined; + private roleFromPlaceholderArn( + scope: Construct, + region: string | undefined, + account: string | undefined, + arn: string | undefined, + ): iam.IRole | undefined { + if (!arn) { + return undefined; + } // Use placeholder arn as construct ID. const id = arn; // https://github.com/aws/aws-cdk/issues/7255 - let existingRole = scope.node.tryFindChild(`ImmutableRole${id}`) as iam.IRole; - if (existingRole) { return existingRole; } + let existingRole = scope.node.tryFindChild( + `ImmutableRole${id}`, + ) as iam.IRole; + if (existingRole) { + return existingRole; + } // For when #7255 is fixed. existingRole = scope.node.tryFindChild(id) as iam.IRole; - if (existingRole) { return existingRole; } + if (existingRole) { + return existingRole; + } const arnToImport = cxapi.EnvironmentPlaceholders.replace(arn, { region: region ?? Aws.REGION, accountId: account ?? Aws.ACCOUNT_ID, partition: Aws.PARTITION, }); - return iam.Role.fromRoleArn(scope, id, arnToImport, { mutable: false, addGrantsToResources: true }); + return iam.Role.fromRoleArn(scope, id, arnToImport, { + mutable: false, + addGrantsToResources: true, + }); } /** @@ -941,8 +1143,12 @@ export class CodePipeline extends PipelineBase { * * Currently only supports tags. */ - private writeTemplateConfiguration(stack: StackDeployment): string | undefined { - if (Object.keys(stack.tags).length === 0) { return undefined; } + private writeTemplateConfiguration( + stack: StackDeployment, + ): string | undefined { + if (Object.keys(stack.tags).length === 0) { + return undefined; + } const absConfigPath = `${stack.absoluteTemplatePath}.config.json`; const relativeConfigPath = path.relative(this.myCxAsmRoot, absConfigPath); @@ -973,23 +1179,28 @@ export class CodePipeline extends PipelineBase { const stack = Stack.of(this); const rolePrefix = assetType === AssetType.DOCKER_IMAGE ? 'Docker' : 'File'; - const assetRole = new AssetSingletonRole(this.assetsScope, `${rolePrefix}Role`, { - roleName: PhysicalName.GENERATE_IF_NEEDED, - assumedBy: new iam.CompositePrincipal( - new iam.ServicePrincipal('codebuild.amazonaws.com'), - new iam.AccountPrincipal(stack.account), - ), - }); + const assetRole = new AssetSingletonRole( + this.assetsScope, + `${rolePrefix}Role`, + { + roleName: PhysicalName.GENERATE_IF_NEEDED, + assumedBy: new iam.CompositePrincipal( + new iam.ServicePrincipal('codebuild.amazonaws.com'), + new iam.AccountPrincipal(stack.account), + ), + }, + ); // Grant pull access for any ECR registries and secrets that exist if (assetType === AssetType.DOCKER_IMAGE) { - this.dockerCredentials.forEach(reg => reg.grantRead(assetRole, DockerCredentialUsage.ASSET_PUBLISHING)); + this.dockerCredentials.forEach((reg) => + reg.grantRead(assetRole, DockerCredentialUsage.ASSET_PUBLISHING), + ); } this.assetCodeBuildRoles.set(assetType, assetRole); return assetRole; } - } function dockerUsageFromCodeBuild(cbt: CodeBuildProjectType): DockerCredentialUsage | undefined { diff --git a/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-graph.ts b/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-graph.ts index cad9fe7769c1a..49862ccf1d45b 100644 --- a/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-graph.ts +++ b/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-graph.ts @@ -1,7 +1,20 @@ -import { DependencyBuilders, Graph, GraphNode, GraphNodeCollection } from './graph'; -import { PipelineQueries } from './pipeline-queries'; -import { AssetType, FileSet, StackAsset, StackDeployment, StageDeployment, Step, Wave } from '../blueprint'; +import { + AssetType, + FileSet, + StackAsset, + StackDeployment, + StageDeployment, + Step, + Wave, +} from '../blueprint'; import { PipelineBase } from '../main/pipeline-base'; +import { + DependencyBuilders, + Graph, + GraphNode, + GraphNodeCollection, +} from './graph'; +import { PipelineQueries } from './pipeline-queries'; export interface PipelineGraphProps { /** @@ -32,6 +45,12 @@ export interface PipelineGraphProps { * @default true */ readonly prepareStep?: boolean; + + /** + * If all "prepare" step should be placed all together as the first actions within a stage/wave + */ + + readonly allPrepareNodesFirst?: boolean; } /** @@ -43,7 +62,7 @@ export class PipelineGraph { /** * A Step object that may be used as the producer of FileSets that should not be represented in the graph */ - public static readonly NO_STEP: Step = new class extends Step { }('NO_STEP'); + public static readonly NO_STEP: Step = new (class extends Step {})('NO_STEP'); public readonly graph: AGraph = Graph.of('', { type: 'group' }); public readonly cloudAssemblyFileSet: FileSet; @@ -54,24 +73,31 @@ export class PipelineGraph { private readonly assetNodesByType = new Map(); private readonly synthNode?: AGraphNode; private readonly selfMutateNode?: AGraphNode; - private readonly stackOutputDependencies = new DependencyBuilders(); + private readonly stackOutputDependencies = + new DependencyBuilders(); /** Mapping steps to depbuilders, satisfied by the step itself */ private readonly nodeDependencies = new DependencyBuilders(); private readonly publishTemplate: boolean; private readonly prepareStep: boolean; private readonly singlePublisher: boolean; + private readonly allPrepareNodesFirst: boolean; private lastPreparationNode?: AGraphNode; private _fileAssetCtr = 0; private _dockerAssetCtr = 0; - constructor(public readonly pipeline: PipelineBase, props: PipelineGraphProps = {}) { + constructor( + public readonly pipeline: PipelineBase, + props: PipelineGraphProps = {}, + ) { this.publishTemplate = props.publishTemplate ?? false; this.prepareStep = props.prepareStep ?? true; this.singlePublisher = props.singlePublisherPerAssetType ?? false; this.queries = new PipelineQueries(pipeline); + this.allPrepareNodesFirst = props.allPrepareNodesFirst ?? false; + if (pipeline.synth instanceof Step) { this.synthNode = this.addBuildStep(pipeline.synth); if (this.synthNode?.data?.type === 'step') { @@ -82,7 +108,9 @@ export class PipelineGraph { const cloudAssembly = pipeline.synth.primaryOutput?.primaryOutput; if (!cloudAssembly) { - throw new Error(`The synth step must produce the cloud assembly artifact, but doesn't: ${pipeline.synth}`); + throw new Error( + `The synth step must produce the cloud assembly artifact, but doesn't: ${pipeline.synth}`, + ); } this.cloudAssemblyFileSet = cloudAssembly; @@ -97,7 +125,7 @@ export class PipelineGraph { this.lastPreparationNode = this.selfMutateNode; } - const waves = pipeline.waves.map(w => this.addWave(w)); + const waves = pipeline.waves.map((w) => this.addWave(w)); // Make sure the waves deploy sequentially for (let i = 1; i < waves.length; i++) { @@ -116,10 +144,29 @@ export class PipelineGraph { } private addWave(wave: Wave): AGraph { + if (wave.postPrepare.length > 0 && this.allPrepareNodesFirst === false) { + throw new Error( + '"postPrepare" is set, but property "allPrepareNodesFirst" is not set to "true"', + ); + } + // If the wave only has one Stage in it, don't add an additional Graph around it - const retGraph: AGraph = wave.stages.length === 1 - ? this.addStage(wave.stages[0]) - : Graph.of(wave.id, { type: 'group' }, wave.stages.map(s => this.addStage(s))); + const retGraph: AGraph = + wave.stages.length === 1 + ? this.addStage( + wave.stages[0], + wave.postPrepare ?? [], + ) + : Graph.of( + wave.id, + { type: 'group' }, + wave.stages.map((s) => + this.addStage( + s, + wave.postPrepare ?? [], + ), + ), + ); this.addPrePost(wave.pre, wave.post, retGraph); retGraph.dependOn(this.lastPreparationNode); @@ -128,14 +175,27 @@ export class PipelineGraph { return retGraph; } - private addStage(stage: StageDeployment): AGraph { + private addStage( + stage: StageDeployment, + wavePostPrepareSteps: Step[], + ): AGraph { const retGraph: AGraph = Graph.of(stage.stageName, { type: 'group' }); - const stackGraphs = new Map(); + if (stage.postPrepare.length > 0 && this.allPrepareNodesFirst === false) { + throw new Error( + '"postPrepare" is set, but property "allPrepareNodesFirst" is not set to "true"', + ); + } + for (const stack of stage.stacks) { - const stackGraph: AGraph = Graph.of(this.simpleStackName(stack.stackName, stage.stageName), { type: 'stack-group', stack }); - const prepareNode: AGraphNode | undefined = this.prepareStep ? aGraphNode('Prepare', { type: 'prepare', stack }) : undefined; + const stackGraph: AGraph = Graph.of( + this.simpleStackName(stack.stackName, stage.stageName), + { type: 'stack-group', stack }, + ); + const prepareNode: AGraphNode | undefined = this.prepareStep + ? aGraphNode('Prepare-' + stack.stackName, { type: 'prepare', stack }) + : undefined; const deployNode: AGraphNode = aGraphNode('Deploy', { type: 'execute', stack, @@ -149,8 +209,38 @@ export class PipelineGraph { // node or node collection that represents first point of contact in each stack let firstDeployNode; if (prepareNode) { - stackGraph.add(prepareNode); - deployNode.dependOn(prepareNode); + if (this.allPrepareNodesFirst) { + retGraph.add(prepareNode); + } else { + stackGraph.add(prepareNode); + } + + const postPrepareNodesWave = this.addPostPrepare( + wavePostPrepareSteps ?? [], + retGraph, + ); + if (postPrepareNodesWave.nodes.length > 0) { + for (const n of postPrepareNodesWave.nodes) { + deployNode.dependOn(n); + n.dependOn(prepareNode); + } + } else { + deployNode.dependOn(prepareNode); + } + + const postPrepareNodes = this.addPostPrepare( + stage.postPrepare, + retGraph, + ); + if (postPrepareNodes.nodes.length > 0) { + for (const n of postPrepareNodes.nodes) { + deployNode.dependOn(n); + n.dependOn(prepareNode); + } + } else { + deployNode.dependOn(prepareNode); + } + firstDeployNode = prepareNode; } else { firstDeployNode = deployNode; @@ -159,9 +249,16 @@ export class PipelineGraph { // add changeset steps at the stack level if (stack.changeSet.length > 0) { if (prepareNode) { - this.addChangeSetNode(stack.changeSet, prepareNode, deployNode, stackGraph); + this.addChangeSetNode( + stack.changeSet, + prepareNode, + deployNode, + stackGraph, + ); } else { - throw new Error(`Cannot use \'changeSet\' steps for stack \'${stack.stackName}\': the pipeline does not support them or they have been disabled`); + throw new Error( + `Cannot use \'changeSet\' steps for stack \'${stack.stackName}\': the pipeline does not support them or they have been disabled`, + ); } } @@ -175,12 +272,16 @@ export class PipelineGraph { const cloudAssembly = this.cloudAssemblyFileSet; - firstDeployNode.dependOn(this.addStepNode(cloudAssembly.producer, retGraph)); + firstDeployNode.dependOn( + this.addStepNode(cloudAssembly.producer, retGraph), + ); // add the template asset if (this.publishTemplate) { if (!stack.templateAsset) { - throw new Error(`"publishTemplate" is enabled, but stack ${stack.stackArtifactId} does not have a template asset`); + throw new Error( + `"publishTemplate" is enabled, but stack ${stack.stackArtifactId} does not have a template asset`, + ); } firstDeployNode.dependOn(this.publishAsset(stack.templateAsset)); @@ -215,11 +316,15 @@ export class PipelineGraph { } this.addPrePost(stage.pre, stage.post, retGraph); - return retGraph; } - private addChangeSetNode(changeSet: Step[], prepareNode: AGraphNode, deployNode: AGraphNode, graph: AGraph) { + private addChangeSetNode( + changeSet: Step[], + prepareNode: AGraphNode, + deployNode: AGraphNode, + graph: AGraph, + ) { for (const c of changeSet) { const changeSetNode = this.addStepNode(c, graph); changeSetNode?.dependOn(prepareNode); @@ -227,6 +332,17 @@ export class PipelineGraph { } } + private addPostPrepare(postPrepare: Step[], parent: AGraph) { + const currentNodes = new GraphNodeCollection(parent.nodes); + const postPrepareNodes = new GraphNodeCollection(new Array()); + for (const p of postPrepare) { + const preNode = this.addStepNode(p, parent); + postPrepareNodes?.dependOn(...currentNodes.nodes); + postPrepareNodes.nodes.push(preNode!); + } + return postPrepareNodes; + } + private addPrePost(pre: Step[], post: Step[], parent: AGraph) { const currentNodes = new GraphNodeCollection(parent.nodes); const preNodes = new GraphNodeCollection(new Array()); @@ -257,10 +373,14 @@ export class PipelineGraph { * Adds all dependencies for that Node to the same Step as well. */ private addStepNode(step: Step, parent: AGraph) { - if (step === PipelineGraph.NO_STEP) { return undefined; } + if (step === PipelineGraph.NO_STEP) { + return undefined; + } const previous = this.added.get(step); - if (previous) { return previous; } + if (previous) { + return previous; + } const node: AGraphNode = aGraphNode(step.id, { type: 'step', step }); @@ -302,25 +422,38 @@ export class PipelineGraph { // May need to do this more than once to recursively add all missing producers let attempts = 20; while (attempts-- > 0) { - const unsatisfied = this.nodeDependencies.unsatisfiedBuilders().filter(([s]) => s !== PipelineGraph.NO_STEP); - if (unsatisfied.length === 0) { return; } + const unsatisfied = this.nodeDependencies + .unsatisfiedBuilders() + .filter(([s]) => s !== PipelineGraph.NO_STEP); + if (unsatisfied.length === 0) { + return; + } for (const [step, builder] of unsatisfied) { // Add a new node for this step to the parent of the "leftmost" consumer. - const leftMostConsumer = new GraphNodeCollection(builder.consumers).first(); + const leftMostConsumer = new GraphNodeCollection( + builder.consumers, + ).first(); const parent = leftMostConsumer.parentGraph; if (!parent) { - throw new Error(`Consumer doesn't have a parent graph: ${leftMostConsumer}`); + throw new Error( + `Consumer doesn't have a parent graph: ${leftMostConsumer}`, + ); } this.addStepNode(step, parent); } } const unsatisfied = this.nodeDependencies.unsatisfiedBuilders(); - throw new Error([ - 'Recursion depth too large while adding dependency nodes:', - unsatisfied.map(([step, builder]) => `${builder.consumersAsString()} awaiting ${step}.`), - ].join(' ')); + throw new Error( + [ + 'Recursion depth too large while adding dependency nodes:', + unsatisfied.map( + ([step, builder]) => + `${builder.consumersAsString()} awaiting ${step}.`, + ), + ].join(' '), + ); } private publishAsset(stackAsset: StackAsset): AGraphNode { @@ -330,14 +463,22 @@ export class PipelineGraph { if (assetNode) { // If there's already a node publishing this asset, add as a new publishing // destination to the same node. - } else if (this.singlePublisher && this.assetNodesByType.has(stackAsset.assetType)) { + } else if ( + this.singlePublisher && + this.assetNodesByType.has(stackAsset.assetType) + ) { // If we're doing a single node per type, lookup by that assetNode = this.assetNodesByType.get(stackAsset.assetType)!; } else { // Otherwise add a new one - const id = stackAsset.assetType === AssetType.FILE - ? (this.singlePublisher ? 'FileAsset' : `FileAsset${++this._fileAssetCtr}`) - : (this.singlePublisher ? 'DockerAsset' : `DockerAsset${++this._dockerAssetCtr}`); + const id = + stackAsset.assetType === AssetType.FILE + ? this.singlePublisher + ? 'FileAsset' + : `FileAsset${++this._fileAssetCtr}` + : this.singlePublisher + ? 'DockerAsset' + : `DockerAsset${++this._dockerAssetCtr}`; assetNode = aGraphNode(id, { type: 'publish-assets', assets: [] }); assetsGraph.add(assetNode); @@ -352,7 +493,9 @@ export class PipelineGraph { throw new Error(`${assetNode} has the wrong data.type: ${data?.type}`); } - if (!data.assets.some(a => a.assetSelector === stackAsset.assetSelector)) { + if ( + !data.assets.some((a) => a.assetSelector === stackAsset.assetSelector) + ) { data.assets.push(stackAsset); } @@ -378,8 +521,7 @@ type GraphAnnotation = // Explicitly disable exhaustiveness checking on GraphAnnotation. This forces all consumers to adding // a 'default' clause which allows us to extend this list in the future. // The code below looks weird, 'type' must be a non-enumerable type that is not assignable to 'string'. - | { readonly type: { error: 'you must add a default case to your switch' } } - ; + | { readonly type: { error: 'you must add a default case to your switch' } }; interface ExecuteAnnotation { readonly type: 'execute'; @@ -412,4 +554,4 @@ function aGraphNode(id: string, x: GraphAnnotation): AGraphNode { function stripPrefix(s: string, prefix: string) { return s.startsWith(prefix) ? s.slice(prefix.length) : s; -} \ No newline at end of file +} diff --git a/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-queries.ts b/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-queries.ts index 3d6fa25f11937..f1e0bdb800701 100644 --- a/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-queries.ts +++ b/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-queries.ts @@ -1,4 +1,4 @@ -import { Step, StackOutputReference, StackDeployment, StackAsset, StageDeployment } from '../blueprint'; +import { StackAsset, StackDeployment, StackOutputReference, StageDeployment, Step } from '../blueprint'; import { PipelineBase } from '../main/pipeline-base'; /** @@ -14,11 +14,11 @@ export class PipelineQueries { public stackOutputsReferenced(stack: StackDeployment): string[] { const steps = new Array(); for (const wave of this.pipeline.waves) { - steps.push(...wave.pre, ...wave.post); + steps.push(...wave.pre, ...wave.post, ...wave.postPrepare); for (const stage of wave.stages) { - steps.push(...stage.pre, ...stage.post); + steps.push(...stage.pre, ...stage.post, ...stage.postPrepare); for (const stackDeployment of stage.stacks) { - steps.push(...stackDeployment.pre, ...stackDeployment.changeSet, ...stackDeployment.post); + steps.push(...stackDeployment.pre, ...stackDeployment.changeSet, ...stackDeployment.post, ...stackDeployment.postPrepare); } } } diff --git a/packages/aws-cdk-lib/pipelines/test/blueprint/helpers-internal/pipeline-graph.test.ts b/packages/aws-cdk-lib/pipelines/test/blueprint/helpers-internal/pipeline-graph.test.ts index f473d4e0cf001..a4eceab4e6e8e 100644 --- a/packages/aws-cdk-lib/pipelines/test/blueprint/helpers-internal/pipeline-graph.test.ts +++ b/packages/aws-cdk-lib/pipelines/test/blueprint/helpers-internal/pipeline-graph.test.ts @@ -3,7 +3,7 @@ import * as cdkp from '../../../lib'; import { ManualApprovalStep, Step } from '../../../lib'; import { Graph, GraphNode, PipelineGraph } from '../../../lib/helpers-internal'; import { flatten } from '../../../lib/private/javascript'; -import { AppWithOutput, AppWithExposedStacks, OneStackApp, TestApp } from '../../testhelpers/test-app'; +import { AppWithExposedStacks, AppWithOutput, OneStackApp, TestApp } from '../../testhelpers/test-app'; let app: TestApp; @@ -114,6 +114,44 @@ describe('blueprint with wave and stage', () => { ]); }); + test('postPrepare and prepareNodes are added correctly inside stack graph', () => { + // GIVEN + const appWithExposedStacks = new AppWithExposedStacks(app, 'Gamma'); + + blueprint.waves[0].addStage(appWithExposedStacks, { + postPrepare: [ + new cdkp.ManualApprovalStep('Step1'), + // new cdkp.ManualApprovalStep('Step2'), + // new cdkp.ManualApprovalStep('Step3'), + ], + // stackSteps: [ + // { + // stack, + // pre: [ + // new cdkp.ManualApprovalStep('Step1'), + // new cdkp.ManualApprovalStep('Step2'), + // new cdkp.ManualApprovalStep('Step3'), + // ], + // changeSet: [new cdkp.ManualApprovalStep('Manual Approval')], + // post: [new cdkp.ManualApprovalStep('Post Approval')], + // }, + // ], + }); + + // WHEN + const graph = new PipelineGraph(blueprint).graph; + console.log(graph); + // THEN + expect(childrenAt(graph, 'Wave', 'Gamma', 'Stack1')).toEqual([ + 'Prepare-Gamma-Stack1', + 'Step1', + 'Step2', + 'Step3', + 'Deploy', + + ]); + }); + test('pre, changeSet, and post are added correctly inside stack graph', () => { // GIVEN const appWithExposedStacks = new AppWithExposedStacks(app, 'Gamma'); diff --git a/packages/aws-cdk-lib/pipelines/test/blueprint/helpers-internal/pipeline-queries.test.ts b/packages/aws-cdk-lib/pipelines/test/blueprint/helpers-internal/pipeline-queries.test.ts index e18806b38c6bf..66212bc7cf914 100644 --- a/packages/aws-cdk-lib/pipelines/test/blueprint/helpers-internal/pipeline-queries.test.ts +++ b/packages/aws-cdk-lib/pipelines/test/blueprint/helpers-internal/pipeline-queries.test.ts @@ -57,17 +57,17 @@ describe('pipeline-queries', () => { }, { description: 'output referenced in stack pre step', - additionalSetup: () => stackDeployment.addStackSteps([step], [], []), + additionalSetup: () => stackDeployment.addStackSteps([step], [], [], []), expectedResultGetter: () => [outputName], }, { description: 'output referenced in stack changeSet step', - additionalSetup: () => stackDeployment.addStackSteps([], [step], []), + additionalSetup: () => stackDeployment.addStackSteps([], [step], [], []), expectedResultGetter: () => [outputName], }, { description: 'output referenced in stack post step', - additionalSetup: () => stackDeployment.addStackSteps([], [], [step]), + additionalSetup: () => stackDeployment.addStackSteps([], [], [step], []), expectedResultGetter: () => [outputName], }, {