Skip to content

Commit e3359e0

Browse files
authored
feat(pipelines): step dependencies (#18256)
Allows users to specify step dependencies and closes #17945. Usage: ```ts const firstStep = new pipelines.ManualApprovalStep('B'); const secondStep = new pipelines.ManualApprovalStep('A'); secondStep.addStepDependency(firstStep); ``` And ```ts // Step A will depend on step B and step B will depend on step C const orderedSteps = pipelines.Step.sequence([ new pipelines.ManualApprovalStep('C'); new pipelines.ManualApprovalStep('B'); new pipelines.ManualApprovalStep('A'); ]); ``` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 2aa3b8e commit e3359e0

File tree

4 files changed

+106
-5
lines changed

4 files changed

+106
-5
lines changed

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

+23-2
Original file line numberDiff line numberDiff line change
@@ -563,14 +563,35 @@ pipeline.addStage(prod, {
563563
stack: prod.stack1,
564564
pre: [new pipelines.ManualApprovalStep('Pre-Stack Check')], // Executed before stack is prepared
565565
changeSet: [new pipelines.ManualApprovalStep('ChangeSet Approval')], // Executed after stack is prepared but before the stack is deployed
566-
post: [new pipelines.ManualApprovalStep('Post-Deploy Check')], // Executed after staack is deployed
566+
post: [new pipelines.ManualApprovalStep('Post-Deploy Check')], // Executed after stack is deployed
567567
}, {
568568
stack: prod.stack2,
569-
post: [new pipelines.ManualApprovalStep('Post-Deploy Check')], // Executed after staack is deployed
569+
post: [new pipelines.ManualApprovalStep('Post-Deploy Check')], // Executed after stack is deployed
570570
}],
571571
});
572572
```
573573

574+
If you specify multiple steps, they will execute in parallel by default. You can add dependencies between them
575+
to if you wish to specify an order. To add a dependency, call `step.addStepDependency()`:
576+
577+
```ts
578+
const firstStep = new pipelines.ManualApprovalStep('A');
579+
const secondStep = new pipelines.ManualApprovalStep('B');
580+
secondStep.addStepDependency(firstStep);
581+
```
582+
583+
For convenience, `Step.sequence()` will take an array of steps and dependencies between adjacent steps,
584+
so that the whole list executes in order:
585+
586+
```ts
587+
// Step A will depend on step B and step B will depend on step C
588+
const orderedSteps = pipelines.Step.sequence([
589+
new pipelines.ManualApprovalStep('A'),
590+
new pipelines.ManualApprovalStep('B'),
591+
new pipelines.ManualApprovalStep('C'),
592+
]);
593+
```
594+
574595
#### Using CloudFormation Stack Outputs in approvals
575596

576597
Because many CloudFormation deployments result in the generation of resources with unpredictable

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

+20-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ import { FileSet, IFileSetProducer } from './file-set';
1111
* useful steps to add to your Pipeline
1212
*/
1313
export abstract class Step implements IFileSetProducer {
14+
/**
15+
* Define a sequence of steps to be executed in order.
16+
*/
17+
public static sequence(steps: Step[]): Step[] {
18+
for (let i = 1; i < steps.length; i++) {
19+
steps[i].addStepDependency(steps[i-1]);
20+
}
21+
return steps;
22+
}
23+
1424
/**
1525
* The list of FileSets consumed by this Step
1626
*/
@@ -25,6 +35,8 @@ export abstract class Step implements IFileSetProducer {
2535

2636
private _primaryOutput?: FileSet;
2737

38+
private _dependencies: Step[] = [];
39+
2840
constructor(
2941
/** Identifier for this step */
3042
public readonly id: string) {
@@ -38,7 +50,7 @@ export abstract class Step implements IFileSetProducer {
3850
* Return the steps this step depends on, based on the FileSets it requires
3951
*/
4052
public get dependencies(): Step[] {
41-
return this.dependencyFileSets.map(f => f.producer);
53+
return this.dependencyFileSets.map(f => f.producer).concat(this._dependencies);
4254
}
4355

4456
/**
@@ -59,6 +71,13 @@ export abstract class Step implements IFileSetProducer {
5971
return this._primaryOutput;
6072
}
6173

74+
/**
75+
* Add a dependency on another step.
76+
*/
77+
public addStepDependency(step: Step) {
78+
this._dependencies.push(step);
79+
}
80+
6281
/**
6382
* Add an additional FileSet to the set of file sets required by this step
6483
*

packages/@aws-cdk/pipelines/test/blueprint/helpers-internal/pipeline-graph.test.ts

+62-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable import/no-extraneous-dependencies */
22
import '@aws-cdk/assert-internal/jest';
33
import * as cdkp from '../../../lib';
4-
import { ManualApprovalStep } from '../../../lib';
4+
import { ManualApprovalStep, Step } from '../../../lib';
55
import { Graph, GraphNode, PipelineGraph } from '../../../lib/helpers-internal';
66
import { flatten } from '../../../lib/private/javascript';
77
import { AppWithOutput, AppWithExposedStacks, OneStackApp, TestApp } from '../../testhelpers/test-app';
@@ -142,6 +142,67 @@ describe('blueprint with wave and stage', () => {
142142
'Post Approval',
143143
]);
144144
});
145+
146+
test('steps that do not depend on each other are ordered lexicographically', () => {
147+
// GIVEN
148+
const goStep = new cdkp.ManualApprovalStep('Gogogo');
149+
const checkStep = new cdkp.ManualApprovalStep('Check');
150+
blueprint.waves[0].stages[0].addPre(
151+
checkStep,
152+
goStep,
153+
);
154+
155+
// WHEN
156+
const graph = new PipelineGraph(blueprint).graph;
157+
158+
// THEN
159+
expect(childrenAt(graph, 'Wave', 'Alpha')).toEqual([
160+
'Check',
161+
'Gogogo',
162+
'Stack',
163+
]);
164+
});
165+
166+
test('steps can depend on each other', () => {
167+
// GIVEN
168+
const goStep = new cdkp.ManualApprovalStep('Gogogo');
169+
const checkStep = new cdkp.ManualApprovalStep('Check');
170+
checkStep.addStepDependency(goStep);
171+
blueprint.waves[0].stages[0].addPre(
172+
checkStep,
173+
goStep,
174+
);
175+
176+
// WHEN
177+
const graph = new PipelineGraph(blueprint).graph;
178+
179+
// THEN
180+
expect(childrenAt(graph, 'Wave', 'Alpha')).toEqual([
181+
'Gogogo',
182+
'Check',
183+
'Stack',
184+
]);
185+
});
186+
187+
test('Steps.sequence adds correct dependencies', () => {
188+
// GIVEN
189+
blueprint.waves[0].stages[0].addPre(...Step.sequence([
190+
new cdkp.ManualApprovalStep('Gogogo'),
191+
new cdkp.ManualApprovalStep('Check'),
192+
new cdkp.ManualApprovalStep('DoubleCheck'),
193+
]));
194+
195+
// WHEN
196+
const graph = new PipelineGraph(blueprint).graph;
197+
198+
// THEN
199+
expect(childrenAt(graph, 'Wave', 'Alpha')).toEqual([
200+
'Gogogo',
201+
'Check',
202+
'DoubleCheck',
203+
'Stack',
204+
]);
205+
});
145206
});
146207

147208
describe('options for other engines', () => {

packages/@aws-cdk/pipelines/test/blueprint/stack-deployment.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Stack, Stage } from '@aws-cdk/core';
44
import { StageDeployment } from '../../lib';
55
import { TestApp } from '../testhelpers/test-app';
66

7-
test('"templateAsset" represents the CFN template of the stack', () => {
7+
test('"templateAsset" represents the CFN template of the stack', () => {
88
// GIVEN
99
const stage = new Stage(new TestApp(), 'MyStage');
1010
new Stack(stage, 'MyStack');

0 commit comments

Comments
 (0)