Skip to content

Commit 2a2406e

Browse files
authored
fix(codepipeline): unhelpful artifact validation messages (#8256)
The artifact validation error messages are pretty unhelpful, just saying things like "artifact X gets consumed before it gets produced" (or similar), without actually referencing the stages/actions involved. This becomes problematic if the pipeline got generated for you by automation and indirection, because you can't simply grep your codebase for the offending artifact name. Make the messages more explicit and clear so it's a lot more obvious what's going on (and hopefully getting a fighting chance to figure out what's wrong). ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent d28c947 commit 2a2406e

File tree

2 files changed

+135
-23
lines changed

2 files changed

+135
-23
lines changed

packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts

+79-20
Original file line numberDiff line numberDiff line change
@@ -749,34 +749,52 @@ export class Pipeline extends PipelineBase {
749749
private validateArtifacts(): string[] {
750750
const ret = new Array<string>();
751751

752-
const outputArtifactNames = new Set<string>();
753-
for (const stage of this._stages) {
754-
const sortedActions = stage.actionDescriptors.sort((a1, a2) => a1.runOrder - a2.runOrder);
755-
756-
for (const action of sortedActions) {
757-
// start with inputs
758-
const inputArtifacts = action.inputs;
759-
for (const inputArtifact of inputArtifacts) {
760-
if (!inputArtifact.artifactName) {
761-
ret.push(`Action '${action.actionName}' has an unnamed input Artifact that's not used as an output`);
762-
} else if (!outputArtifactNames.has(inputArtifact.artifactName)) {
763-
ret.push(`Artifact '${inputArtifact.artifactName}' was used as input before being used as output`);
752+
const producers: Record<string, PipelineLocation> = {};
753+
const firstConsumers: Record<string, PipelineLocation> = {};
754+
755+
for (const [stageIndex, stage] of enumerate(this._stages)) {
756+
// For every output artifact, get the producer
757+
for (const action of stage.actionDescriptors) {
758+
const actionLoc = new PipelineLocation(stageIndex, stage, action);
759+
760+
for (const outputArtifact of action.outputs) {
761+
// output Artifacts always have a name set
762+
const name = outputArtifact.artifactName!;
763+
if (producers[name]) {
764+
ret.push(`Both Actions '${producers[name].actionName}' and '${action.actionName}' are producting Artifact '${name}'. Every artifact can only be produced once.`);
765+
continue;
764766
}
767+
768+
producers[name] = actionLoc;
765769
}
766770

767-
// then process outputs by adding them to the Set
768-
const outputArtifacts = action.outputs;
769-
for (const outputArtifact of outputArtifacts) {
770-
// output Artifacts always have a name set
771-
if (outputArtifactNames.has(outputArtifact.artifactName!)) {
772-
ret.push(`Artifact '${outputArtifact.artifactName}' has been used as an output more than once`);
773-
} else {
774-
outputArtifactNames.add(outputArtifact.artifactName!);
771+
// For every input artifact, get the first consumer
772+
for (const inputArtifact of action.inputs) {
773+
const name = inputArtifact.artifactName;
774+
if (!name) {
775+
ret.push(`Action '${action.actionName}' is using an unnamed input Artifact, which is not being produced in this pipeline`);
776+
continue;
775777
}
778+
779+
firstConsumers[name] = firstConsumers[name] ? firstConsumers[name].first(actionLoc) : actionLoc;
776780
}
777781
}
778782
}
779783

784+
// Now validate that every input artifact is produced before it's
785+
// being consumed.
786+
for (const [artifactName, consumerLoc] of Object.entries(firstConsumers)) {
787+
const producerLoc = producers[artifactName];
788+
if (!producerLoc) {
789+
ret.push(`Action '${consumerLoc.actionName}' is using input Artifact '${artifactName}', which is not being produced in this pipeline`);
790+
continue;
791+
}
792+
793+
if (consumerLoc.beforeOrEqual(producerLoc)) {
794+
ret.push(`${consumerLoc} is consuming input Artifact '${artifactName}' before it is being produced at ${producerLoc}`);
795+
}
796+
}
797+
780798
return ret;
781799
}
782800

@@ -874,3 +892,44 @@ interface CrossRegionInfo {
874892

875893
readonly region?: string;
876894
}
895+
896+
function enumerate<A>(xs: A[]): Array<[number, A]> {
897+
const ret = new Array<[number, A]>();
898+
for (let i = 0; i < xs.length; i++) {
899+
ret.push([i, xs[i]]);
900+
}
901+
return ret;
902+
}
903+
904+
class PipelineLocation {
905+
constructor(private readonly stageIndex: number, private readonly stage: IStage, private readonly action: FullActionDescriptor) {
906+
}
907+
908+
public get stageName() {
909+
return this.stage.stageName;
910+
}
911+
912+
public get actionName() {
913+
return this.action.actionName;
914+
}
915+
916+
/**
917+
* Returns whether a is before or the same order as b
918+
*/
919+
public beforeOrEqual(rhs: PipelineLocation) {
920+
if (this.stageIndex !== rhs.stageIndex) { return rhs.stageIndex < rhs.stageIndex; }
921+
return this.action.runOrder <= rhs.action.runOrder;
922+
}
923+
924+
/**
925+
* Returns the first location between this and the other one
926+
*/
927+
public first(rhs: PipelineLocation) {
928+
return this.beforeOrEqual(rhs) ? this : rhs;
929+
}
930+
931+
public toString() {
932+
// runOrders are 1-based, so make the stageIndex also 1-based otherwise it's going to be confusing.
933+
return `Stage ${this.stageIndex + 1} Action ${this.action.runOrder} ('${this.stageName}'/'${this.actionName}')`;
934+
}
935+
}

packages/@aws-cdk/aws-codepipeline/test/test.artifacts.ts

+56-3
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export = {
4646
test.equal(errors.length, 1);
4747
const error = errors[0];
4848
test.same(error.source, pipeline);
49-
test.equal(error.message, "Action 'Build' has an unnamed input Artifact that's not used as an output");
49+
test.equal(error.message, "Action 'Build' is using an unnamed input Artifact, which is not being produced in this pipeline");
5050

5151
test.done();
5252
},
@@ -82,7 +82,7 @@ export = {
8282
test.equal(errors.length, 1);
8383
const error = errors[0];
8484
test.same(error.source, pipeline);
85-
test.equal(error.message, "Artifact 'named' was used as input before being used as output");
85+
test.equal(error.message, "Action 'Build' is using input Artifact 'named', which is not being produced in this pipeline");
8686

8787
test.done();
8888
},
@@ -119,7 +119,7 @@ export = {
119119
test.equal(errors.length, 1);
120120
const error = errors[0];
121121
test.same(error.source, pipeline);
122-
test.equal(error.message, "Artifact 'Artifact_Source_Source' has been used as an output more than once");
122+
test.equal(error.message, "Both Actions 'Source' and 'Build' are producting Artifact 'Artifact_Source_Source'. Every artifact can only be produced once.");
123123

124124
test.done();
125125
},
@@ -173,6 +173,59 @@ export = {
173173
test.done();
174174
},
175175

176+
'violation of runOrder constraints is detected and reported'(test: Test) {
177+
const stack = new cdk.Stack();
178+
179+
const sourceOutput1 = new codepipeline.Artifact('sourceOutput1');
180+
const buildOutput1 = new codepipeline.Artifact('buildOutput1');
181+
const sourceOutput2 = new codepipeline.Artifact('sourceOutput2');
182+
183+
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', {
184+
stages: [
185+
{
186+
stageName: 'Source',
187+
actions: [
188+
new FakeSourceAction({
189+
actionName: 'source1',
190+
output: sourceOutput1,
191+
}),
192+
new FakeSourceAction({
193+
actionName: 'source2',
194+
output: sourceOutput2,
195+
}),
196+
],
197+
},
198+
{
199+
stageName: 'Build',
200+
actions: [
201+
new FakeBuildAction({
202+
actionName: 'build1',
203+
input: sourceOutput1,
204+
output: buildOutput1,
205+
runOrder: 3,
206+
}),
207+
new FakeBuildAction({
208+
actionName: 'build2',
209+
input: sourceOutput2,
210+
extraInputs: [buildOutput1],
211+
output: new codepipeline.Artifact('buildOutput2'),
212+
runOrder: 2,
213+
}),
214+
],
215+
},
216+
],
217+
});
218+
219+
const errors = validate(stack);
220+
221+
test.equal(errors.length, 1);
222+
const error = errors[0];
223+
test.same(error.source, pipeline);
224+
test.equal(error.message, "Stage 2 Action 2 ('Build'/'build2') is consuming input Artifact 'buildOutput1' before it is being produced at Stage 2 Action 3 ('Build'/'build1')");
225+
226+
test.done();
227+
},
228+
176229
'without a name, sanitize the auto stage-action derived name'(test: Test) {
177230
const stack = new cdk.Stack();
178231

0 commit comments

Comments
 (0)