Skip to content

Commit c5741d2

Browse files
author
CDK Contributor
committed
fix(core): ensure CloudFormation ChangeSets receive tags with explicitStackTags flag
When the @aws-cdk/core:explicitStackTags feature flag was introduced in v2.205.0, it inadvertently caused CloudFormation ChangeSets to not receive stack tags, breaking deployments with SCP policies requiring tags on ChangeSets. This fix adds a new property 'applyToChangeSets' to TagProps (default: true) that ensures tags are still applied to the stack for ChangeSet purposes, while maintaining the correct behavior of not duplicating tags on resources. Fixes regression introduced in v2.205.0 where ChangeSets lost their tags.
1 parent 88f3e04 commit c5741d2

File tree

3 files changed

+192
-0
lines changed

3 files changed

+192
-0
lines changed

packages/aws-cdk-lib/core/lib/tag-aspect.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Annotations } from './annotations';
33
import { IAspect, Aspects, AspectOptions } from './aspect';
44
import { UnscopedValidationError } from './errors';
55
import { FeatureFlags } from './feature-flags';
6+
import { Stack } from './stack';
67
import * as cxapi from '../../cx-api';
78
import { mutatingAspectPrio32333 } from './private/aspect-prio';
89
import { ITaggable, ITaggableV2, TagManager } from './tag-manager';
@@ -38,6 +39,18 @@ export interface TagProps {
3839
*/
3940
readonly includeResourceTypes?: string[];
4041

42+
/**
43+
* Whether to apply tags to CloudFormation ChangeSets
44+
*
45+
* This ensures tags are applied to ChangeSets even when the
46+
* explicitStackTags feature flag excludes stack-level tags.
47+
* This is important for compliance with SCP policies that
48+
* require tags on ChangeSets.
49+
*
50+
* @default true
51+
*/
52+
readonly applyToChangeSets?: boolean;
53+
4154
/**
4255
* Priority of the tag operation
4356
*
@@ -186,7 +199,18 @@ export class Tags {
186199
*/
187200
public add(key: string, value: string, props: TagProps = {}) {
188201
// Implicitly add `aws:cdk:stack` to the `excludeResourceTypes` array in modern behavior
202+
// BUT: If applyToChangeSets is true (default), we need to ensure ChangeSets still get tagged
189203
if (this.explicitStackTags && !props.includeResourceTypes?.includes('aws:cdk:stack')) {
204+
// Check if we should still apply to ChangeSets
205+
const applyToChangeSets = props.applyToChangeSets !== false;
206+
207+
// If applyToChangeSets is true, we need to ensure the stack still gets the tags
208+
// for ChangeSets, even though resources won't get them from the stack
209+
if (applyToChangeSets && Stack.isStack(this.scope)) {
210+
// Apply the tag directly to the stack for ChangeSet purposes
211+
(this.scope as Stack).addStackTag(key, value);
212+
}
213+
190214
props = {
191215
...props,
192216
excludeResourceTypes: [...props.excludeResourceTypes ?? [], 'aws:cdk:stack'],
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Integration test for stack tagging behavior with explicitStackTags feature flag.
3+
* This test verifies that ChangeSets receive proper tags even when the explicitStackTags
4+
* feature flag is enabled, ensuring compliance with SCP policies that require tags on ChangeSets.
5+
*/
6+
7+
import { App, Stack, Tags, CfnOutput } from 'aws-cdk-lib';
8+
9+
const app = new App({
10+
context: {
11+
'@aws-cdk/core:explicitStackTags': true,
12+
},
13+
});
14+
15+
// Stack with Tags.of(stack).add() - should apply to ChangeSets
16+
const stack = new Stack(app, 'integ-stack-tags-changeset', {
17+
env: { region: 'us-east-1' },
18+
});
19+
20+
// Add tags using Tags.of(stack).add() - these should be applied to ChangeSets
21+
Tags.of(stack).add('Environment', 'IntegTest');
22+
Tags.of(stack).add('Project', 'CDK-Core');
23+
Tags.of(stack).add('Owner', 'CDK-Team');
24+
Tags.of(stack).add('CostCenter', 'Engineering');
25+
26+
// Test the new applyToChangeSets property
27+
Tags.of(stack).add('ChangeSetTag', 'ShouldAppear');
28+
Tags.of(stack).add('NoChangeSetTag', 'ShouldNotAppear', { applyToChangeSets: false });
29+
30+
// Add direct stack tags as well
31+
stack.addStackTag('DirectTag', 'DirectValue');
32+
33+
// Add a simple resource to make the stack deployable
34+
new CfnOutput(stack, 'TestOutput', {
35+
value: 'test-value',
36+
description: 'Test output for integration test',
37+
});
38+
39+
app.synth();
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { Annotations, App, Stack, Tags } from '../lib';
2+
import { getWarnings } from './util';
3+
4+
describe('ChangeSet tagging with explicitStackTags', () => {
5+
test('tags are applied to ChangeSets even when explicitStackTags is enabled', () => {
6+
// GIVEN
7+
const app = new App({
8+
context: {
9+
'@aws-cdk/core:explicitStackTags': true,
10+
},
11+
});
12+
const stack = new Stack(app, 'TestStack');
13+
14+
// WHEN
15+
Tags.of(stack).add('TestKey', 'TestValue');
16+
Tags.of(stack).add('Environment', 'Production');
17+
Tags.of(stack).add('Project', 'CDK-Fix');
18+
19+
// THEN
20+
// Stack should have tags for ChangeSets
21+
const stackTags = stack.tags.tagValues();
22+
expect(stackTags).toHaveProperty('TestKey', 'TestValue');
23+
expect(stackTags).toHaveProperty('Environment', 'Production');
24+
expect(stackTags).toHaveProperty('Project', 'CDK-Fix');
25+
});
26+
27+
test('applyToChangeSets can be disabled explicitly', () => {
28+
// GIVEN
29+
const app = new App({
30+
context: {
31+
'@aws-cdk/core:explicitStackTags': true,
32+
},
33+
});
34+
const stack = new Stack(app, 'TestStack');
35+
36+
// WHEN
37+
Tags.of(stack).add('TestKey', 'TestValue', { applyToChangeSets: false });
38+
39+
// THEN
40+
// Stack should NOT have tags when applyToChangeSets is false
41+
const stackTags = stack.tags.tagValues();
42+
expect(Object.keys(stackTags)).toHaveLength(0);
43+
});
44+
45+
test('legacy behavior is maintained when explicitStackTags is not enabled', () => {
46+
// GIVEN
47+
const app = new App({
48+
context: {
49+
'@aws-cdk/core:explicitStackTags': false,
50+
},
51+
});
52+
const stack = new Stack(app, 'TestStack');
53+
54+
// WHEN
55+
Tags.of(stack).add('TestKey', 'TestValue');
56+
57+
// THEN
58+
// Stack should have tags (legacy behavior)
59+
const stackTags = stack.tags.tagValues();
60+
expect(stackTags).toHaveProperty('TestKey', 'TestValue');
61+
});
62+
63+
test('direct stack tagging still works with addStackTag', () => {
64+
// GIVEN
65+
const app = new App({
66+
context: {
67+
'@aws-cdk/core:explicitStackTags': true,
68+
},
69+
});
70+
const stack = new Stack(app, 'TestStack');
71+
72+
// WHEN
73+
stack.addStackTag('DirectTag', 'DirectValue');
74+
75+
// THEN
76+
const stackTags = stack.tags.tagValues();
77+
expect(stackTags).toHaveProperty('DirectTag', 'DirectValue');
78+
});
79+
80+
test('mixed tagging approaches work together', () => {
81+
// GIVEN
82+
const app = new App({
83+
context: {
84+
'@aws-cdk/core:explicitStackTags': true,
85+
},
86+
});
87+
const stack = new Stack(app, 'TestStack');
88+
89+
// WHEN
90+
stack.addStackTag('DirectTag', 'DirectValue');
91+
Tags.of(stack).add('AspectTag', 'AspectValue');
92+
Tags.of(stack).add('NoChangeSetTag', 'NoValue', { applyToChangeSets: false });
93+
94+
// THEN
95+
const stackTags = stack.tags.tagValues();
96+
expect(stackTags).toHaveProperty('DirectTag', 'DirectValue');
97+
expect(stackTags).toHaveProperty('AspectTag', 'AspectValue');
98+
expect(stackTags).not.toHaveProperty('NoChangeSetTag');
99+
});
100+
101+
test('tags with tokens show warning but still apply to ChangeSets', () => {
102+
// GIVEN
103+
const app = new App({
104+
context: {
105+
'@aws-cdk/core:explicitStackTags': true,
106+
},
107+
});
108+
const stack = new Stack(app, 'TestStack');
109+
110+
// WHEN
111+
Tags.of(stack).add('StaticKey', 'StaticValue');
112+
Tags.of(stack).add('TokenKey', stack.stackName); // This contains a token
113+
114+
// Synthesize to trigger warnings
115+
app.synth();
116+
117+
// THEN
118+
const warnings = getWarnings(app.synth());
119+
// Should have warning about token tags
120+
const tokenWarnings = warnings.filter(w =>
121+
w.message.includes('Ignoring stack tags that contain deploy-time values')
122+
);
123+
expect(tokenWarnings.length).toBeGreaterThan(0);
124+
125+
// But static tags should still be applied
126+
const stackTags = stack.tags.tagValues();
127+
expect(stackTags).toHaveProperty('StaticKey', 'StaticValue');
128+
});
129+
});

0 commit comments

Comments
 (0)