From b8e701cd69b0f93b74f509e1a2b850a9e5c109a6 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Tue, 2 Oct 2018 17:04:27 -0700 Subject: [PATCH] feat(aws-codepipeline): make input and output artifact names optional when creating Actions. Previously, we always required customers to explicitly name the output artifacts the Actions used in the Pipeline, and to explicitly "wire together" the outputs of one Action as inputs to another. With this change, the CodePipeline Construct generates artifact names, if the customer didn't provide one explicitly, and tries to find the first available output artifact to use as input to a newly created Action that needs it, thus turning both the input and output artifacts from required to optional properties. --- .../aws-codebuild/lib/pipeline-actions.ts | 10 ++- .../@aws-cdk/aws-codebuild/lib/project.ts | 2 +- .../aws-codecommit/lib/pipeline-action.ts | 4 +- .../@aws-cdk/aws-codecommit/lib/repository.ts | 2 +- .../aws-codepipeline-api/lib/action.ts | 33 +++++++-- .../aws-codepipeline-api/lib/build-action.ts | 8 +-- .../aws-codepipeline-api/lib/source-action.ts | 4 +- packages/@aws-cdk/aws-codepipeline/README.md | 69 ++++++++++++++----- .../lib/github-source-action.ts | 4 +- .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 32 +++++++++ .../@aws-cdk/aws-codepipeline/lib/stage.ts | 8 +++ ...g.pipeline-code-commit-build.expected.json | 6 +- .../test/integ.pipeline-events.expected.json | 6 +- .../aws-codepipeline/test/test.action.ts | 59 +++++++++++++++- .../aws-lambda/lib/pipeline-action.ts | 9 --- .../@aws-cdk/aws-s3/lib/pipeline-action.ts | 4 +- 16 files changed, 211 insertions(+), 49 deletions(-) diff --git a/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts index d29bd8ff66f47..0b165db400065 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts @@ -9,12 +9,16 @@ import { ProjectRef } from './project'; */ export interface CommonPipelineBuildActionProps { /** - * The source to use as input for this build + * The source to use as input for this build. + * + * @default CodePipeline will use the output of the last Action from a previous Stage as input */ - inputArtifact: codepipeline.Artifact; + inputArtifact?: codepipeline.Artifact; /** - * The name of the build's output artifact + * The name of the build's output artifact. + * + * @default an auto-generated name will be used */ artifactName?: string; } diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 9111f1c11bdcf..072d04eb4201d 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -89,7 +89,7 @@ export abstract class ProjectRef extends cdk.Construct implements events.IEventR * @param props the properties of the new Action * @returns the newly created {@link PipelineBuildAction} build Action */ - public addBuildToPipeline(stage: codepipeline.IStage, name: string, props: CommonPipelineBuildActionProps): PipelineBuildAction { + public addBuildToPipeline(stage: codepipeline.IStage, name: string, props?: CommonPipelineBuildActionProps): PipelineBuildAction { return new PipelineBuildAction(this.parent!, name, { stage, project: this, diff --git a/packages/@aws-cdk/aws-codecommit/lib/pipeline-action.ts b/packages/@aws-cdk/aws-codecommit/lib/pipeline-action.ts index 0699e4ff8ace3..9cd8e1603779b 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/pipeline-action.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/pipeline-action.ts @@ -11,8 +11,10 @@ export interface CommonPipelineSourceActionProps { /** * The name of the source's output artifact. * Output artifacts are used by CodePipeline as inputs into other actions. + * + * @default a name will be auto-generated */ - artifactName: string; + artifactName?: string; /** * @default 'master' diff --git a/packages/@aws-cdk/aws-codecommit/lib/repository.ts b/packages/@aws-cdk/aws-codecommit/lib/repository.ts index 465c25cb76b6f..91a75ec475a7a 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/repository.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/repository.ts @@ -68,7 +68,7 @@ export abstract class RepositoryRef extends cdk.Construct { * @param props the properties of the new Action * @returns the newly created {@link PipelineSourceAction} */ - public addToPipeline(stage: actions.IStage, name: string, props: CommonPipelineSourceActionProps): PipelineSourceAction { + public addToPipeline(stage: actions.IStage, name: string, props?: CommonPipelineSourceActionProps): PipelineSourceAction { return new PipelineSourceAction(this.parent!, name, { stage, repository: this, diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts index 6430ad95c6ba4..10e06d9863737 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts @@ -71,6 +71,25 @@ export interface IStage { * @param action the Action to add to this Stage */ _attachAction(action: Action): void; + + /** + * Generates a unique output artifact name for the given Action. + * This is an internal operation - + * you should never need to call it directly. + * + * @param action the Action to generate the output artifact name for + */ + _generateOutputArtifactName(action: Action): string; + + /** + * Finds an input artifact for the given Action + * among the existing output artifacts currently in the Pipeline. + * This is an internal operation - + * you should never need to call it directly. + * + * @param action the Action to find the input artifact for + */ + _findInputArtifactFor(action: Action): Artifact; } /** @@ -192,13 +211,19 @@ export abstract class Action extends cdk.Construct { } } - protected addOutputArtifact(name: string): Artifact { - const artifact = new Artifact(this, name); + protected addOutputArtifact(name?: string): Artifact { + const outputArtifactName = name === undefined + ? this.stage._generateOutputArtifactName(this) + : name; + + const artifact = new Artifact(this, outputArtifactName); return artifact; } - protected addInputArtifact(artifact: Artifact): Action { - this._inputArtifacts.push(artifact); + protected addInputArtifact(artifact?: Artifact): Action { + this._inputArtifacts.push(artifact === undefined + ? this.stage._findInputArtifactFor(this) + : artifact); return this; } } diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/build-action.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/build-action.ts index c124234ae38ce..863f02445997c 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/build-action.ts @@ -9,7 +9,7 @@ export interface BuildActionProps extends CommonActionProps { /** * The source to use as input for this build. */ - inputArtifact: Artifact; + inputArtifact?: Artifact; /** * The service provider that the action calls. For example, a valid provider for Source actions is CodeBuild. @@ -36,7 +36,7 @@ export interface BuildActionProps extends CommonActionProps { * such as {@link codebuild.PipelineBuildAction}. */ export abstract class BuildAction extends Action { - public readonly artifact?: Artifact; + public readonly artifact: Artifact; constructor(parent: cdk.Construct, name: string, props: BuildActionProps) { super(parent, name, { @@ -48,8 +48,6 @@ export abstract class BuildAction extends Action { }); this.addInputArtifact(props.inputArtifact); - if (props.artifactName) { - this.artifact = this.addOutputArtifact(props.artifactName); - } + this.artifact = this.addOutputArtifact(props.artifactName); } } diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/source-action.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/source-action.ts index a3f81d1f68519..b6d9e7824e6d5 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/source-action.ts @@ -23,8 +23,10 @@ export interface SourceActionProps extends CommonActionProps { /** * The name of the source's output artifact. * Output artifacts are used by CodePipeline as inputs into other actions. + * + * @default a name will be auto-generated */ - artifactName: string; + artifactName?: string; /** * The service provider that the action calls. diff --git a/packages/@aws-cdk/aws-codepipeline/README.md b/packages/@aws-cdk/aws-codepipeline/README.md index 177f95b91969d..b7c2633f28071 100644 --- a/packages/@aws-cdk/aws-codepipeline/README.md +++ b/packages/@aws-cdk/aws-codepipeline/README.md @@ -1,14 +1,26 @@ -## AWS CodePipeline construct library +## AWS CodePipeline Construct Library -Construct an empty Pipeline: +### Pipeline + +To construct an empty Pipeline: + +```ts +import codepipeline = require('@aws-cdk/aws-codepipeline'); + +const pipeline = new codepipeline.Pipeline(this, 'MyFirstPipeline'); +``` + +To give the Pipeline a nice, human-readable name: ```ts -const pipeline = new Pipeline(this, 'MyFirstPipeline', { - pipelineName: 'MyFirstPipeline', +const pipeline = new codepipeline.Pipeline(this, 'MyFirstPipeline', { + pipelineName: 'MyPipeline', }); ``` -Append a Stage to the Pipeline: +### Stages + +To append a Stage to a Pipeline: ```ts const sourceStage = pipeline.addStage('Source'); @@ -21,28 +33,47 @@ You can insert the new Stage at an arbitrary point in the Pipeline: ```ts const sourceStage = pipeline.addStage('Source', { - placement: { - // note: you can only specify one of the below properties - rightBefore: anotherStage, - justAfter: anotherStage, - atIndex: 3, // indexing starts at 0 - // pipeline.stageCount returns the number of Stages currently in the Pipeline - } + placement: { + // note: you can only specify one of the below properties + rightBefore: anotherStage, + justAfter: anotherStage, + atIndex: 3, // indexing starts at 0 + // pipeline.stageCount returns the number of Stages currently in the Pipeline + } }); ``` -Add an Action to a Stage: +### Actions + +To add an Action to a Stage: ```ts -new codecommit.PipelineSourceAction(this, 'Source', { - stage: sourceStage, - artifactName: 'MyPackageSourceArtifact', - repository: codecommit.RepositoryRef.import(this, 'MyExistingRepository', { - repositoryName: new codecommit.RepositoryName('MyExistingRepository'), - }), +new codepipeline.GitHubSourceAction(this, 'GitHub_Source', { + stage: sourceStage, + owner: 'awslabs', + repo: 'aws-cdk', + branch: 'develop', // default: 'master' + oauthToken: ..., }) ``` +The Pipeline construct will automatically generate and wire together the artifact names CodePipeline uses. +If you need, you can also name the artifacts explicitly: + +```ts +const sourceAction = new codepipeline.GitHubSourceAction(this, 'GitHub_Source', { + // other properties as above... + artifactName: 'SourceOutput', // this will be the name of the output artifact in the Pipeline +}); + +// in a build Action later... + +new codepipeline.JenkinsBuildAction(this, 'Jenkins_Build', { + // other properties... + inputArtifact: sourceAction.artifact, +}); +``` + ### Events #### Using a pipeline as an event target diff --git a/packages/@aws-cdk/aws-codepipeline/lib/github-source-action.ts b/packages/@aws-cdk/aws-codepipeline/lib/github-source-action.ts index c94a8992889b4..2d2268a3c66cd 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/github-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/github-source-action.ts @@ -8,8 +8,10 @@ export interface GitHubSourceActionProps extends actions.CommonActionProps { /** * The name of the source's output artifact. Output artifacts are used by CodePipeline as * inputs into other actions. + * + * @default a name will be auto-generated */ - artifactName: string; + artifactName?: string; /** * The GitHub account/user that owns the repo. diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 7a3d690d2aadc..f2e3fde769b01 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -72,6 +72,7 @@ export class Pipeline extends cdk.Construct implements events.IEventRuleTarget { private readonly stages = new Array(); private eventsRole?: iam.Role; + private artifactsCounter = 0; constructor(parent: cdk.Construct, name: string, props?: PipelineProps) { super(parent, name); @@ -245,6 +246,37 @@ export class Pipeline extends cdk.Construct implements events.IEventRuleTarget { this.stages.splice(index, 0, stage); } + // ignore unused private method (it's actually used in Stage) + // @ts-ignore + private _generateOutputArtifactName(stage: actions.IStage, action: actions.Action): string { + // for now, just return a generic output artifact, + // ignoring the names of both the Stage, and the Action + return 'Artifact_' + (++this.artifactsCounter); + } + + // ignore unused private method (it's actually used in Stage) + // @ts-ignore + private _findInputArtifactFor(stage: actions.IStage, action: actions.Action): actions.Artifact { + // search for the first Action that has an outputArtifact, + // and return that + const startIndex = this.stages.findIndex(s => s === stage); + for (let i = startIndex; i >= 0; i--) { + const currentStage = this.stages[i]; + + // get all of the Actions in the Stage, sorted by runOrder, descending + const currentActions = currentStage.actions.sort((a1, a2) => -(a1.runOrder - a2.runOrder)); + for (const currentAction of currentActions) { + // for the first Stage (the one that `action` belongs to) + // we need to only take into account Actions with a smaller runOrder than `action` + if ((i !== startIndex || currentAction.runOrder < action.runOrder) && currentAction.outputArtifacts.length > 0) { + return currentAction.outputArtifacts[0]; + } + } + } + throw new Error(`Could not determine the input artifact for Action with name '${action.id}'. ` + + 'Please provide it explicitly with the inputArtifact property.'); + } + private calculateInsertIndexFromPlacement(placement: StagePlacement): number { // check if at most one placement property was provided const providedPlacementProps = ['rightBefore', 'justAfter', 'atIndex'] diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index 44c7c699577a5..f88e55e776dc6 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -146,6 +146,14 @@ export class Stage extends cdk.Construct implements actions.IStage { } } + public _generateOutputArtifactName(action: actions.Action): string { + return (this.pipeline as any)._generateOutputArtifactName(this, action); + } + + public _findInputArtifactFor(action: actions.Action): actions.Artifact { + return (this.pipeline as any)._findInputArtifactFor(this, action); + } + private renderAction(action: actions.Action): cloudformation.PipelineResource.ActionDeclarationProperty { return { name: action.id, diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.expected.json index 4088ef1fa556d..f8ad9e6e96774 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.expected.json @@ -175,7 +175,11 @@ } ], "Name": "build", - "OutputArtifacts": [], + "OutputArtifacts": [ + { + "Name": "Artifact_1" + } + ], "RunOrder": 1 } ], diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-events.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-events.expected.json index df70d70cfd542..5bf9e04487aa0 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-events.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-events.expected.json @@ -168,7 +168,11 @@ } ], "Name": "CodeBuildAction", - "OutputArtifacts": [], + "OutputArtifacts": [ + { + "Name": "Artifact_1" + } + ], "RunOrder": 1 } ], diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.action.ts b/packages/@aws-cdk/aws-codepipeline/test/test.action.ts index fd640a6d59886..7fa8a757ba246 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.action.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.action.ts @@ -1,4 +1,7 @@ // import { validateArtifactBounds, validateSourceAction } from '../lib/validation'; +import { expect, haveResource } from '@aws-cdk/assert'; +import codebuild = require('@aws-cdk/aws-codebuild'); +import codecommit = require('@aws-cdk/aws-codecommit'); import actions = require('@aws-cdk/aws-codepipeline-api'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; @@ -89,7 +92,61 @@ export = { runOrder: 1 }); test.done(); - } + }, + + 'automatically assigns artifact names to the Actions'(test: Test) { + const stack = new cdk.Stack(); + const pipeline = new codepipeline.Pipeline(stack, 'pipeline'); + + const repo = new codecommit.Repository(stack, 'Repo', { + repositoryName: 'Repo', + }); + const sourceStage = pipeline.addStage('Source'); + repo.addToPipeline(sourceStage, 'CodeCommit'); + + const project = new codebuild.PipelineProject(stack, 'Project'); + const buildStage = pipeline.addStage('Build'); + project.addBuildToPipeline(buildStage, 'CodeBuild'); + + expect(stack).to(haveResource('AWS::CodePipeline::Pipeline', { + "Stages": [ + { + "Name": "Source", + "Actions": [ + { + "Name": "CodeCommit", + "InputArtifacts": [], + "OutputArtifacts": [ + { + "Name": "Artifact_1", + }, + ], + } + ], + }, + { + "Name": "Build", + "Actions": [ + { + "Name": "CodeBuild", + "InputArtifacts": [ + { + "Name": "Artifact_1", + } + ], + "OutputArtifacts": [ + { + "Name": "Artifact_2", + }, + ], + } + ], + }, + ], + })); + + test.done(); + }, }; function boundsValidationResult(numberOfArtifacts: number, min: number, max: number): string[] { diff --git a/packages/@aws-cdk/aws-lambda/lib/pipeline-action.ts b/packages/@aws-cdk/aws-lambda/lib/pipeline-action.ts index d8e3e8065b116..75199e3642922 100644 --- a/packages/@aws-cdk/aws-lambda/lib/pipeline-action.ts +++ b/packages/@aws-cdk/aws-lambda/lib/pipeline-action.ts @@ -76,13 +76,4 @@ export class PipelineInvokeAction extends codepipeline.Action { .addAction('codepipeline:PutJobFailureResult')); } } - - /** - * Add an input artifact - * @param artifact - */ - protected addInputArtifact(artifact: codepipeline.Artifact): codepipeline.Action { - super.addInputArtifact(artifact); - return this; - } } diff --git a/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts b/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts index 026beec985ee9..b1e1090475c78 100644 --- a/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts +++ b/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts @@ -11,8 +11,10 @@ export interface CommonPipelineSourceActionProps { /** * The name of the source's output artifact. Output artifacts are used by CodePipeline as * inputs into other actions. + * + * @default a name will be auto-generated */ - artifactName: string; + artifactName?: string; /** * The key within the S3 bucket that stores the source code.