Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(codepipeline): Pipeline Variables #5604

Merged
merged 4 commits into from
Jan 15, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
38 changes: 35 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,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):
Expand All @@ -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);
}

Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 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}.
*/
Expand Down Expand Up @@ -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'),
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
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 ||
Expand Down
Loading