diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 3221c3a5caa43..47c95ba410975 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -338,6 +338,40 @@ const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { You can adapt these examples to your own situation. +#### Migrating from buildspec.yml files + +You may currently have the build instructions for your CodeBuild Projects in a +`buildspec.yml` file in your source repository. In addition to your build +commands, the CodeBuild Project's buildspec also controls some information that +CDK Pipelines manages for you, like artifact identifiers, input artifact +locations, Docker authorization, and exported variables. + +Since there is no way in general for CDK Pipelines to modify the file in your +resource repository, CDK Pipelines configures the BuildSpec directly on the +CodeBuild Project, instead of loading it from the `buildspec.yml` file. +This requires a pipeline self-mutation to update. + +To avoid this, put your build instructions in a separate script, for example +`build.sh`, and call that script from the build `commands` array: + +```ts +declare const source: pipelines.IFileSetProducer; + +const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { + input: source, + commands: [ + // Abstract over doing the build + './build.sh', + ], + }) +}); +``` + +Doing so keeps your exact build instructions in sync with your source code in +the source repository where it belongs, and provides a convenient build script +for developers at the same time. + #### CodePipeline Sources In CodePipeline, *Sources* define where the source of your application lives. @@ -756,6 +790,13 @@ class MyJenkinsStep extends pipelines.Step implements pipelines.ICodePipelineAct private readonly input: pipelines.FileSet, ) { super('MyJenkinsStep'); + + // This is necessary if your step accepts things like environment variables + // that may contain outputs from other steps. It doesn't matter what the + // structure is, as long as it contains the values that may contain outputs. + this.discoverReferencedOutputs({ + env: { /* ... */ } + }); } public produceAction(stage: codepipeline.IStage, options: pipelines.ProduceActionOptions): pipelines.CodePipelineActionFactoryResult { diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts b/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts index 859c279533fa3..31b93f5b9cf87 100644 --- a/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts +++ b/packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts @@ -33,5 +33,7 @@ export class ManualApprovalStep extends Step { super(id); this.comment = props.comment; + + this.discoverReferencedOutputs(props.comment); } } \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts b/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts index d36f0999fca17..fa01624e9635a 100644 --- a/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts +++ b/packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts @@ -87,7 +87,6 @@ export interface ShellStepProps { * @default - No primary output */ readonly primaryOutputDirectory?: string; - } /** @@ -152,6 +151,11 @@ export class ShellStep extends Step { this.env = props.env ?? {}; this.envFromCfnOutputs = mapValues(props.envFromCfnOutputs ?? {}, StackOutputReference.fromCfnOutput); + // 'env' is the only thing that can contain outputs + this.discoverReferencedOutputs({ + env: this.env, + }); + // Inputs if (props.input) { const fileSet = props.input.primaryOutput; diff --git a/packages/@aws-cdk/pipelines/lib/blueprint/step.ts b/packages/@aws-cdk/pipelines/lib/blueprint/step.ts index 65f4636b2ecbc..c17402f22e850 100644 --- a/packages/@aws-cdk/pipelines/lib/blueprint/step.ts +++ b/packages/@aws-cdk/pipelines/lib/blueprint/step.ts @@ -1,4 +1,5 @@ import { Stack, Token } from '@aws-cdk/core'; +import { StepOutput } from '../helpers-internal/step-output'; import { FileSet, IFileSetProducer } from './file-set'; /** @@ -39,7 +40,7 @@ export abstract class Step implements IFileSetProducer { private _primaryOutput?: FileSet; - private _dependencies: Step[] = []; + private _dependencies = new Set(); constructor( /** Identifier for this step */ @@ -54,7 +55,10 @@ export abstract class Step implements IFileSetProducer { * Return the steps this step depends on, based on the FileSets it requires */ public get dependencies(): Step[] { - return this.dependencyFileSets.map(f => f.producer).concat(this._dependencies); + return Array.from(new Set([ + ...this.dependencyFileSets.map(f => f.producer), + ...this._dependencies, + ])); } /** @@ -79,7 +83,7 @@ export abstract class Step implements IFileSetProducer { * Add a dependency on another step. */ public addStepDependency(step: Step) { - this._dependencies.push(step); + this._dependencies.add(step); } /** @@ -97,6 +101,21 @@ export abstract class Step implements IFileSetProducer { protected configurePrimaryOutput(fs: FileSet) { this._primaryOutput = fs; } + + /** + * Crawl the given structure for references to StepOutputs and add dependencies on all steps found + * + * Should be called by subclasses based on what the user passes in as + * construction properties. The format of the structure passed in here does + * not have to correspond exactly to what gets rendered into the engine, it + * just needs to contain the same amount of data. + */ + protected discoverReferencedOutputs(structure: any) { + for (const output of StepOutput.findAll(structure)) { + this._dependencies.add(output.step); + StepOutput.recordProducer(output); + } + } } /** @@ -128,5 +147,4 @@ export interface StackSteps { * @default - no additional steps */ readonly post?: Step[]; - } \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts index f835e261aba3d..c50d715e2cbe9 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts @@ -1,8 +1,10 @@ -import { Duration } from '@aws-cdk/core'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; +import { Duration } from '@aws-cdk/core'; import { ShellStep, ShellStepProps } from '../blueprint'; +import { mergeBuildSpecs } from './private/buildspecs'; +import { makeCodePipelineOutput } from './private/outputs'; /** * Construction props for a CodeBuildStep @@ -96,6 +98,17 @@ export interface CodeBuildStepProps extends ShellStepProps { /** * Run a script as a CodeBuild Project + * + * The BuildSpec must be available inline--it cannot reference a file + * on disk. If your current build instructions are in a file like + * `buildspec.yml` in your repository, extract them to a script + * (say, `build.sh`) and invoke that script as part of the build: + * + * ```ts + * new pipelines.CodeBuildStep('Synth', { + * commands: ['./build.sh'], + * }); + * ``` */ export class CodeBuildStep extends ShellStep { /** @@ -105,13 +118,6 @@ export class CodeBuildStep extends ShellStep { */ public readonly projectName?: string; - /** - * Additional configuration that can only be configured via BuildSpec - * - * @default - No value specified at construction time, use defaults - */ - public readonly partialBuildSpec?: codebuild.BuildSpec; - /** * The VPC where to execute the SimpleSynth. * @@ -164,13 +170,16 @@ export class CodeBuildStep extends ShellStep { readonly timeout?: Duration; private _project?: codebuild.IProject; + private _partialBuildSpec?: codebuild.BuildSpec; + private readonly exportedVariables = new Set(); + private exportedVarsRendered = false; constructor(id: string, props: CodeBuildStepProps) { super(id, props); this.projectName = props.projectName; this.buildEnvironment = props.buildEnvironment; - this.partialBuildSpec = props.partialBuildSpec; + this._partialBuildSpec = props.partialBuildSpec; this.vpc = props.vpc; this.subnetSelection = props.subnetSelection; this.role = props.role; @@ -198,6 +207,44 @@ export class CodeBuildStep extends ShellStep { return this.project.grantPrincipal; } + /** + * Additional configuration that can only be configured via BuildSpec + * + * Contains exported variables + * + * @default - Contains the exported variables + */ + public get partialBuildSpec(): codebuild.BuildSpec | undefined { + this.exportedVarsRendered = true; + + const varsBuildSpec = this.exportedVariables.size > 0 ? codebuild.BuildSpec.fromObject({ + version: '0.2', + env: { + 'exported-variables': Array.from(this.exportedVariables), + }, + }) : undefined; + + return mergeBuildSpecs(varsBuildSpec, this._partialBuildSpec); + } + + /** + * Reference a CodePipeline variable defined by the CodeBuildStep. + * + * The variable must be set in the shell of the CodeBuild step when + * it finishes its `post_build` phase. + * + * @param variableName the name of the variable for reference. + */ + public exportedVariable(variableName: string): string { + if (this.exportedVarsRendered && !this.exportedVariables.has(variableName)) { + throw new Error('exportVariable(): Pipeline has already been produced, cannot call this function anymore'); + } + + this.exportedVariables.add(variableName); + + return makeCodePipelineOutput(this, variableName); + } + /** * Set the internal project value * diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts index 62c9fa86d025b..734c2fefa0128 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts @@ -23,6 +23,15 @@ export interface ProduceActionOptions { */ readonly runOrder: number; + /** + * If this step is producing outputs, the variables namespace assigned to it + * + * Pass this on to the Action you are creating. + * + * @default - Step doesn't produce any outputs + */ + readonly variablesNamespace?: string; + /** * Helper object to translate FileSets to CodePipeline Artifacts */ @@ -87,6 +96,8 @@ export interface ICodePipelineActionFactory { export interface CodePipelineActionFactoryResult { /** * How many RunOrders were consumed + * + * If you add 1 action, return the value 1 here. */ readonly runOrdersConsumed: number; diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts index dd40d0d6cf0e7..ee815a6f94492 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-source.ts @@ -9,6 +9,7 @@ import { SecretValue, Token } from '@aws-cdk/core'; import { Node } from 'constructs'; import { FileSet, Step } from '../blueprint'; import { CodePipelineActionFactoryResult, ProduceActionOptions, ICodePipelineActionFactory } from './codepipeline-action-factory'; +import { makeCodePipelineOutput } from './private/outputs'; /** * Factory for CodePipeline source steps @@ -104,12 +105,44 @@ export abstract class CodePipelineSource extends Step implements ICodePipelineAc public produceAction(stage: cp.IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult { const output = options.artifacts.toCodePipeline(this.primaryOutput!); - const action = this.getAction(output, options.actionName, options.runOrder); + + const action = this.getAction(output, options.actionName, options.runOrder, options.variablesNamespace); stage.addAction(action); return { runOrdersConsumed: 1 }; } - protected abstract getAction(output: Artifact, actionName: string, runOrder: number): Action; + protected abstract getAction(output: Artifact, actionName: string, runOrder: number, variablesNamespace?: string): Action; + + /** + * Return an attribute of the current source revision + * + * These values can be passed into the environment variables of pipeline steps, + * so your steps can access information about the source revision. + * + * What attributes are available depends on the type of source. These attributes + * are supported: + * + * - GitHub, CodeCommit, and CodeStar connection + * - `AuthorDate` + * - `BranchName` + * - `CommitId` + * - `CommitMessage` + * - GitHub and CodeCommit + * - `CommitterDate` + * - `RepositoryName` + * - GitHub + * - `CommitUrl` + * - CodeStar Connection + * - `FullRepositoryName` + * - S3 + * - `ETag` + * - `VersionId` + * + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-variables.html#reference-variables-list + */ + public sourceAttribute(name: string): string { + return makeCodePipelineOutput(this, name); + } } /** @@ -173,7 +206,7 @@ class GitHubSource extends CodePipelineSource { this.configurePrimaryOutput(new FileSet('Source', this)); } - protected getAction(output: Artifact, actionName: string, runOrder: number) { + protected getAction(output: Artifact, actionName: string, runOrder: number, variablesNamespace?: string) { return new cp_actions.GitHubSourceAction({ output, actionName, @@ -183,6 +216,7 @@ class GitHubSource extends CodePipelineSource { repo: this.repo, branch: this.branch, trigger: this.props.trigger, + variablesNamespace, }); } } @@ -216,7 +250,7 @@ class S3Source extends CodePipelineSource { this.configurePrimaryOutput(new FileSet('Source', this)); } - protected getAction(output: Artifact, _actionName: string, runOrder: number) { + protected getAction(output: Artifact, _actionName: string, runOrder: number, variablesNamespace?: string) { return new cp_actions.S3SourceAction({ output, // Bucket names are guaranteed to conform to ActionName restrictions @@ -225,6 +259,7 @@ class S3Source extends CodePipelineSource { bucketKey: this.objectKey, trigger: this.props.trigger, bucket: this.bucket, + variablesNamespace, }); } } @@ -284,7 +319,7 @@ class CodeStarConnectionSource extends CodePipelineSource { this.configurePrimaryOutput(new FileSet('Source', this)); } - protected getAction(output: Artifact, actionName: string, runOrder: number) { + protected getAction(output: Artifact, actionName: string, runOrder: number, variablesNamespace?: string) { return new cp_actions.CodeStarConnectionsSourceAction({ output, actionName, @@ -295,6 +330,7 @@ class CodeStarConnectionSource extends CodePipelineSource { branch: this.branch, codeBuildCloneOutput: this.props.codeBuildCloneOutput, triggerOnPush: this.props.triggerOnPush, + variablesNamespace, }); } } @@ -341,7 +377,7 @@ class CodeCommitSource extends CodePipelineSource { this.configurePrimaryOutput(new FileSet('Source', this)); } - protected getAction(output: Artifact, _actionName: string, runOrder: number) { + protected getAction(output: Artifact, _actionName: string, runOrder: number, variablesNamespace?: string) { return new cp_actions.CodeCommitSourceAction({ output, // Guaranteed to be okay as action name @@ -352,6 +388,7 @@ class CodeCommitSource extends CodePipelineSource { repository: this.repository, eventRole: this.props.eventRole, codeBuildCloneOutput: this.props.codeBuildCloneOutput, + variablesNamespace, }); } } diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts index 0beae3ea56fa1..84562b07aec8b 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts @@ -18,10 +18,11 @@ import { toPosixPath } from '../private/fs'; import { actionName, stackVariableNamespace } from '../private/identifiers'; import { enumerate, flatten, maybeSuffix, noUndefined } from '../private/javascript'; import { writeTemplateConfiguration } from '../private/template-configuration'; -import { CodeBuildFactory, mergeCodeBuildOptions } from './_codebuild-factory'; import { ArtifactMap } from './artifact-map'; import { CodeBuildStep } from './codebuild-step'; import { CodePipelineActionFactoryResult, ICodePipelineActionFactory } from './codepipeline-action-factory'; +import { CodeBuildFactory, mergeCodeBuildOptions } from './private/codebuild-factory'; +import { namespaceStepOutputs } from './private/outputs'; /** @@ -418,9 +419,14 @@ export class CodePipeline extends PipelineBase { const factory = this.actionFromNode(node); const nodeType = this.nodeTypeFromNode(node); + const name = actionName(node, sharedParent); + + const variablesNamespace = node.data?.type === 'step' + ? namespaceStepOutputs(node.data.step, pipelineStage, name) + : undefined; const result = factory.produceAction(pipelineStage, { - actionName: actionName(node, sharedParent), + actionName: name, runOrder, artifacts: this.artifacts, scope: obtainScope(this.pipeline, stageName), @@ -429,6 +435,7 @@ export class CodePipeline extends PipelineBase { // If this step happens to produce a CodeBuild job, set the default options codeBuildDefaults: nodeType ? this.codeBuildDefaultsFor(nodeType) : undefined, beforeSelfMutation, + variablesNamespace, }); if (node.data?.type === 'self-update') { diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/private/buildspecs.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/private/buildspecs.ts new file mode 100644 index 0000000000000..f904d7c174629 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/private/buildspecs.ts @@ -0,0 +1,10 @@ +import * as codebuild from '@aws-cdk/aws-codebuild'; + +export function mergeBuildSpecs(a: codebuild.BuildSpec, b?: codebuild.BuildSpec): codebuild.BuildSpec; +export function mergeBuildSpecs(a: codebuild.BuildSpec | undefined, b: codebuild.BuildSpec): codebuild.BuildSpec; +export function mergeBuildSpecs(a?: codebuild.BuildSpec, b?: codebuild.BuildSpec): codebuild.BuildSpec | undefined; +export function mergeBuildSpecs(a?: codebuild.BuildSpec, b?: codebuild.BuildSpec) { + if (!a || !b) { return a ?? b; } + return codebuild.mergeBuildSpecs(a, b); +} + diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/private/codebuild-factory.ts similarity index 94% rename from packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts rename to packages/@aws-cdk/pipelines/lib/codepipeline/private/codebuild-factory.ts index 3414554cd5197..84a0cf934a4ca 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/private/codebuild-factory.ts @@ -7,15 +7,17 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { IDependable, Stack, Token } from '@aws-cdk/core'; import { Construct, Node } from 'constructs'; -import { FileSetLocation, ShellStep, StackOutputReference } from '../blueprint'; -import { PipelineQueries } from '../helpers-internal/pipeline-queries'; -import { cloudAssemblyBuildSpecDir, obtainScope } from '../private/construct-internals'; -import { hash, stackVariableNamespace } from '../private/identifiers'; -import { mapValues, mkdict, noEmptyObject, noUndefined, partition } from '../private/javascript'; -import { ArtifactMap } from './artifact-map'; -import { CodeBuildStep } from './codebuild-step'; -import { CodeBuildOptions } from './codepipeline'; -import { ICodePipelineActionFactory, ProduceActionOptions, CodePipelineActionFactoryResult } from './codepipeline-action-factory'; +import { FileSetLocation, ShellStep, StackOutputReference } from '../../blueprint'; +import { PipelineQueries } from '../../helpers-internal/pipeline-queries'; +import { StepOutput } from '../../helpers-internal/step-output'; +import { cloudAssemblyBuildSpecDir, obtainScope } from '../../private/construct-internals'; +import { hash, stackVariableNamespace } from '../../private/identifiers'; +import { mapValues, mkdict, noEmptyObject, noUndefined, partition } from '../../private/javascript'; +import { ArtifactMap } from '../artifact-map'; +import { CodeBuildStep } from '../codebuild-step'; +import { CodeBuildOptions } from '../codepipeline'; +import { ICodePipelineActionFactory, ProduceActionOptions, CodePipelineActionFactoryResult } from '../codepipeline-action-factory'; +import { mergeBuildSpecs } from './buildspecs'; export interface CodeBuildFactoryProps { /** @@ -110,6 +112,11 @@ export interface CodeBuildFactoryProps { * @default false */ readonly isSynth?: boolean; + + /** + * StepOutputs produced by this CodeBuild step + */ + readonly producedStepOutputs?: StepOutput[]; } /** @@ -129,6 +136,7 @@ export class CodeBuildFactory implements ICodePipelineActionFactory { outputs: shellStep.outputs, stepId: shellStep.id, installCommands: shellStep.installCommands, + producedStepOutputs: StepOutput.producedStepOutputs(shellStep), ...additional, }); } @@ -314,6 +322,7 @@ export class CodeBuildFactory implements ICodePipelineActionFactory { outputs: outputArtifacts, project, runOrder: options.runOrder, + variablesNamespace: options.variablesNamespace, // Inclusion of the hash here will lead to the pipeline structure for any changes // made the config of the underlying CodeBuild Project. @@ -421,14 +430,6 @@ function mergeBuildEnvironments(a?: codebuild.BuildEnvironment, b?: codebuild.Bu }; } -export function mergeBuildSpecs(a: codebuild.BuildSpec, b?: codebuild.BuildSpec): codebuild.BuildSpec; -export function mergeBuildSpecs(a: codebuild.BuildSpec | undefined, b: codebuild.BuildSpec): codebuild.BuildSpec; -export function mergeBuildSpecs(a?: codebuild.BuildSpec, b?: codebuild.BuildSpec): codebuild.BuildSpec | undefined; -export function mergeBuildSpecs(a?: codebuild.BuildSpec, b?: codebuild.BuildSpec) { - if (!a || !b) { return a ?? b; } - return codebuild.mergeBuildSpecs(a, b); -} - function isDefined(x: A | undefined): x is NonNullable { return x !== undefined; } @@ -452,7 +453,7 @@ function serializeBuildEnvironment(env: codebuild.BuildEnvironment) { * Whether the given string contains a reference to a CodePipeline variable */ function containsPipelineVariable(s: string) { - return !!s.match(/#\{[^}]+\}/); + return !!s.match(/#\{[^}]+\}/) || StepOutput.findAll(s).length > 0; } /** @@ -507,4 +508,4 @@ function filterBuildSpecCommands(buildSpec: codebuild.BuildSpec, osType: ec2.Ope } return [undefined, x]; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/private/outputs.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/private/outputs.ts new file mode 100644 index 0000000000000..f721cb3e5212e --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/private/outputs.ts @@ -0,0 +1,38 @@ +import * as cp from '@aws-cdk/aws-codepipeline'; +import { Step } from '../../blueprint/step'; +import { StepOutput } from '../../helpers-internal'; + +const CODEPIPELINE_ENGINE_NAME = 'codepipeline'; + +export function makeCodePipelineOutput(step: Step, variableName: string) { + return new StepOutput(step, CODEPIPELINE_ENGINE_NAME, variableName).toString(); +} + +/** + * If the step is producing outputs, determine a variableNamespace for it, and configure that on the outputs + */ +export function namespaceStepOutputs(step: Step, stage: cp.IStage, name: string): string | undefined { + let ret: string | undefined; + for (const output of StepOutput.producedStepOutputs(step)) { + ret = namespaceName(stage, name); + if (output.engineName !== CODEPIPELINE_ENGINE_NAME) { + throw new Error(`Found unrecognized output type: ${output.engineName}`); + } + + if (typeof output.engineSpecificInformation !== 'string') { + throw new Error(`CodePipeline requires that 'engineSpecificInformation' is a string, got: ${JSON.stringify(output.engineSpecificInformation)}`); + } + output.defineResolution(`#{${ret}.${output.engineSpecificInformation}}`); + } + return ret; +} + +/** + * Generate a variable namespace from stage and action names + * + * Variable namespaces cannot have '.', but they can have '@'. Other than that, + * action names are more limited so they translate easily. + */ +export function namespaceName(stage: cp.IStage, name: string) { + return `${stage.stageName}/${name}`.replace(/[^a-zA-Z0-9@_-]/g, '@'); +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts index 6709f7c84488f..8f081e10d88d8 100644 --- a/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/index.ts @@ -1,2 +1,3 @@ export * from './pipeline-graph'; -export * from './graph'; \ No newline at end of file +export * from './graph'; +export * from './step-output'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/lib/helpers-internal/step-output.ts b/packages/@aws-cdk/pipelines/lib/helpers-internal/step-output.ts new file mode 100644 index 0000000000000..eb71485184f68 --- /dev/null +++ b/packages/@aws-cdk/pipelines/lib/helpers-internal/step-output.ts @@ -0,0 +1,160 @@ +import { IResolvable, IResolveContext, Token, Tokenization } from '@aws-cdk/core'; +import { Step } from '../blueprint/step'; + + +const STEP_OUTPUT_SYM = Symbol.for('@aws-cdk/pipelines.StepOutput'); + +const PRODUCED_OUTPUTS_SYM = Symbol.for('@aws-cdk/pipelines.outputs'); + + +/** + * A symbolic reference to a value produced by another step + * + * Generating and consuming outputs is engine-specific. Many engines will be + * able to support a feature like "outputs", but it's not guaranteed that + * all of them will. + * + * Outputs can only be generated by engine-specific steps (CodeBuildStep instead + * of ShellStep, etc), but can (currently) be consumed anywhere(*). When + * an engine-specific step generates an Output, it should put a well-known + * string and arbitrary data that is useful to the engine into the engine-specific + * fields on the StepOutput. + * + * The graph blueprint will take care of dependencies and ordering, the engine + * is responsible interpreting and rendering StepOutputs. The engine should call + * `defineResolution()` on all outputs. + * + * StepOutputs currently purposely aren't part of the public API because users + * shouldn't see the innards poking out. So, instead of keeping state on `Step`, + * we keep side-state here in a WeakMap which can be accessed via static members + * on `StepOutput`. + * + * (*) If we need to restrict this, we add the checking and erroring in the engine. + */ +export class StepOutput implements IResolvable { + /** + * Return true if the given IResolvable is a StepOutput + */ + public static isStepOutput(resolvable: IResolvable): resolvable is StepOutput { + return !!(resolvable as any)[STEP_OUTPUT_SYM]; + } + + /** + * Find all StepOutputs referenced in the given structure + */ + public static findAll(structure: any): StepOutput[] { + return findAllStepOutputs(structure); + } + + /** + * Return the produced outputs for the given step + */ + public static producedStepOutputs(step: Step): StepOutput[] { + return (step as any)[PRODUCED_OUTPUTS_SYM] ?? []; + } + + /** + * Add produced outputs for the given step + */ + public static recordProducer(...outputs: StepOutput[]) { + for (const output of outputs) { + const step = output.step; + let list = (step as any)[PRODUCED_OUTPUTS_SYM]; + if (!list) { + list = []; + (step as any)[PRODUCED_OUTPUTS_SYM] = list; + } + list.push(...outputs); + } + } + + /** + * The step that produces this output + */ + public readonly step: Step; + + /** + * Name of the engine for which this output is intended + */ + public readonly engineName: string; + + /** + * Additional data on the output, to be interpreted by the engine + */ + public readonly engineSpecificInformation: any; + + public readonly creationStack: string[] = []; + private resolution: any = undefined; + + constructor(step: Step, engineName: string, engineSpecificInformation: any) { + this.step = step; + this.engineName = engineName; + this.engineSpecificInformation = engineSpecificInformation; + Object.defineProperty(this, STEP_OUTPUT_SYM, { value: true }); + } + + /** + * Define the resolved value for this StepOutput. + * + * Should be called by the engine. + */ + public defineResolution(value: any) { + this.resolution = value; + } + + public resolve(_context: IResolveContext) { + if (this.resolution === undefined) { + throw new Error(`Output for step ${this.step} not configured. Either the step is not in the pipeline, or this engine does not support Outputs for this step.`); + } + return this.resolution; + } + + public toString(): string { + return Token.asString(this); + } +} + +function findAllStepOutputs(structure: any): StepOutput[] { + const ret = new Set(); + recurse(structure); + return Array.from(ret); + + function checkToken(x?: IResolvable) { + if (x && StepOutput.isStepOutput(x)) { + ret.add(x); + return true; + } + + // Return false if it wasn't a Token in the first place (in which case we recurse) + return x !== undefined; + } + + function recurse(x: any): void { + if (!x) { return; } + + if (Tokenization.isResolvable(x)) { + checkToken(x); + return; + } + if (Array.isArray(x)) { + if (!checkToken(Tokenization.reverseList(x))) { + x.forEach(recurse); + } + return; + } + if (typeof x === 'number') { + checkToken(Tokenization.reverseNumber(x)); + return; + } + if (typeof x === 'string') { + Tokenization.reverseString(x).tokens.forEach(checkToken); + return; + } + if (typeof x === 'object') { + for (const [k, v] of Object.entries(x)) { + recurse(k); + recurse(v); + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/codepipeline/codebuild-step.test.ts b/packages/@aws-cdk/pipelines/test/codepipeline/codebuild-step.test.ts index 393f1ffb965ba..5b9538a5a4d00 100644 --- a/packages/@aws-cdk/pipelines/test/codepipeline/codebuild-step.test.ts +++ b/packages/@aws-cdk/pipelines/test/codepipeline/codebuild-step.test.ts @@ -141,4 +141,70 @@ test('envFromOutputs works even with very long stage and stack names', () => { }); // THEN - did not throw an error about identifier lengths +}); + +test('exportedVariables', () => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); + + // GIVEN + const producer = new cdkp.CodeBuildStep('Produce', { + commands: ['export MY_VAR=hello'], + }); + + const consumer = new cdkp.CodeBuildStep('Consume', { + env: { + THE_VAR: producer.exportedVariable('MY_VAR'), + }, + commands: [ + 'echo "The variable was: $THE_VAR"', + ], + }); + + // WHEN + pipeline.addWave('MyWave', { + post: [consumer, producer], + }); + + // THEN + const template = Template.fromStack(pipelineStack); + template.hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: [ + { Name: 'Source' }, + { Name: 'Build' }, + { Name: 'UpdatePipeline' }, + { + Name: 'MyWave', + Actions: [ + Match.objectLike({ + Name: 'Produce', + Namespace: 'MyWave@Produce', + RunOrder: 1, + }), + Match.objectLike({ + Name: 'Consume', + RunOrder: 2, + Configuration: Match.objectLike({ + EnvironmentVariables: Match.serializedJson(Match.arrayWith([ + { + name: 'THE_VAR', + type: 'PLAINTEXT', + value: '#{MyWave@Produce.MY_VAR}', + }, + ])), + }), + }), + ], + }, + ], + }); + + template.hasResourceProperties('AWS::CodeBuild::Project', { + Source: { + BuildSpec: Match.serializedJson(Match.objectLike({ + env: { + 'exported-variables': ['MY_VAR'], + }, + })), + }, + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts index 46fb468c37623..721c65d6d8b21 100644 --- a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts +++ b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts @@ -178,4 +178,45 @@ test('artifact names are never longer than 128 characters', () => { }); expect(artifactId.asString().length).toBeLessThanOrEqual(128); +}); + +test('can use source attributes in pipeline', () => { + const gitHub = cdkp.CodePipelineSource.gitHub('owner/my-repo', 'main'); + + // WHEN + new ModernTestGitHubNpmPipeline(pipelineStack, 'Pipeline', { + input: gitHub, + synth: new cdkp.ShellStep('Synth', { + env: { + GITHUB_URL: gitHub.sourceAttribute('CommitUrl'), + }, + commands: [ + 'echo "Click here: $GITHUB_URL"', + ], + }), + selfMutation: false, + }); + + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: [ + { Name: 'Source' }, + { + Name: 'Build', + Actions: [ + { + Name: 'Synth', + Configuration: Match.objectLike({ + EnvironmentVariables: Match.serializedJson([ + { + name: 'GITHUB_URL', + type: 'PLAINTEXT', + value: '#{Source@owner_my-repo.CommitUrl}', + }, + ]), + }), + }, + ], + }, + ], + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json b/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json index 4bd2e638afb4c..8aa6f3c8893ca 100644 --- a/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json @@ -2462,4 +2462,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json b/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json index 37cd5d99fd7f8..414a43cc3fd0e 100644 --- a/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json @@ -2383,4 +2383,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-variables.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-variables.expected.json new file mode 100644 index 0000000000000..ccc779347f32f --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-variables.expected.json @@ -0,0 +1,1014 @@ +{ + "Resources": { + "PipelineArtifactsBucketAEA9A052": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "PipelineArtifactsBucketPolicyF53CCC52": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "PipelineArtifactsBucketAEA9A052" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleB27FAA37": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicy7BDC1ABB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineBuildSynthCodePipelineActionRole4E7A6C97", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineMyWaveProduceCodePipelineActionRoleE0DCE9D3", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineMyWaveConsumeCodePipelineActionRole7FAA4EFA", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicy7BDC1ABB", + "Roles": [ + { + "Ref": "PipelineRoleB27FAA37" + } + ] + } + }, + "Pipeline9850B417": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleB27FAA37", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "ThirdParty", + "Provider": "GitHub", + "Version": "1" + }, + "Configuration": { + "Owner": "cdklabs", + "Repo": "construct-hub-probe", + "Branch": "main", + "OAuthToken": "{{resolve:secretsmanager:github-token:SecretString:::}}", + "PollForSourceChanges": true + }, + "Name": "cdklabs_construct-hub-probe", + "OutputArtifacts": [ + { + "Name": "cdklabs_construct_hub_probe_Source" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + } + }, + "InputArtifacts": [ + { + "Name": "cdklabs_construct_hub_probe_Source" + } + ], + "Name": "Synth", + "OutputArtifacts": [ + { + "Name": "Synth_Output" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "PipelineBuildSynthCodePipelineActionRole4E7A6C97", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Build" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineMyWaveProduce884410D6" + } + }, + "InputArtifacts": [ + { + "Name": "cdklabs_construct_hub_probe_Source" + } + ], + "Name": "Produce", + "Namespace": "MyWave@Produce", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineMyWaveProduceCodePipelineActionRoleE0DCE9D3", + "Arn" + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "PipelineMyWaveConsumeC5D5CCD7" + }, + "EnvironmentVariables": "[{\"name\":\"THE_VAR\",\"type\":\"PLAINTEXT\",\"value\":\"#{MyWave@Produce.MY_VAR}\"}]" + }, + "InputArtifacts": [ + { + "Name": "cdklabs_construct_hub_probe_Source" + } + ], + "Name": "Consume", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineMyWaveConsumeCodePipelineActionRole7FAA4EFA", + "Arn" + ] + }, + "RunOrder": 2 + } + ], + "Name": "MyWave" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucketAEA9A052" + }, + "Type": "S3" + }, + "RestartExecutionOnUpdate": true + }, + "DependsOn": [ + "PipelineRoleDefaultPolicy7BDC1ABB", + "PipelineRoleB27FAA37" + ] + }, + "PipelineBuildSynthCdkBuildProjectRole231EEA2A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineBuildSynthCdkBuildProjectRoleDefaultPolicyFB6C941C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineBuildSynthCdkBuildProject6BEFA8E6" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineBuildSynthCdkBuildProjectRoleDefaultPolicyFB6C941C", + "Roles": [ + { + "Ref": "PipelineBuildSynthCdkBuildProjectRole231EEA2A" + } + ] + } + }, + "PipelineBuildSynthCdkBuildProject6BEFA8E6": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProjectRole231EEA2A", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"mkdir cdk.out\",\n \"touch cdk.out/dummy\"\n ]\n }\n },\n \"artifacts\": {\n \"base-directory\": \"cdk.out\",\n \"files\": \"**/*\"\n }\n}", + "Type": "CODEPIPELINE" + }, + "Cache": { + "Type": "NO_CACHE" + }, + "Description": "Pipeline step VariablePipelineStack/Pipeline/Build/Synth", + "EncryptionKey": "alias/aws/s3" + } + }, + "PipelineBuildSynthCodePipelineActionRole4E7A6C97": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineBuildSynthCodePipelineActionRoleDefaultPolicy92C90290": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineBuildSynthCdkBuildProject6BEFA8E6", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineBuildSynthCodePipelineActionRoleDefaultPolicy92C90290", + "Roles": [ + { + "Ref": "PipelineBuildSynthCodePipelineActionRole4E7A6C97" + } + ] + } + }, + "PipelineMyWaveProduceRole24E3565D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineMyWaveProduceRoleDefaultPolicy209239D4": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineMyWaveProduce884410D6" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineMyWaveProduce884410D6" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineMyWaveProduce884410D6" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineMyWaveProduceRoleDefaultPolicy209239D4", + "Roles": [ + { + "Ref": "PipelineMyWaveProduceRole24E3565D" + } + ] + } + }, + "PipelineMyWaveProduce884410D6": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineMyWaveProduceRole24E3565D", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"env\": {\n \"exported-variables\": [\n \"MY_VAR\"\n ]\n },\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"export MY_VAR=hello\"\n ]\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "Cache": { + "Type": "NO_CACHE" + }, + "Description": "Pipeline step VariablePipelineStack/Pipeline/MyWave/Produce", + "EncryptionKey": "alias/aws/s3" + } + }, + "PipelineMyWaveProduceCodePipelineActionRoleE0DCE9D3": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineMyWaveProduceCodePipelineActionRoleDefaultPolicy34DCB79A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineMyWaveProduce884410D6", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineMyWaveProduceCodePipelineActionRoleDefaultPolicy34DCB79A", + "Roles": [ + { + "Ref": "PipelineMyWaveProduceCodePipelineActionRoleE0DCE9D3" + } + ] + } + }, + "PipelineMyWaveConsumeRole2A96FF33": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineMyWaveConsumeRoleDefaultPolicyC80F0194": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineMyWaveConsumeC5D5CCD7" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:test-region:12345678:log-group:/aws/codebuild/", + { + "Ref": "PipelineMyWaveConsumeC5D5CCD7" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:test-region:12345678:report-group/", + { + "Ref": "PipelineMyWaveConsumeC5D5CCD7" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucketAEA9A052", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineMyWaveConsumeRoleDefaultPolicyC80F0194", + "Roles": [ + { + "Ref": "PipelineMyWaveConsumeRole2A96FF33" + } + ] + } + }, + "PipelineMyWaveConsumeC5D5CCD7": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:5.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "PipelineMyWaveConsumeRole2A96FF33", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"echo \\\"The variable was: $THE_VAR\\\"\"\n ]\n }\n }\n}", + "Type": "CODEPIPELINE" + }, + "Cache": { + "Type": "NO_CACHE" + }, + "Description": "Pipeline step VariablePipelineStack/Pipeline/MyWave/Consume", + "EncryptionKey": "alias/aws/s3" + } + }, + "PipelineMyWaveConsumeCodePipelineActionRole7FAA4EFA": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineMyWaveConsumeCodePipelineActionRoleDefaultPolicy3666898A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineMyWaveConsumeC5D5CCD7", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineMyWaveConsumeCodePipelineActionRoleDefaultPolicy3666898A", + "Roles": [ + { + "Ref": "PipelineMyWaveConsumeCodePipelineActionRole7FAA4EFA" + } + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-variables.ts b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-variables.ts new file mode 100644 index 0000000000000..2a2351375ef62 --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-variables.ts @@ -0,0 +1,52 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +/// !cdk-integ VariablePipelineStack pragma:set-context:@aws-cdk/core:newStyleStackSynthesis=true +import { GitHubTrigger } from '@aws-cdk/aws-codepipeline-actions'; +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as pipelines from '../lib'; + +class PipelineStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const pipeline = new pipelines.CodePipeline(this, 'Pipeline', { + synth: new pipelines.ShellStep('Synth', { + input: pipelines.CodePipelineSource.gitHub('cdklabs/construct-hub-probe', 'main', { + trigger: GitHubTrigger.POLL, + }), + commands: ['mkdir cdk.out', 'touch cdk.out/dummy'], + }), + selfMutation: false, + }); + + const producer = new pipelines.CodeBuildStep('Produce', { + commands: ['export MY_VAR=hello'], + }); + + const consumer = new pipelines.CodeBuildStep('Consume', { + env: { + THE_VAR: producer.exportedVariable('MY_VAR'), + }, + commands: [ + 'echo "The variable was: $THE_VAR"', + ], + }); + + // WHEN + pipeline.addWave('MyWave', { + post: [consumer, producer], + }); + } +} + +const app = new App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': '1', + }, +}); + +new PipelineStack(app, 'VariablePipelineStack', { + env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, +}); + +app.synth(); \ No newline at end of file