diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index ff8a047d256e3..92102fc07e18a 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -44,6 +44,26 @@ pipeline.addStage({ }); ``` +The CodeCommit source action emits variables: + +```typescript +const sourceAction = new codepipeline_actions.CodeCommitSourceAction({ + // ... + variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you +}); + +// later: + +new codepipeline_actions.CodeBuildAction({ + // ... + environmentVariables: { + COMMIT_ID: { + value: sourceAction.variables.commitId, + }, + }, +}); +``` + #### GitHub To use GitHub as the source of a CodePipeline: @@ -66,6 +86,26 @@ pipeline.addStage({ }); ``` +The GitHub source action emits variables: + +```typescript +const sourceAction = new codepipeline_actions.GitHubSourceAction({ + // ... + variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you +}); + +// later: + +new codepipeline_actions.CodeBuildAction({ + // ... + environmentVariables: { + COMMIT_URL: { + value: sourceAction.variables.commitUrl, + }, + }, +}); +``` + #### AWS S3 To use an S3 Bucket as a source in CodePipeline: @@ -116,6 +156,26 @@ const sourceAction = new codepipeline_actions.S3SourceAction({ }); ``` +The S3 source action emits variables: + +```typescript +const sourceAction = new codepipeline_actions.S3SourceAction({ + // ... + variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you +}); + +// later: + +new codepipeline_actions.CodeBuildAction({ + // ... + environmentVariables: { + VERSION_ID: { + value: sourceAction.variables.versionId, + }, + }, +}); +``` + #### AWS ECR To use an ECR Repository as a source in a Pipeline: @@ -137,6 +197,26 @@ pipeline.addStage({ }); ``` +The ECR source action emits variables: + +```typescript +const sourceAction = new codepipeline_actions.EcrSourceAction({ + // ... + variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you +}); + +// later: + +new codepipeline_actions.CodeBuildAction({ + // ... + environmentVariables: { + IMAGE_URI: { + value: sourceAction.variables.imageUri, + }, + }, +}); +``` + ### Build & test #### AWS CodeBuild @@ -266,6 +346,48 @@ const project = new codebuild.PipelineProject(this, 'MyProject', { }); ``` +##### Variables + +The CodeBuild action emits variables. +Unlike many other actions, the variables are not static, +but dynamic, defined in the buildspec, +in the 'exported-variables' subsection of the 'env' section. +Example: + +```typescript +const buildAction = new codepipeline_actions.CodeBuildAction({ + actionName: 'Build1', + input: sourceOutput, + project: new codebuild.PipelineProject(this, 'Project', { + buildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + env: { + 'exported-variables': [ + 'MY_VAR', + ], + }, + phases: { + build: { + commands: 'export MY_VAR="some value"', + }, + }, + }), + }), + variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you +}); + +// later: + +new codepipeline_actions.CodeBuildAction({ + // ... + environmentVariables: { + MyVar: { + value: buildAction.variable('MY_VAR'), + }, + }, +}); +``` + #### Jenkins In order to use Jenkins Actions in the Pipeline, @@ -304,7 +426,7 @@ const buildAction = new codepipeline_actions.JenkinsAction({ actionName: 'JenkinsBuild', jenkinsProvider: jenkinsProvider, projectName: 'MyProject', - type: ccodepipeline_actions.JenkinsActionType.BUILD, + type: codepipeline_actions.JenkinsActionType.BUILD, }); ``` @@ -421,7 +543,7 @@ const func = new lambda.Function(lambdaStack, 'Lambda', { runtime: lambda.Runtime.NODEJS_10_X, }); // used to make sure each CDK synthesis produces a different Version -const version = func.addVersion('NewVersion') +const version = func.addVersion('NewVersion'); const alias = new lambda.Alias(lambdaStack, 'LambdaAlias', { aliasName: 'Prod', version, @@ -598,5 +720,48 @@ const lambdaAction = new codepipeline_actions.LambdaInvokeAction({ }); ``` +The Lambda invoke action emits variables. +Unlike many other actions, the variables are not static, +but dynamic, defined by the function calling the `PutJobSuccessResult` +API with the `outputVariables` property filled with the map of variables +Example: + +```typescript +import lambda = require('@aws-cdk/aws-lambda'); + +const lambdaInvokeAction = new codepipeline_actions.LambdaInvokeAction({ + actionName: 'Lambda', + lambda: new lambda.Function(this, 'Func', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromInline(` + var AWS = require('aws-sdk'); + + exports.handler = async function(event, context) { + var codepipeline = new AWS.CodePipeline(); + await codepipeline.putJobSuccessResult({ + jobId: event['CodePipeline.job'].id, + outputVariables: { + MY_VAR: "some value", + }, + }).promise(); + } + `), + }), + variablesNamespace: 'MyNamespace', // optional - by default, a name will be generated for you +}); + +// later: + +new codepipeline_actions.CodeBuildAction({ + // ... + environmentVariables: { + MyVar: { + value: lambdaInvokeAction.variable('MY_VAR'), + }, + }, +}); +``` + See [the AWS documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-invoke-lambda-function.html) on how to write a Lambda function invoked from CodePipeline. diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts index f6efb8bc4fdf9..024cd1a66fc9a 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts @@ -1,6 +1,6 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as events from '@aws-cdk/aws-events'; -import { Construct } from '@aws-cdk/core'; +import { Construct, Lazy } from '@aws-cdk/core'; /** * Low-level class for generic CodePipeline Actions. @@ -13,12 +13,34 @@ import { Construct } from '@aws-cdk/core'; * @experimental */ export abstract class Action implements codepipeline.IAction { + public readonly actionProperties: codepipeline.ActionProperties; private _pipeline?: codepipeline.IPipeline; private _stage?: codepipeline.IStage; private _scope?: Construct; + private readonly customerProvidedNamespace?: string; + private readonly namespaceOrToken: string; + private actualNamespace?: string; + private variableReferenced = false; - constructor(public readonly actionProperties: codepipeline.ActionProperties) { - // nothing to do + protected constructor(actionProperties: codepipeline.ActionProperties) { + this.customerProvidedNamespace = actionProperties.variablesNamespace; + this.namespaceOrToken = Lazy.stringValue({ produce: () => { + // make sure the action was bound (= added to a pipeline) + if (this.actualNamespace !== undefined) { + return this.customerProvidedNamespace !== undefined + // if a customer passed a namespace explicitly, always use that + ? this.customerProvidedNamespace + // otherwise, only return a namespace if any variable was referenced + : (this.variableReferenced ? this.actualNamespace : undefined); + } else { + throw new Error(`Cannot reference variables of action '${this.actionProperties.actionName}', ` + + 'as that action was never added to a pipeline'); + } + }}); + this.actionProperties = { + ...actionProperties, + variablesNamespace: this.namespaceOrToken, + }; } public bind(scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): @@ -27,6 +49,11 @@ export abstract class Action implements codepipeline.IAction { this._stage = stage; this._scope = scope; + this.actualNamespace = this.customerProvidedNamespace === undefined + // default a namespace name, based on the stage and action names + ? `${stage.stageName}_${this.actionProperties.actionName}_NS` + : this.customerProvidedNamespace; + return this.bound(scope, stage, options); } @@ -45,6 +72,11 @@ export abstract class Action implements codepipeline.IAction { return rule; } + protected variableExpression(variableName: string): string { + this.variableReferenced = true; + return `#{${this.namespaceOrToken}.${variableName}}`; + } + /** * The method called when an Action is attached to a Pipeline. * This method is guaranteed to be called only once for each Action instance. diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts index 845a3a052f4cd..10348f02533a3 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts @@ -92,6 +92,20 @@ export class CodeBuildAction extends Action { this.props = props; } + /** + * Reference a CodePipeline variable defined by the CodeBuild project this action points to. + * Variables in CodeBuild actions are defined using the 'exported-variables' subsection of the 'env' + * section of the buildspec. + * + * @param variableName the name of the variable to reference. + * A variable by this name must be present in the 'exported-variables' section of the buildspec + * + * @see https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html#build-spec-ref-syntax + */ + public variable(variableName: string): string { + return this.variableExpression(variableName); + } + protected bound(scope: cdk.Construct, _stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): codepipeline.ActionConfig { // check for a cross-account action if there are any outputs diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts index 35c3cbea50f8c..76183aa51aa83 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts @@ -29,6 +29,29 @@ export enum CodeCommitTrigger { EVENTS = 'Events', } +/** + * The CodePipeline variables emitted by the CodeCommit source Action. + */ +export interface CodeCommitSourceVariables { + /** The name of the repository this action points to. */ + readonly repositoryName: string; + + /** The name of the branch this action tracks. */ + readonly branchName: string; + + /** The date the currently last commit on the tracked branch was authored, in ISO-8601 format. */ + readonly authorDate: string; + + /** The date the currently last commit on the tracked branch was committed, in ISO-8601 format. */ + readonly committerDate: string; + + /** The SHA1 hash of the currently last commit on the tracked branch. */ + readonly commitId: string; + + /** The message of the currently last commit on the tracked branch. */ + readonly commitMessage: string; +} + /** * Construction properties of the {@link CodeCommitSourceAction CodeCommit source CodePipeline Action}. */ @@ -79,6 +102,18 @@ export class CodeCommitSourceAction extends Action { this.props = props; } + /** The variables emitted by this action. */ + public get variables(): CodeCommitSourceVariables { + return { + repositoryName: this.variableExpression('RepositoryName'), + branchName: this.variableExpression('BranchName'), + authorDate: this.variableExpression('AuthorDate'), + committerDate: this.variableExpression('CommitterDate'), + commitId: this.variableExpression('CommitId'), + commitMessage: this.variableExpression('CommitMessage'), + }; + } + protected bound(_scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): codepipeline.ActionConfig { const createEvent = this.props.trigger === undefined || diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts index 05f1932705fba..09ee9d3744af3 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts @@ -6,6 +6,26 @@ import { Construct } from '@aws-cdk/core'; import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; +/** + * The CodePipeline variables emitted by the ECR source Action. + */ +export interface EcrSourceVariables { + /** The identifier of the registry. In ECR, this is usually the ID of the AWS account owning it. */ + readonly registryId: string; + + /** The physical name of the repository that this action tracks. */ + readonly repositoryName: string; + + /** The digest of the current image, in the form ':'. */ + readonly imageDigest: string; + + /** The Docker tag of the current image. */ + readonly imageTag: string; + + /** The full ECR Docker URI of the current image. */ + readonly imageUri: string; +} + /** * Construction properties of {@link EcrSourceAction}. */ @@ -51,6 +71,17 @@ export class EcrSourceAction extends Action { this.props = props; } + /** The variables emitted by this action. */ + public get variables(): EcrSourceVariables { + return { + registryId: this.variableExpression('RegistryId'), + repositoryName: this.variableExpression('RepositoryName'), + imageDigest: this.variableExpression('ImageDigest'), + imageTag: this.variableExpression('ImageTag'), + imageUri: this.variableExpression('ImageURI'), + }; + } + protected bound(_scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): codepipeline.ActionConfig { options.role.addToPolicy(new iam.PolicyStatement({ diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts index 6b1b718846631..42062e9040dde 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts @@ -12,6 +12,26 @@ export enum GitHubTrigger { WEBHOOK = 'WebHook', } +/** + * The CodePipeline variables emitted by GitHub source Action. + */ +export interface GitHubSourceVariables { + /** The name of the repository this action points to. */ + readonly repositoryName: string; + /** The name of the branch this action tracks. */ + readonly branchName: string; + /** The date the currently last commit on the tracked branch was authored, in ISO-8601 format. */ + readonly authorDate: string; + /** The date the currently last commit on the tracked branch was committed, in ISO-8601 format. */ + readonly committerDate: string; + /** The SHA1 hash of the currently last commit on the tracked branch. */ + readonly commitId: string; + /** The message of the currently last commit on the tracked branch. */ + readonly commitMessage: string; + /** The GitHub API URL of the currently last commit on the tracked branch. */ + readonly commitUrl: string; +} + /** * Construction properties of the {@link GitHubSourceAction GitHub source action}. */ @@ -79,6 +99,19 @@ export class GitHubSourceAction extends Action { this.props = props; } + /** The variables emitted by this action. */ + public get variables(): GitHubSourceVariables { + return { + repositoryName: this.variableExpression('RepositoryName'), + branchName: this.variableExpression('BranchName'), + authorDate: this.variableExpression('AuthorDate'), + committerDate: this.variableExpression('CommitterDate'), + commitId: this.variableExpression('CommitId'), + commitMessage: this.variableExpression('CommitMessage'), + commitUrl: this.variableExpression('CommitUrl'), + }; + } + protected bound(scope: Construct, stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): codepipeline.ActionConfig { if (!this.props.trigger || this.props.trigger === GitHubTrigger.WEBHOOK) { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts index 839cbb0a42acd..a0220fc6e7d2b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts @@ -74,6 +74,21 @@ export class LambdaInvokeAction extends Action { this.props = props; } + /** + * Reference a CodePipeline variable defined by the Lambda function this action points to. + * Variables in Lambda invoke actions are defined by calling the PutJobSuccessResult CodePipeline API call + * with the 'outputVariables' property filled. + * + * @param variableName the name of the variable to reference. + * A variable by this name must be present in the 'outputVariables' section of the PutJobSuccessResult + * request that the Lambda function calls when the action is invoked + * + * @see https://docs.aws.amazon.com/codepipeline/latest/APIReference/API_PutJobSuccessResult.html + */ + public variable(variableName: string): string { + return this.variableExpression(variableName); + } + protected bound(scope: Construct, _stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): codepipeline.ActionConfig { // allow pipeline to list functions diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts index e311d624d54f2..1cf1d821dd0ac 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts @@ -30,6 +30,17 @@ export enum S3Trigger { EVENTS = 'Events', } +/** + * The CodePipeline variables emitted by the S3 source Action. + */ +export interface S3SourceVariables { + /** The identifier of the S3 version of the object that triggered the build. */ + readonly versionId: string; + + /** The e-tag of the S3 version of the object that triggered the build. */ + readonly eTag: string; +} + /** * Construction properties of the {@link S3SourceAction S3 source Action}. */ @@ -88,6 +99,14 @@ export class S3SourceAction extends Action { this.props = props; } + /** The variables emitted by this action. */ + public get variables(): S3SourceVariables { + return { + versionId: this.variableExpression('VersionId'), + eTag: this.variableExpression('ETag'), + }; + } + protected bound(_scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions): codepipeline.ActionConfig { if (this.props.trigger === S3Trigger.EVENTS) { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/test.codebuild-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/test.codebuild-action.ts index 45ff0a49654b4..dacfae6d21cd1 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/test.codebuild-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/test.codebuild-action.ts @@ -3,6 +3,7 @@ import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; +import * as sns from '@aws-cdk/aws-sns'; import { App, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as cpactions from '../../lib'; @@ -122,5 +123,84 @@ export = { test.done(); }, + + 'exposes variables for other actions to consume'(test: Test) { + const stack = new Stack(); + + const sourceOutput = new codepipeline.Artifact(); + const codeBuildAction = new cpactions.CodeBuildAction({ + actionName: 'CodeBuild', + input: sourceOutput, + project: new codebuild.PipelineProject(stack, 'CodeBuild', { + buildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + env: { + 'exported-variables': [ + 'SomeVar', + ], + }, + phases: { + build: { + commands: [ + 'export SomeVar="Some Value"', + ], + }, + }, + }), + }), + }); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new cpactions.S3SourceAction({ + actionName: 'S3_Source', + bucket: s3.Bucket.fromBucketName(stack, 'Bucket', 'bucket'), + bucketKey: 'key', + output: sourceOutput, + }), + ], + }, + { + stageName: 'Build', + actions: [ + codeBuildAction, + new cpactions.ManualApprovalAction({ + actionName: 'Approve', + additionalInformation: codeBuildAction.variable('SomeVar'), + notificationTopic: sns.Topic.fromTopicArn(stack, 'Topic', 'arn:aws:sns:us-east-1:123456789012:mytopic'), + runOrder: 2, + }), + ], + }, + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [ + { + "Name": "Source", + }, + { + "Name": "Build", + "Actions": [ + { + "Name": "CodeBuild", + "Namespace": "Build_CodeBuild_NS", + }, + { + "Name": "Approve", + "Configuration": { + "CustomData": "#{Build_CodeBuild_NS.SomeVar}", + }, + }, + ], + }, + ], + })); + + test.done(); + }, }, }; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts index 1086d3b0d82f4..e67cf3139d4b2 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts @@ -109,6 +109,61 @@ export = { test.done(); }, + + 'exposes variables for other actions to consume'(test: Test) { + const stack = new Stack(); + + const sourceOutput = new codepipeline.Artifact(); + const codeCommitSourceAction = new cpactions.CodeCommitSourceAction({ + actionName: 'Source', + repository: new codecommit.Repository(stack, 'MyRepo', { + repositoryName: 'my-repo', + }), + output: sourceOutput, + }); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [codeCommitSourceAction], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'Build', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput, + environmentVariables: { + AuthorDate: { value: codeCommitSourceAction.variables.authorDate }, + }, + }), + ], + }, + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [ + { + "Name": "Source", + }, + { + "Name": "Build", + "Actions": [ + { + "Name": "Build", + "Configuration": { + "EnvironmentVariables": '[{"name":"AuthorDate","type":"PLAINTEXT","value":"#{Source_Source_NS.AuthorDate}"}]', + }, + }, + ], + }, + ], + })); + + test.done(); + }, }, }; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/test.ecr-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/test.ecr-source-action.ts new file mode 100644 index 0000000000000..d00283d33c9eb --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/test.ecr-source-action.ts @@ -0,0 +1,66 @@ +import { expect, haveResourceLike } from "@aws-cdk/assert"; +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as ecr from '@aws-cdk/aws-ecr'; +import { Stack } from "@aws-cdk/core"; +import { Test } from 'nodeunit'; +import * as cpactions from '../../lib'; + +// tslint:disable:object-literal-key-quotes + +export = { + 'ECR source Action': { + 'exposes variables for other actions to consume'(test: Test) { + const stack = new Stack(); + + const sourceOutput = new codepipeline.Artifact(); + const ecrSourceAction = new cpactions.EcrSourceAction({ + actionName: 'Source', + output: sourceOutput, + repository: ecr.Repository.fromRepositoryName(stack, 'Repo', 'repo'), + }); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [ecrSourceAction], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'Build', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput, + environmentVariables: { + ImageDigest: { value: ecrSourceAction.variables.imageDigest }, + }, + }), + ], + }, + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [ + { + "Name": "Source", + }, + { + "Name": "Build", + "Actions": [ + { + "Name": "Build", + "Configuration": { + "EnvironmentVariables": '[{"name":"ImageDigest","type":"PLAINTEXT","value":"#{Source_Source_NS.ImageDigest}"}]', + }, + }, + ], + }, + ], + })); + + test.done(); + }, + }, +}; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/github/test.github-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/github/test.github-source-action.ts new file mode 100644 index 0000000000000..1707c245defa7 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/github/test.github-source-action.ts @@ -0,0 +1,211 @@ +import { expect, haveResourceLike, SynthUtils } from "@aws-cdk/assert"; +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import { SecretValue, Stack } from "@aws-cdk/core"; +import { Test } from 'nodeunit'; +import * as cpactions from '../../lib'; + +// tslint:disable:object-literal-key-quotes + +export = { + 'GitHub source Action': { + 'exposes variables for other actions to consume'(test: Test) { + const stack = new Stack(); + + const sourceOutput = new codepipeline.Artifact(); + const gitHubSourceAction = new cpactions.GitHubSourceAction({ + actionName: 'Source', + owner: 'aws', + repo: 'aws-cdk', + output: sourceOutput, + oauthToken: SecretValue.plainText('secret'), + }); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [gitHubSourceAction], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'Build', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput, + environmentVariables: { + CommitUrl: { value: gitHubSourceAction.variables.commitUrl }, + }, + }), + ], + }, + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [ + { + "Name": "Source", + }, + { + "Name": "Build", + "Actions": [ + { + "Name": "Build", + "Configuration": { + "EnvironmentVariables": '[{"name":"CommitUrl","type":"PLAINTEXT","value":"#{Source_Source_NS.CommitUrl}"}]', + }, + }, + ], + }, + ], + })); + + test.done(); + }, + + 'always renders the customer-supplied namespace, even if none of the variables are used'(test: Test) { + const stack = new Stack(); + + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new cpactions.GitHubSourceAction({ + actionName: 'Source', + owner: 'aws', + repo: 'aws-cdk', + output: sourceOutput, + oauthToken: SecretValue.plainText('secret'), + variablesNamespace: 'MyNamespace', + }), + ], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'Build', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput, + }), + ], + }, + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [ + { + "Name": "Source", + "Actions": [ + { + "Name": "Source", + "Namespace": "MyNamespace", + }, + ], + }, + { + }, + ], + })); + + test.done(); + }, + + 'fails if a variable from an action without a namespace set that is not part of a pipeline is referenced'(test: Test) { + const stack = new Stack(); + + const unusedSourceAction = new cpactions.GitHubSourceAction({ + actionName: 'Source2', + owner: 'aws', + repo: 'aws-cdk', + output: new codepipeline.Artifact(), + oauthToken: SecretValue.plainText('secret'), + }); + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [new cpactions.GitHubSourceAction({ + actionName: 'Source1', + owner: 'aws', + repo: 'aws-cdk', + output: sourceOutput, + oauthToken: SecretValue.plainText('secret'), + })], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'Build', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput, + environmentVariables: { + 'VAR1': { value: unusedSourceAction.variables.authorDate }, + }, + }), + ], + }, + ], + }); + + test.throws(() => { + SynthUtils.synthesize(stack); + }, /Cannot reference variables of action 'Source2', as that action was never added to a pipeline/); + + test.done(); + }, + + 'fails if a variable from an action with a namespace set that is not part of a pipeline is referenced'(test: Test) { + const stack = new Stack(); + + const unusedSourceAction = new cpactions.GitHubSourceAction({ + actionName: 'Source2', + owner: 'aws', + repo: 'aws-cdk', + output: new codepipeline.Artifact(), + oauthToken: SecretValue.plainText('secret'), + variablesNamespace: 'MyNamespace', + }); + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [new cpactions.GitHubSourceAction({ + actionName: 'Source1', + owner: 'aws', + repo: 'aws-cdk', + output: sourceOutput, + oauthToken: SecretValue.plainText('secret'), + })], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'Build', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput, + environmentVariables: { + 'VAR1': { value: unusedSourceAction.variables.authorDate }, + }, + }), + ], + }, + ], + }); + + test.throws(() => { + SynthUtils.synthesize(stack); + }, /Cannot reference variables of action 'Source2', as that action was never added to a pipeline/); + + test.done(); + }, + }, +}; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/test.lambda-invoke-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/test.lambda-invoke-action.ts index fa7b63f51346a..e972ed2f33992 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/test.lambda-invoke-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/test.lambda-invoke-action.ts @@ -1,6 +1,8 @@ import { expect, haveResourceLike } from "@aws-cdk/assert"; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as lambda from '@aws-cdk/aws-lambda'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as sns from '@aws-cdk/aws-sns'; import { Aws, Lazy, SecretValue, Stack, Token } from "@aws-cdk/core"; import { Test } from 'nodeunit'; import * as cpactions from '../../lib'; @@ -230,6 +232,68 @@ export = { test.done(); }, + + 'exposes variables for other actions to consume'(test: Test) { + const stack = new Stack(); + + const sourceOutput = new codepipeline.Artifact(); + const lambdaInvokeAction = new cpactions.LambdaInvokeAction({ + actionName: 'LambdaInvoke', + lambda: lambda.Function.fromFunctionArn(stack, 'Func', 'arn:aws:lambda:us-east-1:123456789012:function:some-func'), + }); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new cpactions.S3SourceAction({ + actionName: 'S3_Source', + bucket: s3.Bucket.fromBucketName(stack, 'Bucket', 'bucket'), + bucketKey: 'key', + output: sourceOutput, + }), + ], + }, + { + stageName: 'Invoke', + actions: [ + lambdaInvokeAction, + new cpactions.ManualApprovalAction({ + actionName: 'Approve', + additionalInformation: lambdaInvokeAction.variable('SomeVar'), + notificationTopic: sns.Topic.fromTopicArn(stack, 'Topic', 'arn:aws:sns:us-east-1:123456789012:mytopic'), + runOrder: 2, + }), + ], + }, + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [ + { + "Name": "Source", + }, + { + "Name": "Invoke", + "Actions": [ + { + "Name": "LambdaInvoke", + "Namespace": "Invoke_LambdaInvoke_NS", + }, + { + "Name": "Approve", + "Configuration": { + "CustomData": "#{Invoke_LambdaInvoke_NS.SomeVar}", + }, + }, + ], + }, + ], + })); + + test.done(); + }, }, }; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/test.s3-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/test.s3-source-action.ts index 8f6ed6b882b15..e3d91a9bb7daa 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/test.s3-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/test.s3-source-action.ts @@ -175,6 +175,60 @@ export = { test.done(); }, + + 'exposes variables for other actions to consume'(test: Test) { + const stack = new Stack(); + + const sourceOutput = new codepipeline.Artifact(); + const s3SourceAction = new cpactions.S3SourceAction({ + actionName: 'Source', + output: sourceOutput, + bucket: new s3.Bucket(stack, 'Bucket'), + bucketKey: 'key.zip', + }); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [s3SourceAction], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'Build', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput, + environmentVariables: { + VersionId: { value: s3SourceAction.variables.versionId }, + }, + }), + ], + }, + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [ + { + "Name": "Source", + }, + { + "Name": "Build", + "Actions": [ + { + "Name": "Build", + "Configuration": { + "EnvironmentVariables": '[{"name":"VersionId","type":"PLAINTEXT","value":"#{Source_Source_NS.VersionId}"}]', + }, + }, + ], + }, + ], + })); + + test.done(); + }, }, }; diff --git a/packages/@aws-cdk/aws-codepipeline/README.md b/packages/@aws-cdk/aws-codepipeline/README.md index 4e439c665aa90..5e77c98e492bb 100644 --- a/packages/@aws-cdk/aws-codepipeline/README.md +++ b/packages/@aws-cdk/aws-codepipeline/README.md @@ -83,7 +83,6 @@ sourceStage.addAction(someAction); ### Cross-region CodePipelines You can also use the cross-region feature to deploy resources -(currently, only CloudFormation Stacks are supported) into a different region than your Pipeline is in. It works like this: @@ -180,6 +179,56 @@ const replicationBucket = new s3.Bucket(replicationStack, 'ReplicationBucket', { }); ``` +### Variables + +The library supports the CodePipeline Variables feature. +Each action class that emits variables has a separate variables interface, +accessed as a property of the action instance called `variables`. +You instantiate the action class and assign it to a local variable; +when you want to use a variable in the configuration of a different action, +you access the appropriate property of the interface returned from `variables`, +which represents a single variable. +Example: + +```typescript +// MyAction is some action type that produces variables +const myAction = new MyAction({ + // ... +}); +new OtherAction({ + // ... + config: myAction.variables.myVariable, +}); +``` + +The namespace name that will be used will be automatically generated by the pipeline construct, +based on the stage and action name; +you can pass a custom name when creating the action instance: + +```typescript +const myAction = new MyAction({ + // ... + variablesNamespace: 'MyNamespace', +}); +``` + +There are also global variables available, +not tied to any action; +these are accessed through static properties of the `GlobalVariables` class: + +```typescript +new OtherAction({ + // ... + config: codepipeline.GlobalVariables.executionId, +}); +``` + +Check the documentation of the `@aws-cdk/aws-codepipeline-actions` +for details on how to use the variables for each action class. + +See the [CodePipeline documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-variables.html) +for more details on how to use the variables feature. + ### Events #### Using a pipeline as an event target diff --git a/packages/@aws-cdk/aws-codepipeline/lib/action.ts b/packages/@aws-cdk/aws-codepipeline/lib/action.ts index a8a2a74024520..e5c8e66dc0c15 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/action.ts @@ -27,6 +27,17 @@ export interface ActionArtifactBounds { readonly maxOutputs: number; } +/** + * The CodePipeline variables that are global, + * not bound to a specific action. + * This class defines a bunch of static fields that represent the different variables. + * These can be used can be used in any action configuration. + */ +export class GlobalVariables { + /** The identifier of the current pipeline execution. */ + public static readonly executionId = '#{codepipeline.PipelineExecutionId}'; +} + export interface ActionProperties { readonly actionName: string; readonly role?: iam.IRole; @@ -84,6 +95,13 @@ export interface ActionProperties { readonly artifactBounds: ActionArtifactBounds; readonly inputs?: Artifact[]; readonly outputs?: Artifact[]; + + /** + * The name of the namespace to use for variables emitted by this action. + * + * @default - a name will be generated, based on the stage and action names + */ + readonly variablesNamespace?: string; } export interface ActionBindOptions { @@ -181,6 +199,15 @@ export interface CommonActionProps { * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html */ readonly runOrder?: number; + + /** + * The name of the namespace to use for variables emitted by this action. + * + * @default - a name will be generated, based on the stage and action names, + * if any of the action's variables were referenced - otherwise, + * no namespace will be set + */ + readonly variablesNamespace?: string; } /** diff --git a/packages/@aws-cdk/aws-codepipeline/lib/full-action-descriptor.ts b/packages/@aws-cdk/aws-codepipeline/lib/full-action-descriptor.ts index e2aa2774bb5c3..b2759ff353b85 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/full-action-descriptor.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/full-action-descriptor.ts @@ -21,6 +21,7 @@ export class FullActionDescriptor { public readonly version: string; public readonly runOrder: number; public readonly artifactBounds: ActionArtifactBounds; + public readonly namespace?: string; public readonly inputs: Artifact[]; public readonly outputs: Artifact[]; public readonly region?: string; @@ -37,6 +38,7 @@ export class FullActionDescriptor { this.version = actionProperties.version || '1'; this.runOrder = actionProperties.runOrder === undefined ? 1 : actionProperties.runOrder; this.artifactBounds = actionProperties.artifactBounds; + this.namespace = actionProperties.variablesNamespace; this.inputs = deduplicateArtifacts(actionProperties.inputs); this.outputs = deduplicateArtifacts(actionProperties.outputs); this.region = props.actionRegion || actionProperties.region; diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 70480091e8371..5cb2e1465882f 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -8,7 +8,7 @@ import { CfnPipeline } from './codepipeline.generated'; import { CrossRegionSupportConstruct, CrossRegionSupportStack } from './cross-region-support-stack'; import { FullActionDescriptor } from './full-action-descriptor'; import { Stage } from './stage'; -import { validateName, validateSourceAction } from "./validation"; +import { validateName, validateNamespaceName, validateSourceAction } from "./validation"; /** * Allows you to control where to place a new Stage when it's added to the Pipeline. @@ -354,15 +354,18 @@ export class Pipeline extends PipelineBase { // get the role for the given action const actionRole = this.getRoleForAction(stage, action, actionScope); + // // CodePipeline Variables + validateNamespaceName(action.actionProperties.variablesNamespace); + // bind the Action - const actionDescriptor = action.bind(actionScope, stage, { + const actionConfig = action.bind(actionScope, stage, { role: actionRole ? actionRole : this.role, bucket: crossRegionInfo.artifactBucket, }); return new FullActionDescriptor({ action, - actionConfig: actionDescriptor, + actionConfig, actionRole, actionRegion: crossRegionInfo.region, }); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index 15b31cefaf1d4..de236a3c30263 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -157,6 +157,7 @@ export class Stage implements IStage { runOrder: action.runOrder, roleArn: action.role ? action.role.roleArn : undefined, region: action.region, + namespace: action.namespace, }; } diff --git a/packages/@aws-cdk/aws-codepipeline/lib/validation.ts b/packages/@aws-cdk/aws-codepipeline/lib/validation.ts index c30e90eb0c2d4..821a13a2c9a1f 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/validation.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/validation.ts @@ -53,6 +53,10 @@ export function validateArtifactName(artifactName: string | undefined): void { validateAgainstRegex(/^[a-zA-Z0-9_-]{1,100}$/, 'Artifact', artifactName); } +export function validateNamespaceName(namespaceName: string | undefined): void { + validateAgainstRegex(/^[A-Za-z0-9@_-]{1,100}$/, 'Namespace', namespaceName); +} + function validateAgainstRegex(regex: RegExp, thing: string, name: string | undefined) { // name could be a Token - in that case, skip validation altogether if (cdk.Token.isUnresolved(name)) { diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index d5c2a04eeab02..505b0ab033f0e 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -120,6 +120,7 @@ "docs-public-apis:@aws-cdk/aws-codepipeline.ActionArtifactBounds.maxOutputs", "docs-public-apis:@aws-cdk/aws-codepipeline.ActionArtifactBounds.minInputs", "docs-public-apis:@aws-cdk/aws-codepipeline.ActionArtifactBounds.minOutputs", + "public-static-props-all-caps:@aws-cdk/aws-codepipeline.GlobalVariables.executionId", "docs-public-apis:@aws-cdk/aws-codepipeline.ActionBindOptions", "docs-public-apis:@aws-cdk/aws-codepipeline.ActionBindOptions.bucket", "docs-public-apis:@aws-cdk/aws-codepipeline.ActionBindOptions.role", diff --git a/packages/@aws-cdk/aws-codepipeline/test/fake-build-action.ts b/packages/@aws-cdk/aws-codepipeline/test/fake-build-action.ts index 4496626a55c25..f1ba06d1f3128 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/fake-build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/fake-build-action.ts @@ -17,10 +17,13 @@ export interface FakeBuildActionProps extends codepipeline.CommonActionProps { account?: string; region?: string; + + customConfigKey?: string; } export class FakeBuildAction implements codepipeline.IAction { public readonly actionProperties: codepipeline.ActionProperties; + private readonly customConfigKey: string | undefined; constructor(props: FakeBuildActionProps) { this.actionProperties = { @@ -31,11 +34,16 @@ export class FakeBuildAction implements codepipeline.IAction { inputs: [props.input, ...props.extraInputs || []], outputs: props.output ? [props.output] : undefined, }; + this.customConfigKey = props.customConfigKey; } public bind(_scope: Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): codepipeline.ActionConfig { - return {}; + return { + configuration: { + CustomConfigKey: this.customConfigKey, + }, + }; } public onStateChange(_name: string, _target?: events.IRuleTarget, _options?: events.RuleProps): events.Rule { diff --git a/packages/@aws-cdk/aws-codepipeline/test/fake-source-action.ts b/packages/@aws-cdk/aws-codepipeline/test/fake-source-action.ts index c6f772e73a7df..099efb65b359a 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/fake-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/fake-source-action.ts @@ -1,18 +1,23 @@ import * as events from '@aws-cdk/aws-events'; -import { Construct } from '@aws-cdk/core'; +import { Construct, Lazy } from '@aws-cdk/core'; import * as codepipeline from '../lib'; +export interface IFakeSourceActionVariables { + readonly firstVariable: string; +} + export interface FakeSourceActionProps extends codepipeline.CommonActionProps { - output: codepipeline.Artifact; + readonly output: codepipeline.Artifact; - extraOutputs?: codepipeline.Artifact[]; + readonly extraOutputs?: codepipeline.Artifact[]; - region?: string; + readonly region?: string; } export class FakeSourceAction implements codepipeline.IAction { public readonly inputs?: codepipeline.Artifact[]; public readonly outputs?: codepipeline.Artifact[]; + public readonly variables: IFakeSourceActionVariables; public readonly actionProperties: codepipeline.ActionProperties; @@ -24,6 +29,9 @@ export class FakeSourceAction implements codepipeline.IAction { artifactBounds: { minInputs: 0, maxInputs: 0, minOutputs: 1, maxOutputs: 4 }, outputs: [props.output, ...props.extraOutputs || []], }; + this.variables = { + firstVariable: Lazy.stringValue({ produce: () => `#{${this.actionProperties.variablesNamespace}.FirstVariable}` }), + }; } public bind(_scope: Construct, _stage: codepipeline.IStage, _options: codepipeline.ActionBindOptions): diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.variables.ts b/packages/@aws-cdk/aws-codepipeline/test/test.variables.ts new file mode 100644 index 0000000000000..4ae685ee976de --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/test.variables.ts @@ -0,0 +1,144 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as codepipeline from '../lib'; +import { FakeBuildAction } from './fake-build-action'; +import { FakeSourceAction } from './fake-source-action'; + +// tslint:disable:object-literal-key-quotes + +export = { + 'Pipeline Variables': { + 'uses the passed namespace when its passed when constructing the Action'(test: Test) { + const stack = new cdk.Stack(); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [new FakeSourceAction({ + actionName: 'Source', + output: new codepipeline.Artifact(), + variablesNamespace: 'MyNamespace', + })], + }, + ], + }); + + expect(stack, true).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [ + { + "Name": "Source", + "Actions": [ + { + "Name": "Source", + "Namespace": "MyNamespace", + }, + ], + }, + ], + })); + + test.done(); + }, + + 'allows using the variable in the configuration of a different action'(test: Test) { + const stack = new cdk.Stack(); + const sourceOutput = new codepipeline.Artifact(); + const fakeSourceAction = new FakeSourceAction({ + actionName: 'Source', + output: sourceOutput, + variablesNamespace: 'SourceVariables', + }); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [fakeSourceAction], + }, + { + stageName: 'Build', + actions: [new FakeBuildAction({ + actionName: 'Build', + input: sourceOutput, + customConfigKey: fakeSourceAction.variables.firstVariable, + })], + }, + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [ + { + "Name": "Source", + }, + { + "Name": "Build", + "Actions": [ + { + "Name": "Build", + "Configuration": { + "CustomConfigKey": "#{SourceVariables.FirstVariable}", + }, + }, + ], + }, + ], + })); + + test.done(); + }, + + 'fails when trying add an action using variables with an empty string for the namespace to a pipeline'(test: Test) { + const stack = new cdk.Stack(); + const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); + const sourceStage = pipeline.addStage({ stageName: 'Source' }); + + const sourceAction = new FakeSourceAction({ + actionName: 'Source', + output: new codepipeline.Artifact(), + variablesNamespace: '', + }); + + test.throws(() => { + sourceStage.addAction(sourceAction); + }, /Namespace name must match regular expression:/); + + test.done(); + }, + + 'can use global variables'(test: Test) { + const stack = new cdk.Stack(); + + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [new FakeBuildAction({ + actionName: 'Build', + input: new codepipeline.Artifact(), + customConfigKey: codepipeline.GlobalVariables.executionId, + })], + }, + ], + }); + + expect(stack, true).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + "Stages": [ + { + "Name": "Source", + "Actions": [ + { + "Name": "Build", + "Configuration": { + "CustomConfigKey": "#{codepipeline.PipelineExecutionId}", + }, + }, + ], + }, + ], + })); + + test.done(); + }, + }, +};