Skip to content

Commit 0dec2ee

Browse files
authored
feat(pipelines): step outputs (#19024)
Make it possible to export environment variables from a CodeBuildStep, and pipeline sources, and use them in the environment variables of a CodeBuildStep or ShellStep. Closes #17189, closes #18893, closes #15943, closes #16407. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent f203845 commit 0dec2ee

19 files changed

+1594
-44
lines changed

packages/@aws-cdk/pipelines/README.md

+41
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,40 @@ const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
338338

339339
You can adapt these examples to your own situation.
340340

341+
#### Migrating from buildspec.yml files
342+
343+
You may currently have the build instructions for your CodeBuild Projects in a
344+
`buildspec.yml` file in your source repository. In addition to your build
345+
commands, the CodeBuild Project's buildspec also controls some information that
346+
CDK Pipelines manages for you, like artifact identifiers, input artifact
347+
locations, Docker authorization, and exported variables.
348+
349+
Since there is no way in general for CDK Pipelines to modify the file in your
350+
resource repository, CDK Pipelines configures the BuildSpec directly on the
351+
CodeBuild Project, instead of loading it from the `buildspec.yml` file.
352+
This requires a pipeline self-mutation to update.
353+
354+
To avoid this, put your build instructions in a separate script, for example
355+
`build.sh`, and call that script from the build `commands` array:
356+
357+
```ts
358+
declare const source: pipelines.IFileSetProducer;
359+
360+
const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
361+
synth: new pipelines.ShellStep('Synth', {
362+
input: source,
363+
commands: [
364+
// Abstract over doing the build
365+
'./build.sh',
366+
],
367+
})
368+
});
369+
```
370+
371+
Doing so keeps your exact build instructions in sync with your source code in
372+
the source repository where it belongs, and provides a convenient build script
373+
for developers at the same time.
374+
341375
#### CodePipeline Sources
342376

343377
In CodePipeline, *Sources* define where the source of your application lives.
@@ -756,6 +790,13 @@ class MyJenkinsStep extends pipelines.Step implements pipelines.ICodePipelineAct
756790
private readonly input: pipelines.FileSet,
757791
) {
758792
super('MyJenkinsStep');
793+
794+
// This is necessary if your step accepts things like environment variables
795+
// that may contain outputs from other steps. It doesn't matter what the
796+
// structure is, as long as it contains the values that may contain outputs.
797+
this.discoverReferencedOutputs({
798+
env: { /* ... */ }
799+
});
759800
}
760801

761802
public produceAction(stage: codepipeline.IStage, options: pipelines.ProduceActionOptions): pipelines.CodePipelineActionFactoryResult {

packages/@aws-cdk/pipelines/lib/blueprint/manual-approval.ts

+2
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,7 @@ export class ManualApprovalStep extends Step {
3333
super(id);
3434

3535
this.comment = props.comment;
36+
37+
this.discoverReferencedOutputs(props.comment);
3638
}
3739
}

packages/@aws-cdk/pipelines/lib/blueprint/shell-step.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ export interface ShellStepProps {
8787
* @default - No primary output
8888
*/
8989
readonly primaryOutputDirectory?: string;
90-
9190
}
9291

9392
/**
@@ -152,6 +151,11 @@ export class ShellStep extends Step {
152151
this.env = props.env ?? {};
153152
this.envFromCfnOutputs = mapValues(props.envFromCfnOutputs ?? {}, StackOutputReference.fromCfnOutput);
154153

154+
// 'env' is the only thing that can contain outputs
155+
this.discoverReferencedOutputs({
156+
env: this.env,
157+
});
158+
155159
// Inputs
156160
if (props.input) {
157161
const fileSet = props.input.primaryOutput;

packages/@aws-cdk/pipelines/lib/blueprint/step.ts

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Stack, Token } from '@aws-cdk/core';
2+
import { StepOutput } from '../helpers-internal/step-output';
23
import { FileSet, IFileSetProducer } from './file-set';
34

45
/**
@@ -39,7 +40,7 @@ export abstract class Step implements IFileSetProducer {
3940

4041
private _primaryOutput?: FileSet;
4142

42-
private _dependencies: Step[] = [];
43+
private _dependencies = new Set<Step>();
4344

4445
constructor(
4546
/** Identifier for this step */
@@ -54,7 +55,10 @@ export abstract class Step implements IFileSetProducer {
5455
* Return the steps this step depends on, based on the FileSets it requires
5556
*/
5657
public get dependencies(): Step[] {
57-
return this.dependencyFileSets.map(f => f.producer).concat(this._dependencies);
58+
return Array.from(new Set([
59+
...this.dependencyFileSets.map(f => f.producer),
60+
...this._dependencies,
61+
]));
5862
}
5963

6064
/**
@@ -79,7 +83,7 @@ export abstract class Step implements IFileSetProducer {
7983
* Add a dependency on another step.
8084
*/
8185
public addStepDependency(step: Step) {
82-
this._dependencies.push(step);
86+
this._dependencies.add(step);
8387
}
8488

8589
/**
@@ -97,6 +101,21 @@ export abstract class Step implements IFileSetProducer {
97101
protected configurePrimaryOutput(fs: FileSet) {
98102
this._primaryOutput = fs;
99103
}
104+
105+
/**
106+
* Crawl the given structure for references to StepOutputs and add dependencies on all steps found
107+
*
108+
* Should be called by subclasses based on what the user passes in as
109+
* construction properties. The format of the structure passed in here does
110+
* not have to correspond exactly to what gets rendered into the engine, it
111+
* just needs to contain the same amount of data.
112+
*/
113+
protected discoverReferencedOutputs(structure: any) {
114+
for (const output of StepOutput.findAll(structure)) {
115+
this._dependencies.add(output.step);
116+
StepOutput.recordProducer(output);
117+
}
118+
}
100119
}
101120

102121
/**
@@ -128,5 +147,4 @@ export interface StackSteps {
128147
* @default - no additional steps
129148
*/
130149
readonly post?: Step[];
131-
132150
}

packages/@aws-cdk/pipelines/lib/codepipeline/codebuild-step.ts

+56-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { Duration } from '@aws-cdk/core';
21
import * as codebuild from '@aws-cdk/aws-codebuild';
32
import * as ec2 from '@aws-cdk/aws-ec2';
43
import * as iam from '@aws-cdk/aws-iam';
4+
import { Duration } from '@aws-cdk/core';
55
import { ShellStep, ShellStepProps } from '../blueprint';
6+
import { mergeBuildSpecs } from './private/buildspecs';
7+
import { makeCodePipelineOutput } from './private/outputs';
68

79
/**
810
* Construction props for a CodeBuildStep
@@ -96,6 +98,17 @@ export interface CodeBuildStepProps extends ShellStepProps {
9698

9799
/**
98100
* Run a script as a CodeBuild Project
101+
*
102+
* The BuildSpec must be available inline--it cannot reference a file
103+
* on disk. If your current build instructions are in a file like
104+
* `buildspec.yml` in your repository, extract them to a script
105+
* (say, `build.sh`) and invoke that script as part of the build:
106+
*
107+
* ```ts
108+
* new pipelines.CodeBuildStep('Synth', {
109+
* commands: ['./build.sh'],
110+
* });
111+
* ```
99112
*/
100113
export class CodeBuildStep extends ShellStep {
101114
/**
@@ -105,13 +118,6 @@ export class CodeBuildStep extends ShellStep {
105118
*/
106119
public readonly projectName?: string;
107120

108-
/**
109-
* Additional configuration that can only be configured via BuildSpec
110-
*
111-
* @default - No value specified at construction time, use defaults
112-
*/
113-
public readonly partialBuildSpec?: codebuild.BuildSpec;
114-
115121
/**
116122
* The VPC where to execute the SimpleSynth.
117123
*
@@ -164,13 +170,16 @@ export class CodeBuildStep extends ShellStep {
164170
readonly timeout?: Duration;
165171

166172
private _project?: codebuild.IProject;
173+
private _partialBuildSpec?: codebuild.BuildSpec;
174+
private readonly exportedVariables = new Set<string>();
175+
private exportedVarsRendered = false;
167176

168177
constructor(id: string, props: CodeBuildStepProps) {
169178
super(id, props);
170179

171180
this.projectName = props.projectName;
172181
this.buildEnvironment = props.buildEnvironment;
173-
this.partialBuildSpec = props.partialBuildSpec;
182+
this._partialBuildSpec = props.partialBuildSpec;
174183
this.vpc = props.vpc;
175184
this.subnetSelection = props.subnetSelection;
176185
this.role = props.role;
@@ -198,6 +207,44 @@ export class CodeBuildStep extends ShellStep {
198207
return this.project.grantPrincipal;
199208
}
200209

210+
/**
211+
* Additional configuration that can only be configured via BuildSpec
212+
*
213+
* Contains exported variables
214+
*
215+
* @default - Contains the exported variables
216+
*/
217+
public get partialBuildSpec(): codebuild.BuildSpec | undefined {
218+
this.exportedVarsRendered = true;
219+
220+
const varsBuildSpec = this.exportedVariables.size > 0 ? codebuild.BuildSpec.fromObject({
221+
version: '0.2',
222+
env: {
223+
'exported-variables': Array.from(this.exportedVariables),
224+
},
225+
}) : undefined;
226+
227+
return mergeBuildSpecs(varsBuildSpec, this._partialBuildSpec);
228+
}
229+
230+
/**
231+
* Reference a CodePipeline variable defined by the CodeBuildStep.
232+
*
233+
* The variable must be set in the shell of the CodeBuild step when
234+
* it finishes its `post_build` phase.
235+
*
236+
* @param variableName the name of the variable for reference.
237+
*/
238+
public exportedVariable(variableName: string): string {
239+
if (this.exportedVarsRendered && !this.exportedVariables.has(variableName)) {
240+
throw new Error('exportVariable(): Pipeline has already been produced, cannot call this function anymore');
241+
}
242+
243+
this.exportedVariables.add(variableName);
244+
245+
return makeCodePipelineOutput(this, variableName);
246+
}
247+
201248
/**
202249
* Set the internal project value
203250
*

packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts

+11
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ export interface ProduceActionOptions {
2323
*/
2424
readonly runOrder: number;
2525

26+
/**
27+
* If this step is producing outputs, the variables namespace assigned to it
28+
*
29+
* Pass this on to the Action you are creating.
30+
*
31+
* @default - Step doesn't produce any outputs
32+
*/
33+
readonly variablesNamespace?: string;
34+
2635
/**
2736
* Helper object to translate FileSets to CodePipeline Artifacts
2837
*/
@@ -87,6 +96,8 @@ export interface ICodePipelineActionFactory {
8796
export interface CodePipelineActionFactoryResult {
8897
/**
8998
* How many RunOrders were consumed
99+
*
100+
* If you add 1 action, return the value 1 here.
90101
*/
91102
readonly runOrdersConsumed: number;
92103

0 commit comments

Comments
 (0)