Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/cli/commands/deploy/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
synthesizeCdk,
validateProject,
} from '../../operations/deploy';
import { formatTargetStatus, getGatewayTargetStatuses } from '../../operations/deploy/gateway-status';
import type { DeployResult } from './types';

export interface ValidatedDeployOptions {
Expand Down Expand Up @@ -316,12 +317,20 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
);
await configIO.writeDeployedState(deployedState);

// Show gateway URLs if any were deployed
// Show gateway URLs and target sync status
if (Object.keys(gateways).length > 0) {
const gatewayUrls = Object.entries(gateways)
.map(([name, gateway]) => `${name}: ${gateway.gatewayArn}`)
.join(', ');
logger.log(`Gateway URLs: ${gatewayUrls}`);

// Query target sync statuses (non-blocking)
for (const [, gateway] of Object.entries(gateways)) {
const statuses = await getGatewayTargetStatuses(gateway.gatewayId, target.region);
for (const targetStatus of statuses) {
logger.log(` ${targetStatus.name}: ${formatTargetStatus(targetStatus.status)}`);
}
}
}

endStep('success');
Expand Down
83 changes: 83 additions & 0 deletions src/cli/operations/deploy/__tests__/gateway-status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { formatTargetStatus, getGatewayTargetStatuses } from '../gateway-status.js';
import { afterEach, describe, expect, it, vi } from 'vitest';

const { mockSend } = vi.hoisted(() => ({
mockSend: vi.fn(),
}));

vi.mock('@aws-sdk/client-bedrock-agentcore-control', () => ({
BedrockAgentCoreControlClient: class {
send = mockSend;
},
ListGatewayTargetsCommand: class {
constructor(public input: unknown) {}
},
}));

describe('getGatewayTargetStatuses', () => {
afterEach(() => vi.clearAllMocks());

it('returns statuses for all targets', async () => {
mockSend.mockResolvedValue({
items: [
{ name: 'target-1', status: 'READY' },
{ name: 'target-2', status: 'SYNCHRONIZING' },
{ name: 'target-3', status: 'READY' },
],
});

const result = await getGatewayTargetStatuses('gw-123', 'us-east-1');

expect(result).toEqual([
{ name: 'target-1', status: 'READY' },
{ name: 'target-2', status: 'SYNCHRONIZING' },
{ name: 'target-3', status: 'READY' },
]);
});

it('returns empty array on API error', async () => {
mockSend.mockRejectedValue(new Error('Access denied'));

const result = await getGatewayTargetStatuses('gw-123', 'us-east-1');

expect(result).toEqual([]);
});

it('returns empty array when no targets', async () => {
mockSend.mockResolvedValue({ items: [] });

const result = await getGatewayTargetStatuses('gw-123', 'us-east-1');

expect(result).toEqual([]);
});

it('handles undefined items', async () => {
mockSend.mockResolvedValue({});

const result = await getGatewayTargetStatuses('gw-123', 'us-east-1');

expect(result).toEqual([]);
});
});

describe('formatTargetStatus', () => {
it('formats READY', () => {
expect(formatTargetStatus('READY')).toBe('✓ synced');
});

it('formats SYNCHRONIZING', () => {
expect(formatTargetStatus('SYNCHRONIZING')).toBe('⟳ syncing...');
});

it('formats SYNCHRONIZE_UNSUCCESSFUL', () => {
expect(formatTargetStatus('SYNCHRONIZE_UNSUCCESSFUL')).toBe('⚠ sync failed');
});

it('formats FAILED', () => {
expect(formatTargetStatus('FAILED')).toBe('✗ failed');
});

it('returns raw status for unknown values', () => {
expect(formatTargetStatus('UNKNOWN_STATUS')).toBe('UNKNOWN_STATUS');
});
});
42 changes: 42 additions & 0 deletions src/cli/operations/deploy/gateway-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Query gateway target sync statuses after deployment.
*/
import { BedrockAgentCoreControlClient, ListGatewayTargetsCommand } from '@aws-sdk/client-bedrock-agentcore-control';

export interface TargetSyncStatus {
name: string;
status: string;
}

const STATUS_DISPLAY: Record<string, string> = {
READY: '✓ synced',
SYNCHRONIZING: '⟳ syncing...',
SYNCHRONIZE_UNSUCCESSFUL: '⚠ sync failed',
CREATING: '⟳ creating...',
UPDATING: '⟳ updating...',
UPDATE_UNSUCCESSFUL: '⚠ update failed',
FAILED: '✗ failed',
DELETING: '⟳ deleting...',
};

export function formatTargetStatus(status: string): string {
return STATUS_DISPLAY[status] ?? status;
}

/**
* Get sync statuses for all targets in a gateway.
* Returns empty array on error (non-blocking).
*/
export async function getGatewayTargetStatuses(gatewayId: string, region: string): Promise<TargetSyncStatus[]> {
try {
const client = new BedrockAgentCoreControlClient({ region });
const response = await client.send(new ListGatewayTargetsCommand({ gatewayIdentifier: gatewayId, maxResults: 100 }));

return (response.items ?? []).map(target => ({
name: target.name ?? 'unknown',
status: target.status ?? 'UNKNOWN',
}));
} catch {
return [];
}
}
14 changes: 14 additions & 0 deletions src/cli/tui/screens/deploy/DeployScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ConfigIO } from '../../../../lib';
import type { AgentCoreMcpSpec } from '../../../../schema';
import { formatTargetStatus } from '../../../operations/deploy/gateway-status';
import {
AwsTargetConfigUI,
ConfirmPrompt,
Expand Down Expand Up @@ -58,6 +59,7 @@ export function DeployScreen({ isInteractive, onExit, autoConfirm, onNavigate, p
isComplete,
hasStartedCfn,
logFilePath,
targetStatuses,
missingCredentials,
startDeploy,
confirmTeardown,
Expand Down Expand Up @@ -279,6 +281,18 @@ export function DeployScreen({ isInteractive, onExit, autoConfirm, onNavigate, p
</Box>
)}

{allSuccess && targetStatuses.length > 0 && (
<Box flexDirection="column" marginTop={1}>
<Text bold>Gateway Targets:</Text>
{targetStatuses.map(t => (
<Text key={t.name}>
{' '}
{t.name}: {formatTargetStatus(t.status)}
</Text>
))}
</Box>
)}

{logFilePath && (
<Box marginTop={1}>
<LogLink filePath={logFilePath} />
Expand Down
14 changes: 14 additions & 0 deletions src/cli/tui/screens/deploy/useDeployFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { buildDeployedState, getStackOutputs, parseAgentOutputs, parseGatewayOut
import { getErrorMessage, isChangesetInProgressError, isExpiredTokenError } from '../../../errors';
import { ExecLogger } from '../../../logging';
import { performStackTeardown } from '../../../operations/deploy';
import { getGatewayTargetStatuses } from '../../../operations/deploy/gateway-status';
import { type Step, areStepsComplete, hasStepError } from '../../components';
import { type MissingCredential, type PreflightContext, useCdkPreflight } from '../../hooks';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
Expand Down Expand Up @@ -45,6 +46,7 @@ interface DeployFlowState {
deployOutput: string | null;
deployMessages: DeployMessage[];
stackOutputs: Record<string, string>;
targetStatuses: { name: string; status: string }[];
hasError: boolean;
/** True if the error is specifically due to expired/invalid AWS credentials */
hasTokenExpiredError: boolean;
Expand Down Expand Up @@ -96,6 +98,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
const [deployOutput, setDeployOutput] = useState<string | null>(null);
const [deployMessages, setDeployMessages] = useState<DeployMessage[]>([]);
const [stackOutputs, setStackOutputs] = useState<Record<string, string>>({});
const [targetStatuses, setTargetStatuses] = useState<{ name: string; status: string }[]>([]);
const [shouldStartDeploy, setShouldStartDeploy] = useState(false);
const [hasTokenExpiredError, setHasTokenExpiredError] = useState(false);
// Track if CloudFormation has started (received first resource event)
Expand Down Expand Up @@ -196,6 +199,16 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
Object.keys(oauthCredentials).length > 0 ? oauthCredentials : undefined
);
await configIO.writeDeployedState(deployedState);

// Query gateway target sync statuses (non-blocking)
const allStatuses: { name: string; status: string }[] = [];
for (const [, gateway] of Object.entries(gateways)) {
const statuses = await getGatewayTargetStatuses(gateway.gatewayId, target.region);
allStatuses.push(...statuses);
}
if (allStatuses.length > 0) {
setTargetStatuses(allStatuses);
}
}, [context, stackNames, logger, identityKmsKeyArn, oauthCredentials]);

// Start deploy when preflight completes OR when shouldStartDeploy is set
Expand Down Expand Up @@ -400,6 +413,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
deployOutput,
deployMessages,
stackOutputs,
targetStatuses,
hasError,
hasTokenExpiredError: combinedTokenExpiredError,
hasCredentialsError: preflight.hasCredentialsError,
Expand Down
Loading