From 51c27369a1b11d8245ceee3dfa42fb6e2700f940 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 13:22:48 +0200 Subject: [PATCH 01/17] fix: validation report handling is inconsistent Validation report handling used to depend on a lot of factors: - Whether the CDK app had a feature flag set to include annotations into the validation report or not. - Whether the command was `cdk validate` or a different command. The main goal of this PR is to unify the code paths: all validation problems reported via either the annotations or via the validation report file are call combined into a single report that is always printed the same. Mechanically, the CLI will add the annotations to the report if the CDK app hasn't already done so, then always print the validation report (instead of annotations) during app synth validation. Also has the following changes: - Synthesize with debugging enabled for `cdk validate`. - Always tell the app that the CLI will handle the validation report (so the app doesn't need to print and exit). - Make the same changes to the report formatter as we did in https://github.com/aws/aws-cdk/pull/38166. --- .../cloud-assembly-api/lib/metadata.ts | 3 + .../toolkit-lib/lib/actions/validate/index.ts | 2 + .../lib/api/cloud-assembly/environment.ts | 4 + .../api/cloud-assembly/stack-collection.ts | 2 + .../private/collect-annotation-report.ts | 96 ++++++++++++ .../lib/toolkit/private/validation-report.ts | 139 ++++++++++++++++++ .../toolkit-lib/lib/toolkit/toolkit.ts | 115 ++++----------- .../@aws-cdk/toolkit-lib/lib/toolkit/types.ts | 2 + packages/@aws-cdk/toolkit-lib/package.json | 2 +- .../toolkit-lib/test/actions/validate.test.ts | 2 - packages/aws-cdk/lib/api-private.ts | 1 + packages/aws-cdk/lib/cli/cdk-toolkit.ts | 41 ++---- packages/aws-cdk/lib/cli/cli.ts | 5 - .../aws-cdk/lib/cxapp/cloud-executable.ts | 12 ++ packages/aws-cdk/package.json | 2 +- yarn.lock | 37 ++--- 16 files changed, 312 insertions(+), 153 deletions(-) create mode 100644 packages/@aws-cdk/toolkit-lib/lib/toolkit/private/collect-annotation-report.ts create mode 100644 packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/metadata.ts b/packages/@aws-cdk/cloud-assembly-api/lib/metadata.ts index bed769c23..d919db1ac 100644 --- a/packages/@aws-cdk/cloud-assembly-api/lib/metadata.ts +++ b/packages/@aws-cdk/cloud-assembly-api/lib/metadata.ts @@ -25,6 +25,9 @@ export type StackMetadata = { [path: string]: cxschema.MetadataEntry[] }; export interface SynthesisMessage { readonly level: SynthesisMessageLevel; + /** + * The construct path for the construct that emitted this message. + */ readonly id: string; readonly entry: cxschema.MetadataEntry; } diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/validate/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/validate/index.ts index ee4f3a09d..dc7f88111 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/validate/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/validate/index.ts @@ -32,6 +32,8 @@ export interface ValidateResult { /** * The title of the validation report + * + * @deprecated This field is never populated */ readonly title?: string; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/environment.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/environment.ts index 40daa9aab..a69daeec5 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/environment.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/environment.ts @@ -74,6 +74,10 @@ export function contextFromSettings( const bundlingStacks = settings.get(['bundlingStacks']) ?? ['**']; context[cxapi.BUNDLING_STACKS] = bundlingStacks; + // We unconditionally tell the CDK app that the toolkit/CLI will handle validation reports. + // The app never has to exit with an error code because of it. + context[cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT] = true; + return context; } 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 cfc4fe376..e90942470 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 @@ -94,6 +94,8 @@ export class StackCollection { /** * Extracts 'aws:cdk:warning|info|error' metadata entries from the stack synthesis + * + * @deprecated The formatting of this function is lackluster. Use `throwIfValidationFailures()` instead. */ public async validateMetadata( failAt: 'warn' | 'error' | 'none' = 'error', diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/collect-annotation-report.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/collect-annotation-report.ts new file mode 100644 index 000000000..c68bc1344 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/collect-annotation-report.ts @@ -0,0 +1,96 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cloud-assembly-api'; +import { StackCollection } from '../../api/cloud-assembly/stack-collection'; +import { PluginReportJson } from '@aws-cdk/cloud-assembly-schema'; + +const ANNOTATION_PLUGIN_NAME = 'Construct Annotations'; + +/** + * Collect annotation metadata (warnings and errors) from the construct tree + * and convert them into a NamedValidationPluginReport that can be merged + * into the same report pipeline as plugin violations. + * + * Effectively the same as what happens here: + * + */ +export function collectAnnotationReport(stacks: StackCollection): PluginReportJson { + // The return type requires that we combine violations by rule, so we have to group them first here. + const ruleMap = new Map(); + + for (const stack of stacks.stackArtifacts) { + for (const entry of stack.messages) { + let severity: cxschema.PolicyViolationSeverity | undefined; + + switch (entry.level) { + case cxapi.SynthesisMessageLevel.WARNING: + severity = 'warning'; + break; + case cxapi.SynthesisMessageLevel.ERROR: + severity = 'error'; + break; + case cxapi.SynthesisMessageLevel.INFO: + severity = 'info'; + break; + } + + const { message, ruleName } = splitDescriptionAndId(String(entry.entry.data)); + const ruleKey = `${ruleName}|${severity}|${message}`; + let violation = ruleMap.get(ruleKey); + if (!violation) { + violation = { + ruleName: ruleName ?? `${severity}-annotation`, + description: message, + severity, + violatingConstructs: [], + ruleMetadata: { + 'cdk:annotation': 'true', + }, + }; + ruleMap.set(ruleKey, violation); + } + + violation.violatingConstructs.push({ + constructPath: entry.id, + + // TODO: see if this information can be obtained from tree.json + // cloudFormationResource + // constructFqn: + // libraryVersion + + // TODO: see if we can get this from metadata stack traces. We may need to re-enable them for + // annotations in the core library. Otherwise we should probably get a stack trace to the resource itself. + // stackTraces + }); + } + } + + const violations = Array.from(ruleMap.values()); + const hasErrors = violations.some(v => v.severity === 'error'); + return { + pluginName: ANNOTATION_PLUGIN_NAME, + conclusion: hasErrors ? 'failure' : 'success', + violations, + }; +} + +/** + * Annotations have IDs in two places: + * + * - Warnings have `[ack:]` in the message. + * - Errors have `(::)` in the message. + * + * Separate the rule name from the rest of the description. + */ +function splitDescriptionAndId(message: string): { message: string; ruleName?: string } { + const ackMatch = message.match(/\[ack: ([^\]]+)\]/); + if (ackMatch) { + return { message: message.replace(ackMatch[0], '').trim(), ruleName: ackMatch[1] }; + } + + const idMatch = message.match(/\(([^()]+::[^()]+)\)$/); + if (idMatch) { + return { message: message.replace(idMatch[0], '').trim(), ruleName: idMatch[1] }; + } + + return { message }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts new file mode 100644 index 000000000..6779e6b0e --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts @@ -0,0 +1,139 @@ +import * as fs from 'fs-extra'; +import { Manifest, PluginReportJson, PolicyValidationReportConclusion } from "@aws-cdk/cloud-assembly-schema"; +import * as path from "path"; +import { StackCollection } from "../../api/cloud-assembly/stack-collection"; +import { collectAnnotationReport } from "./collect-annotation-report"; +import { MinimumSeverity } from '../types'; +import { IoHelper } from '../../api/io/private/io-helper'; +import { ValidateResult } from '../../actions/validate'; +import { hostMessageFromValidation } from '../../api/validate/validate-formatting'; +import { AssemblyError } from '../toolkit-error'; + +const VALIDATION_REPORT_FILE = 'validation-report.json'; + +/** + * The name of the plugin that emits construct annotations into the validation report. + * + * @see https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/core/lib/private/annotation-plugin.ts + */ +const CONSTRUCT_ANNOTATIONS_PLUGINNAME = 'Construct Annotations'; + +interface AssemblyLike { + readonly directory: string; +} + +/** + * Return a validation report that contains the validation report that the CDK app has written, as well as any Construct Metadata annotations in the manifest. + * + * This function takes into account the CDK app can already have written the + * construct annotations into the validation report, or not, depending on the + * setting of a deprecated feature flag. If the annotations are already in the report, + * they are not copied. + * + * Afterwards, the list of violations is filtered to only include those that are relevant to the stacks selected for validation. + * + * Returns whether an explicit report file was found or not. + */ +export async function obtainUnifiedValidationReport(assembly: AssemblyLike, stacks: StackCollection): Promise { + const ret: PluginReportJson[] = []; + + const reportPath = path.join(assembly.directory, VALIDATION_REPORT_FILE); + if (await fs.pathExists(reportPath)) { + const selectedStackIds = new Set(stacks.hierarchicalIds); + const report = Manifest.loadValidationReport(reportPath); + + // Filter the report to only include violations for the selected stacks + const filteredReports = filterReportsByStacks(report.pluginReports, selectedStackIds); + ret.push(...filteredReports); + } + + const alreadyHasAnnotations = ret.some((r) => r.pluginName === CONSTRUCT_ANNOTATIONS_PLUGINNAME); + if (!alreadyHasAnnotations) { + const annotationReport = collectAnnotationReport(stacks); + if (annotationReport.violations.length > 0 || annotationReport.conclusion === 'failure') { + ret.push(annotationReport); + } + } + + // Remove all inconsequential reports + return ret; +} + +/** + * Return a success/failure conclusion from the given report + */ +export function combineConclusions(reports: PluginReportJson[]): PolicyValidationReportConclusion { + const reportHasFailures = reports.some((r) => r.conclusion === 'failure'); + return reportHasFailures ? 'failure' : 'success'; +} + +/** + * For operations that are NOT `cdk validate`, read the validation report and produce a failure if validation failed. + * + * Validation failed if there are any plugin reports with a failure conclusion, or if there are any warnings and the assembly is in strict mode. + */ +export async function throwIfValidationFailures(assembly: AssemblyLike, stacks: StackCollection, failAt: MinimumSeverity, ioHelper: IoHelper): Promise { + const pluginReports = await obtainUnifiedValidationReport(assembly, stacks); + if (pluginReports.length === 0) { + return; + } + + const conclusion = combineConclusions(pluginReports); + const result: ValidateResult = { conclusion, pluginReports }; + await ioHelper.notify(hostMessageFromValidation(result)); + + + switch (failAt) { + case 'error': + if (conclusion === 'failure') { + const error = AssemblyError.withStacks('Found errors', stacks.stackArtifacts); + error.attachSynthesisErrorCode('AnnotationErrors'); + throw error; + } + break; + case 'warn': + // if we're failing at 'warn', then both warnings and errors cause failure, so the initial conclusion is correct + if (conclusion === 'failure' || hasWarnings(pluginReports)) { + const error = AssemblyError.withStacks('Found warnings (--strict mode)', stacks.stackArtifacts); + error.attachSynthesisErrorCode('StrictAnnotationWarnings'); + throw error; + } + + break; + case 'none': + // if we're not failing at all, then the conclusion is always success + break; + } +} + +function hasWarnings(reports: PluginReportJson[]): boolean { + return reports.some((r) => r.violations.some((v) => v.severity === 'warning')); +} + +/** + * Remove violations that aren't in onde of the given stacks + */ +function filterReportsByStacks(reports: PluginReportJson[], selectedStackIds: Set): PluginReportJson[] { + return reports.map((report) => { + const filteredViolations = report.violations.filter((violation) => { + if (violation.violatingConstructs.length === 0) return true; + return violation.violatingConstructs.some((c) => + selectedStackIds.has(c.constructPath?.split('/')[0] ?? ''), + ); + }).map((violation) => { + if (violation.violatingConstructs.length === 0) return violation; + return { + ...violation, + violatingConstructs: violation.violatingConstructs.filter((c) => + selectedStackIds.has(c.constructPath?.split('/')[0] ?? ''), + ), + }; + }); + + return { + ...report, + violations: filteredViolations, + conclusion: filteredViolations.length > 0 ? report.conclusion : ('success' as const), + }; + }); +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index f48bc5dc6..c389d9c6a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -2,8 +2,8 @@ import '../private/dispose-polyfill'; import { randomUUID } from 'node:crypto'; import * as path from 'node:path'; import * as cxapi from '@aws-cdk/cloud-assembly-api'; -import type { FeatureFlagReportProperties, PolicyValidationReportConclusion, PluginReportJson } from '@aws-cdk/cloud-assembly-schema'; -import { ArtifactType, Manifest } from '@aws-cdk/cloud-assembly-schema'; +import type { FeatureFlagReportProperties, PluginReportJson } from '@aws-cdk/cloud-assembly-schema'; +import { ArtifactType } from '@aws-cdk/cloud-assembly-schema'; import type { TemplateDiff } from '@aws-cdk/cloudformation-diff'; import * as chalk from 'chalk'; import * as chokidar from 'chokidar'; @@ -28,7 +28,7 @@ import { NonInteractiveIoHost } from './non-interactive-io-host'; import type { ToolkitServices } from './private'; import { assemblyFromSource } from './private'; import { ToolkitError } from './toolkit-error'; -import type { DeployResult, DestroyResult, FeatureFlag, RollbackResult } from './types'; +import type { DeployResult, DestroyResult, FeatureFlag, MinimumSeverity, RollbackResult } from './types'; import type { BootstrapEnvironments, BootstrapOptions, @@ -86,7 +86,7 @@ import { CloudFormationStackDiagnoser } from '../api/diagnosing/stack-diagnoser' import { DiffFormatter } from '../api/diff'; import { detectStackDrift } from '../api/drift'; import { DriftFormatter } from '../api/drift/drift-formatter'; -import type { IIoHost, IoMessageLevel, ToolkitAction } from '../api/io'; +import type { IIoHost, ToolkitAction } from '../api/io'; import type { ElapsedTime, IoHelper } from '../api/io/private'; import { asIoHelper, IO, SPAN, withoutColor, withoutEmojis, withTrimmedWhitespace } from '../api/io/private'; import { CloudWatchLogEventMonitor, findCloudWatchLogGroups } from '../api/logs-monitor'; @@ -116,8 +116,7 @@ import { formatErrorMessage, formatTime, obscureTemplate, serializeStructure, va import { pLimit } from '../util/concurrency'; import { createIgnoreMatcher } from '../util/glob-matcher'; import { promiseWithResolvers } from '../util/promises'; - -const VALIDATION_REPORT_FILE = 'validation-report.json'; +import { combineConclusions, obtainUnifiedValidationReport, throwIfValidationFailures } from './private/validation-report'; export interface ToolkitOptions { /** @@ -155,11 +154,11 @@ export interface ToolkitOptions { readonly toolkitStackName?: string; /** - * Fail Cloud Assemblies + * Fail Cloud Assembly operations with an error if there are validation errors in the assembly with at least the indicated severity. * * @default "error" */ - readonly assemblyFailureAt?: 'error' | 'warn' | 'none'; + readonly assemblyFailureAt?: MinimumSeverity; /** * The plugin host to use for loading and querying plugins @@ -214,9 +213,12 @@ export class Toolkit extends CloudAssemblySourceBuilder { private readonly unstableFeatures: Array; + private readonly assemblyFailureAt: MinimumSeverity; + public constructor(private readonly props: ToolkitOptions = {}) { super(); this.toolkitStackName = props.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME; + this.assemblyFailureAt = props.assemblyFailureAt ?? 'error'; this.pluginHost = props.pluginHost ?? new PluginHost(); @@ -345,7 +347,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { await using assembly = new AsyncDisposableBox(await synthAndMeasure(ioHelper, cx, stacksOpt(options))); const stacks = await assembly.value.selectStacksV2(stacksOpt(options)); const autoValidateStacks = options.validateStacks ? [assembly.value.selectStacksForValidation()] : []; - await this.validateStacksMetadata(stacks.concat(...autoValidateStacks), ioHelper); + await throwIfValidationFailures(assembly.value, stacks.concat(...autoValidateStacks), this.assemblyFailureAt, ioHelper); // if we have a single stack, print it to STDOUT const message = `Successfully synthesized to ${chalk.blue(path.resolve(stacks.assembly.directory))}`; @@ -526,7 +528,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { await using assembly = await synthAndMeasure(ioHelper, cx, selectStacks); const stackCollection = await assembly.selectStacksV2(selectStacks); - await this.validateStacksMetadata(stackCollection, ioHelper); + await throwIfValidationFailures(assembly, stackCollection, this.assemblyFailureAt, ioHelper); if (stackCollection.stackCount === 0) { await ioHelper.notify(IO.CDK_TOOLKIT_E5001.msg('No stacks selected')); @@ -667,25 +669,12 @@ export class Toolkit extends CloudAssemblySourceBuilder { public async validate(cx: ICloudAssemblySource, options: ValidateOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'validate'); const selectStacks = stacksOpt(options); + await using assembly = await synthAndMeasure(ioHelper, cx, selectStacks); - const pluginReports: PluginReportJson[] = []; - let title: string | undefined; const stacks = await assembly.selectStacksV2(selectStacks); - const selectedStackIds = new Set(stacks.hierarchicalIds); - - // Offline validation: read the policy validation report from the cloud assembly - const reportPath = path.join(assembly.directory, VALIDATION_REPORT_FILE); - if (await fs.pathExists(reportPath)) { - const report = Manifest.loadValidationReport(reportPath); - title = report.title; - - // Filter the report to only include violations for the selected stacks - const filteredReports = filterReportsByStacks(report.pluginReports, selectedStackIds); - pluginReports.push(...filteredReports); - } else if (options.online === false) { - await ioHelper.notify(IO.CDK_TOOLKIT_I9601.msg('No validation plugins configured. Add a plugin to your CDK app to enable validation.')); - } + + const reports = await obtainUnifiedValidationReport(assembly, stacks); // Online validation: submit templates to CloudFormation for early validation if (options.online ?? true) { @@ -693,25 +682,21 @@ export class Toolkit extends CloudAssemblySourceBuilder { const onlineReport = await this.validateOnline(ioHelper, stacks, deployments); if (onlineReport) { - pluginReports.push(onlineReport); + reports.push(onlineReport); } } - if (pluginReports.length === 0) { - await ioHelper.notify(IO.CDK_TOOLKIT_I9601.msg('No validation plugins configured. Add a plugin to your CDK app to enable validation.')); - } - - const conclusion: PolicyValidationReportConclusion = pluginReports.some( - (pr) => pr.conclusion === 'failure', - ) ? 'failure' : 'success'; - const result: ValidateResult = { - conclusion, - title, - pluginReports, + conclusion: combineConclusions(reports), + title: undefined, + pluginReports: reports, }; - await ioHelper.notify(hostMessageFromValidation(result)); + if (reports.length === 0) { + await ioHelper.notify(IO.CDK_TOOLKIT_I9601.msg('No validation plugins configured. Add a plugin to your CDK app to enable validation.')); + } else { + await ioHelper.notify(hostMessageFromValidation(result)); + } return result; } @@ -786,7 +771,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { const ioHelper = asIoHelper(this.ioHost, action); const selectStacks = stacksOpt(options); const stackCollection = await assembly.selectStacksV2(selectStacks); - await this.validateStacksMetadata(stackCollection, ioHelper); + await throwIfValidationFailures(assembly, stackCollection, this.assemblyFailureAt, ioHelper); const ret: DeployResult = { stacks: [], @@ -1302,7 +1287,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { const ioHelper = asIoHelper(this.ioHost, action); const stacks = await assembly.selectStacksV2(selectStacks); - await this.validateStacksMetadata(stacks, ioHelper); + await throwIfValidationFailures(assembly, stacks, this.assemblyFailureAt, ioHelper); const ret: RollbackResult = { stacks: [], @@ -1669,26 +1654,6 @@ export class Toolkit extends CloudAssemblySourceBuilder { } } - /** - * Validate the stacks for errors and warnings according to the CLI's current settings - */ - private async validateStacksMetadata(stacks: StackCollection, ioHost: IoHelper) { - const builder = (level: IoMessageLevel) => { - switch (level) { - case 'error': - return IO.CDK_ASSEMBLY_E9999; - case 'warn': - return IO.CDK_ASSEMBLY_W9999; - default: - return IO.CDK_ASSEMBLY_I9999; - } - }; - await stacks.validateMetadata( - this.props.assemblyFailureAt, - async (level, msg) => ioHost.notify(builder(level).msg(`[${level} at ${msg.id}] ${msg.entry.data}`, msg)), - ); - } - /** * Create a deployments class */ @@ -1824,6 +1789,9 @@ export class Toolkit extends CloudAssemblySourceBuilder { /** * Centralize the default stack selection logic in a single place + * + * Defaults to all stacks in the assembly (including nested assemblies) if no explicit + * selector is given. */ function stacksOpt(o: { stacks?: StackSelector }): StackSelector { return o.stacks ?? ALL_STACKS; @@ -1854,28 +1822,3 @@ async function synthAndMeasure( function zeroTime(): ElapsedTime { return { asMs: 0, asSec: 0 }; } - -function filterReportsByStacks(reports: PluginReportJson[], selectedStackIds: Set): PluginReportJson[] { - return reports.map((report) => { - const filteredViolations = report.violations.filter((violation) => { - if (violation.violatingConstructs.length === 0) return true; - return violation.violatingConstructs.some((c) => - selectedStackIds.has(c.constructPath?.split('/')[0] ?? ''), - ); - }).map((violation) => { - if (violation.violatingConstructs.length === 0) return violation; - return { - ...violation, - violatingConstructs: violation.violatingConstructs.filter((c) => - selectedStackIds.has(c.constructPath?.split('/')[0] ?? ''), - ), - }; - }); - - return { - ...report, - violations: filteredViolations, - conclusion: filteredViolations.length > 0 ? report.conclusion : ('success' as const), - }; - }); -} diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts index 7130805a8..d4e0cf701 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/types.ts @@ -160,3 +160,5 @@ export interface FeatureFlag { readonly explanation?: string; readonly unconfiguredBehavesLike?: { v2?: any }; } + +export type MinimumSeverity = 'error' | 'warn' | 'none'; diff --git a/packages/@aws-cdk/toolkit-lib/package.json b/packages/@aws-cdk/toolkit-lib/package.json index 4ff8e7eb4..7dda03763 100644 --- a/packages/@aws-cdk/toolkit-lib/package.json +++ b/packages/@aws-cdk/toolkit-lib/package.json @@ -88,7 +88,7 @@ "@aws-cdk/cloud-assembly-api": "^0.0.0", "@aws-cdk/cloud-assembly-schema": "^0.0.0", "@aws-cdk/cloudformation-diff": "^0.0.0", - "@aws-cdk/cx-api": "^2", + "@aws-cdk/cx-api": "^2.260.0", "@aws-sdk/client-appsync": "^3", "@aws-sdk/client-bedrock-agentcore-control": "^3", "@aws-sdk/client-cloudcontrol": "^3", diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/validate.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/validate.test.ts index c94c5bc40..46d1b2435 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/validate.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/validate.test.ts @@ -22,7 +22,6 @@ describe('validate', () => { const result = await toolkit.validate(cx, { online: false }); expect(result.conclusion).toBe('failure'); - expect(result.title).toBe('Validation Report'); expect(result.pluginReports).toHaveLength(2); expect(result.pluginReports[0].pluginName).toBe('TestPlugin'); expect(result.pluginReports[0].conclusion).toBe('failure'); @@ -121,7 +120,6 @@ describe('validate', () => { expect(msg).toBeDefined(); expect(msg!.data).toMatchObject({ conclusion: 'failure', - title: 'Validation Report', pluginReports: expect.arrayContaining([ expect.objectContaining({ pluginName: 'TestPlugin', diff --git a/packages/aws-cdk/lib/api-private.ts b/packages/aws-cdk/lib/api-private.ts index 102510da0..c20411d15 100644 --- a/packages/aws-cdk/lib/api-private.ts +++ b/packages/aws-cdk/lib/api-private.ts @@ -8,3 +8,4 @@ export * from '../../@aws-cdk/toolkit-lib/lib/api/tags/private'; export * from '../../@aws-cdk/toolkit-lib/lib/private/activity-printer'; export * from '../../@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/borrowed-assembly'; export * from '../../@aws-cdk/toolkit-lib/lib/toolkit/private/count-assembly-results'; +export { throwIfValidationFailures } from '../../@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report'; diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index e00ceb427..8f35662f3 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -14,7 +14,7 @@ import { CliIoHost } from './io-host'; import type { Configuration } from './user-configuration'; import { PROJECT_CONFIG } from './user-configuration'; import type { ActionLessRequest, IMessageSpan, IoHelper } from '../../lib/api-private'; -import { asIoHelper, cfnApi, createIgnoreMatcher, IO, tagsForStack } from '../../lib/api-private'; +import { asIoHelper, cfnApi, createIgnoreMatcher, IO, tagsForStack, throwIfValidationFailures } from '../../lib/api-private'; import type { AssetBuildNode, AssetPublishNode, Concurrency, MarkerNode, StackNode, WorkGraph, WorkGraphActions } from '../api'; import { buildDestroyWorkGraph, @@ -585,6 +585,9 @@ export class CdkToolkit { * Validate synthesized templates against policy rules */ public async validate(options: ValidateOptions): Promise { + // Implicitly switch 'debug' mode to true; more stack traces = more useful. + this.props.cloudExecutable.switchOnDebugging(); + const result = await this.toolkit.validate(this.props.cloudExecutable, options); return result.conclusion === 'failure' ? 1 : 0; } @@ -593,6 +596,9 @@ export class CdkToolkit { * Diagnose errors */ public async diagnose(options: DiagnoseOptions): Promise { + // Implicitly switch 'debug' mode to true; more stack traces = more useful. + this.props.cloudExecutable.switchOnDebugging(); + const results = await this.toolkit.diagnose(this.props.cloudExecutable, options); if (results.stacks.some(s => s.result.type !== 'no-problem')) { @@ -1270,7 +1276,7 @@ export class CdkToolkit { }); this.validateStacksSelected(stacks, selector.patterns); - await this.validateStacks(stacks); + await this.validateStacks(assembly, stacks); return stacks; } @@ -1296,7 +1302,7 @@ export class CdkToolkit { : new StackCollection(assembly, []); this.validateStacksSelected(selectedForDiff.concat(autoValidateStacks), stackNames); - await this.validateStacks(selectedForDiff.concat(autoValidateStacks)); + await this.validateStacks(assembly, selectedForDiff.concat(autoValidateStacks)); return selectedForDiff; } @@ -1316,9 +1322,9 @@ export class CdkToolkit { /** * Validate the stacks for errors and warnings according to the CLI's current settings */ - private async validateStacks(stacks: StackCollection) { + private async validateStacks(assembly: CloudAssembly, stacks: StackCollection) { const failAt = this.validateMetadataFailAt(); - await stacks.validateMetadata(failAt, stackMetadataLogger(this.ioHost.asIoHelper(), this.props.verbose)); + await throwIfValidationFailures(assembly, stacks, failAt, this.ioHost.asIoHelper()); } private validateMetadataFailAt(): 'warn' | 'error' | 'none' { @@ -2089,31 +2095,6 @@ export async function displayFlagsMessage(ioHost: IoHelper, toolkit: InternalToo } } -/** - * Logger for processing stack metadata - */ -function stackMetadataLogger(ioHelper: IoHelper, verbose?: boolean): (level: 'info' | 'error' | 'warn', msg: cxapi.SynthesisMessage) => Promise { - const makeLogger = (level: string): [logger: (m: string) => void, prefix: string] => { - switch (level) { - case 'error': - return [(m) => ioHelper.defaults.error(m), 'Error']; - case 'warn': - return [(m) => ioHelper.defaults.warn(m), 'Warning']; - default: - return [(m) => ioHelper.defaults.info(m), 'Info']; - } - }; - - return async (level, msg) => { - const [logFn, prefix] = makeLogger(level); - await logFn(`[${prefix} at ${msg.id}] ${msg.entry.data}`); - - if (verbose && msg.entry.trace) { - logFn(` ${msg.entry.trace.join('\n ')}`); - } - }; -} - interface WorkGraphDeploymentActionsOptions { readonly roleArn?: string; readonly force?: boolean; diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 4db7aa019..6966f1420 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -463,11 +463,6 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise { - settings.set(['debug'], true); - }); - return cli.diagnose({ stacks: specificStacksOrAllRecursively(args.STACKS), concurrency: args.concurrency, diff --git a/packages/aws-cdk/lib/cxapp/cloud-executable.ts b/packages/aws-cdk/lib/cxapp/cloud-executable.ts index 489e10929..d748c53c6 100644 --- a/packages/aws-cdk/lib/cxapp/cloud-executable.ts +++ b/packages/aws-cdk/lib/cxapp/cloud-executable.ts @@ -65,6 +65,18 @@ export class CloudExecutable implements ICloudAssemblySource { return !!this.props.configuration.settings.get(['app']); } + /** + * Switch on debugging for the cloud executable. + * + * This will cause it to log more stack traces and other information that will make error + * reports more useful (at the cost of increased execution time). + */ + public switchOnDebugging() { + this.props.configuration.settings.temporarilyMutable((settings) => { + settings.set(['debug'], true); + }); + } + /** * Synthesize a set of stacks. * diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 738f792f7..68f16cfd6 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -87,7 +87,7 @@ "@aws-cdk/cloud-assembly-api": "^0.0.0", "@aws-cdk/cloud-assembly-schema": "^0.0.0", "@aws-cdk/cloudformation-diff": "^0.0.0", - "@aws-cdk/cx-api": "^2", + "@aws-cdk/cx-api": "^2.260.0", "@aws-cdk/private-tools": "^0.0.0", "@aws-cdk/toolkit-lib": "^0.0.0", "@aws-sdk/client-appsync": "^3", diff --git a/yarn.lock b/yarn.lock index c59aa11e5..3c4c1b95e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -223,18 +223,6 @@ __metadata: languageName: unknown linkType: soft -"@aws-cdk/cloud-assembly-api@npm:^2.2.2": - version: 2.2.2 - resolution: "@aws-cdk/cloud-assembly-api@npm:2.2.2" - dependencies: - jsonschema: "npm:~1.4.1" - semver: "npm:^7.7.4" - peerDependencies: - "@aws-cdk/cloud-assembly-schema": ">=53.15.0" - checksum: 10c0/cc5e7edd13d9d7e4ad7878c16f85a1a15c68e9ec51b71ad4a4c884ef0b3ed28fc25dc0627f0c226d65615cc6011ca02de94401862b650a996f8ccdff651e4d44 - languageName: node - linkType: hard - "@aws-cdk/cloud-assembly-api@npm:^2.2.5": version: 2.2.5 resolution: "@aws-cdk/cloud-assembly-api@npm:2.2.5" @@ -338,15 +326,15 @@ __metadata: languageName: unknown linkType: soft -"@aws-cdk/cx-api@npm:^2": - version: 2.251.0 - resolution: "@aws-cdk/cx-api@npm:2.251.0" +"@aws-cdk/cx-api@npm:^2.260.0": + version: 2.260.0 + resolution: "@aws-cdk/cx-api@npm:2.260.0" dependencies: - "@aws-cdk/cloud-assembly-api": "npm:^2.2.2" - semver: "npm:^7.7.4" + "@aws-cdk/cloud-assembly-api": "npm:^2.2.5" + semver: "npm:^7.8.1" peerDependencies: - "@aws-cdk/cloud-assembly-schema": ">=53.18.0" - checksum: 10c0/480af47d19353bb6f5d52e1ea1e8f6179fed7039ff614f05a6c4f57668c054a00332cb5d02ecf39f7847140f41ecae69db594c4afce9749949f9fafdd79684fa + "@aws-cdk/cloud-assembly-schema": ">=53.25.0" + checksum: 10c0/cd4ebf070a8ce066c030fcc526074911c1b29494280cc7c4e3c19640c8d168e2a553f9c3cd2d5c7d03b611bfad7c3b920418aeb6f51b869208360e4a3119d383 languageName: node linkType: hard @@ -465,7 +453,7 @@ __metadata: "@aws-cdk/cloud-assembly-api": "npm:^0.0.0" "@aws-cdk/cloud-assembly-schema": "npm:^0.0.0" "@aws-cdk/cloudformation-diff": "npm:^0.0.0" - "@aws-cdk/cx-api": "npm:^2" + "@aws-cdk/cx-api": "npm:^2.260.0" "@aws-cdk/private-tools": "npm:^0.0.0" "@aws-sdk/client-appsync": "npm:^3" "@aws-sdk/client-bedrock-agentcore-control": "npm:^3" @@ -8104,7 +8092,7 @@ __metadata: "@aws-cdk/cloud-assembly-api": "npm:^0.0.0" "@aws-cdk/cloud-assembly-schema": "npm:^0.0.0" "@aws-cdk/cloudformation-diff": "npm:^0.0.0" - "@aws-cdk/cx-api": "npm:^2" + "@aws-cdk/cx-api": "npm:^2.260.0" "@aws-cdk/private-tools": "npm:^0.0.0" "@aws-cdk/toolkit-lib": "npm:^0.0.0" "@aws-cdk/user-input-gen": "npm:^0.0.0" @@ -13633,13 +13621,6 @@ __metadata: languageName: node linkType: hard -"jsonschema@npm:~1.4.1": - version: 1.4.1 - resolution: "jsonschema@npm:1.4.1" - checksum: 10c0/c3422d3fc7d33ff7234a806ffa909bb6fb5d1cd664bea229c64a1785dc04cbccd5fc76cf547c6ab6dd7881dbcaf3540a6a9f925a5956c61a9cd3e23a3c1796ef - languageName: node - linkType: hard - "jszip@npm:^3.10.1": version: 3.10.1 resolution: "jszip@npm:3.10.1" From 87b0847aa4ac5e21cccd7c26a9e70ca984991c87 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 14:05:04 +0200 Subject: [PATCH 02/17] Formatting --- .../lib/api/validate/validate-formatting.ts | 159 ++++++++++++------ .../private/collect-annotation-report.ts | 9 +- .../lib/toolkit/private/validation-report.ts | 27 +-- packages/@aws-cdk/toolkit-lib/package.json | 2 +- packages/aws-cdk/package.json | 2 +- yarn.lock | 6 +- 6 files changed, 132 insertions(+), 73 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts b/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts index 1d132d919..f6526d82c 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts @@ -1,46 +1,26 @@ import * as path from 'node:path'; -import type { PluginReportJson, ViolatingConstructJson } from '@aws-cdk/cloud-assembly-schema'; +import type { PluginReportJson, PolicyViolationJson, ViolatingConstructJson } from '@aws-cdk/cloud-assembly-schema'; import * as chalk from 'chalk'; import type { ValidateResult } from '../../actions/validate'; import type { ActionLessMessage } from '../io/private'; import { IO } from '../io/private'; -// Matches C0 control chars (except \t and \n), DEL, and CSI (8-bit mode). -// Strips ANSI escape sequences, carriage returns, backspaces, BEL, and -// bidirectional overrides that could spoof terminal output. -const CONTROL_CHARS = /[\x00-\x08\x0B-\x1F\x7F\x9B]/g; -function sanitize(s: string | undefined): string { - return (s ?? '').replace(CONTROL_CHARS, '�'); -} - -interface FlattenedViolation { - readonly severity: string; - readonly description: string; - readonly ruleName: string; - readonly pluginName: string; - readonly construct: ViolatingConstructJson; -} - -const SEVERITY_ORDER: Record = { - fatal: 0, - error: 1, - warning: 2, - info: 3, -}; - -export function hostMessageFromValidation(result: ValidateResult): ActionLessMessage { +export function hostMessageFromValidation(fileRoot: string, result: ValidateResult): ActionLessMessage { // Always emit at info level so the CLI IoHost doesn't wrap the entire output // in a single color. The formatter handles per-severity coloring internally. // Consumers detect failure via the structured `data.conclusion` field or exit code. - return IO.CDK_TOOLKIT_I9600.msg(formatValidateResult(result), result); + return IO.CDK_TOOLKIT_I9600.msg(formatValidateResult(fileRoot, result), result); } -export function formatValidateResult(result: ValidateResult): string { - const violations = flattenViolations(result.pluginReports); +export function formatValidateResult(fileRoot: string, result: ValidateResult): string { + return formatValidationReports(fileRoot, result.pluginReports).join('\n\n') +} - if (violations.length === 0) { - return '\nPolicy validation passed. No problems found.'; - } +export function formatValidationReports(fileRoot: string, reports: PluginReportJson[]): string[] { + const successfullyExecutedPlugins = reports.filter((r) => isPluginFailure(r) === undefined); + const pluginFailures = reports.map(isPluginFailure).filter((e) => e !== undefined); + + const violations = flattenViolations(successfullyExecutedPlugins); violations.sort((a, b) => { const aOrder = SEVERITY_ORDER[a.severity.toLowerCase()] ?? 4; @@ -48,9 +28,10 @@ export function formatValidateResult(result: ValidateResult): string { return aOrder - bOrder; }); - const title = result.title ?? 'Validation Report'; - const blocks = violations.map((v) => formatViolationBlock(v)); - return `\n${title}\n${'-'.repeat(title.length)}\n\n${blocks.join('\n\n')}`; + return [ + ...pluginFailures.map(formatPluginFailure), + ...violations.map((v) => formatViolationBlock(fileRoot, v)), + ]; } function flattenViolations(pluginReports: PluginReportJson[]): FlattenedViolation[] { @@ -82,25 +63,33 @@ function normalizeSeverity(severity: string | undefined): string { return safe.charAt(0).toUpperCase() + safe.slice(1); } -function formatViolationBlock(v: FlattenedViolation): string { +function formatViolationBlock(fileRoot: string, v: FlattenedViolation): string { const lines: string[] = []; - const location = getLeafLocation(v.construct.stackTraces); + const location = sourceLocation(fileRoot, v.construct.stackTraces); if (location) { lines.push(chalk.underline(sanitize(location))); } - const severityColor = getSeverityColor(v.severity); - const description = stripAckTag(sanitize(v.description)); - const severityAndDesc = severityColor(chalk.bold(`${v.severity}: ${description}`)); - lines.push(`${severityAndDesc} ${sanitize(v.pluginName)}`); + lines.push([ + chalk.bold(getSeverityColor(v.severity)(sanitize(v.severity))), + chalk.bold(stripAckTag(sanitize(v.description))), + chalk.grey(`(${sanitize(v.pluginName)})`), + ].join(' ')); - const constructInfo = formatConstructInfo(v.construct); + const constructInfo = formatConstructInfo(fileRoot, v.construct); lines.push(` ${constructInfo}`); - if (v.severity.toLowerCase() !== 'fatal') { + if (v.suggestedFix) { + lines.push(` Suggested fix: ${sanitize(v.suggestedFix).replace(/\n/g, '\n ')}`); + } + + if (isSuppressibleViolation(v)) { const ackId = `${sanitize(v.pluginName)}::${sanitize(v.ruleName)}`.replace(/ /g, '-'); - lines.push(` Acknowledge '${ackId}'`); + lines.push(` ${chalk.grey(`Acknowledge with '${ackId}'`)}`); + } else { + // If not acknowledgeable, we should still show the rule name for reference. + lines.push(` ${chalk.grey(`Rule ${sanitize(v.ruleName)}`)}`); } return lines.join('\n'); @@ -115,19 +104,29 @@ function getSeverityColor(severity: string): (str: string) => string { } } -function formatConstructInfo(construct: ViolatingConstructJson): string { +function formatPluginFailure(f: PluginError): string { + return `${chalk.ansi256(208)('ERROR')} ${sanitize(f.error)}`; +} + +function formatConstructInfo(fileRoot: string, construct: ViolatingConstructJson): string { const parts: string[] = []; const logicalId = sanitize(construct.cloudFormationResource?.logicalId); if (construct.constructPath) { const cPath = sanitize(construct.constructPath); parts.push(logicalId ? `${chalk.bold(cPath)} (${logicalId})` : chalk.bold(cPath)); - } else if (logicalId) { - parts.push(chalk.bold(logicalId)); + } else { + // No construct information, show template path and logical ID + if (construct.cloudFormationResource?.templatePath) { + parts.push(humanFriendlyFilename(fileRoot, sanitize(construct.cloudFormationResource.templatePath))); + } + if (logicalId) { + parts.push(chalk.bold(logicalId)); + } } if (construct.constructFqn) { - parts.push(sanitize(construct.constructFqn)); + parts.push(chalk.grey(sanitize(construct.constructFqn))); } return parts.join(' '); @@ -137,10 +136,18 @@ function stripAckTag(description: string): string { return description.replace(/\s*\[ack:\s*[^\]]+\]\s*/g, '').trim(); } -function getLeafLocation(stackTraces: string[] | undefined): string | undefined { - if (!stackTraces || stackTraces.length === 0) return undefined; - const lastTrace = stackTraces[stackTraces.length - 1]; - const frames = lastTrace.split('\n'); +function sourceLocation(fileRoot: string, stackTraces: string[] | undefined): string | undefined { + for (const trace of stackTraces ?? []) { + const frame = getLeafLocation(trace); + if (frame && frame.fileName) { + return `${humanFriendlyFilename(fileRoot, frame.fileName)}:${frame.sourceLocation}`; + } + } + return undefined; +} + +function getLeafLocation(stackTrace: string) { + const frames = stackTrace.split('\n'); if (frames.length === 0) return undefined; // Find the first frame that's user code (not in node_modules or aws-cdk-lib) @@ -149,5 +156,53 @@ function getLeafLocation(stackTraces: string[] | undefined): string | undefined const match = frame.match(/\((.+)\)$/) || frame.match(/at\s+(.+)$/); const location = match ? match[1] : frame; - return path.isAbsolute(location.split(':')[0]) ? path.relative(process.cwd(), location) : location; + return { fileName: location.split(':')[0], sourceLocation: location.split(':').slice(1).join(':') }; +} + +// Matches C0 control chars (except \t and \n), DEL, and CSI (8-bit mode). +// Strips ANSI escape sequences, carriage returns, backspaces, BEL, and +// bidirectional overrides that could spoof terminal output. +const CONTROL_CHARS = /[\x00-\x08\x0B-\x1F\x7F\x9B]/g; +function sanitize(s: string | undefined): string { + return (s ?? '').replace(CONTROL_CHARS, '�'); +} + +export type FlattenedViolation = + & Pick + & Pick + & { severity: string; construct: ViolatingConstructJson }; + +const SEVERITY_ORDER: Record = { + fatal: 0, + error: 1, + warning: 2, + info: 3, +}; + +export function humanFriendlyFilename(root: string, filename: string): string { + const absPath = filename; + const relPath = path.relative(root, filename); + return relPath.length < absPath.length ? relPath : absPath; +} + +interface PluginError { + readonly error: string; +} + +function isPluginFailure(r: PluginReportJson): PluginError | undefined { + if (r.conclusion === 'success' || r.violations.length > 0 || !r.metadata?.error) { + return undefined; + } + return { error: r.metadata.error }; +} + +/** + * Report whether it is possible to suppress this violation. + * + * Violations that are reported as "fatal", or that have been converted from annotations, cannot be suppressed. + */ +function isSuppressibleViolation(violation: { severity?: string; ruleMetadata?: { [key: string]: string } }): boolean { + const isFatal = violation.severity?.toLowerCase() === 'fatal'; + const isErrorAnnotation = violation.ruleMetadata?.['cdk:annotation'] && violation.severity?.toLowerCase() === 'error'; + return !isFatal && !isErrorAnnotation; } diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/collect-annotation-report.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/collect-annotation-report.ts index c68bc1344..a649de3c2 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/collect-annotation-report.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/collect-annotation-report.ts @@ -1,7 +1,6 @@ -import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cloud-assembly-api'; -import { StackCollection } from '../../api/cloud-assembly/stack-collection'; -import { PluginReportJson } from '@aws-cdk/cloud-assembly-schema'; +import type * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import type { StackCollection } from '../../api/cloud-assembly/stack-collection'; const ANNOTATION_PLUGIN_NAME = 'Construct Annotations'; @@ -13,7 +12,7 @@ const ANNOTATION_PLUGIN_NAME = 'Construct Annotations'; * Effectively the same as what happens here: * */ -export function collectAnnotationReport(stacks: StackCollection): PluginReportJson { +export function collectAnnotationReport(stacks: StackCollection): cxschema.PluginReportJson { // The return type requires that we combine violations by rule, so we have to group them first here. const ruleMap = new Map(); @@ -93,4 +92,4 @@ function splitDescriptionAndId(message: string): { message: string; ruleName?: s } return { message }; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts index 6779e6b0e..a32b6d55a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts @@ -1,13 +1,14 @@ +import * as path from 'path'; +import type { PluginReportJson, PolicyValidationReportConclusion } from '@aws-cdk/cloud-assembly-schema'; +import { Manifest } from '@aws-cdk/cloud-assembly-schema'; import * as fs from 'fs-extra'; -import { Manifest, PluginReportJson, PolicyValidationReportConclusion } from "@aws-cdk/cloud-assembly-schema"; -import * as path from "path"; -import { StackCollection } from "../../api/cloud-assembly/stack-collection"; -import { collectAnnotationReport } from "./collect-annotation-report"; -import { MinimumSeverity } from '../types'; -import { IoHelper } from '../../api/io/private/io-helper'; -import { ValidateResult } from '../../actions/validate'; +import { collectAnnotationReport } from './collect-annotation-report'; +import type { ValidateResult } from '../../actions/validate'; +import type { StackCollection } from '../../api/cloud-assembly/stack-collection'; +import type { IoHelper } from '../../api/io/private/io-helper'; import { hostMessageFromValidation } from '../../api/validate/validate-formatting'; import { AssemblyError } from '../toolkit-error'; +import type { MinimumSeverity } from '../types'; const VALIDATION_REPORT_FILE = 'validation-report.json'; @@ -34,7 +35,7 @@ interface AssemblyLike { * * Returns whether an explicit report file was found or not. */ -export async function obtainUnifiedValidationReport(assembly: AssemblyLike, stacks: StackCollection): Promise { +export async function obtainUnifiedValidationReport(assembly: AssemblyLike, stacks: StackCollection): Promise { const ret: PluginReportJson[] = []; const reportPath = path.join(assembly.directory, VALIDATION_REPORT_FILE); @@ -72,7 +73,12 @@ export function combineConclusions(reports: PluginReportJson[]): PolicyValidatio * * Validation failed if there are any plugin reports with a failure conclusion, or if there are any warnings and the assembly is in strict mode. */ -export async function throwIfValidationFailures(assembly: AssemblyLike, stacks: StackCollection, failAt: MinimumSeverity, ioHelper: IoHelper): Promise { +export async function throwIfValidationFailures( + assembly: AssemblyLike, + stacks: StackCollection, + failAt: MinimumSeverity, + ioHelper: IoHelper, +): Promise { const pluginReports = await obtainUnifiedValidationReport(assembly, stacks); if (pluginReports.length === 0) { return; @@ -82,7 +88,6 @@ export async function throwIfValidationFailures(assembly: AssemblyLike, stacks: const result: ValidateResult = { conclusion, pluginReports }; await ioHelper.notify(hostMessageFromValidation(result)); - switch (failAt) { case 'error': if (conclusion === 'failure') { @@ -136,4 +141,4 @@ function filterReportsByStacks(reports: PluginReportJson[], selectedStackIds: Se conclusion: filteredViolations.length > 0 ? report.conclusion : ('success' as const), }; }); -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/toolkit-lib/package.json b/packages/@aws-cdk/toolkit-lib/package.json index 7dda03763..4ff8e7eb4 100644 --- a/packages/@aws-cdk/toolkit-lib/package.json +++ b/packages/@aws-cdk/toolkit-lib/package.json @@ -88,7 +88,7 @@ "@aws-cdk/cloud-assembly-api": "^0.0.0", "@aws-cdk/cloud-assembly-schema": "^0.0.0", "@aws-cdk/cloudformation-diff": "^0.0.0", - "@aws-cdk/cx-api": "^2.260.0", + "@aws-cdk/cx-api": "^2", "@aws-sdk/client-appsync": "^3", "@aws-sdk/client-bedrock-agentcore-control": "^3", "@aws-sdk/client-cloudcontrol": "^3", diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 68f16cfd6..738f792f7 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -87,7 +87,7 @@ "@aws-cdk/cloud-assembly-api": "^0.0.0", "@aws-cdk/cloud-assembly-schema": "^0.0.0", "@aws-cdk/cloudformation-diff": "^0.0.0", - "@aws-cdk/cx-api": "^2.260.0", + "@aws-cdk/cx-api": "^2", "@aws-cdk/private-tools": "^0.0.0", "@aws-cdk/toolkit-lib": "^0.0.0", "@aws-sdk/client-appsync": "^3", diff --git a/yarn.lock b/yarn.lock index 3c4c1b95e..68b6fbaa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -326,7 +326,7 @@ __metadata: languageName: unknown linkType: soft -"@aws-cdk/cx-api@npm:^2.260.0": +"@aws-cdk/cx-api@npm:^2": version: 2.260.0 resolution: "@aws-cdk/cx-api@npm:2.260.0" dependencies: @@ -453,7 +453,7 @@ __metadata: "@aws-cdk/cloud-assembly-api": "npm:^0.0.0" "@aws-cdk/cloud-assembly-schema": "npm:^0.0.0" "@aws-cdk/cloudformation-diff": "npm:^0.0.0" - "@aws-cdk/cx-api": "npm:^2.260.0" + "@aws-cdk/cx-api": "npm:^2" "@aws-cdk/private-tools": "npm:^0.0.0" "@aws-sdk/client-appsync": "npm:^3" "@aws-sdk/client-bedrock-agentcore-control": "npm:^3" @@ -8092,7 +8092,7 @@ __metadata: "@aws-cdk/cloud-assembly-api": "npm:^0.0.0" "@aws-cdk/cloud-assembly-schema": "npm:^0.0.0" "@aws-cdk/cloudformation-diff": "npm:^0.0.0" - "@aws-cdk/cx-api": "npm:^2.260.0" + "@aws-cdk/cx-api": "npm:^2" "@aws-cdk/private-tools": "npm:^0.0.0" "@aws-cdk/toolkit-lib": "npm:^0.0.0" "@aws-cdk/user-input-gen": "npm:^0.0.0" From 3c2fa838edcb4c9a6e0d2ca834b192082b67b395 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 14:13:26 +0200 Subject: [PATCH 03/17] Fix tests --- .../lib/api/validate/validate-formatting.ts | 23 ++++++++-------- .../lib/toolkit/private/validation-report.ts | 2 +- .../toolkit-lib/lib/toolkit/toolkit.ts | 2 +- .../toolkit-lib/test/actions/validate.test.ts | 7 ----- .../api/validate/validate-formatting.test.ts | 27 +++++++++---------- 5 files changed, 27 insertions(+), 34 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts b/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts index f6526d82c..1967ff85a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts @@ -1,5 +1,5 @@ import * as path from 'node:path'; -import type { PluginReportJson, PolicyViolationJson, ViolatingConstructJson } from '@aws-cdk/cloud-assembly-schema'; +import type { PluginReportJson, PolicyViolationJson, PolicyViolationSeverity, ViolatingConstructJson } from '@aws-cdk/cloud-assembly-schema'; import * as chalk from 'chalk'; import type { ValidateResult } from '../../actions/validate'; import type { ActionLessMessage } from '../io/private'; @@ -41,7 +41,7 @@ function flattenViolations(pluginReports: PluginReportJson[]): FlattenedViolatio const pluginName = report.pluginName; for (const violation of report.violations) { - const severity = normalizeSeverity(violation.severity); + const severity = normalizeSeverity(violation.severity, violation.customSeverity); for (const construct of violation.violatingConstructs) { result.push({ severity, description: violation.description, ruleName: violation.ruleName, pluginName, construct }); @@ -52,15 +52,16 @@ function flattenViolations(pluginReports: PluginReportJson[]): FlattenedViolatio return result; } -function normalizeSeverity(severity: string | undefined): string { - if (!severity) return 'Warning'; - const lower = severity.toLowerCase(); - if (lower === 'fatal') return 'Fatal'; - if (lower === 'error') return 'Error'; - if (lower === 'warning') return 'Warning'; - if (lower === 'info') return 'Info'; - const safe = sanitize(severity); - return safe.charAt(0).toUpperCase() + safe.slice(1); +function normalizeSeverity(severity: PolicyViolationSeverity, customSeverity?: string): string { + switch (severity) { + case 'fatal': + case 'error': + case 'warning': + case 'info': + return severity.toUpperCase(); + case 'custom': + return customSeverity ?? 'INFO'; + } } function formatViolationBlock(fileRoot: string, v: FlattenedViolation): string { diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts index a32b6d55a..3698772b6 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts @@ -86,7 +86,7 @@ export async function throwIfValidationFailures( const conclusion = combineConclusions(pluginReports); const result: ValidateResult = { conclusion, pluginReports }; - await ioHelper.notify(hostMessageFromValidation(result)); + await ioHelper.notify(hostMessageFromValidation(process.cwd(), result)); switch (failAt) { case 'error': diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index c389d9c6a..716fbad87 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -695,7 +695,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { if (reports.length === 0) { await ioHelper.notify(IO.CDK_TOOLKIT_I9601.msg('No validation plugins configured. Add a plugin to your CDK app to enable validation.')); } else { - await ioHelper.notify(hostMessageFromValidation(result)); + await ioHelper.notify(hostMessageFromValidation(process.cwd(), result)); } return result; diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/validate.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/validate.test.ts index 46d1b2435..7935c6d30 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/validate.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/validate.test.ts @@ -49,13 +49,6 @@ describe('validate', () => { ioHost.expectMessage({ containing: 'No validation plugins configured', level: 'info' }); }); - test('emits info IO message on success', async () => { - const cx = await cdkOutFixture(toolkit, 'stack-with-passing-validation'); - await toolkit.validate(cx, { online: false }); - - ioHost.expectMessage({ containing: 'No problems found', level: 'info' }); - }); - test('can invoke without options', async () => { const cx = await cdkOutFixture(toolkit, 'stack-with-bucket'); const result = await toolkit.validate(cx, { online: false }); diff --git a/packages/@aws-cdk/toolkit-lib/test/api/validate/validate-formatting.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/validate/validate-formatting.test.ts index 19fbdfafd..854487198 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/validate/validate-formatting.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/validate/validate-formatting.test.ts @@ -1,6 +1,6 @@ import * as chalk from 'chalk'; import type { ValidateResult } from '../../../lib/actions/validate'; -import { formatValidateResult } from '../../../lib/api/validate/validate-formatting'; +import { formatValidateResult as formatValidateResult_ } from '../../../lib/api/validate/validate-formatting'; // Disable chalk for predictable assertions — set level directly because // env vars may not take effect when chalk is already loaded by another test in the same worker. @@ -12,13 +12,6 @@ function makeResult(pluginReports: ValidateResult['pluginReports']): ValidateRes } describe('formatValidateResult', () => { - test('returns pass message when no violations', () => { - const result = makeResult([ - { pluginName: 'TestPlugin', conclusion: 'success', violations: [] }, - ]); - expect(formatValidateResult(result)).toContain('No problems found.'); - }); - test('sorts violations by severity (fatal > error > warning > info > custom)', () => { const result = makeResult([{ pluginName: 'TestPlugin', @@ -34,11 +27,13 @@ describe('formatValidateResult', () => { const output = formatValidateResult(result); const lines = output.split('\n\n').filter(l => l.trim()); - expect(lines[1]).toContain('fatal issue'); - expect(lines[2]).toContain('error issue'); - expect(lines[3]).toContain('warning issue'); - expect(lines[4]).toContain('info issue'); - expect(lines[5]).toContain('custom issue'); + expect(lines).toEqual([ + expect.stringContaining('fatal issue'), + expect.stringContaining('error issue'), + expect.stringContaining('warning issue'), + expect.stringContaining('info issue'), + expect.stringContaining('custom issue'), + ]); }); test('formats construct path with logical id', () => { @@ -147,7 +142,7 @@ describe('formatValidateResult', () => { }]); const output = formatValidateResult(result); - expect(output).toContain("Acknowledge 'SecurityPlugin::no-public-buckets'"); + expect(output).toContain("Acknowledge with 'SecurityPlugin::no-public-buckets'"); }); test('includes constructFqn when present', () => { @@ -194,3 +189,7 @@ describe('formatValidateResult', () => { expect(output).toContain('Evil'); }); }); + +function formatValidateResult(result: ValidateResult) { + return formatValidateResult_(process.cwd(), result); +} \ No newline at end of file From 002ccd0ff18894f196633b7dcce5d477bbdfb47a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:19:01 +0000 Subject: [PATCH 04/17] chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../toolkit-lib/lib/api/validate/validate-formatting.ts | 2 +- .../toolkit-lib/test/api/validate/validate-formatting.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts b/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts index 1967ff85a..ded8f061e 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts @@ -13,7 +13,7 @@ export function hostMessageFromValidation(fileRoot: string, result: ValidateResu } export function formatValidateResult(fileRoot: string, result: ValidateResult): string { - return formatValidationReports(fileRoot, result.pluginReports).join('\n\n') + return formatValidationReports(fileRoot, result.pluginReports).join('\n\n'); } export function formatValidationReports(fileRoot: string, reports: PluginReportJson[]): string[] { diff --git a/packages/@aws-cdk/toolkit-lib/test/api/validate/validate-formatting.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/validate/validate-formatting.test.ts index 854487198..41f047cf7 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/validate/validate-formatting.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/validate/validate-formatting.test.ts @@ -192,4 +192,4 @@ describe('formatValidateResult', () => { function formatValidateResult(result: ValidateResult) { return formatValidateResult_(process.cwd(), result); -} \ No newline at end of file +} From c89f4e25a0f930ce979a1a34d128f57457332a54 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 14:21:07 +0200 Subject: [PATCH 05/17] Should be false --- .../@aws-cdk/toolkit-lib/lib/api/cloud-assembly/environment.ts | 2 +- .../toolkit-lib/lib/api/validate/validate-formatting.ts | 2 +- .../toolkit-lib/test/api/validate/validate-formatting.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/environment.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/environment.ts index a69daeec5..00063be43 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/environment.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/environment.ts @@ -76,7 +76,7 @@ export function contextFromSettings( // We unconditionally tell the CDK app that the toolkit/CLI will handle validation reports. // The app never has to exit with an error code because of it. - context[cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT] = true; + context[cxapi.FAIL_SYNTH_ON_VALIDATION_ERRORS_CONTEXT] = false; return context; } diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts b/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts index 1967ff85a..ded8f061e 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts @@ -13,7 +13,7 @@ export function hostMessageFromValidation(fileRoot: string, result: ValidateResu } export function formatValidateResult(fileRoot: string, result: ValidateResult): string { - return formatValidationReports(fileRoot, result.pluginReports).join('\n\n') + return formatValidationReports(fileRoot, result.pluginReports).join('\n\n'); } export function formatValidationReports(fileRoot: string, reports: PluginReportJson[]): string[] { diff --git a/packages/@aws-cdk/toolkit-lib/test/api/validate/validate-formatting.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/validate/validate-formatting.test.ts index 854487198..41f047cf7 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/validate/validate-formatting.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/validate/validate-formatting.test.ts @@ -192,4 +192,4 @@ describe('formatValidateResult', () => { function formatValidateResult(result: ValidateResult) { return formatValidateResult_(process.cwd(), result); -} \ No newline at end of file +} From 2f2bb6fc01b26ea6be3fcfa2d1966679ce4783de Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 14:34:10 +0200 Subject: [PATCH 06/17] yay --- .../toolkit-lib/lib/toolkit/private/validation-report.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts index 3698772b6..a1a9eeb47 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/validation-report.ts @@ -91,7 +91,7 @@ export async function throwIfValidationFailures( switch (failAt) { case 'error': if (conclusion === 'failure') { - const error = AssemblyError.withStacks('Found errors', stacks.stackArtifacts); + const error = AssemblyError.withStacks('Synthesis finished with errors', stacks.stackArtifacts); error.attachSynthesisErrorCode('AnnotationErrors'); throw error; } @@ -99,7 +99,7 @@ export async function throwIfValidationFailures( case 'warn': // if we're failing at 'warn', then both warnings and errors cause failure, so the initial conclusion is correct if (conclusion === 'failure' || hasWarnings(pluginReports)) { - const error = AssemblyError.withStacks('Found warnings (--strict mode)', stacks.stackArtifacts); + const error = AssemblyError.withStacks('Synthesis finished with warnings (--strict mode)', stacks.stackArtifacts); error.attachSynthesisErrorCode('StrictAnnotationWarnings'); throw error; } From b16d84f3376e5da322a1bdbe3c2efc14cf2ee134 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 14:35:21 +0200 Subject: [PATCH 07/17] Remove leading slash --- .../lib/toolkit/private/collect-annotation-report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/collect-annotation-report.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/collect-annotation-report.ts index a649de3c2..f46d0c07b 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/collect-annotation-report.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/collect-annotation-report.ts @@ -49,7 +49,7 @@ export function collectAnnotationReport(stacks: StackCollection): cxschema.Plugi } violation.violatingConstructs.push({ - constructPath: entry.id, + constructPath: entry.id.replace(/^\//, ''), // remove leading slash // TODO: see if this information can be obtained from tree.json // cloudFormationResource From 8eec47bb4b62a42438e4d45788a0288e9c74285a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 14:38:55 +0200 Subject: [PATCH 08/17] Fix flattened violation --- .../lib/api/validate/validate-formatting.ts | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts b/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts index ded8f061e..41a4b1a3c 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts @@ -34,22 +34,21 @@ export function formatValidationReports(fileRoot: string, reports: PluginReportJ ]; } -function flattenViolations(pluginReports: PluginReportJson[]): FlattenedViolation[] { - const result: FlattenedViolation[] = []; - - for (const report of pluginReports) { +function flattenViolations(reports: PluginReportJson[]): FlattenedViolation[] { + return reports.flatMap((report) => { const pluginName = report.pluginName; - - for (const violation of report.violations) { - const severity = normalizeSeverity(violation.severity, violation.customSeverity); - - for (const construct of violation.violatingConstructs) { - result.push({ severity, description: violation.description, ruleName: violation.ruleName, pluginName, construct }); - } - } - } - - return result; + return report.violations.flatMap((violation) => { + return violation.violatingConstructs.map((construct) => ({ + severity: normalizeSeverity(violation.severity), + description: violation.description, + ruleName: violation.ruleName, + pluginName, + construct, + suggestedFix: violation.suggestedFix, + ruleMetadata: violation.ruleMetadata, + })); + }); + }); } function normalizeSeverity(severity: PolicyViolationSeverity, customSeverity?: string): string { From 471bf0d556c7949950ba93690014d1c2a185845d Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 14:44:07 +0200 Subject: [PATCH 09/17] Update tests --- packages/@aws-cdk/toolkit-lib/test/actions/synth.test.ts | 2 +- packages/@aws-cdk/toolkit-lib/test/toolkit/toolkit.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/synth.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/synth.test.ts index d83ed6c76..5a82167ef 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/synth.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/synth.test.ts @@ -131,7 +131,7 @@ describe('synth', () => { }); // WHEN - await expect(toolkit.synth(cx)).rejects.toThrow(/Found errors/); + await expect(toolkit.synth(cx)).rejects.toThrow(/Synthesis finished with errors/); // There should not be a lock remaining in the given output directory const lock = new RWLock(synthDir.dir); diff --git a/packages/@aws-cdk/toolkit-lib/test/toolkit/toolkit.test.ts b/packages/@aws-cdk/toolkit-lib/test/toolkit/toolkit.test.ts index 76316b4a5..e932b33eb 100644 --- a/packages/@aws-cdk/toolkit-lib/test/toolkit/toolkit.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/toolkit/toolkit.test.ts @@ -90,7 +90,7 @@ test('outputs of assembly are measured', async () => { return app.synth(); }); - await expect(() => toolkit.synth(builder)).rejects.toThrow(/Found errors/); + await expect(() => toolkit.synth(builder)).rejects.toThrow(/Synthesis finished with errors/); expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ data: expect.objectContaining({ From 602a74c3747f9e5136071b009da8c89cf866b308 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 14:53:53 +0200 Subject: [PATCH 10/17] More messaging updates --- packages/aws-cdk/test/commands/diff.test.ts | 2 +- packages/aws-cdk/test/cxapp/cloud-assembly.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk/test/commands/diff.test.ts b/packages/aws-cdk/test/commands/diff.test.ts index 068e9bc90..68c0dca66 100644 --- a/packages/aws-cdk/test/commands/diff.test.ts +++ b/packages/aws-cdk/test/commands/diff.test.ts @@ -622,7 +622,7 @@ describe('non-nested stacks', () => { toolkit.diff({ stackNames: ['C'], }), - ).rejects.toThrow(/Found errors/); + ).rejects.toThrow(/Synthesis finished with errors/); }); test('when quiet mode is enabled, stacks with no diffs should not print stack name & no differences to stdout', async () => { diff --git a/packages/aws-cdk/test/cxapp/cloud-assembly.test.ts b/packages/aws-cdk/test/cxapp/cloud-assembly.test.ts index 60cb8da5f..10f11a421 100644 --- a/packages/aws-cdk/test/cxapp/cloud-assembly.test.ts +++ b/packages/aws-cdk/test/cxapp/cloud-assembly.test.ts @@ -229,7 +229,7 @@ describe('StackCollection', () => { // THEN expect(selected.stackCount).toBe(1); - await expect(async () => selected.validateMetadata()).rejects.toThrow(/Found errors/); + await expect(async () => selected.validateMetadata()).rejects.toThrow(/Synthesis finished with errors/); }); test('do throw when selecting stack with warnings and we are on strict mode', async () => { @@ -243,7 +243,7 @@ describe('StackCollection', () => { // THEN expect(selected.stackCount).toBe(1); - await expect(async () => selected.validateMetadata('warn')).rejects.toThrow(/Found warnings/); + await expect(async () => selected.validateMetadata('warn')).rejects.toThrow(/Synthesis finished with warnings/); }); }); }); From 546f2ec94038a36eebf77f62e20e8afce9d5231a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 15:23:13 +0200 Subject: [PATCH 11/17] Incorrectly changed these tests --- packages/aws-cdk/test/cxapp/cloud-assembly.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/test/cxapp/cloud-assembly.test.ts b/packages/aws-cdk/test/cxapp/cloud-assembly.test.ts index 10f11a421..60cb8da5f 100644 --- a/packages/aws-cdk/test/cxapp/cloud-assembly.test.ts +++ b/packages/aws-cdk/test/cxapp/cloud-assembly.test.ts @@ -229,7 +229,7 @@ describe('StackCollection', () => { // THEN expect(selected.stackCount).toBe(1); - await expect(async () => selected.validateMetadata()).rejects.toThrow(/Synthesis finished with errors/); + await expect(async () => selected.validateMetadata()).rejects.toThrow(/Found errors/); }); test('do throw when selecting stack with warnings and we are on strict mode', async () => { @@ -243,7 +243,7 @@ describe('StackCollection', () => { // THEN expect(selected.stackCount).toBe(1); - await expect(async () => selected.validateMetadata('warn')).rejects.toThrow(/Synthesis finished with warnings/); + await expect(async () => selected.validateMetadata('warn')).rejects.toThrow(/Found warnings/); }); }); }); From e89d0692b501d9b5f077155d0d248a3913b163c2 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 19:28:15 +0200 Subject: [PATCH 12/17] Fix one integ test --- .../validate/cdk-validate-passes-clean-app.integtest.ts | 2 +- .../@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts | 2 +- .../toolkit-lib/lib/api/validate/validate-formatting.ts | 2 +- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 6 ++++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate/cdk-validate-passes-clean-app.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate/cdk-validate-passes-clean-app.integtest.ts index 05d721eaf..8bc57ae35 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate/cdk-validate-passes-clean-app.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate/cdk-validate-passes-clean-app.integtest.ts @@ -7,6 +7,6 @@ integTest( ['--unstable=validate', 'validate', fixture.fullStackName('validate-passing')], ); - expect(output).toContain('Policy validation passed. No problems found.'); + expect(output).toContain('Validation did not find any problems.'); }), ); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts index bc9a819b5..d13911828 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts @@ -508,7 +508,7 @@ export const IO = { // validate (96xx) CDK_TOOLKIT_I9600: make.info({ code: 'CDK_TOOLKIT_I9600', - description: 'Policy validation passed', + description: 'Validation did not find any problems', interface: 'ValidateResult', }), diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts b/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts index 41a4b1a3c..a84adc666 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/validate/validate-formatting.ts @@ -9,7 +9,7 @@ export function hostMessageFromValidation(fileRoot: string, result: ValidateResu // Always emit at info level so the CLI IoHost doesn't wrap the entire output // in a single color. The formatter handles per-severity coloring internally. // Consumers detect failure via the structured `data.conclusion` field or exit code. - return IO.CDK_TOOLKIT_I9600.msg(formatValidateResult(fileRoot, result), result); + return IO.CDK_TOOLKIT_E9600.msg(formatValidateResult(fileRoot, result), result); } export function formatValidateResult(fileRoot: string, result: ValidateResult): string { diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 716fbad87..3a430a74a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -686,14 +686,16 @@ export class Toolkit extends CloudAssemblySourceBuilder { } } + const hasAnyViolations = reports.some(report => report.violations && report.violations.length > 0); + const result: ValidateResult = { conclusion: combineConclusions(reports), title: undefined, pluginReports: reports, }; - if (reports.length === 0) { - await ioHelper.notify(IO.CDK_TOOLKIT_I9601.msg('No validation plugins configured. Add a plugin to your CDK app to enable validation.')); + if (!hasAnyViolations) { + await ioHelper.notify(IO.CDK_TOOLKIT_I9600.msg('Validation did not find any problems.', result)); } else { await ioHelper.notify(hostMessageFromValidation(process.cwd(), result)); } From 76bee6464671992c23a0d56d89ed9ba59279b5ae Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 20:12:56 +0200 Subject: [PATCH 13/17] Offline errors fail initial deployment, so we never get to CFN errors --- .../cli-integ/resources/cdk-apps/validate-online-app/app.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/validate-online-app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/validate-online-app/app.js index fc6fc15ce..6f4987d08 100644 --- a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/validate-online-app/app.js +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/validate-online-app/app.js @@ -40,7 +40,9 @@ class SecurityPlugin { } const app = new cdk.App(); -cdk.Validations.of(app).addPlugins(new SecurityPlugin()); +if (process.env.INJECT_OFFLINE_ERRORS) { + cdk.Validations.of(app).addPlugins(new SecurityPlugin()); +} // Valid stack — no offline or online errors class ValidStack extends cdk.Stack { From a54e44d0662e4db1144668ce06e8bf20b03d0732 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 21:26:25 +0200 Subject: [PATCH 14/17] Update some tests with new behavior --- .../toolkit-lib/test/actions/validate.test.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/validate.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/validate.test.ts index 7935c6d30..9f9a291e8 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/validate.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/validate.test.ts @@ -40,15 +40,6 @@ describe('validate', () => { expect(result.pluginReports[0].violations).toHaveLength(0); }); - test('returns success with no reports when no report file exists', async () => { - const cx = await cdkOutFixture(toolkit, 'stack-with-bucket'); - const result = await toolkit.validate(cx, { online: false }); - - expect(result.conclusion).toBe('success'); - expect(result.pluginReports).toHaveLength(0); - ioHost.expectMessage({ containing: 'No validation plugins configured', level: 'info' }); - }); - test('can invoke without options', async () => { const cx = await cdkOutFixture(toolkit, 'stack-with-bucket'); const result = await toolkit.validate(cx, { online: false }); @@ -108,7 +99,7 @@ describe('validate', () => { await toolkit.validate(cx, { online: false }); const msg = ioHost.messages.find( - (m) => m.code === 'CDK_TOOLKIT_I9600', + (m) => m.code === 'CDK_TOOLKIT_E9600', ); expect(msg).toBeDefined(); expect(msg!.data).toMatchObject({ From daa89db34a3a63fd4e0162179799f1dc1340abf9 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 21:28:17 +0200 Subject: [PATCH 15/17] Integ tests --- ...dk-validate-online-combined-offline-and-online.integtest.ts | 3 +++ .../cdk-validate-online-passes-valid-template.integtest.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate-online/cdk-validate-online-combined-offline-and-online.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate-online/cdk-validate-online-combined-offline-and-online.integtest.ts index 8f0698d16..5196ef613 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate-online/cdk-validate-online-combined-offline-and-online.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate-online/cdk-validate-online-combined-offline-and-online.integtest.ts @@ -14,6 +14,9 @@ integTest( const output = await fixture.cdk( ['--unstable=validate', 'validate', '--online', fixture.fullStackName('validate-online-combined')], { + env: { + INJECT_OFFLINE_ERRORS: 'true', + }, allowErrExit: true, }, ); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate-online/cdk-validate-online-passes-valid-template.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate-online/cdk-validate-online-passes-valid-template.integtest.ts index fe68efe12..0527ebfc9 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate-online/cdk-validate-online-passes-valid-template.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate-online/cdk-validate-online-passes-valid-template.integtest.ts @@ -7,6 +7,6 @@ integTest( ['--unstable=validate', 'validate', '--online', fixture.fullStackName('validate-online-valid')], ); - expect(output).toContain('No problems found'); + expect(output).toContain('Validation did not find any problems'); }), ); From 46240c2443de40d2579d9d368f23c58de810ee58 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:33:54 +0000 Subject: [PATCH 16/17] chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- packages/@aws-cdk/toolkit-lib/docs/message-registry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/toolkit-lib/docs/message-registry.md b/packages/@aws-cdk/toolkit-lib/docs/message-registry.md index 3206c3d3d..d40e7dc66 100644 --- a/packages/@aws-cdk/toolkit-lib/docs/message-registry.md +++ b/packages/@aws-cdk/toolkit-lib/docs/message-registry.md @@ -146,7 +146,7 @@ Please let us know by [opening an issue](https://github.com/aws/aws-cdk-cli/issu | `CDK_TOOLKIT_I9500` | Stack diagnosis (no problems found) | `info` | {@link DiagnosedStack} | | `CDK_TOOLKIT_E9500` | Stack diagnosis (problems found) | `error` | {@link DiagnosedStack} | | `CDK_TOOLKIT_W9501` | Stack diagnosis (diagnosis could not be performed) | `warn` | {@link DiagnosedStack} | -| `CDK_TOOLKIT_I9600` | Policy validation passed | `info` | {@link ValidateResult} | +| `CDK_TOOLKIT_I9600` | Validation did not find any problems | `info` | {@link ValidateResult} | | `CDK_TOOLKIT_E9600` | Policy validation failed | `error` | {@link ValidateResult} | | `CDK_TOOLKIT_I9601` | No policy validation report found | `info` | n/a | | `CDK_TOOLKIT_W9602` | Online validation could not be completed for a stack | `warn` | n/a | From a3fb8d8108a414678afa27fbbadb0479605e6670 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 19 Jun 2026 21:55:21 +0200 Subject: [PATCH 17/17] modEnv instead --- ...cdk-validate-online-combined-offline-and-online.integtest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate-online/cdk-validate-online-combined-offline-and-online.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate-online/cdk-validate-online-combined-offline-and-online.integtest.ts index 5196ef613..befa396f9 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate-online/cdk-validate-online-combined-offline-and-online.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/validate-online/cdk-validate-online-combined-offline-and-online.integtest.ts @@ -14,7 +14,7 @@ integTest( const output = await fixture.cdk( ['--unstable=validate', 'validate', '--online', fixture.fullStackName('validate-online-combined')], { - env: { + modEnv: { INJECT_OFFLINE_ERRORS: 'true', }, allowErrExit: true,