diff --git a/src/cli/tui/components/DeployStatus.tsx b/src/cli/tui/components/DeployStatus.tsx
index 837109a46..277168a64 100644
--- a/src/cli/tui/components/DeployStatus.tsx
+++ b/src/cli/tui/components/DeployStatus.tsx
@@ -66,7 +66,13 @@ type ResourceStatus =
| 'UPDATE_FAILED'
| 'DELETE_IN_PROGRESS'
| 'DELETE_COMPLETE'
- | 'DELETE_FAILED';
+ | 'DELETE_FAILED'
+ | 'ROLLBACK_IN_PROGRESS'
+ | 'ROLLBACK_COMPLETE'
+ | 'ROLLBACK_FAILED'
+ | 'UPDATE_ROLLBACK_IN_PROGRESS'
+ | 'UPDATE_ROLLBACK_COMPLETE'
+ | 'UPDATE_ROLLBACK_FAILED';
interface ParsedResource {
resourceType: string;
@@ -75,8 +81,16 @@ interface ParsedResource {
/**
* Get color for a resource status.
+ *
+ * Rollback states are checked first: in CloudFormation a ROLLBACK indicates a
+ * failed deploy whose changes are being reverted, so even a "clean" finish
+ * (ROLLBACK_COMPLETE / UPDATE_ROLLBACK_COMPLETE) is a failure, not a success.
+ * Coloring those green would mislead the user (see #1610). A failed rollback is
+ * the worst case (red); an in-progress or completed rollback signals
+ * "recovering from failure" (yellow).
*/
function getStatusColor(status: ResourceStatus): string | undefined {
+ if (status.includes('ROLLBACK')) return status.endsWith('_FAILED') ? 'red' : 'yellow';
if (status.endsWith('_COMPLETE')) return 'green';
if (status.endsWith('_FAILED')) return 'red';
if (status.endsWith('_IN_PROGRESS')) return 'cyan';
@@ -103,7 +117,7 @@ function parseResourceMessage(msg: DeployMessage): ParsedResource | null {
// Format: "StackName | STATUS | AWS::Service::Resource | LogicalId"
const resourceMatch = /(AWS::\S+)/.exec(text);
const statusMatch =
- /(CREATE_IN_PROGRESS|CREATE_COMPLETE|CREATE_FAILED|UPDATE_IN_PROGRESS|UPDATE_COMPLETE|UPDATE_FAILED|DELETE_IN_PROGRESS|DELETE_COMPLETE|DELETE_FAILED)/.exec(
+ /(UPDATE_ROLLBACK_IN_PROGRESS|UPDATE_ROLLBACK_COMPLETE|UPDATE_ROLLBACK_FAILED|ROLLBACK_IN_PROGRESS|ROLLBACK_COMPLETE|ROLLBACK_FAILED|CREATE_IN_PROGRESS|CREATE_COMPLETE|CREATE_FAILED|UPDATE_IN_PROGRESS|UPDATE_COMPLETE|UPDATE_FAILED|DELETE_IN_PROGRESS|DELETE_COMPLETE|DELETE_FAILED)/.exec(
text
);
diff --git a/src/cli/tui/components/__tests__/DeployStatus.test.tsx b/src/cli/tui/components/__tests__/DeployStatus.test.tsx
index 63aaa26e2..5b18b2e14 100644
--- a/src/cli/tui/components/__tests__/DeployStatus.test.tsx
+++ b/src/cli/tui/components/__tests__/DeployStatus.test.tsx
@@ -3,7 +3,17 @@ import { DeployStatus } from '../DeployStatus.js';
import { render } from 'ink-testing-library';
import React from 'react';
import stripAnsi from 'strip-ansi';
-import { describe, expect, it } from 'vitest';
+import { describe, expect, it, vi } from 'vitest';
+
+// Force ink/chalk to emit ANSI color codes so the status color-coding tests are
+// deterministic regardless of TTY/CI. vi.hoisted is lifted above the ink import
+// by vitest, so FORCE_COLOR is set before ink evaluates its color support.
+vi.hoisted(() => {
+ process.env.FORCE_COLOR = '1';
+});
+
+// ink/chalk ANSI foreground color codes.
+const ANSI = { green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', cyan: '\x1b[36m' } as const;
function makeMsg(
message: string,
@@ -95,6 +105,21 @@ describe('DeployStatus', () => {
expect(lastFrame()).not.toContain('Some general info');
});
+ it('renders ROLLBACK statuses instead of dropping the events', () => {
+ const messages = [
+ makeResourceMsg('CloudFormation::Stack', 'ROLLBACK_IN_PROGRESS'),
+ makeResourceMsg('BedrockAgentCore::Gateway', 'UPDATE_ROLLBACK_IN_PROGRESS'),
+ makeResourceMsg('CloudFormation::Stack', 'ROLLBACK_FAILED'),
+ ];
+
+ const { lastFrame } = render();
+ const frame = lastFrame()!;
+
+ expect(frame).toContain('ROLLBACK_IN_PROGRESS');
+ expect(frame).toContain('UPDATE_ROLLBACK_IN_PROGRESS');
+ expect(frame).toContain('ROLLBACK_FAILED');
+ });
+
it('shows only last 8 resource events', () => {
const messages = Array.from({ length: 12 }, (_, i) =>
makeResourceMsg(`Service::Resource${i}`, 'CREATE_COMPLETE')
@@ -112,6 +137,60 @@ describe('DeployStatus', () => {
});
});
+ describe('status color coding', () => {
+ // Returns the ANSI color code wrapping the line that contains the status, e.g.
+ // for "...\x1b[33mService::Resource ROLLBACK_COMPLETE\x1b[39m" -> "\x1b[33m".
+ // A single resource line is rendered as one colored Text node, so the opening
+ // code is the last ANSI escape before the status word on that line.
+ function colorOf(frame: string, status: string): string | undefined {
+ const line = frame.split('\n').find(l => l.includes(status));
+ if (!line) return undefined;
+ const before = line.slice(0, line.indexOf(status));
+ // eslint-disable-next-line no-control-regex
+ const codes = before.match(/\x1b\[\d+m/g);
+ return codes?.[codes.length - 1];
+ }
+
+ it('renders CREATE_COMPLETE green (sanity check)', () => {
+ const messages = [makeResourceMsg('Lambda::Function', 'CREATE_COMPLETE')];
+ const { lastFrame } = render();
+
+ expect(colorOf(lastFrame()!, 'CREATE_COMPLETE')).toBe(ANSI.green);
+ });
+
+ it('does NOT render ROLLBACK_COMPLETE green — it is a failed deploy, not a success (#1610)', () => {
+ const messages = [makeResourceMsg('CloudFormation::Stack', 'ROLLBACK_COMPLETE')];
+ const { lastFrame } = render();
+
+ const color = colorOf(lastFrame()!, 'ROLLBACK_COMPLETE');
+ expect(color).not.toBe(ANSI.green);
+ expect(color).toBe(ANSI.yellow);
+ });
+
+ it('does NOT render UPDATE_ROLLBACK_COMPLETE green', () => {
+ const messages = [makeResourceMsg('BedrockAgentCore::Gateway', 'UPDATE_ROLLBACK_COMPLETE')];
+ const { lastFrame } = render();
+
+ const color = colorOf(lastFrame()!, 'UPDATE_ROLLBACK_COMPLETE');
+ expect(color).not.toBe(ANSI.green);
+ expect(color).toBe(ANSI.yellow);
+ });
+
+ it('renders rollback in-progress states yellow (recovering from failure)', () => {
+ const messages = [makeResourceMsg('CloudFormation::Stack', 'ROLLBACK_IN_PROGRESS')];
+ const { lastFrame } = render();
+
+ expect(colorOf(lastFrame()!, 'ROLLBACK_IN_PROGRESS')).toBe(ANSI.yellow);
+ });
+
+ it('renders ROLLBACK_FAILED red (worst case)', () => {
+ const messages = [makeResourceMsg('CloudFormation::Stack', 'ROLLBACK_FAILED')];
+ const { lastFrame } = render();
+
+ expect(colorOf(lastFrame()!, 'ROLLBACK_FAILED')).toBe(ANSI.red);
+ });
+ });
+
describe('progress bar', () => {
it('renders progress bar with completed/total count', () => {
const messages = [makeMsg('deploying', 'CDK_TOOLKIT_I5502', { completed: 3, total: 10 })];