X Tutup
Skip to content

feat(gen2-migration): redesign drift detection output to category-based format #14647

Open
9pace wants to merge 15 commits intogen2-migrationfrom
drift-ui
Open

feat(gen2-migration): redesign drift detection output to category-based format #14647
9pace wants to merge 15 commits intogen2-migrationfrom
drift-ui

Conversation

@9pace
Copy link

@9pace 9pace commented Mar 5, 2026

Description of changes

Screenshot 2026-03-09 at 4 03 50 PM

Issue #, if available

#14563

Description of how you validated changes

Checklist

  • PR description included
  • yarn test passes
  • Tests are changed or added
  • Relevant documentation is changed or added (and PR referenced)
  • New AWS SDK calls or CloudFormation actions have been added to relevant test and service IAM policies
  • Pull request labels are added

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

9pace and others added 11 commits March 4, 2026 14:51
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.
@9pace 9pace marked this pull request as ready for review March 6, 2026 16:50
@9pace 9pace requested a review from a team as a code owner March 6, 2026 16:50
9pace added 2 commits March 9, 2026 12:52
…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.
@@ -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');
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

9pace added 2 commits March 9, 2026 14:47
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.
@9pace 9pace changed the title feat: redesign drift detection output to category-based format feat(gen2-migration): redesign drift detection output to category-based format Mar 9, 2026
@9pace
Copy link
Author

9pace commented Mar 9, 2026

Hoist id up, allow for multiple [category] blocks. Display logical id clearly


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 = {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reimplement all functions from printer.

...printer,
info: (msg: string) => this.withSpinnerPaused(() => basePrint.info(msg)),
debug: (msg: string) => {
if (!isDebug) return;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if debug, no spinner

print.debug(`detectStackDriftRecursive: ${stackName}`);

print.debug(`Total drifted resources: ${totalDrifted}`);
const root = await buildDriftNode(cfn, stackName, stackName, print);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pass null instead of stackname for logicalid

skippedNestedStacks: skippedNestedStacks.length > 0 ? skippedNestedStacks : undefined,
};
// Override root category to 'Core Infrastructure'
root.category = 'Core Infrastructure';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be set when stackname null

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inside the buildDriftNode function

* CloudFormation drift results including nested stacks
* Enriched drift tree node — one per stack (root or nested)
*/
export interface StackDriftNode {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make interfaces readonly

function countDriftedResources(driftOutput: DescribeStackResourceDriftsCommandOutput): number {
if (!driftOutput.StackResourceDrifts) {
return 0;
function countTotalDrifted(node: StackDriftNode): number {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not going to be used - remove

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 {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    // 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);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const categories = collectDriftCategories(phase1, phase2, phase3);
const driftedCategories = collectDriftCategories(phase1, phase2, phase3);

/**
* Resource information for Phase 3
*/
export interface ResourceInfo {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate ResourceInfo

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

service? is not optional

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;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove in favor of no number

@9pace
Copy link
Author

9pace commented Mar 9, 2026

  • Expected and deployed state instead of + and -.
  • Make drift id and changeset id a link to the console.
  • remove drift count in favor of "drift detected"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

X Tutup