Skip to content

Commit

Permalink
feat(pipelines): step dependencies (aws#18256)
Browse files Browse the repository at this point in the history
Allows users to specify step dependencies and closes aws#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*
  • Loading branch information
kaizencc authored and TikiTDO committed Feb 21, 2022
1 parent 1871ea6 commit d3cf427
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 5 deletions.
25 changes: 23 additions & 2 deletions packages/@aws-cdk/pipelines/README.md
Expand Up @@ -563,14 +563,35 @@ pipeline.addStage(prod, {
stack: prod.stack1,
pre: [new pipelines.ManualApprovalStep('Pre-Stack Check')], // Executed before stack is prepared
changeSet: [new pipelines.ManualApprovalStep('ChangeSet Approval')], // Executed after stack is prepared but before the stack is deployed
post: [new pipelines.ManualApprovalStep('Post-Deploy Check')], // Executed after staack is deployed
post: [new pipelines.ManualApprovalStep('Post-Deploy Check')], // Executed after stack is deployed
}, {
stack: prod.stack2,
post: [new pipelines.ManualApprovalStep('Post-Deploy Check')], // Executed after staack is deployed
post: [new pipelines.ManualApprovalStep('Post-Deploy Check')], // Executed after stack is deployed
}],
});
```

If you specify multiple steps, they will execute in parallel by default. You can add dependencies between them
to if you wish to specify an order. To add a dependency, call `step.addStepDependency()`:

```ts
const firstStep = new pipelines.ManualApprovalStep('A');
const secondStep = new pipelines.ManualApprovalStep('B');
secondStep.addStepDependency(firstStep);
```

For convenience, `Step.sequence()` will take an array of steps and dependencies between adjacent steps,
so that the whole list executes in order:

```ts
// Step A will depend on step B and step B will depend on step C
const orderedSteps = pipelines.Step.sequence([
new pipelines.ManualApprovalStep('A'),
new pipelines.ManualApprovalStep('B'),
new pipelines.ManualApprovalStep('C'),
]);
```

#### Using CloudFormation Stack Outputs in approvals

Because many CloudFormation deployments result in the generation of resources with unpredictable
Expand Down
21 changes: 20 additions & 1 deletion packages/@aws-cdk/pipelines/lib/blueprint/step.ts
Expand Up @@ -11,6 +11,16 @@ import { FileSet, IFileSetProducer } from './file-set';
* useful steps to add to your Pipeline
*/
export abstract class Step implements IFileSetProducer {
/**
* Define a sequence of steps to be executed in order.
*/
public static sequence(steps: Step[]): Step[] {
for (let i = 1; i < steps.length; i++) {
steps[i].addStepDependency(steps[i-1]);
}
return steps;
}

/**
* The list of FileSets consumed by this Step
*/
Expand All @@ -25,6 +35,8 @@ export abstract class Step implements IFileSetProducer {

private _primaryOutput?: FileSet;

private _dependencies: Step[] = [];

constructor(
/** Identifier for this step */
public readonly id: string) {
Expand All @@ -38,7 +50,7 @@ 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);
return this.dependencyFileSets.map(f => f.producer).concat(this._dependencies);
}

/**
Expand All @@ -59,6 +71,13 @@ export abstract class Step implements IFileSetProducer {
return this._primaryOutput;
}

/**
* Add a dependency on another step.
*/
public addStepDependency(step: Step) {
this._dependencies.push(step);
}

/**
* Add an additional FileSet to the set of file sets required by this step
*
Expand Down
@@ -1,7 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies */
import '@aws-cdk/assert-internal/jest';
import * as cdkp from '../../../lib';
import { ManualApprovalStep } from '../../../lib';
import { ManualApprovalStep, Step } from '../../../lib';
import { Graph, GraphNode, PipelineGraph } from '../../../lib/helpers-internal';
import { flatten } from '../../../lib/private/javascript';
import { AppWithOutput, AppWithExposedStacks, OneStackApp, TestApp } from '../../testhelpers/test-app';
Expand Down Expand Up @@ -142,6 +142,67 @@ describe('blueprint with wave and stage', () => {
'Post Approval',
]);
});

test('steps that do not depend on each other are ordered lexicographically', () => {
// GIVEN
const goStep = new cdkp.ManualApprovalStep('Gogogo');
const checkStep = new cdkp.ManualApprovalStep('Check');
blueprint.waves[0].stages[0].addPre(
checkStep,
goStep,
);

// WHEN
const graph = new PipelineGraph(blueprint).graph;

// THEN
expect(childrenAt(graph, 'Wave', 'Alpha')).toEqual([
'Check',
'Gogogo',
'Stack',
]);
});

test('steps can depend on each other', () => {
// GIVEN
const goStep = new cdkp.ManualApprovalStep('Gogogo');
const checkStep = new cdkp.ManualApprovalStep('Check');
checkStep.addStepDependency(goStep);
blueprint.waves[0].stages[0].addPre(
checkStep,
goStep,
);

// WHEN
const graph = new PipelineGraph(blueprint).graph;

// THEN
expect(childrenAt(graph, 'Wave', 'Alpha')).toEqual([
'Gogogo',
'Check',
'Stack',
]);
});

test('Steps.sequence adds correct dependencies', () => {
// GIVEN
blueprint.waves[0].stages[0].addPre(...Step.sequence([
new cdkp.ManualApprovalStep('Gogogo'),
new cdkp.ManualApprovalStep('Check'),
new cdkp.ManualApprovalStep('DoubleCheck'),
]));

// WHEN
const graph = new PipelineGraph(blueprint).graph;

// THEN
expect(childrenAt(graph, 'Wave', 'Alpha')).toEqual([
'Gogogo',
'Check',
'DoubleCheck',
'Stack',
]);
});
});

describe('options for other engines', () => {
Expand Down
Expand Up @@ -4,7 +4,7 @@ import { Stack, Stage } from '@aws-cdk/core';
import { StageDeployment } from '../../lib';
import { TestApp } from '../testhelpers/test-app';

test('"templateAsset" represents the CFN template of the stack', () => {
test('"templateAsset" represents the CFN template of the stack', () => {
// GIVEN
const stage = new Stage(new TestApp(), 'MyStage');
new Stack(stage, 'MyStack');
Expand Down

0 comments on commit d3cf427

Please sign in to comment.