diff --git a/packages/aws-cdk-lib/pipelines/README.md b/packages/aws-cdk-lib/pipelines/README.md index 85d7a85c45ba2..2004dae19b32e 100644 --- a/packages/aws-cdk-lib/pipelines/README.md +++ b/packages/aws-cdk-lib/pipelines/README.md @@ -565,6 +565,7 @@ pass in order to promote from the `PreProd` to the `Prod` environment: declare const pipeline: pipelines.CodePipeline; const preprod = new MyApplicationStage(this, 'PreProd'); const prod = new MyApplicationStage(this, 'Prod'); +const topic = new sns.Topic(this, 'ChangeApprovalTopic'); pipeline.addStage(preprod, { post: [ @@ -574,7 +575,12 @@ pipeline.addStage(preprod, { ], }); pipeline.addStage(prod, { - pre: [new pipelines.ManualApprovalStep('PromoteToProd')], + pre: [new pipelines.ManualApprovalStep('PromoteToProd', { + //All options below are optional + comment: 'Please validate changes', + reviewUrl: 'https://my.webservice.com/', + notificationTopic: topic, + })], }); ``` diff --git a/packages/aws-cdk-lib/pipelines/lib/blueprint/manual-approval.ts b/packages/aws-cdk-lib/pipelines/lib/blueprint/manual-approval.ts index fee9cd5ab5190..bc230568e938f 100644 --- a/packages/aws-cdk-lib/pipelines/lib/blueprint/manual-approval.ts +++ b/packages/aws-cdk-lib/pipelines/lib/blueprint/manual-approval.ts @@ -1,5 +1,5 @@ import { Step } from './step'; - +import { ITopic } from '../../../aws-sns'; /** * Construction properties for a `ManualApprovalStep` */ @@ -10,6 +10,20 @@ export interface ManualApprovalStepProps { * @default - No comment */ readonly comment?: string; + + /** + * The URL for review associated with this manual approval + * + * @default - No URL + */ + readonly reviewUrl?: string; + + /** + * Optional SNS topic to send notifications to when an approval is pending + * + * @default - No notifications + */ + readonly notificationTopic?: ITopic; } /** @@ -29,10 +43,26 @@ export class ManualApprovalStep extends Step { */ public readonly comment?: string; + /** + * The URL for review associated with this manual approval + * + * @default - No URL + */ + public readonly reviewUrl?: string; + + /** + * Optional SNS topic to send notifications + * + * @default - No notifications + */ + public readonly notificationTopic?: ITopic; + constructor(id: string, props: ManualApprovalStepProps = {}) { super(id); this.comment = props.comment; + this.reviewUrl = props.reviewUrl; + this.notificationTopic = props.notificationTopic; this.discoverReferencedOutputs(props.comment); } diff --git a/packages/aws-cdk-lib/pipelines/lib/codepipeline/codepipeline.ts b/packages/aws-cdk-lib/pipelines/lib/codepipeline/codepipeline.ts index 023fa421c860a..67356826d9b90 100644 --- a/packages/aws-cdk-lib/pipelines/lib/codepipeline/codepipeline.ts +++ b/packages/aws-cdk-lib/pipelines/lib/codepipeline/codepipeline.ts @@ -714,6 +714,8 @@ export class CodePipeline extends PipelineBase { actionName: options.actionName, runOrder: options.runOrder, additionalInformation: step.comment, + externalEntityLink: step.reviewUrl, + notificationTopic: step.notificationTopic, })); return { runOrdersConsumed: 1 }; }, diff --git a/packages/aws-cdk-lib/pipelines/test/codepipeline/codepipeline.test.ts b/packages/aws-cdk-lib/pipelines/test/codepipeline/codepipeline.test.ts index b6b83f9b92016..1c3372d9990f7 100644 --- a/packages/aws-cdk-lib/pipelines/test/codepipeline/codepipeline.test.ts +++ b/packages/aws-cdk-lib/pipelines/test/codepipeline/codepipeline.test.ts @@ -4,6 +4,7 @@ import * as ccommit from '../../../aws-codecommit'; import { Pipeline, PipelineType } from '../../../aws-codepipeline'; import * as iam from '../../../aws-iam'; import * as s3 from '../../../aws-s3'; +import * as sns from '../../../aws-sns'; import * as sqs from '../../../aws-sqs'; import * as cdk from '../../../core'; import { Stack } from '../../../core'; @@ -731,6 +732,77 @@ test('throws when deploy role session tags are used', () => { }).toThrow('Deployment of stack SampleStage-123456789012-us-east-1-SampleStack requires assuming the role arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-deploy-role-123456789012-us-east-1 with session tags, but assuming roles with session tags is not supported by CodePipeline.'); }); +test('test add external link for manual approval', () => { + const pipelineStack = new cdk.Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); + + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + crossAccountKeys: true, + }); + + const stage = new TwoStackApp(app, 'TheApp', { withDependency: false }); + + const approval = new cdkp.ManualApprovalStep('Approval', { + comment: 'Please approve', + reviewUrl: 'https://approve-confirm.com', + }); + + pipeline.addStage(stage, { + pre: [approval], + }); + + const template = Template.fromStack(pipelineStack); + template.hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ + Name: 'TheApp', + Actions: Match.arrayWith([ + Match.objectLike({ + Name: 'Approval', + RunOrder: 1, + Configuration: Match.objectLike({ + ExternalEntityLink: 'https://approve-confirm.com', + }), + }), + ]), + }]), + }); +}); + +test('sns topic for manual approval', () => { + const pipelineStack = new cdk.Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { + crossAccountKeys: true, + }); + const topic = new sns.Topic(pipelineStack, 'Topic', { + topicName: 'MyTopic', + }); + const approval = new cdkp.ManualApprovalStep('Approval', { + comment: 'Please approve', + notificationTopic: topic, + }); + const stage = new TwoStackApp(app, 'TheApp', { withDependency: false }); + pipeline.addStage(stage, { + pre: [approval], + }); + const template = Template.fromStack(pipelineStack); + template.hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ + Name: 'TheApp', + Actions: Match.arrayWith([ + Match.objectLike({ + Name: 'Approval', + RunOrder: 1, + Configuration: Match.objectLike({ + NotificationArn: { Ref: 'TopicBFC7AF6E' }, + }), + }), + ]), + }]), + }); + template.hasResourceProperties('AWS::SNS::Topic', { + TopicName: 'MyTopic', + }); +}); + interface ReuseCodePipelineStackProps extends cdk.StackProps { reuseCrossRegionSupportStacks?: boolean; reuseStageProps?: ReuseStageProps;