From f3289f4a68d09d38257fed8da7b3b62130753397 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 23 May 2025 13:25:00 +0200 Subject: [PATCH 01/14] feat(cli): metadata can live in a separate file When CDK apps grow extremely large (think 10-100 stacks, 1000-10000 constructs), all metadata together begins to exceed 512MB, the maximum string size in NodeJS. People usually deal with this by disabling metadata, but they shouldn't have to. In addition, even for manifests that don't exceed 512MB the extremely large size of the single JSON object slows down its writing and reading every time, even if the metadata doesn't need to be accessed. An effective solution is to write the metadata of an artifact to a separate file. This PR introduces the ability for that into the Cloud Assembly schema, and updates the CLI to read from both sources if available. Relates to https://github.com/aws/aws-cdk/pull/34480. --- .../lib/cloud-assembly/schema.ts | 14 +++++++++++++ .../toolkit-lib/lib/api/utilities/index.ts | 1 + .../lib/api/utilities/manifest-reading.ts | 21 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 packages/@aws-cdk/toolkit-lib/lib/api/utilities/index.ts create mode 100644 packages/@aws-cdk/toolkit-lib/lib/api/utilities/manifest-reading.ts diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts index e593328e9..d02b39e5e 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts @@ -81,10 +81,24 @@ export interface ArtifactManifest { /** * Associated metadata. * + * Metadata can be stored directly in the assembly manifest, as well as in a + * separate file (see `additionalMetadataFile`). It should prefer to be stored + * in the additional file, as that will reduce the size of the assembly + * manifest in cases of a lot of metdata (which CDK does emit by default). + * * @default - no metadata. */ readonly metadata?: { [path: string]: MetadataEntry[] }; + /** + * A file with additional metadata entries. + * + * The schema of this file is exactly the same as the type of the `metadata` field. + * + * @default - no additional metadata + */ + readonly additionalMetadataFile?: string; + /** * IDs of artifacts that must be deployed before this artifact. * diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/utilities/index.ts b/packages/@aws-cdk/toolkit-lib/lib/api/utilities/index.ts new file mode 100644 index 000000000..77fb751a2 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/api/utilities/index.ts @@ -0,0 +1 @@ +export * from './manifest-reading'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/utilities/manifest-reading.ts b/packages/@aws-cdk/toolkit-lib/lib/api/utilities/manifest-reading.ts new file mode 100644 index 000000000..6adbcfa26 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/api/utilities/manifest-reading.ts @@ -0,0 +1,21 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import type { ArtifactManifest, MetadataEntry } from '@aws-cdk/cloud-assembly-schema'; + +/** + * Read the metadata for the given artifact + * + * You must use this instead of accessing `ArtifactManifest.metadata` + * directly; this can also deal with the case of where the metadata + * has been written to a file. + */ +export function readArtifactMetadata(assemblyDirectory: string, x: ArtifactManifest): Record { + const ret = {}; + if (x.additionalMetadataFile) { + Object.assign(ret, JSON.parse(fs.readFileSync(path.join(assemblyDirectory, x.additionalMetadataFile), 'utf-8'))); + } + // FIXME: Conflicting paths + // FIXME: Rewrite stack tags + Object.assign(ret, x.metadata); + return ret; +} From 1820699e9c232c0d8494448660d9029f79fcbc11 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Jun 2025 13:27:07 +0000 Subject: [PATCH 02/14] chore: self mutation Signed-off-by: github-actions --- .../cloud-assembly-schema/schema/cloud-assembly.schema.json | 6 +++++- packages/@aws-cdk/cloud-assembly-schema/schema/version.json | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index 855b0ba0b..ce3360b1c 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -49,7 +49,7 @@ "type": "string" }, "metadata": { - "description": "Associated metadata. (Default - no metadata.)", + "description": "Associated metadata.\n\nMetadata can be stored directly in the assembly manifest, as well as in a\nseparate file (see `additionalMetadataFile`). It should prefer to be stored\nin the additional file, as that will reduce the size of the assembly\nmanifest in cases of a lot of metdata (which CDK does emit by default). (Default - no metadata.)", "type": "object", "additionalProperties": { "type": "array", @@ -58,6 +58,10 @@ } } }, + "additionalMetadataFile": { + "description": "A file with additional metadata entries.\n\nThe schema of this file is exactly the same as the type of the `metadata` field. (Default - no additional metadata)", + "type": "string" + }, "dependencies": { "description": "IDs of artifacts that must be deployed before this artifact. (Default - no dependencies.)", "type": "array", diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json index 8caae8e30..0d6f8054c 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json @@ -1,4 +1,4 @@ { - "schemaHash": "96958a4c40e0a00e850f0c14dd6e9c0fc8db0b075f1f155cea41ab198c0514be", - "revision": 43 + "schemaHash": "e03b716ea6f59e089369ce279c03a194917456d466994d434e5f9f366b745f12", + "revision": 44 } \ No newline at end of file From 2e139d810464f64715ef41f531b61036b31f7650 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 13 Jan 2026 16:11:03 +0100 Subject: [PATCH 03/14] Reset version --- packages/@aws-cdk/cloud-assembly-schema/schema/version.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json index 0d6f8054c..19e41429f 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json @@ -1,4 +1,5 @@ { - "schemaHash": "e03b716ea6f59e089369ce279c03a194917456d466994d434e5f9f366b745f12", - "revision": 44 + "schemaHash": "c0092c55280aa03c568e1d591f2034783597e942a400d1d58d7f05a8215f51f4", + "$comment": "Do not hold back the version on additions: jsonschema validation of the manifest by the consumer will trigger errors on unexpected fields.", + "revision": 50 } \ No newline at end of file From 90310c87036957b98a00390904e79a8fbfce0c4d Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 13 Jan 2026 19:00:27 +0100 Subject: [PATCH 04/14] Bumpybump --- packages/@aws-cdk/cloud-assembly-schema/schema/version.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json index 19e41429f..0216d0dc0 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json @@ -1,5 +1,5 @@ { - "schemaHash": "c0092c55280aa03c568e1d591f2034783597e942a400d1d58d7f05a8215f51f4", + "schemaHash": "08bb9043379538c518c5a18a466adf5c971fcd3a348a26b43ff9e4cc3a72f1a2", "$comment": "Do not hold back the version on additions: jsonschema validation of the manifest by the consumer will trigger errors on unexpected fields.", - "revision": 50 + "revision": 51 } \ No newline at end of file From b3a298fcb44f696069a7a027ed5b739aeba9e7e4 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 14 Jan 2026 15:20:19 +0100 Subject: [PATCH 05/14] Stash this for a moment --- .../lib/cloud-assembly/artifact-schema.ts | 6 + .../lib/cloud-assembly/schema.ts | 2 + .../cloud-assembly-schema/lib/manifest.ts | 136 ++++++------------ .../lib/api/utilities/manifest-reading.ts | 16 ++- 4 files changed, 59 insertions(+), 101 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts index 22e14379d..7e90d5eec 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts @@ -61,6 +61,12 @@ export interface AwsCloudFormationStackProperties { /** * Values for CloudFormation stack tags that should be passed when the stack is deployed. * + * N.B.: Tags are also written to stack metadata, under the path of the Stack + * construct. Only in CDK CLI v1 are those tags found in metadata used for + * actual deployments; in all stable versions of CDK only the stack tags + * directly found in the `tags` property of `AwsCloudFormationStack` artifact + * (i.e., this property) are used. + * * @default - No tags */ readonly tags?: { [id: string]: string }; diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts index b0408cd67..f19379027 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/schema.ts @@ -99,6 +99,8 @@ export interface ArtifactManifest { * A file with additional metadata entries. * * The schema of this file is exactly the same as the type of the `metadata` field. + * In other words, that file contains an object mapping construct paths to arrays + * of metadata entries. * * @default - no additional metadata */ diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts index ac1020df7..558ab6ef3 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts @@ -80,7 +80,7 @@ export class Manifest { * @param filePath - output file path. */ public static saveAssemblyManifest(manifest: assembly.AssemblyManifest, filePath: string) { - Manifest.saveManifest(manifest, filePath, ASSEMBLY_SCHEMA, Manifest.patchStackTagsOnWrite); + Manifest.saveManifest(manifest, filePath, ASSEMBLY_SCHEMA); } /** @@ -102,7 +102,7 @@ export class Manifest { * @param filePath - output file path. */ public static saveAssetManifest(manifest: assets.AssetManifest, filePath: string) { - Manifest.saveManifest(manifest, filePath, ASSETS_SCHEMA, Manifest.patchStackTagsOnRead); + Manifest.saveManifest(manifest, filePath, ASSETS_SCHEMA); } /** @@ -111,7 +111,7 @@ export class Manifest { * @param filePath - path to the manifest file. */ public static loadAssetManifest(filePath: string): assets.AssetManifest { - return this.loadManifest(filePath, ASSETS_SCHEMA); + return Manifest.loadManifest(filePath, ASSETS_SCHEMA); } /** @@ -266,27 +266,49 @@ export class Manifest { } /** - * This requires some explaining... + * Fix the casing of stack tags entries * - * We previously used `{ Key, Value }` for the object that represents a stack tag. (Notice the casing) - * @link https://github.com/aws/aws-cdk/blob/v1.27.0/packages/aws-cdk/lib/api/cxapp/stacks.ts#L427. + * At the very beginning of the CDK we used to emit stack tags as an object with + * `{ Key, Value }` keys; this had the "advantage" that we could stick those + * tags directly into the `CreateChangeSet` call. * - * When that object moved to this package, it had to be JSII compliant, which meant the property - * names must be `camelCased`, and not `PascalCased`. This meant it no longer matches the structure in the `manifest.json` file. - * In order to support current manifest files, we have to translate the `PascalCased` representation to the new `camelCased` one. + * Then we later on used jsii on the assembly schema and we were forced to type + * the in-memory objects as `{ key, value }` with lowercase letters. Now the + * objects have a different on-disk and in-memory format, and we need to convert + * between them. * - * Note that the serialization itself still writes `PascalCased` because it relates to how CloudFormation expects it. + * For backwards compatibility reasons, we used to convert lowercase in-memory + * to uppercase on-disk variant until very recently. This is now unnecessary, + * since no officially supported CDK tools read the stack tags from the + * metadata; the CLI and toolkit library read stack tags from the artifact + * properties. * - * Ideally, we would start writing the `camelCased` and translate to how CloudFormation expects it when needed. But this requires nasty - * backwards-compatibility code and it just doesn't seem to be worth the effort. + * So although we don't emit uppercase stack tag objects anymore, we might still read + * manifests that have them. Because the manifest we read must pass JSON Schema + * validation (which expects lowercase tag objects), we have to fix the casing + * of these objects after reading from disk and before validating. + * + * That's what this function does. */ private static patchStackTagsOnRead(this: void, manifest: assembly.AssemblyManifest) { - return Manifest.replaceStackTags(manifest, (tags) => - tags.map((diskTag: any) => ({ - key: diskTag.Key, - value: diskTag.Value, - })), - ); + for (const artifact of Object.values(manifest.artifacts ?? {})) { + if (artifact.type !== assembly.ArtifactType.AWS_CLOUDFORMATION_STACK) { + continue; + } + + for (const entries of Object.values(artifact.metadata ?? {})) { + for (const entry of entries) { + if (entry.type === assembly.ArtifactMetadataEntryType.STACK_TAGS && (entry.data as any)?.Key) { + (entry as any).data = { + key: (entry.data as any).Key, + value: (entry.data as any).Value, + }; + } + } + } + } + + return manifest; } /** @@ -317,88 +339,10 @@ export class Manifest { throw new Error(`ExternalId is not allowed inside '${key}'`); } } - - /** - * See explanation on `patchStackTagsOnRead` - * - * Translate stack tags metadata if it has the "right" casing. - */ - private static patchStackTagsOnWrite(this: void, manifest: assembly.AssemblyManifest) { - return Manifest.replaceStackTags(manifest, (tags) => - tags.map( - (memTag) => - // Might already be uppercased (because stack synthesis generates it in final form yet) - ('Key' in memTag ? memTag : { Key: memTag.key, Value: memTag.value }) as any, - ), - ); - } - - /** - * Recursively replace stack tags in the stack metadata - */ - private static replaceStackTags( - manifest: assembly.AssemblyManifest, - fn: Endofunctor, - ): assembly.AssemblyManifest { - // Need to add in the `noUndefined`s because otherwise jest snapshot tests are going to freak out - // about the keys with values that are `undefined` (even though they would never be JSON.stringified) - return noUndefined({ - ...manifest, - artifacts: mapValues(manifest.artifacts, (artifact) => { - if (artifact.type !== assembly.ArtifactType.AWS_CLOUDFORMATION_STACK) { - return artifact; - } - return noUndefined({ - ...artifact, - metadata: mapValues(artifact.metadata, (metadataEntries) => - metadataEntries.map((metadataEntry) => { - if ( - metadataEntry.type !== assembly.ArtifactMetadataEntryType.STACK_TAGS || - !metadataEntry.data - ) { - return metadataEntry; - } - return { - ...metadataEntry, - data: fn(metadataEntry.data as assembly.StackTagsMetadataEntry), - }; - }), - ), - } as assembly.ArtifactManifest); - }), - }); - } - private constructor() { } } -type Endofunctor = (x: A) => A; - -function mapValues( - xs: Record | undefined, - fn: (x: A) => B, -): Record | undefined { - if (!xs) { - return undefined; - } - const ret: Record | undefined = {}; - for (const [k, v] of Object.entries(xs)) { - ret[k] = fn(v); - } - return ret; -} - -function noUndefined(xs: A): A { - const ret: any = {}; - for (const [k, v] of Object.entries(xs)) { - if (v !== undefined) { - ret[k] = v; - } - } - return ret; -} - function stripEnumErrors(errors: jsonschema.ValidationError[]) { return errors.filter((e) => typeof e.schema === 'string' || !('enum' in e.schema)); } diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/utilities/manifest-reading.ts b/packages/@aws-cdk/toolkit-lib/lib/api/utilities/manifest-reading.ts index 6adbcfa26..85c1da8b9 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/utilities/manifest-reading.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/utilities/manifest-reading.ts @@ -10,12 +10,18 @@ import type { ArtifactManifest, MetadataEntry } from '@aws-cdk/cloud-assembly-sc * has been written to a file. */ export function readArtifactMetadata(assemblyDirectory: string, x: ArtifactManifest): Record { - const ret = {}; + const ret: Record = {}; if (x.additionalMetadataFile) { Object.assign(ret, JSON.parse(fs.readFileSync(path.join(assemblyDirectory, x.additionalMetadataFile), 'utf-8'))); } - // FIXME: Conflicting paths - // FIXME: Rewrite stack tags - Object.assign(ret, x.metadata); + + for (const [path, entries] of Object.entries(x.metadata ?? {})) { + if (ret[path]) { + ret[path].push(...entries); + } else { + ret[path] = entries; + } + } + return ret; -} +} \ No newline at end of file From 9c2585ddb00458e8e3514fb7e8bc75c2cf75dcff Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 20 Jan 2026 12:00:05 +0100 Subject: [PATCH 06/14] Use the right metadata accessor in more places --- .../cloud-assembly-api/lib/cloud-artifact.ts | 16 +++++++++++++--- .../lib/private}/manifest-reading.ts | 0 .../lib/runner/private/cloud-assembly.ts | 1 + .../lib/api/cloud-assembly/stack-collection.ts | 6 +++++- .../toolkit-lib/lib/api/refactoring/exclude.ts | 1 + .../api/resource-metadata/resource-metadata.ts | 2 +- .../api/stack-events/stack-activity-monitor.ts | 2 +- .../toolkit-lib/lib/api/utilities/index.ts | 1 - packages/aws-cdk/lib/cli/cdk-toolkit.ts | 2 +- 9 files changed, 23 insertions(+), 8 deletions(-) rename packages/@aws-cdk/{toolkit-lib/lib/api/utilities => cloud-assembly-api/lib/private}/manifest-reading.ts (100%) delete mode 100644 packages/@aws-cdk/toolkit-lib/lib/api/utilities/index.ts diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/cloud-artifact.ts b/packages/@aws-cdk/cloud-assembly-api/lib/cloud-artifact.ts index 0f064adbe..1917f0128 100644 --- a/packages/@aws-cdk/cloud-assembly-api/lib/cloud-artifact.ts +++ b/packages/@aws-cdk/cloud-assembly-api/lib/cloud-artifact.ts @@ -3,6 +3,7 @@ import type { CloudAssembly } from './cloud-assembly'; import type { MetadataEntryResult, SynthesisMessage } from './metadata'; import { SynthesisMessageLevel } from './metadata'; import { CloudAssemblyError } from './private/error'; +import { readArtifactMetadata } from './private/manifest-reading'; /** * Artifact properties for CloudFormation stacks. @@ -77,6 +78,13 @@ export class CloudArtifact { this._dependencyIDs = manifest.dependencies || []; } + /** + * Returns the metadata associated with this Cloud Artifact + */ + public get metadata() { + return readArtifactMetadata(this.assembly.directory, this.manifest); + } + /** * Returns all the artifacts that this artifact depends on. */ @@ -100,9 +108,11 @@ export class CloudArtifact { * @returns all the metadata entries of a specific type in this artifact. */ public findMetadataByType(type: string): MetadataEntryResult[] { + const metadata = this.metadata; + const result = new Array(); - for (const path of Object.keys(this.manifest.metadata || {})) { - for (const entry of (this.manifest.metadata || {})[path]) { + for (const path of Object.keys(metadata || {})) { + for (const entry of (metadata || {})[path]) { if (entry.type === type) { result.push({ path, ...entry }); } @@ -114,7 +124,7 @@ export class CloudArtifact { private renderMessages() { const messages = new Array(); - for (const [id, metadata] of Object.entries(this.manifest.metadata || { })) { + for (const [id, metadata] of Object.entries(this.metadata || { })) { for (const entry of metadata) { let level: SynthesisMessageLevel; switch (entry.type) { diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/utilities/manifest-reading.ts b/packages/@aws-cdk/cloud-assembly-api/lib/private/manifest-reading.ts similarity index 100% rename from packages/@aws-cdk/toolkit-lib/lib/api/utilities/manifest-reading.ts rename to packages/@aws-cdk/cloud-assembly-api/lib/private/manifest-reading.ts diff --git a/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts b/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts index 146b2a3d5..c189e35c4 100644 --- a/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts +++ b/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts @@ -166,6 +166,7 @@ export class AssemblyManifestReader { */ private assetsFromAssemblyManifest(artifact: ArtifactManifest): (ContainerImageAssetMetadataEntry | FileAssetMetadataEntry)[] { const assets: (ContainerImageAssetMetadataEntry | FileAssetMetadataEntry)[] = []; + // FIXME: OH NO for (const metadata of Object.values(artifact.metadata ?? {})) { metadata.forEach(data => { if (data.type === ArtifactMetadataEntryType.ASSET) { diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/stack-collection.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/stack-collection.ts index 1198f0cd3..fb6bdd04f 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/stack-collection.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/stack-collection.ts @@ -42,7 +42,11 @@ export class StackCollection { id: stack.displayName ?? stack.id, name: stack.stackName, environment: stack.environment, - metadata: stack.manifest.metadata, + + // Might be huge so load it lazily + get metadata() { + return stack.metadata; + }, dependencies: [], }; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/exclude.ts b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/exclude.ts index 3c9d23aa7..400c8a8c2 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/exclude.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/exclude.ts @@ -33,6 +33,7 @@ export class ManifestExcludeList extends AbstractExcludeList { const result: CfnResourceLocation[] = []; for (let [stackName, manifest] of stackManifests) { + // FIXME: OH NO const locations = Object.values(manifest.metadata ?? {}) // Then pick only the resources in each stack marked with DO_NOT_REFACTOR .filter((entries) => diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/resource-metadata/resource-metadata.ts b/packages/@aws-cdk/toolkit-lib/lib/api/resource-metadata/resource-metadata.ts index a655d4ef5..4f9c85835 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/resource-metadata/resource-metadata.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/resource-metadata/resource-metadata.ts @@ -24,7 +24,7 @@ export interface ResourceMetadata { * @returns The resource metadata, or undefined if the resource was not found */ export function resourceMetadata(stack: CloudFormationStackArtifact, logicalId: string): ResourceMetadata | undefined { - const metadata = stack.manifest?.metadata; + const metadata = stack.metadata; if (!metadata) { return undefined; } diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/stack-events/stack-activity-monitor.ts b/packages/@aws-cdk/toolkit-lib/lib/api/stack-events/stack-activity-monitor.ts index 17bef32de..5756039f3 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/stack-events/stack-activity-monitor.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/stack-events/stack-activity-monitor.ts @@ -179,7 +179,7 @@ export class StackActivityMonitor { } private findMetadataFor(logicalId: string | undefined) { - const metadata = this.stack.manifest?.metadata; + const metadata = this.stack.metadata; if (!logicalId || !metadata) { return undefined; } diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/utilities/index.ts b/packages/@aws-cdk/toolkit-lib/lib/api/utilities/index.ts deleted file mode 100644 index 77fb751a2..000000000 --- a/packages/@aws-cdk/toolkit-lib/lib/api/utilities/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './manifest-reading'; diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index 726e5f55b..fa34e2f55 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -195,7 +195,7 @@ export class CdkToolkit { public async metadata(stackName: string, json: boolean) { const stacks = await this.selectSingleStackByName(stackName); - await printSerializedObject(this.ioHost.asIoHelper(), stacks.firstStack.manifest.metadata ?? {}, json); + await printSerializedObject(this.ioHost.asIoHelper(), stacks.firstStack.metadata ?? {}, json); } public async acknowledge(noticeId: string) { From 090de82f3dc8961e1410863a068e4baf9fef0b06 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 20 Jan 2026 14:06:26 +0100 Subject: [PATCH 07/14] Fix tag reading --- .../cloud-assembly-api/lib/cloud-artifact.ts | 33 +++++++++++++++++-- .../lib/private/manifest-reading.ts | 27 --------------- .../cloud-assembly-schema/lib/manifest.ts | 31 ++++++++++------- .../schema/cloud-assembly.schema.json | 4 +-- .../cloud-assembly-schema/schema/version.json | 4 +-- .../lib/runner/private/cloud-assembly.ts | 4 +-- .../lib/api/refactoring/exclude.ts | 9 ++--- .../test/api/refactoring/exclude.test.ts | 4 +-- 8 files changed, 63 insertions(+), 53 deletions(-) delete mode 100644 packages/@aws-cdk/cloud-assembly-api/lib/private/manifest-reading.ts diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/cloud-artifact.ts b/packages/@aws-cdk/cloud-assembly-api/lib/cloud-artifact.ts index 1917f0128..8fc5847c1 100644 --- a/packages/@aws-cdk/cloud-assembly-api/lib/cloud-artifact.ts +++ b/packages/@aws-cdk/cloud-assembly-api/lib/cloud-artifact.ts @@ -1,9 +1,10 @@ +import * as fs from 'fs'; +import * as path from 'path'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import type { CloudAssembly } from './cloud-assembly'; import type { MetadataEntryResult, SynthesisMessage } from './metadata'; import { SynthesisMessageLevel } from './metadata'; import { CloudAssemblyError } from './private/error'; -import { readArtifactMetadata } from './private/manifest-reading'; /** * Artifact properties for CloudFormation stacks. @@ -37,6 +38,34 @@ export interface AwsCloudFormationStackProperties { * Represents an artifact within a cloud assembly. */ export class CloudArtifact { + /** + * Read the metadata for the given artifact + * + * HISTORICAL OR PRIVATE USE ONLY + * + * This is publicly exposed as a static function for downstream libraries that + * don't use the `CloudAssembly`/`CloudArtifact` API, yet still need to read + * an artifact's metadata. + * + * 99% of consumers should just access `artifact.metadata`. + */ + public static readMetadata(assemblyDirectory: string, x: cxschema.ArtifactManifest): Record { + const ret: Record = {}; + if (x.additionalMetadataFile) { + Object.assign(ret, JSON.parse(fs.readFileSync(path.join(assemblyDirectory, x.additionalMetadataFile), 'utf-8'))); + } + + for (const [path, entries] of Object.entries(x.metadata ?? {})) { + if (ret[path]) { + ret[path].push(...entries); + } else { + ret[path] = entries; + } + } + + return ret; + } + /** * Returns a subclass of `CloudArtifact` based on the artifact type defined in the artifact manifest. * @@ -82,7 +111,7 @@ export class CloudArtifact { * Returns the metadata associated with this Cloud Artifact */ public get metadata() { - return readArtifactMetadata(this.assembly.directory, this.manifest); + return CloudArtifact.readMetadata(this.assembly.directory, this.manifest); } /** diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/private/manifest-reading.ts b/packages/@aws-cdk/cloud-assembly-api/lib/private/manifest-reading.ts deleted file mode 100644 index 85c1da8b9..000000000 --- a/packages/@aws-cdk/cloud-assembly-api/lib/private/manifest-reading.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import type { ArtifactManifest, MetadataEntry } from '@aws-cdk/cloud-assembly-schema'; - -/** - * Read the metadata for the given artifact - * - * You must use this instead of accessing `ArtifactManifest.metadata` - * directly; this can also deal with the case of where the metadata - * has been written to a file. - */ -export function readArtifactMetadata(assemblyDirectory: string, x: ArtifactManifest): Record { - const ret: Record = {}; - if (x.additionalMetadataFile) { - Object.assign(ret, JSON.parse(fs.readFileSync(path.join(assemblyDirectory, x.additionalMetadataFile), 'utf-8'))); - } - - for (const [path, entries] of Object.entries(x.metadata ?? {})) { - if (ret[path]) { - ret[path].push(...entries); - } else { - ret[path] = entries; - } - } - - return ret; -} \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts index 558ab6ef3..edd251e34 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts @@ -291,18 +291,25 @@ export class Manifest { * That's what this function does. */ private static patchStackTagsOnRead(this: void, manifest: assembly.AssemblyManifest) { - for (const artifact of Object.values(manifest.artifacts ?? {})) { - if (artifact.type !== assembly.ArtifactType.AWS_CLOUDFORMATION_STACK) { - continue; - } - - for (const entries of Object.values(artifact.metadata ?? {})) { - for (const entry of entries) { - if (entry.type === assembly.ArtifactMetadataEntryType.STACK_TAGS && (entry.data as any)?.Key) { - (entry as any).data = { - key: (entry.data as any).Key, - value: (entry.data as any).Value, - }; + const artifacts = Object.values(manifest.artifacts ?? {}) + .filter(artifact => artifact.type === assembly.ArtifactType.AWS_CLOUDFORMATION_STACK); + + for (const artifact of artifacts) { + const tagMetadata = Object.values(artifact.metadata ?? {}) + .flatMap(x => x) + .filter(entry => entry.type === assembly.ArtifactMetadataEntryType.STACK_TAGS); + + for (const entry of tagMetadata) { + const tags = entry.data as unknown as assembly.StackTagsMetadataEntry[]; + for (const tag of tags) { + const t: any = tag; + if ('Key' in t) { + t.key = t.Key; + delete t.Key; + } + if ('Value' in t) { + t.value = t.Value; + delete t.Value; } } } diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index ca8a8e483..2e3e5fa40 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -59,7 +59,7 @@ } }, "additionalMetadataFile": { - "description": "A file with additional metadata entries.\n\nThe schema of this file is exactly the same as the type of the `metadata` field. (Default - no additional metadata)", + "description": "A file with additional metadata entries.\n\nThe schema of this file is exactly the same as the type of the `metadata` field.\nIn other words, that file contains an object mapping construct paths to arrays\nof metadata entries. (Default - no additional metadata)", "type": "string" }, "dependencies": { @@ -355,7 +355,7 @@ } }, "tags": { - "description": "Values for CloudFormation stack tags that should be passed when the stack is deployed. (Default - No tags)", + "description": "Values for CloudFormation stack tags that should be passed when the stack is deployed.\n\nN.B.: Tags are also written to stack metadata, under the path of the Stack\nconstruct. Only in CDK CLI v1 are those tags found in metadata used for\nactual deployments; in all stable versions of CDK only the stack tags\ndirectly found in the `tags` property of `AwsCloudFormationStack` artifact\n(i.e., this property) are used. (Default - No tags)", "type": "object", "additionalProperties": { "type": "string" diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json index 0216d0dc0..5b7c6a5df 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/version.json @@ -1,5 +1,5 @@ { - "schemaHash": "08bb9043379538c518c5a18a466adf5c971fcd3a348a26b43ff9e4cc3a72f1a2", + "schemaHash": "22c511a4ddd185761b8d56ac21d48c8384873ffe4b953b3567654746f8dd26f1", "$comment": "Do not hold back the version on additions: jsonschema validation of the manifest by the consumer will trigger errors on unexpected fields.", - "revision": 51 + "revision": 52 } \ No newline at end of file diff --git a/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts b/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts index c189e35c4..f671d0c32 100644 --- a/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts +++ b/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts @@ -4,6 +4,7 @@ import { AssetManifest } from '@aws-cdk/cdk-assets-lib'; import type { AssemblyManifest, AwsCloudFormationStackProperties, ArtifactManifest, MetadataEntry, AssetManifestProperties, ContainerImageAssetMetadataEntry, FileAssetMetadataEntry } from '@aws-cdk/cloud-assembly-schema'; import { Manifest, ArtifactType, ArtifactMetadataEntryType } from '@aws-cdk/cloud-assembly-schema'; import * as fs from 'fs-extra'; +import { CloudArtifact } from '@aws-cdk/cloud-assembly-api'; /** * Trace information for stack @@ -166,8 +167,7 @@ export class AssemblyManifestReader { */ private assetsFromAssemblyManifest(artifact: ArtifactManifest): (ContainerImageAssetMetadataEntry | FileAssetMetadataEntry)[] { const assets: (ContainerImageAssetMetadataEntry | FileAssetMetadataEntry)[] = []; - // FIXME: OH NO - for (const metadata of Object.values(artifact.metadata ?? {})) { + for (const metadata of Object.values(CloudArtifact.readMetadata(this.directory, artifact) ?? {})) { metadata.forEach(data => { if (data.type === ArtifactMetadataEntryType.ASSET) { const asset = (data.data as ContainerImageAssetMetadataEntry | FileAssetMetadataEntry); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/exclude.ts b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/exclude.ts index 400c8a8c2..f3161cab6 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/exclude.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/exclude.ts @@ -2,6 +2,7 @@ import type { AssemblyManifest } from '@aws-cdk/cloud-assembly-schema'; import { ArtifactMetadataEntryType, ArtifactType } from '@aws-cdk/cloud-assembly-schema'; import type { ResourceLocation as CfnResourceLocation } from '@aws-sdk/client-cloudformation'; import type { ResourceLocation } from './cloudformation'; +import { CloudArtifact } from '@aws-cdk/cloud-assembly-api'; export interface ExcludeList { isExcluded(location: ResourceLocation): boolean; @@ -20,12 +21,12 @@ abstract class AbstractExcludeList implements ExcludeList { export class ManifestExcludeList extends AbstractExcludeList { private readonly excludedLocations: CfnResourceLocation[]; - constructor(manifest: AssemblyManifest) { + constructor(directory: string, manifest: AssemblyManifest) { super(); - this.excludedLocations = this.getExcludedLocations(manifest); + this.excludedLocations = this.getExcludedLocations(directory, manifest); } - private getExcludedLocations(asmManifest: AssemblyManifest): CfnResourceLocation[] { + private getExcludedLocations(directory: string, asmManifest: AssemblyManifest): CfnResourceLocation[] { // First, we need to filter the artifacts to only include CloudFormation stacks const stackManifests = Object.entries(asmManifest.artifacts ?? {}).filter( ([_, manifest]) => manifest.type === ArtifactType.AWS_CLOUDFORMATION_STACK, @@ -34,7 +35,7 @@ export class ManifestExcludeList extends AbstractExcludeList { const result: CfnResourceLocation[] = []; for (let [stackName, manifest] of stackManifests) { // FIXME: OH NO - const locations = Object.values(manifest.metadata ?? {}) + const locations = Object.values(CloudArtifact.readMetadata(directory, manifest)) // Then pick only the resources in each stack marked with DO_NOT_REFACTOR .filter((entries) => entries.some((entry) => entry.type === ArtifactMetadataEntryType.DO_NOT_REFACTOR && entry.data === true), diff --git a/packages/@aws-cdk/toolkit-lib/test/api/refactoring/exclude.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/refactoring/exclude.test.ts index b658ebc80..aa76f7f24 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/refactoring/exclude.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/refactoring/exclude.test.ts @@ -72,7 +72,7 @@ describe('ManifestExcludeList', () => { }, }; - const excludeList = new ManifestExcludeList(manifest as any); + const excludeList = new ManifestExcludeList('/', manifest as any); expect(excludeList.isExcluded(resource1)).toBe(true); expect(excludeList.isExcluded(resource2)).toBe(true); @@ -91,7 +91,7 @@ describe('ManifestExcludeList', () => { }, }; - const excludeList = new ManifestExcludeList(manifest as any); + const excludeList = new ManifestExcludeList('/', manifest as any); expect(excludeList.isExcluded(resource1)).toBe(false); }); }); From 6b6d8ff6bea21c102ae2536fa6e6a56bcf2a04d3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 20 Jan 2026 14:07:48 +0100 Subject: [PATCH 08/14] Raise line coverage by having fewer lines --- packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts index edd251e34..2dab29f38 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts @@ -72,7 +72,7 @@ export interface LoadManifestOptions { /** * Protocol utility class. */ -export class Manifest { +export abstract class Manifest { /** * Validates and saves the cloud assembly manifest to file. * @@ -346,8 +346,6 @@ export class Manifest { throw new Error(`ExternalId is not allowed inside '${key}'`); } } - private constructor() { - } } function stripEnumErrors(errors: jsonschema.ValidationError[]) { From 3476855673c5eb93ffb45e232f99a061a14182bd Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 20 Jan 2026 14:09:15 +0100 Subject: [PATCH 09/14] We don't do this anymore --- .../test/stack-artifact.test.ts | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly-api/test/stack-artifact.test.ts b/packages/@aws-cdk/cloud-assembly-api/test/stack-artifact.test.ts index 6378abc12..591d00a04 100644 --- a/packages/@aws-cdk/cloud-assembly-api/test/stack-artifact.test.ts +++ b/packages/@aws-cdk/cloud-assembly-api/test/stack-artifact.test.ts @@ -58,40 +58,6 @@ test('read tags from artifact properties', () => { expect(assembly.getStackByName('Stack').tags).toEqual({ foo: 'bar' }); }); -test('stack tags get uppercased when written to Cloud Assembly', () => { - // Backwards compatibility test - // GIVEN - builder.addArtifact('Stack', { - ...stackBase, - metadata: { - '/Stack': [ - { - type: 'aws:cdk:stack-tags', - data: [{ key: 'foo', value: 'bar' }], - }, - ], - }, - }); - - // WHEN - const assembly = builder.buildAssembly(); - - // THEN - const manifestStructure = JSON.parse(fs.readFileSync(path.join(assembly.directory, 'manifest.json'), { encoding: 'utf-8' })); - expect(manifestStructure.artifacts.Stack.metadata['/Stack']).toEqual([ - { - type: 'aws:cdk:stack-tags', - data: [ - { - // Note: uppercase due to historical accident - Key: 'foo', - Value: 'bar', - }, - ], - }, - ]); -}); - test('already uppercased stack tags get left alone', () => { // Backwards compatibility test // GIVEN From 289d8f285639d01c7aa9bb231f151b4a2be50255 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 20 Jan 2026 14:17:01 +0100 Subject: [PATCH 10/14] Aha --- .../cloud-assembly-api/lib/cloud-artifact.ts | 14 +++++++------- .../@aws-cdk/cloud-assembly-schema/lib/manifest.ts | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/cloud-artifact.ts b/packages/@aws-cdk/cloud-assembly-api/lib/cloud-artifact.ts index 8fc5847c1..e278d1c1f 100644 --- a/packages/@aws-cdk/cloud-assembly-api/lib/cloud-artifact.ts +++ b/packages/@aws-cdk/cloud-assembly-api/lib/cloud-artifact.ts @@ -55,11 +55,11 @@ export class CloudArtifact { Object.assign(ret, JSON.parse(fs.readFileSync(path.join(assemblyDirectory, x.additionalMetadataFile), 'utf-8'))); } - for (const [path, entries] of Object.entries(x.metadata ?? {})) { - if (ret[path]) { - ret[path].push(...entries); + for (const [p, entries] of Object.entries(x.metadata ?? {})) { + if (ret[p]) { + ret[p].push(...entries); } else { - ret[path] = entries; + ret[p] = entries; } } @@ -140,10 +140,10 @@ export class CloudArtifact { const metadata = this.metadata; const result = new Array(); - for (const path of Object.keys(metadata || {})) { - for (const entry of (metadata || {})[path]) { + for (const p of Object.keys(metadata || {})) { + for (const entry of (metadata || {})[p]) { if (entry.type === type) { - result.push({ path, ...entry }); + result.push({ path: p, ...entry }); } } } diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts index 2dab29f38..0d970e389 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts @@ -301,6 +301,7 @@ export abstract class Manifest { for (const entry of tagMetadata) { const tags = entry.data as unknown as assembly.StackTagsMetadataEntry[]; + console.log(tags); for (const tag of tags) { const t: any = tag; if ('Key' in t) { From 6c1612afa16b70321b4b2bc6bf4b35b850c842c9 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 20 Jan 2026 15:37:49 +0100 Subject: [PATCH 11/14] Fix some build bits --- .vscode/settings.json | 3 +++ packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts | 5 ++--- .../integ-runner/lib/runner/private/cloud-assembly.ts | 2 +- packages/@aws-cdk/toolkit-lib/lib/api/refactoring/exclude.ts | 2 +- packages/@aws-cdk/toolkit-lib/test/actions/list.test.ts | 3 +++ 5 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..01d970570 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eslint.useFlatConfig": false, +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts index 0d970e389..202487fab 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts @@ -300,9 +300,8 @@ export abstract class Manifest { .filter(entry => entry.type === assembly.ArtifactMetadataEntryType.STACK_TAGS); for (const entry of tagMetadata) { - const tags = entry.data as unknown as assembly.StackTagsMetadataEntry[]; - console.log(tags); - for (const tag of tags) { + const tags = entry.data as unknown as assembly.StackTagsMetadataEntry[] | undefined; + for (const tag of tags ?? []) { const t: any = tag; if ('Key' in t) { t.key = t.Key; diff --git a/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts b/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts index f671d0c32..7587d4e62 100644 --- a/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts +++ b/packages/@aws-cdk/integ-runner/lib/runner/private/cloud-assembly.ts @@ -1,10 +1,10 @@ import * as path from 'path'; import type { FileManifestEntry, DockerImageManifestEntry } from '@aws-cdk/cdk-assets-lib'; import { AssetManifest } from '@aws-cdk/cdk-assets-lib'; +import { CloudArtifact } from '@aws-cdk/cloud-assembly-api'; import type { AssemblyManifest, AwsCloudFormationStackProperties, ArtifactManifest, MetadataEntry, AssetManifestProperties, ContainerImageAssetMetadataEntry, FileAssetMetadataEntry } from '@aws-cdk/cloud-assembly-schema'; import { Manifest, ArtifactType, ArtifactMetadataEntryType } from '@aws-cdk/cloud-assembly-schema'; import * as fs from 'fs-extra'; -import { CloudArtifact } from '@aws-cdk/cloud-assembly-api'; /** * Trace information for stack diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/exclude.ts b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/exclude.ts index f3161cab6..7e45f55ce 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/exclude.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/exclude.ts @@ -1,8 +1,8 @@ +import { CloudArtifact } from '@aws-cdk/cloud-assembly-api'; import type { AssemblyManifest } from '@aws-cdk/cloud-assembly-schema'; import { ArtifactMetadataEntryType, ArtifactType } from '@aws-cdk/cloud-assembly-schema'; import type { ResourceLocation as CfnResourceLocation } from '@aws-sdk/client-cloudformation'; import type { ResourceLocation } from './cloudformation'; -import { CloudArtifact } from '@aws-cdk/cloud-assembly-api'; export interface ExcludeList { isExcluded(location: ResourceLocation): boolean; diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/list.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/list.test.ts index cd8855d9c..e21292de7 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/list.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/list.test.ts @@ -496,6 +496,7 @@ const MOCK_STACK_A: TestStackArtifact = { '/Test-Stack-A': [ { type: ArtifactMetadataEntryType.STACK_TAGS, + data: [], }, ], }, @@ -508,6 +509,7 @@ const MOCK_STACK_B: TestStackArtifact = { '/Test-Stack-B': [ { type: ArtifactMetadataEntryType.STACK_TAGS, + data: [], }, ], }, @@ -530,6 +532,7 @@ const MOCK_STACK_C: TestStackArtifact = { '/Test-Stack-C': [ { type: ArtifactMetadataEntryType.STACK_TAGS, + data: [], }, ], }, From c059285eed41027b5f7b3414f08e16a2701ed121 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 20 Jan 2026 15:53:05 +0100 Subject: [PATCH 12/14] Add a unit test for additional metadata --- .../lib/artifacts/cloudformation-artifact.ts | 12 +--- .../test/stack-artifact.test.ts | 59 ++++++++++++++++++- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/artifacts/cloudformation-artifact.ts b/packages/@aws-cdk/cloud-assembly-api/lib/artifacts/cloudformation-artifact.ts index 4bfd0e853..dd90de551 100644 --- a/packages/@aws-cdk/cloud-assembly-api/lib/artifacts/cloudformation-artifact.ts +++ b/packages/@aws-cdk/cloud-assembly-api/lib/artifacts/cloudformation-artifact.ts @@ -175,7 +175,7 @@ export class CloudFormationStackArtifact extends CloudArtifact { // We get the tags from 'properties' if available (cloud assembly format >= 6.0.0), otherwise // from the stack metadata - this.tags = properties.tags ?? this.tagsFromMetadata(); + this.tags = properties.tags ?? {}; this.notificationArns = properties.notificationArns; this.assumeRoleArn = properties.assumeRoleArn; this.assumeRoleExternalId = properties.assumeRoleExternalId; @@ -215,16 +215,6 @@ export class CloudFormationStackArtifact extends CloudArtifact { } return this._template; } - - private tagsFromMetadata() { - const ret: Record = {}; - for (const metadataEntry of this.findMetadataByType(cxschema.ArtifactMetadataEntryType.STACK_TAGS)) { - for (const tag of (metadataEntry.data ?? []) as cxschema.StackTagsMetadataEntry) { - ret[tag.key] = tag.value; - } - } - return ret; - } } /** diff --git a/packages/@aws-cdk/cloud-assembly-api/test/stack-artifact.test.ts b/packages/@aws-cdk/cloud-assembly-api/test/stack-artifact.test.ts index 591d00a04..88013f8c4 100644 --- a/packages/@aws-cdk/cloud-assembly-api/test/stack-artifact.test.ts +++ b/packages/@aws-cdk/cloud-assembly-api/test/stack-artifact.test.ts @@ -92,7 +92,7 @@ test('already uppercased stack tags get left alone', () => { ]); }); -test('read tags from stack metadata', () => { +test('tags are NO LONGER read from stack metadata', () => { // Backwards compatibility test // GIVEN builder.addArtifact('Stack', { @@ -111,7 +111,62 @@ test('read tags from stack metadata', () => { const assembly = builder.buildAssembly(); // THEN - expect(assembly.getStackByName('Stack').tags).toEqual({ foo: 'bar' }); + expect(assembly.getStackByName('Stack').tags).toEqual({}); +}); + +test('stack metadata can be read both from artifact and a file', () => { + // Backwards compatibility test + // GIVEN + builder.addArtifact('Stack', { + ...stackBase, + metadata: { + '/Stack': [ + { + type: 'custom:metadata', + data: 'custom 1', + }, + ], + }, + additionalMetadataFile: 'addl-meta.json', + }); + + fs.writeFileSync(path.join(builder.outdir, 'addl-meta.json'), JSON.stringify({ + '/Stack': [ + { + type: 'custom:metadata', + data: 'custom 2', + }, + ], + '/Stack/Sub': [ + { + type: 'custom:metadata', + data: 'custom 3', + }, + ], + }), 'utf-8'); + + // WHEN + const assembly = builder.buildAssembly(); + + // THEN + expect(assembly.getStackByName('Stack').metadata).toEqual({ + '/Stack': [ + { + type: 'custom:metadata', + data: 'custom 2', + }, + { + type: 'custom:metadata', + data: 'custom 1', + }, + ], + '/Stack/Sub': [ + { + type: 'custom:metadata', + data: 'custom 3', + }, + ], + }); }); test('user friendly id is the assembly display name', () => { From e775bd92a1043ccb64598d56b08e077fc8219fc7 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 20 Jan 2026 16:11:12 +0100 Subject: [PATCH 13/14] Fix some stack tags in a test --- .../lib/cli/parse-command-line-arguments.ts | 2 +- packages/aws-cdk/test/cli/cdk-toolkit.test.ts | 54 +++++-------------- 2 files changed, 13 insertions(+), 43 deletions(-) diff --git a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts index 1f8e7377b..b4b39acd2 100644 --- a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts @@ -3,8 +3,8 @@ // Do not edit by hand; all changes will be overwritten at build time from the config file. // ------------------------------------------------------------------------------------------- /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports */ -import { Argv } from 'yargs'; import * as helpers from './util/yargs-helpers'; +import { Argv } from 'yargs'; // @ts-ignore TS6133 export function parseCommandLineArguments(args: Array): any { diff --git a/packages/aws-cdk/test/cli/cdk-toolkit.test.ts b/packages/aws-cdk/test/cli/cdk-toolkit.test.ts index 5559f5243..f43942cea 100644 --- a/packages/aws-cdk/test/cli/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cli/cdk-toolkit.test.ts @@ -1740,13 +1740,8 @@ class MockStack { stackName: 'Test-Stack-A', template: { Resources: { TemplateName: 'Test-Stack-A' } }, env: 'aws://123456789012/bermuda-triangle-1', - metadata: { - '/Test-Stack-A': [ - { - type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, - data: [{ key: 'Foo', value: 'Bar' }], - }, - ], + properties: { + tags: { Foo: 'Bar' }, }, displayName: 'Test-Stack-A-Display-Name', }; @@ -1754,26 +1749,16 @@ class MockStack { stackName: 'Test-Stack-B', template: { Resources: { TemplateName: 'Test-Stack-B' } }, env: 'aws://123456789012/bermuda-triangle-1', - metadata: { - '/Test-Stack-B': [ - { - type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, - data: [{ key: 'Baz', value: 'Zinga!' }], - }, - ], + properties: { + tags: { Baz: 'Zinga!' }, }, }; public static readonly MOCK_STACK_C: TestStackArtifact = { stackName: 'Test-Stack-C', template: { Resources: { TemplateName: 'Test-Stack-C' } }, env: 'aws://123456789012/bermuda-triangle-1', - metadata: { - '/Test-Stack-C': [ - { - type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, - data: [{ key: 'Baz', value: 'Zinga!' }], - }, - ], + properties: { + tags: { Baz: 'Zinga!' }, }, displayName: 'Test-Stack-A/Test-Stack-C', }; @@ -1781,13 +1766,8 @@ class MockStack { stackName: 'Test-Stack-D', template: { Resources: { TemplateName: 'Test-Stack-D' } }, env: 'aws://123456789012/bermuda-triangle-1', - metadata: { - '/Test-Stack-D': [ - { - type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, - data: [{ key: 'Baz', value: 'Zinga!' }], - }, - ], + properties: { + tags: { Baz: 'Zinga!' }, }, depends: [MockStack.MOCK_STACK_C.stackName], }; @@ -1832,13 +1812,8 @@ class MockStack { notificationArns: ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic'], template: { Resources: { TemplateName: 'Test-Stack-Notification-Arns' } }, env: 'aws://123456789012/bermuda-triangle-1337', - metadata: { - '/Test-Stack-Notification-Arns': [ - { - type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, - data: [{ key: 'Foo', value: 'Bar' }], - }, - ], + properties: { + tags: { Foo: 'Bar' }, }, }; @@ -1847,13 +1822,8 @@ class MockStack { notificationArns: ['arn:1337:123456789012:sns:bad'], template: { Resources: { TemplateName: 'Test-Stack-Bad-Notification-Arns' } }, env: 'aws://123456789012/bermuda-triangle-1337', - metadata: { - '/Test-Stack-Bad-Notification-Arns': [ - { - type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, - data: [{ key: 'Foo', value: 'Bar' }], - }, - ], + properties: { + tags: { Foo: 'Bar' }, }, }; } From ba47870c311460c30d6ec13b5ba49501133f94f8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:19:38 +0000 Subject: [PATCH 14/14] chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- packages/aws-cdk/lib/cli/parse-command-line-arguments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts index b4b39acd2..1f8e7377b 100644 --- a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts @@ -3,8 +3,8 @@ // Do not edit by hand; all changes will be overwritten at build time from the config file. // ------------------------------------------------------------------------------------------- /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports */ -import * as helpers from './util/yargs-helpers'; import { Argv } from 'yargs'; +import * as helpers from './util/yargs-helpers'; // @ts-ignore TS6133 export function parseCommandLineArguments(args: Array): any {