diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/notices/filter.ts b/packages/@aws-cdk/toolkit-lib/lib/api/notices/filter.ts index 3c7239ea7..976c1c82e 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/notices/filter.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/notices/filter.ts @@ -12,6 +12,17 @@ function normalizeComponents(xs: Array): Component[][] return xs.map(x => Array.isArray(x) ? x : [x]); } +/** + * Returns the DNF components for a notice. + * Uses componentsV2 when schemaVersion is '2', otherwise falls back to components. + */ +function dnfComponents(notice: Notice): Component[][] { + if (notice.schemaVersion === '2' && notice.componentsV2) { + return normalizeComponents(notice.componentsV2); + } + return normalizeComponents(notice.components); +} + function renderComponent(c: Component): string { if (c.name.startsWith('language:')) { return `${languageDisplayName(c.name.slice('language:'.length))} apps`; @@ -142,7 +153,7 @@ export class NoticesFilter { */ private findForNamedComponents(data: Notice[], actualComponents: ActualComponent[]): FilteredNotice[] { return data.flatMap(notice => { - const ors = this.resolveAliases(normalizeComponents(notice.components)); + const ors = this.resolveAliases(dnfComponents(notice)); // Find the first set of the disjunctions of which all components match against the actual components. // Return the actual components we found so that we can inject their dynamic values. A single filter @@ -255,7 +266,7 @@ export class FilteredNotice { } public format(): string { - const componentsValue = normalizeComponents(this.notice.components).map(renderConjunction).join(', '); + const componentsValue = dnfComponents(this.notice).map(renderConjunction).join(', '); return this.resolveDynamicValues([ `${this.notice.issueNumber}\t${this.notice.title}`, this.formatOverview(), diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/notices/types.ts b/packages/@aws-cdk/toolkit-lib/lib/api/notices/types.ts index e406e84c5..86845a109 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/notices/types.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/notices/types.ts @@ -14,17 +14,20 @@ export interface Notice { issueNumber: number; overview: string; /** - * A set of affected components + * A flat list of affected components, evaluated as an OR. * - * The canonical form of a list of components is in Disjunctive Normal Form - * (i.e., an OR of ANDs). This is the form when the list of components is a - * doubly nested array: the notice matches if all components of at least one - * of the top-level array matches. + * The notice matches if any single component matches. + */ + components: Array; + /** + * A list of affected components in Disjunctive Normal Form (OR of ANDs). + * + * The outer array is an OR, the inner arrays are ANDs. The notice matches + * if all components of at least one inner array match. * - * If the `components` is a single-level array, it is evaluated as an OR; it - * matches if any of the components matches. + * Only available when `schemaVersion` is `'2'`. */ - components: Array; + componentsV2?: Array; schemaVersion: string; severity?: string; } diff --git a/packages/@aws-cdk/toolkit-lib/test/api/notices.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/notices.test.ts index 46f4373dd..5b9e7ccad 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/notices.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/notices.test.ts @@ -552,8 +552,9 @@ describe(NoticesFilter, () => { title: 'combined', overview: 'combined issue', issueNumber: 1, - schemaVersion: '1', - components: [[ + schemaVersion: '2', + components: [], + componentsV2: [[ { name: 'language:typescript', version: '*' }, { name: 'cli', version: '<=1.0.0' }, ]], @@ -591,8 +592,9 @@ describe(NoticesFilter, () => { title: 'match', overview: 'match', issueNumber: 1, - schemaVersion: '1', - components: components.map((ands) => ands.map(parseTestComponent)), + schemaVersion: '2', + components: [], + componentsV2: components.map((ands) => ands.map(parseTestComponent)), }, ] satisfies Notice[], cliVersion, @@ -603,6 +605,76 @@ describe(NoticesFilter, () => { // THEN expect((await filtered).map((f) => f.notice.title)).toEqual(shouldMatch ? ['match'] : []); }); + + test('schemaVersion 1 ignores componentsV2', async () => { + const outDir = path.join(fixtures, 'built-with-2_12_0'); + + const filtered = await noticesFilter.filter({ + data: [ + { + title: 'v1 notice', + overview: 'overview', + issueNumber: 1, + schemaVersion: '1', + components: [{ name: 'cli', version: '>=999.0.0' }], + componentsV2: [{ name: 'cli', version: '<=1.0.0' }], + }, + ] satisfies Notice[], + cliVersion: '1.0.0', + outDir, + bootstrappedEnvironments: [], + }); + + // Should NOT match because schemaVersion 1 uses components (>=999.0.0), not componentsV2 + expect(filtered.map((f) => f.notice.title)).toEqual([]); + }); + + test('schemaVersion 2 falls back to components when componentsV2 is absent', async () => { + const outDir = path.join(fixtures, 'built-with-2_12_0'); + + const filtered = await noticesFilter.filter({ + data: [ + { + title: 'v2 fallback', + overview: 'overview', + issueNumber: 1, + schemaVersion: '2', + components: [{ name: 'cli', version: '<=1.0.0' }], + }, + ] satisfies Notice[], + cliVersion: '1.0.0', + outDir, + bootstrappedEnvironments: [], + }); + + expect(filtered.map((f) => f.notice.title)).toEqual(['v2 fallback']); + }); + + test('schemaVersion 2 uses componentsV2 for DNF matching', async () => { + const outDir = path.join(fixtures, 'built-with-2_12_0'); + + const filtered = await noticesFilter.filter({ + data: [ + { + title: 'v2 dnf', + overview: 'overview', + issueNumber: 1, + schemaVersion: '2', + components: [{ name: 'cli', version: '>=999.0.0' }], + componentsV2: [[ + { name: 'cli', version: '<=1.0.0' }, + { name: 'node', version: '>=14.x' }, + ]], + }, + ] satisfies Notice[], + cliVersion: '1.0.0', + outDir, + bootstrappedEnvironments: [], + }); + + // Should match via componentsV2 (AND of cli + node), ignoring components + expect(filtered.map((f) => f.notice.title)).toEqual(['v2 dnf']); + }); }); });