feat(gen2-migration): redesign drift detection output to category-based format #14647
Open
9pace wants to merge 15 commits intogen2-migrationfrom
Open
feat(gen2-migration): redesign drift detection output to category-based format #146479pace wants to merge 15 commits intogen2-migrationfrom
9pace wants to merge 15 commits intogen2-migrationfrom
Conversation
Replace the tree/hierarchy view with a unified category-based output that groups all drift types (CloudFormation, Template, Local) under their respective categories (Auth, Api, Storage, etc.). - Category-grouped output with drift types nested underneath - Drift detection IDs and changeset IDs for console tracking - Verbose progress messages moved to --debug level - displayResults uses injected printer instead of global
Previously, detectStackDriftRecursive returned raw CloudFormation API outputs (flat maps of drift results, physical IDs, and detection IDs), then drift.ts walked that same tree a second time in processData/ collectNestedStacks to fetch templates, compute counts, assign categories, and produce a ProcessedDriftData object for the formatter. Now detection builds an enriched StackDriftNode tree in a single pass: each node carries its category, resource counts, and drift detection ID. The formatter receives phase results directly and uses flattenTree for uniform iteration. (also root node no longer handled separate, it is just the base case) This reduces size of drift.ts (processData, collectNestedStacks, and associated imports all removed), deletes the ProcessedDriftData/StackDriftData intermediates, and moves count functions to detect-stack-drift.ts where they belong. Additional changes: - Formatter receives (phase1, phase2, phase3, projectName) directly - Count functions and types moved from drift-formatter to stack cfn module (this decouples the formatter, and now formatter no longer processes data) yay!
The API returns max 100 results per page. Previously a single call was made, silently dropping resources beyond the first page. Now loops on NextToken until all drift results are collected. Also simplifies the return type from DescribeStackResourceDriftsCommandOutput to StackResourceDrift[] since the wrapper was immediately unwrapped by the only caller. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…yToAllowChange Typed the propDiff parameter using the SDK's PropertyDifference type instead of any, enabling compile-time validation of field accesses.
…o module scope - Add PhysicalResourceId check before passing to ARN parser in analyzeChangeSet to satisfy TypeScript and guard against undefined - Move stripAnsi from closure inside formatDashboardLine to module level to avoid re-creating it on every call
- countUnchecked now excludes nested stack logical IDs from the parent's unchecked count, since those are already handled by child nodes - formatPropertyDiffs uses != null instead of truthiness so empty string values are displayed instead of silently omitted - Convert require() calls to static imports in detect-local-drift - Export ResourceChangeWithNested type from index barrel Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep only warn (consistent with gen2-migration logger and the underlying amplify-prompts printer). Update the single call site in detect-stack-drift.ts from print.warning() to print.warn(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convert CommonJS require calls to ES module imports for CloudFormation provider class and S3BackendZipFileName constant, enabling type checking and consistency with the rest of the codebase.
Was exported from both drift-detection/index.ts and drift-detection/services/index.ts. Keep only the services barrel since that's where the consumer imports from. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…drift Static import of resource-status-data transitively loads amplify-provider-awscloudformation which has top-level FeatureFlags side effects that crash in test environments. Restore the lazy require() pattern used elsewhere in the codebase (see amplify-toolkit.ts). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap drift detection phases in an AmplifySpinner that updates as each phase progresses. Printer methods pause/resume the spinner so log output never collides with the animation.
…dashboard Remove the custom Print interface and DriftOptions in favor of the standard Printer type from @aws-amplify/amplify-prompts. The built-in printer already handles the --debug flag via isDebug, eliminating the duplicated debug guard. The spinner-paused debug wrapper uses isDebug to skip unnecessary spinner pause/resume cycles that caused empty newlines. Also removes unused formatDriftResults/createSummaryDashboard code and the displayResults method, using createUnifiedCategoryView directly.
9pace
commented
Mar 9, 2026
| @@ -63,7 +68,6 @@ export async function detectLocalDrift(context: $TSContext): Promise<LocalDriftR | |||
| // Use existing status logic to compare local vs cloud backend | |||
| // Note: The cloud backend has already been synced from S3 | |||
| const { getResourceStatus } = require('../../extensions/amplify-helpers/resource-status-data'); | |||
Author
There was a problem hiding this comment.
comment refers to this require^
// Lazy require — resource-status-data transitively imports amplify-provider-awscloudformation
// which has top-level side effects (FeatureFlags.getNumber) that crash in test environments.
// This is the established pattern in this codebase (see amplify-toolkit.ts).
Replace DriftSummary/ResourceCounts with a flat totalDrifted field on CloudFormationDriftResults and a simple recursive countTotalDrifted. Remove countInSync, countUnchecked, countFailed, countDrifted, and the GetTemplateCommand call that only existed to support countUnchecked.
Author
|
Hoist id up, allow for multiple [category] blocks. Display logical id clearly |
9pace
commented
Mar 9, 2026
|
|
||
| constructor(private readonly context: $TSContext, basePrint: Pick<Printer, 'info' | 'debug' | 'warn'> = printer) { | ||
| // Wrap each method to pause/resume spinner so output never collides | ||
| this.printer = { |
Author
There was a problem hiding this comment.
reimplement all functions from printer.
9pace
commented
Mar 9, 2026
| ...printer, | ||
| info: (msg: string) => this.withSpinnerPaused(() => basePrint.info(msg)), | ||
| debug: (msg: string) => { | ||
| if (!isDebug) return; |
9pace
commented
Mar 9, 2026
| print.debug(`detectStackDriftRecursive: ${stackName}`); | ||
|
|
||
| print.debug(`Total drifted resources: ${totalDrifted}`); | ||
| const root = await buildDriftNode(cfn, stackName, stackName, print); |
Author
There was a problem hiding this comment.
pass null instead of stackname for logicalid
| skippedNestedStacks: skippedNestedStacks.length > 0 ? skippedNestedStacks : undefined, | ||
| }; | ||
| // Override root category to 'Core Infrastructure' | ||
| root.category = 'Core Infrastructure'; |
Author
There was a problem hiding this comment.
inside the buildDriftNode function
| * CloudFormation drift results including nested stacks | ||
| * Enriched drift tree node — one per stack (root or nested) | ||
| */ | ||
| export interface StackDriftNode { |
| function countDriftedResources(driftOutput: DescribeStackResourceDriftsCommandOutput): number { | ||
| if (!driftOutput.StackResourceDrifts) { | ||
| return 0; | ||
| function countTotalDrifted(node: StackDriftNode): number { |
| this.printer.info('Checking for template drift using changesets...'); | ||
| phase2Results = await detectTemplateDrift(stackName, this.printer, cfn); | ||
| this.printer.debug(`Phase 2 complete: totalDrifted=${phase2Results.totalDrifted}`); | ||
| try { |
Author
There was a problem hiding this comment.
// Start drift detection phases with spinner
let phase1Results: CloudFormationDriftResults;
let phase2Results: TemplateDriftResults;
let phase3Results: LocalDriftResults;
try {
// Sync cloud backend from S3 before running any phases
this.startSpinner('Syncing cloud backend from S3...');
const syncSuccess = await this.cfnService.syncCloudBackendFromS3(this.context);
// Phase 1: Detect CloudFormation drift recursively
this.updateSpinner('Detecting CloudFormation drift...');
phase1Results = await detectStackDriftRecursive(cfn, stackName, this.printer);
this.printer.debug(`Phase 1 complete: ${phase1Results.totalDrifted} drifted resources detected`);
if (!syncSuccess) {
phase2Results = {
totalDrifted: 0,
changes: [],
skipped: true,
skipReason: 'S3 backend sync failed - cannot compare templates',
};
phase3Results = {
totalDrifted: 0,
skipped: true,
skipReason: 'S3 backend sync failed - cannot compare local vs cloud',
};
this.printer.warn(chalk.yellow('Cloud backend sync failed - template drift and local drift will be skipped'));
} else {
this.printer.debug('S3 sync completed successfully');
// Phase 2: Template drift detection
this.updateSpinner('Analyzing template changes...');
this.printer.debug('Checking for template drift using changesets...');
phase2Results = await detectTemplateDrift(stackName, this.printer, cfn);
this.printer.debug(`template drift totalDrifted=${phase2Results.totalDrifted}`);
// Phase 3: Local drift detection
this.updateSpinner('Checking local changes...');
this.printer.debug('Checking local files vs cloud backend...');
phase3Results = await detectLocalDrift(this.context);
this.printer.debug(`local drift totalDrifted=${phase3Results.totalDrifted}`);
}
| phase2: TemplateDriftResults, | ||
| phase3: LocalDriftResults, | ||
| ): string | undefined { | ||
| const categories = collectDriftCategories(phase1, phase2, phase3); |
Author
There was a problem hiding this comment.
Suggested change
| const categories = collectDriftCategories(phase1, phase2, phase3); | |
| const driftedCategories = collectDriftCategories(phase1, phase2, phase3); |
| /** | ||
| * Resource information for Phase 3 | ||
| */ | ||
| export interface ResourceInfo { |
| const hasPhase1Errors = Boolean(phase1Results.skippedNestedStacks && phase1Results.skippedNestedStacks.length > 0); | ||
| const hasAnyErrors = hasPhase1Errors || phase2Results.skipped || phase3Results.skipped; | ||
| // Summary line | ||
| const totalDriftCount = phase1Results.totalDrifted + phase2Results.totalDrifted + phase3Results.totalDrifted; |
Author
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description of changes
Issue #, if available
#14563
Description of how you validated changes
Checklist
yarn testpassesBy submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.