@aws-amplify/cli-internal
Version:
Amplify CLI
413 lines (322 loc) • 18.5 kB
Markdown
# ADR: `gen2-migration assess` Command
> **Created:** 2026-03-16
## Context
The `gen2-migration` workflow has four steps: `lock`, `generate`,
`refactor`, and `decommission`. Users currently have no way to know
whether their Gen1 project can be migrated before they start. If an
unsupported feature is discovered mid-migration, the user has already
invested time and may need to roll back.
We need a pre-flight command that reads the user's Gen1 project and
reports exactly which features are supported, which require manual
work, and which block migration entirely.
## Decision
Add a new `assess` subcommand to `gen2-migration` that reads the
user's `amplify-meta.json`, evaluates each resource against the
tool's current capabilities, and displays a per-category assessment
with a summary and migration verdict.
## Requirements
### R1 — Read the user's Gen1 project
The command reads `amplify-meta.json` (via `Gen1App`) to discover
every category, resource, and service the user has configured. Only
categories with at least one resource are displayed.
### R2 — Evaluate support per resource
Each resource is evaluated by asking the generate and refactor steps
directly. Each step owns the knowledge of what it supports — the
assessment does not maintain a separate registry or consult
`STATEFUL_RESOURCES`.
Each step exposes a static `assess` method that accepts a
`DiscoveredResource` (and `Gen1App` where needed) and returns a
`SupportResponse`:
```typescript
interface SupportResponse {
readonly supported: boolean;
readonly notes: readonly string[];
}
```
- `supported: true`, empty notes → ✔ (fully supported)
- `supported: true`, non-empty notes → ⚠ (supported with gaps)
- `supported: false` → ✘ (not supported)
The refactor step returns `{ supported: true, notes: [] }` for
categories that have no stateful resources to move (e.g. functions).
This is a no-op — the refactor step knows this internally and the
assessment does not need to reason about CloudFormation resource
types.
### R3 — Evaluate sub-feature support
Sub-feature gaps are reported as `notes` in the `SupportResponse`
returned by each step's `assess` method. For example, the generate
step's assess for a Lambda function with `custom-policies.json`
returns `{ supported: true, notes: ['custom-policies not supported'] }`.
A resource with non-empty notes shows ⚠ instead of ✔ in the
relevant operation column. The notes appear as footnotes beneath
the category table.
### R4 — Display per-category tables
Output is grouped by category. Each category renders a table:
```
Auth
┌──────────────────┬─────────┬──────────┬──────────┐
│ Resource │ Service │ Generate │ Refactor │
├──────────────────┼─────────┼──────────┼──────────┤
│ userPoolGroups │ Cognito │ ✔ │ ✔ │
└──────────────────┴─────────┴──────────┴──────────┘
```
Categories with no resources in the user's project are omitted.
### R5 — Display sub-feature warnings
When a resource has unsupported sub-features, a footnote appears
below the category table:
```
Function
┌──────────────────┬─────────┬──────────┬──────────┐
│ Resource │ Service │ Generate │ Refactor │
├──────────────────┼─────────┼──────────┼──────────┤
│ processOrder │ Lambda │ ✔ │ ✔ │
│ sendNotification │ Lambda │ ⚠ │ ✔ │
└──────────────────┴─────────┴──────────┴──────────┘
⚠ sendNotification:
- custom-policies not supported
```
### R6 — Display summary and verdict
After all category tables, a summary reports:
1. How many resources are fully supported vs total.
2. Resources that lack generate support (warning — user can write
code manually).
3. Resources that lack refactor support (blocker — stateful data
at risk).
4. Resources with unsupported sub-features (warning — generated
code will be incomplete).
5. A final verdict: `✔ Migration can proceed.` or
`✘ Migration blocked.`
### R7 — Blocker condition
Migration is blocked if and only if at least one resource receives
`{ supported: false }` from the refactor step's `assess` method.
Missing generate support is never a blocker. The user can
acknowledge the gap and write Gen2 code manually after the generate
step.
Non-empty notes (sub-feature gaps) are never a blocker. The
generated code will be incomplete but functional.
### R8 — Example output (fully supported)
```
Assessment for "ecommerce-app" (env: prod)
Auth
┌─────────────────────────┬─────────┬──────────┬──────────┐
│ Resource │ Service │ Generate │ Refactor │
├─────────────────────────┼─────────┼──────────┼──────────┤
│ userPoolGroups │ Cognito │ ✔ │ ✔ │
└─────────────────────────┴─────────┴──────────┴──────────┘
Storage
┌─────────────────────────┬──────────┬──────────┬──────────┐
│ Resource │ Service │ Generate │ Refactor │
├─────────────────────────┼──────────┼──────────┼──────────┤
│ productImages │ S3 │ ✔ │ ✔ │
│ ordersTable │ DynamoDB │ ✔ │ ✔ │
└─────────────────────────┴──────────┴──────────┴──────────┘
Function
┌─────────────────────────┬─────────┬──────────┬──────────┐
│ Resource │ Service │ Generate │ Refactor │
├─────────────────────────┼─────────┼──────────┼──────────┤
│ processOrder │ Lambda │ ✔ │ ✔ │
│ sendNotification │ Lambda │ ✔ │ ✔ │
└─────────────────────────┴─────────┴──────────┴──────────┘
Summary: 5/5 resources across 3 categories fully supported.
✔ Migration can proceed.
```
### R9 — Example output (requires manual code)
```
Assessment for "ecommerce-app" (env: prod)
Auth
┌─────────────────────────┬─────────┬──────────┬──────────┐
│ Resource │ Service │ Generate │ Refactor │
├─────────────────────────┼─────────┼──────────┼──────────┤
│ userPoolGroups │ Cognito │ ✔ │ ✔ │
└─────────────────────────┴─────────┴──────────┴──────────┘
Function
┌─────────────────────────┬─────────┬──────────┬──────────┐
│ Resource │ Service │ Generate │ Refactor │
├─────────────────────────┼─────────┼──────────┼──────────┤
│ processOrder │ Lambda │ ✔ │ ✔ │
│ sendNotification │ Lambda │ ⚠ │ ✔ │
└─────────────────────────┴─────────┴──────────┴──────────┘
⚠ sendNotification:
- custom-policies not supported
Notifications
┌─────────────────────────┬──────────┬──────────┬──────────┐
│ Resource │ Service │ Generate │ Refactor │
├─────────────────────────┼──────────┼──────────┼──────────┤
│ pushNotifications │ Pinpoint │ ✘ │ ✔ │
└─────────────────────────┴──────────┴──────────┴──────────┘
Summary: 2/4 resources across 3 categories fully supported.
⚠ 1 resource does not support code generation:
Notifications/pushNotifications
You will need to write Gen2 code for this manually
after the generate step.
⚠ 1 resource has unsupported sub-features:
Function/sendNotification (custom-policies)
Generated code will be incomplete. Review and add
missing configuration manually after the generate step.
✔ Migration can proceed.
```
### R10 — Example output (migration blocked)
```
Assessment for "ecommerce-app" (env: prod)
Auth
┌─────────────────────────┬─────────┬──────────┬──────────┐
│ Resource │ Service │ Generate │ Refactor │
├─────────────────────────┼─────────┼──────────┼──────────┤
│ userPoolGroups │ Cognito │ ✔ │ ✔ │
└─────────────────────────┴─────────┴──────────┴──────────┘
Storage
┌─────────────────────────┬──────────┬──────────┬──────────┐
│ Resource │ Service │ Generate │ Refactor │
├─────────────────────────┼──────────┼──────────┼──────────┤
│ productImages │ S3 │ ✔ │ ✔ │
│ ordersTable │ DynamoDB │ ✔ │ ✔ │
└─────────────────────────┴──────────┴──────────┴──────────┘
Notifications
┌─────────────────────────┬──────────┬──────────┬──────────┐
│ Resource │ Service │ Generate │ Refactor │
├─────────────────────────┼──────────┼──────────┼──────────┤
│ pushNotifications │ Pinpoint │ ✘ │ ✘ │
└─────────────────────────┴──────────┴──────────┴──────────┘
Geo
┌─────────────────────────┬──────────┬──────────┬──────────┐
│ Resource │ Service │ Generate │ Refactor │
├─────────────────────────┼──────────┼──────────┼──────────┤
│ storeLocator │ Location │ ✘ │ ✔ │
└─────────────────────────┴──────────┴──────────┴──────────┘
Summary: 3/5 resources across 4 categories fully supported.
⚠ 2 resources do not support code generation:
Notifications/pushNotifications, Geo/storeLocator
You will need to write Gen2 code for these manually
after the generate step.
✘ 1 resource has stateful data that cannot be refactored:
Notifications/pushNotifications
Automatic migration cannot proceed until refactoring
support is added for this resource. Stateful resources
require refactoring to avoid data loss.
✘ Migration blocked.
```
## Implementation Plan
### Shared types
```typescript
/** A resource discovered from amplify-meta.json. */
interface DiscoveredResource {
readonly category: string; // 'auth', 'storage', etc.
readonly resourceName: string; // 'userPoolGroups', 'myBucket', etc.
readonly service: string; // 'Cognito', 'S3', 'Lambda', etc.
}
/** Response from a step's assess method. */
interface SupportResponse {
readonly supported: boolean;
readonly notes: readonly string[];
}
```
### `Gen1App.discover()`
New method on `Gen1App` that iterates all categories in
`amplify-meta.json` and returns `DiscoveredResource[]`. Pure local
read, no AWS calls. Both the generate and refactor steps use this
as their starting point for resource iteration.
### Step `assess` methods
Both `AmplifyMigrationGenerateStep` and
`AmplifyMigrationRefactorStep` expose a static `assess` method:
```typescript
// Generate step
static assess(
gen1App: Gen1App,
resource: DiscoveredResource,
): SupportResponse
// Refactor step
static assess(
resource: DiscoveredResource,
): SupportResponse
```
Generate's `assess` needs `Gen1App` because sub-feature detection
requires reading local files (e.g., checking if
`custom-policies.json` exists for a function). Refactor's `assess`
is purely a lookup — "do I have a refactorer for this
category/service?" — and returns `{ supported: true }` for
categories with no stateful resources (no-op).
Each step owns its support knowledge entirely. The assessment does
not maintain a separate registry, consult `STATEFUL_RESOURCES`, or
reason about CloudFormation resource types.
### Refactor step: adopt discover-first pattern
The refactor step currently hardcodes three refactorers
(`AuthForwardRefactorer`, `StorageForwardRefactorer`,
`AnalyticsForwardRefactorer`) and instantiates all three
unconditionally. As part of this work, refactor the step to:
1. Call `gen1App.discover()` to get all resources.
2. For each resource, call its own `assess` to check support.
3. Instantiate refactorers only for supported resources.
This aligns the refactor step with the generate step's existing
"discover all, instantiate some" pattern and makes the `assess`
method the single source of truth for both assessment display and
runtime instantiation.
### Generate step: extract discovery
The generate step already follows the discover-first pattern in
its `execute()` method (reads `amplify-meta.json`, dispatches by
service type). Refactor it to use `gen1App.discover()` and its own
`assess` method, so the same logic drives both assessment and
execution.
### Assessment orchestrator
```typescript
class Assessment {
constructor(private readonly gen1App: Gen1App, private readonly appName: string, private readonly envName: string) {}
public evaluate(): AssessmentResult {
const resources = this.gen1App.discover();
const assessments = resources.map((r) => ({
...r,
generate: AmplifyMigrationGenerateStep.assess(this.gen1App, r),
refactor: AmplifyMigrationRefactorStep.assess(r),
}));
// group by category, derive blockers/warnings
}
}
```
### `AssessmentResult`
```typescript
interface ResourceAssessment {
readonly resource: DiscoveredResource;
readonly generate: SupportResponse;
readonly refactor: SupportResponse;
}
interface AssessmentResult {
readonly appName: string;
readonly envName: string;
readonly categories: ReadonlyMap<string, readonly ResourceAssessment[]>;
readonly blocked: boolean;
readonly blockers: readonly ResourceAssessment[];
readonly generateWarnings: readonly ResourceAssessment[];
readonly subFeatureWarnings: readonly ResourceAssessment[];
}
```
### `AssessmentRenderer`
Separate class that takes an `AssessmentResult` and renders the
tables, footnotes, summary, and verdict to the terminal using
`printer` from `@aws-amplify/amplify-prompts`. Keeping rendering
separate from evaluation makes the assessment logic unit-testable
without mocking terminal output.
### Data flow
```
Gen1App.create()
│
├── Gen1App.discover() → DiscoveredResource[]
│
▼
Assessment.evaluate()
│
├── GenerateStep.assess(gen1App, resource) → SupportResponse
├── RefactorStep.assess(resource) → SupportResponse
│
▼
AssessmentResult
│
▼
AssessmentRenderer.render() → terminal output
```
### Not a migration step
`assess` does not follow the `AmplifyMigrationStep` lifecycle
(`validate → execute → rollback`). It is read-only and has no
side effects. It is implemented as a standalone command handler,
not as a step subclass.
### No AWS calls beyond Gen1App
The assessment does not make AWS calls beyond what `Gen1App`
already does (downloading the cloud backend from S3). All
evaluation logic is local.