Skip to content

Commit

Permalink
feat(codepipeline): CodePipeline Variables
Browse files Browse the repository at this point in the history
Added support for the CodePipeline Variables feature,
by adding action class-specific interfaces that represent the
collection of variables they emit,
and a readonly property of the action instance that returns that interface.
Plus a class representing the global pipeline variables
with static properties.

Fixes aws#5219
  • Loading branch information
skinny85 committed Jan 14, 2020
1 parent c02741e commit 29454c8
Show file tree
Hide file tree
Showing 24 changed files with 1,155 additions and 14 deletions.
169 changes: 167 additions & 2 deletions packages/@aws-cdk/aws-codepipeline-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
});
```

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
44 changes: 41 additions & 3 deletions packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -13,12 +13,27 @@ 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 actualNamespace?: string;
private variableReferenced = false;

constructor(public readonly actionProperties: codepipeline.ActionProperties) {
// nothing to do
protected constructor(actionProperties: codepipeline.ActionProperties) {
this.customerProvidedNamespace = actionProperties.variablesNamespace;
const variablesNamespace = actionProperties.variablesNamespace !== undefined
// if a customer passed a namespace explicitly, always use that
? actionProperties.variablesNamespace
: Lazy.stringValue({ produce: () => {
// otherwise, only return a namespace if any variable was referenced
return this.variableReferenced ? this.actualNamespace : undefined;
}});
this.actionProperties = {
...actionProperties,
variablesNamespace,
};
}

public bind(scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions):
Expand All @@ -27,6 +42,13 @@ export abstract class Action implements codepipeline.IAction {
this._stage = stage;
this._scope = scope;

if (this.customerProvidedNamespace === undefined) {
// default a namespace name, based on the stage and action names
this.actualNamespace = `${stage.stageName}_${this.actionProperties.actionName}_NS`;
} else {
this.actualNamespace = this.customerProvidedNamespace;
}

return this.bound(scope, stage, options);
}

Expand All @@ -45,6 +67,22 @@ export abstract class Action implements codepipeline.IAction {
return rule;
}

protected variableWasReferenced(): void {
this.variableReferenced = true;
}

protected variableExpression(variableName: string): string {
return Lazy.stringValue({ produce: () => {
// make sure the action was bound (= added to a pipeline)
if (this.actualNamespace) {
return `#{${this.actualNamespace}.${variableName}}`;
} else {
throw new Error(`Cannot reference variables of action '${this.actionProperties.actionName}', ` +
'as that action was never added to a pipeline');
}
}});
}

/**
* The method called when an Action is attached to a Pipeline.
* This method is guaranteed to be called only once for each Action instance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@ 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 {
this.variableWasReferenced();
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,29 @@ export enum CodeCommitTrigger {
EVENTS = 'Events',
}

/**
* The CodePipeline variables emitted by the CodeCommit source Action.
*/
export interface ICodeCommitSourceVariables {
/** 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}.
*/
Expand Down Expand Up @@ -62,6 +85,7 @@ export interface CodeCommitSourceActionProps extends codepipeline.CommonAwsActio
export class CodeCommitSourceAction extends Action {
private readonly branch: string;
private readonly props: CodeCommitSourceActionProps;
private readonly _variables: ICodeCommitSourceVariables;

constructor(props: CodeCommitSourceActionProps) {
const branch = props.branch || 'master';
Expand All @@ -77,6 +101,20 @@ export class CodeCommitSourceAction extends Action {

this.branch = branch;
this.props = props;
this._variables = {
repositoryName: this.variableExpression('RepositoryName'),
branchName: this.variableExpression('BranchName'),
authorDate: this.variableExpression('AuthorDate'),
committerDate: this.variableExpression('CommitterDate'),
commitId: this.variableExpression('CommitId'),
commitMessage: this.variableExpression('CommitMessage'),
};
}

/** The variables emitted by this action. */
public get variables(): ICodeCommitSourceVariables {
this.variableWasReferenced();
return this._variables;
}

protected bound(_scope: Construct, stage: codepipeline.IStage, options: codepipeline.ActionBindOptions):
Expand Down
Loading

0 comments on commit 29454c8

Please sign in to comment.