Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pipelines): step dependencies #18256

Merged
merged 7 commits into from
Jan 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions packages/@aws-cdk/pipelines/README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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