diff --git a/packages/aws-cdk-lib/pipelines/lib/helpers-internal/graph.ts b/packages/aws-cdk-lib/pipelines/lib/helpers-internal/graph.ts index 4dc58664dc406..109a447514af2 100644 --- a/packages/aws-cdk-lib/pipelines/lib/helpers-internal/graph.ts +++ b/packages/aws-cdk-lib/pipelines/lib/helpers-internal/graph.ts @@ -230,6 +230,10 @@ export class Graph extends GraphNode { return this.children.get(name); } + public containsId(id: string) { + return this.tryGetChild(id) !== undefined; + } + public contains(node: GraphNode) { return this.nodes.has(node); } diff --git a/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-graph.ts b/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-graph.ts index cad9fe7769c1a..dbe61b3640dc8 100644 --- a/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-graph.ts +++ b/packages/aws-cdk-lib/pipelines/lib/helpers-internal/pipeline-graph.ts @@ -134,7 +134,12 @@ export class PipelineGraph { const stackGraphs = new Map(); for (const stack of stage.stacks) { - const stackGraph: AGraph = Graph.of(this.simpleStackName(stack.stackName, stage.stageName), { type: 'stack-group', stack }); + const stackGraphName = findUniqueName(retGraph, [ + this.simpleStackName(stack.stackName, stage.stageName), + ...stack.account ? [stack.account] : [], + ...stack.region ? [stack.region] : [], + ]); + const stackGraph: AGraph = Graph.of(stackGraphName, { type: 'stack-group', stack }); const prepareNode: AGraphNode | undefined = this.prepareStep ? aGraphNode('Prepare', { type: 'prepare', stack }) : undefined; const deployNode: AGraphNode = aGraphNode('Deploy', { type: 'execute', @@ -412,4 +417,14 @@ function aGraphNode(id: string, x: GraphAnnotation): AGraphNode { function stripPrefix(s: string, prefix: string) { return s.startsWith(prefix) ? s.slice(prefix.length) : s; +} + +function findUniqueName(parent: Graph, parts: string[]): string { + for (let i = 1; i <= parts.length; i++) { + const candidate = parts.slice(0, i).join('.'); + if (!parent.containsId(candidate)) { + return candidate; + } + } + return parts.join('.'); } \ No newline at end of file diff --git a/packages/aws-cdk-lib/pipelines/test/compliance/basic-behavior.test.ts b/packages/aws-cdk-lib/pipelines/test/compliance/basic-behavior.test.ts index 8ca4d83650a8f..2b51dd914c429 100644 --- a/packages/aws-cdk-lib/pipelines/test/compliance/basic-behavior.test.ts +++ b/packages/aws-cdk-lib/pipelines/test/compliance/basic-behavior.test.ts @@ -81,6 +81,33 @@ test('overridden stack names are respected', () => { }); }); +test('two stacks can have the same name', () => { + const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk', { useChangeSets: false }); + pipeline.addStage(new TwoStacksApp(app, 'App')); + + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([ + { + Name: 'App', + Actions: Match.arrayWith([ + Match.objectLike({ + Name: stringLike('MyFancyStack.Deploy'), + Configuration: Match.objectLike({ + StackName: 'MyFancyStack', + }), + }), + Match.objectLike({ + Name: stringLike('MyFancyStack.eu-west-2.Deploy'), + Configuration: Match.objectLike({ + StackName: 'MyFancyStack', + }), + }), + ]), + }, + ]), + }); +}); + test('changing CLI version leads to a different pipeline structure (restarting it)', () => { // GIVEN @@ -154,3 +181,17 @@ class OneStackAppWithCustomName extends Stage { }); } } + +class TwoStacksApp extends Stage { + constructor(scope: Construct, id: string, props?: StageProps) { + super(scope, id, props); + new BucketStack(this, 'Stack1', { + env: { region: 'eu-west-1' }, + stackName: 'MyFancyStack', + }); + new BucketStack(this, 'Stack2', { + env: { region: 'eu-west-2' }, + stackName: 'MyFancyStack', + }); + } +} \ No newline at end of file