Skip to content

Commit 4bce411

Browse files
sai-raySai Ray
andauthored
feat(gen2-migration): add deployment status validation and unit tests for safe migrations (#14313)
chore: resolving merge conflicts Co-authored-by: Sai Ray <saisujit@amazon.com>
1 parent 338685f commit 4bce411

File tree

2 files changed

+162
-4
lines changed

2 files changed

+162
-4
lines changed

packages/amplify-cli/src/__tests__/commands/gen2-migration/_validations.test.ts

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { AmplifyGen2MigrationValidations } from '../../../commands/gen2-migration/_validations';
2-
import { $TSContext } from '@aws-amplify/amplify-cli-core';
2+
import { $TSContext, stateManager } from '@aws-amplify/amplify-cli-core';
33
import { CloudFormationClient, DescribeChangeSetOutput } from '@aws-sdk/client-cloudformation';
44

55
jest.mock('@aws-sdk/client-cloudformation');
6+
jest.mock('@aws-amplify/amplify-cli-core', () => ({
7+
...jest.requireActual('@aws-amplify/amplify-cli-core'),
8+
stateManager: {
9+
getMeta: jest.fn(),
10+
},
11+
}));
612

713
describe('AmplifyGen2MigrationValidations', () => {
814
let mockContext: $TSContext;
@@ -491,4 +497,123 @@ describe('AmplifyGen2MigrationValidations', () => {
491497
});
492498
});
493499
});
500+
501+
describe('validateDeploymentStatus', () => {
502+
let mockSend: jest.Mock;
503+
504+
beforeEach(() => {
505+
mockSend = jest.fn();
506+
(CloudFormationClient as jest.Mock).mockImplementation(() => ({
507+
send: mockSend,
508+
}));
509+
});
510+
511+
afterEach(() => {
512+
jest.clearAllMocks();
513+
});
514+
515+
it('should throw StackNotFoundError when stackName is missing', async () => {
516+
jest.spyOn(stateManager, 'getMeta').mockReturnValue({
517+
providers: {
518+
awscloudformation: {},
519+
},
520+
});
521+
522+
await expect(validations.validateDeploymentStatus()).rejects.toMatchObject({
523+
name: 'StackNotFoundError',
524+
message: 'Root stack not found',
525+
resolution: 'Ensure the project is initialized and deployed.',
526+
});
527+
});
528+
529+
it('should throw StackNotFoundError when stack not found in CloudFormation', async () => {
530+
jest.spyOn(stateManager, 'getMeta').mockReturnValue({
531+
providers: {
532+
awscloudformation: {
533+
StackName: 'test-stack',
534+
},
535+
},
536+
});
537+
538+
mockSend.mockResolvedValue({ Stacks: [] });
539+
540+
await expect(validations.validateDeploymentStatus()).rejects.toMatchObject({
541+
name: 'StackNotFoundError',
542+
message: 'Stack test-stack not found in CloudFormation',
543+
resolution: 'Ensure the project is deployed.',
544+
});
545+
});
546+
547+
it('should pass when stack status is UPDATE_COMPLETE', async () => {
548+
jest.spyOn(stateManager, 'getMeta').mockReturnValue({
549+
providers: {
550+
awscloudformation: {
551+
StackName: 'test-stack',
552+
},
553+
},
554+
});
555+
556+
mockSend.mockResolvedValue({
557+
Stacks: [{ StackStatus: 'UPDATE_COMPLETE' }],
558+
});
559+
560+
await expect(validations.validateDeploymentStatus()).resolves.not.toThrow();
561+
});
562+
563+
it('should pass when stack status is CREATE_COMPLETE', async () => {
564+
jest.spyOn(stateManager, 'getMeta').mockReturnValue({
565+
providers: {
566+
awscloudformation: {
567+
StackName: 'test-stack',
568+
},
569+
},
570+
});
571+
572+
mockSend.mockResolvedValue({
573+
Stacks: [{ StackStatus: 'CREATE_COMPLETE' }],
574+
});
575+
576+
await expect(validations.validateDeploymentStatus()).resolves.not.toThrow();
577+
});
578+
579+
it('should throw StackStateError when status is UPDATE_IN_PROGRESS', async () => {
580+
jest.spyOn(stateManager, 'getMeta').mockReturnValue({
581+
providers: {
582+
awscloudformation: {
583+
StackName: 'test-stack',
584+
},
585+
},
586+
});
587+
588+
mockSend.mockResolvedValue({
589+
Stacks: [{ StackStatus: 'UPDATE_IN_PROGRESS' }],
590+
});
591+
592+
await expect(validations.validateDeploymentStatus()).rejects.toMatchObject({
593+
name: 'StackStateError',
594+
message: 'Root stack status is UPDATE_IN_PROGRESS, expected UPDATE_COMPLETE or CREATE_COMPLETE',
595+
resolution: 'Complete the deployment before proceeding.',
596+
});
597+
});
598+
599+
it('should throw StackStateError when status is ROLLBACK_COMPLETE', async () => {
600+
jest.spyOn(stateManager, 'getMeta').mockReturnValue({
601+
providers: {
602+
awscloudformation: {
603+
StackName: 'test-stack',
604+
},
605+
},
606+
});
607+
608+
mockSend.mockResolvedValue({
609+
Stacks: [{ StackStatus: 'ROLLBACK_COMPLETE' }],
610+
});
611+
612+
await expect(validations.validateDeploymentStatus()).rejects.toMatchObject({
613+
name: 'StackStateError',
614+
message: 'Root stack status is ROLLBACK_COMPLETE, expected UPDATE_COMPLETE or CREATE_COMPLETE',
615+
resolution: 'Complete the deployment before proceeding.',
616+
});
617+
});
618+
});
494619
});

packages/amplify-cli/src/commands/gen2-migration/_validations.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { AmplifyDriftDetector } from '../drift';
2-
import { $TSContext, AmplifyError } from '@aws-amplify/amplify-cli-core';
2+
import { $TSContext, AmplifyError, stateManager } from '@aws-amplify/amplify-cli-core';
33
import { printer } from '@aws-amplify/amplify-prompts';
4-
import { CloudFormationClient, DescribeChangeSetOutput, DescribeStackResourcesCommand } from '@aws-sdk/client-cloudformation';
4+
import {
5+
DescribeChangeSetOutput,
6+
CloudFormationClient,
7+
DescribeStacksCommand,
8+
DescribeStackResourcesCommand,
9+
} from '@aws-sdk/client-cloudformation';
510
import { STATEFUL_RESOURCES } from './stateful-resources';
611

712
export class AmplifyGen2MigrationValidations {
@@ -16,7 +21,35 @@ export class AmplifyGen2MigrationValidations {
1621
}
1722

1823
public async validateDeploymentStatus(): Promise<void> {
19-
printer.warn('Not implemented');
24+
const amplifyMeta = stateManager.getMeta();
25+
const stackName = amplifyMeta?.providers?.awscloudformation?.StackName;
26+
27+
if (!stackName) {
28+
throw new AmplifyError('StackNotFoundError', {
29+
message: 'Root stack not found',
30+
resolution: 'Ensure the project is initialized and deployed.',
31+
});
32+
}
33+
34+
const cfnClient = new CloudFormationClient({});
35+
const response = await cfnClient.send(new DescribeStacksCommand({ StackName: stackName }));
36+
37+
if (!response.Stacks || response.Stacks.length === 0) {
38+
throw new AmplifyError('StackNotFoundError', {
39+
message: `Stack ${stackName} not found in CloudFormation`,
40+
resolution: 'Ensure the project is deployed.',
41+
});
42+
}
43+
44+
const stackStatus = response.Stacks[0].StackStatus;
45+
const validStatuses = ['UPDATE_COMPLETE', 'CREATE_COMPLETE'];
46+
47+
if (!validStatuses.includes(stackStatus)) {
48+
throw new AmplifyError('StackStateError', {
49+
message: `Root stack status is ${stackStatus}, expected ${validStatuses.join(' or ')}`,
50+
resolution: 'Complete the deployment before proceeding.',
51+
});
52+
}
2053
}
2154

2255
public async validateDeploymentVersion(): Promise<void> {

0 commit comments

Comments
 (0)