Skip to content
Open
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
18 changes: 9 additions & 9 deletions src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -636,18 +636,18 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/package.json should m
"format:check": "prettier --check ."
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^24.10.1",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"aws-cdk": "2.1126.0",
"prettier": "^3.4.2",
"@types/jest": "~29.5.14",
"@types/node": "~24.13.3",
"jest": "~29.7.0",
"ts-jest": "~29.4.11",
"aws-cdk": "~2.1126.0",
"prettier": "~3.9.5",
"typescript": "~5.9.3"
},
"dependencies": {
"@aws/agentcore-cdk": "^0.1.0-alpha.19",
"aws-cdk-lib": "^2.248.0",
"constructs": "^10.0.0"
"@aws/agentcore-cdk": "0.1.0-alpha.45",
"aws-cdk-lib": "~2.261.0",
"constructs": "~10.7.0"
}
}
"
Expand Down
18 changes: 9 additions & 9 deletions src/assets/cdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@
"format:check": "prettier --check ."
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^24.10.1",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"aws-cdk": "2.1126.0",
"prettier": "^3.4.2",
"@types/jest": "~29.5.14",
"@types/node": "~24.13.3",
"jest": "~29.7.0",
"ts-jest": "~29.4.11",
"aws-cdk": "~2.1126.0",
"prettier": "~3.9.5",
"typescript": "~5.9.3"
},
"dependencies": {
"@aws/agentcore-cdk": "^0.1.0-alpha.19",
"aws-cdk-lib": "^2.248.0",
"constructs": "^10.0.0"
"@aws/agentcore-cdk": "0.1.0-alpha.45",
"aws-cdk-lib": "~2.261.0",
"constructs": "~10.7.0"
}
}
78 changes: 73 additions & 5 deletions src/cli/commands/deploy/actions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { ConfigIO, ResourceNotFoundError, SecureCredentials, ValidationError, toError } from '../../../lib';
import {
ConfigIO,
DependencySyncError,
ResourceNotFoundError,
SecureCredentials,
ValidationError,
toError,
} from '../../../lib';
import type { DependencySyncResult } from '../../../lib/dependency-management';
import type { Result } from '../../../lib/result';
import type { AgentCoreMcpSpec, DeployedState } from '../../../schema';
import { applyTargetRegionToEnv } from '../../aws';
Expand Down Expand Up @@ -26,13 +34,15 @@ import { getErrorMessage } from '../../errors';
import { ExecLogger } from '../../logging';
import {
MANAGED_MEMORY_DEPLOY_NOTICE,
SYNC_CDK_DEPENDENCIES_STEP,
assertEnvFileExists,
backfillContainerVpcIds,
bootstrapEnvironment,
buildCdkProject,
checkBootstrapNeeded,
checkStackDeployability,
ensureDefaultDeploymentTarget,
ensureManagedDependencies,
getAllCredentials,
hasIdentityApiProviders,
hasIdentityOAuthProviders,
Expand All @@ -42,6 +52,7 @@ import {
setupOAuth2Providers,
setupTransactionSearch,
synthesizeCdk,
teardownSyncFailureResult,
validateProject,
} from '../../operations/deploy';
import { computeProjectDeployHash } from '../../operations/deploy/change-detection';
Expand Down Expand Up @@ -168,6 +179,10 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
const logger = new ExecLogger({ command: 'deploy' });
const { onProgress } = options;
let currentStepName = '';
// Hoisted above the try so every failure result — the catch AND the explicit failure
// returns after the sync step — still carries the sync outcome: dep_sync_* telemetry and
// the user-facing rewrite notice must survive a deploy that fails after the sync ran.
let depSync: DependencySyncResult | undefined;

const startStep = (name: string) => {
currentStepName = name;
Expand Down Expand Up @@ -261,6 +276,38 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
endStep('success');
}

// Sync managed dependencies (#1540): pin agentcore/cdk/package.json to the versions this
// CLI was tested with, migrating pre-pinning (caret) projects. Must run before the build so
// the compile sees the reinstalled tree. Throws CliVersionTooOldError on newer-than-CLI skew.
// Preview modes (--dry-run / --diff) run check-only — they must never mutate the working
// tree — and teardown deploys downgrade every sync failure to a warning: skew via
// treatSkewAsWarning, and write/install failures (DependencySyncError, e.g. a broken npm
// env or a registry outage) via the catch below. Nothing about pinning may block
// destroying a cost-incurring stack — if node_modules is genuinely unusable, the build
// step fails on its own.
const isPreview = !!options.plan || !!options.diff;
startStep(SYNC_CDK_DEPENDENCIES_STEP);
try {
depSync = await ensureManagedDependencies(context.cdkProject, {
checkOnly: isPreview,
treatSkewAsWarning: context.isTeardownDeploy,
});
} catch (syncErr) {
if (!(context.isTeardownDeploy && syncErr instanceof DependencySyncError)) {
throw syncErr;
}
// The warning-only result flows through the normal depSync.warnings channel below.
depSync = teardownSyncFailureResult(syncErr);
}
for (const warning of depSync.warnings) {
logger.log(warning, 'warn');
}
if (depSync.notice) {
logger.log(depSync.notice);
options.onNotice?.(depSync.notice);
}
endStep('success');

// Build CDK project
startStep('Build CDK project');
await buildCdkProject(context.cdkProject);
Expand All @@ -274,7 +321,12 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
const envFileAssertionResult = assertEnvFileExists(context.projectSpec, configIO.getConfigRoot());
if (!envFileAssertionResult.success) {
logger.finalize(false);
return { success: false, error: envFileAssertionResult.error, logPath: logger.getRelativeLogPath() };
return {
success: false,
error: envFileAssertionResult.error,
logPath: logger.getRelativeLogPath(),
dependencySync: depSync,
};
}

// Read runtime credentials from process.env (enables non-interactive deploy with -y)
Expand Down Expand Up @@ -315,6 +367,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
success: false,
error: errorResult?.error ?? new Error('unknown error occurred when setting up api key providers'),
logPath: logger.getRelativeLogPath(),
dependencySync: depSync,
};
}
identityKmsKeyArn = identityResult.kmsKeyArn;
Expand Down Expand Up @@ -352,6 +405,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
success: false,
error: errorResult?.error ?? new Error(`an unexpected error ocurred when setting up oauth providers`),
logPath: logger.getRelativeLogPath(),
dependencySync: depSync,
};
}

Expand Down Expand Up @@ -389,6 +443,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
success: false,
error: paymentPreDeployResult.errors[0] ?? new Error('payment deploy preflight steps failed'),
logPath: logger.getRelativeLogPath(),
dependencySync: depSync,
};
}

Expand Down Expand Up @@ -420,7 +475,6 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
// process — which re-reads agentcore.json / harness.json — has the value it needs. Fresh creates
// already carry a vpcId, so this is a no-op for them. In preview modes (--dry-run / --diff) the
// write is reverted after synth via backfill.restore() so a preview leaves the working tree clean.
const isPreview = !!options.plan || !!options.diff;
const backfill = await backfillContainerVpcIds(configIO, context.projectSpec, target.region, !isPreview);
// In preview mode, revert the on-disk backfill in `finally` so every exit path (including a synth
// throw or an early return) leaves the working tree clean. restore() is a no-op on a real deploy.
Expand Down Expand Up @@ -448,6 +502,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
success: false,
error: stackSelection.error,
logPath: logger.getRelativeLogPath(),
dependencySync: depSync,
};
}
const stackName = stackSelection.stackName;
Expand All @@ -467,6 +522,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
success: false,
error: new ValidationError('AWS environment needs bootstrapping. Run with --yes to auto-bootstrap.'),
logPath: logger.getRelativeLogPath(),
dependencySync: depSync,
};
}
}
Expand All @@ -483,6 +539,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
success: false,
error: new Error(deployabilityCheck.message ?? 'Stack is not in a deployable state'),
logPath: logger.getRelativeLogPath(),
dependencySync: depSync,
};
}
endStep('success');
Expand All @@ -498,6 +555,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
targetName: target.name,
stackName,
logPath: logger.getRelativeLogPath(),
dependencySync: depSync,
};
}

Expand All @@ -516,6 +574,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
targetName: target.name,
stackName,
logPath: logger.getRelativeLogPath(),
dependencySync: depSync,
};
}

Expand Down Expand Up @@ -582,6 +641,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
success: false,
error: teardown.error,
logPath: logger.getRelativeLogPath(),
dependencySync: depSync,
};
}
endStep('success');
Expand All @@ -593,6 +653,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
targetName: target.name,
stackName,
logPath: logger.getRelativeLogPath(),
dependencySync: depSync,
};
}

Expand Down Expand Up @@ -973,7 +1034,11 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
logger.finalize(true);

// Surface orphan-harness warnings (collected pre-deploy) to the terminal alongside any
// post-deploy warnings — logger.log only writes to the log file.
// post-deploy warnings — logger.log only writes to the log file. Dependency-sync warnings
// are deliberately NOT merged here: postDeployWarnings signals partial failure (exit 2),
// while dep-sync warnings are informational (e.g. the bundled-tarball override is always
// "left unmanaged"). They reach the terminal via result.dependencySync.warnings, which the
// CLI (printDeployResult), dev path (runCliDeploy), and TUI each render.
const allWarnings = [...orphanWarnings, ...postDeployWarnings];

return {
Expand All @@ -985,11 +1050,14 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
nextSteps,
notes,
postDeployWarnings: allWarnings.length > 0 ? allWarnings : undefined,
dependencySync: depSync,
};
} catch (err: unknown) {
logger.log(getErrorMessage(err), 'error');
logger.finalize(false);
return { success: false, error: toError(err), logPath: logger.getRelativeLogPath() };
// Carry the dep-sync outcome on the failure result too, so dep_sync_* telemetry
// is recorded even when a later step throws.
return { success: false, error: toError(err), logPath: logger.getRelativeLogPath(), dependencySync: depSync };
} finally {
// Each cleanup step must run regardless of whether an earlier one fails — a throw from
// dispose() (common after a synth/bootstrap failure on a creds-less preview) must not skip the
Expand Down
25 changes: 23 additions & 2 deletions src/cli/commands/deploy/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { renderTUI } from '../../tui';
import { requireProject, requireTTY } from '../../tui/guards';
import { handleDeploy } from './actions';
import type { DeployOptions, DeployResult } from './types';
import { DEFAULT_DEPLOY_ATTRS, computeDeployAttrs } from './utils';
import { DEFAULT_DEPLOY_ATTRS, computeDeployAttrs, toDepSyncAttrs } from './utils';
import { validateDeployOptions } from './validate';
import type { Command } from '@commander-js/extra-typings';
import { Text, render } from 'ink';
Expand Down Expand Up @@ -42,10 +42,15 @@ async function handleDeployCLI(options: DeployOptions): Promise<void> {
.then(spec => computeDeployAttrs(spec, mode))
.catch(() => ({ ...DEFAULT_DEPLOY_ATTRS, mode }) as const);

const { deployResult } = await withCommandRunTelemetry('deploy', attrs, async () => {
const { deployResult } = await withCommandRunTelemetry('deploy', attrs, async recorder => {
const result = await executeDeploy(options).catch(
(e): DeployResult => ({ success: false, error: e instanceof Error ? e : new Error(getErrorMessage(e)) })
);
// Record dep_sync attrs whenever the sync ran — including on failed deploys, where the
// failure result still carries the outcome (see handleDeploy's catch).
if (result.dependencySync) {
recorder.set(toDepSyncAttrs(result.dependencySync));
}
if (!result.success) {
return { success: false as const, error: result.error, deployResult: result };
}
Expand All @@ -57,6 +62,13 @@ async function handleDeployCLI(options: DeployOptions): Promise<void> {
if (options.json) {
console.log(JSON.stringify(serializeResult(deployResult)));
} else {
// Dependency sync warnings (#1540) still matter on a failed deploy: a downgraded-skew
// warning may explain the failure itself, and printDeployResult only runs on success.
// (The sync notice is NOT re-printed here — onNotice already printed it live during the
// sync step for every non-JSON run.)
for (const warning of deployResult.dependencySync?.warnings ?? []) {
console.error(`⚠ ${warning}`);
}
console.error(deployResult.error.message);
if (deployResult.logPath) {
console.error(`Log: ${deployResult.logPath}`);
Expand Down Expand Up @@ -146,6 +158,15 @@ function printDeployResult(result: DeployResult & { success: true }, options: De
return;
}

// Managed dependency sync warnings (#1540): downgraded skew, skipped specifiers. Informational
// only — they must reach the terminal but must NOT flip the exit code to 2 (the bundled-tarball
// override in e2e/dev builds is always "left unmanaged", and exit 2 would fail every deploy).
if (result.dependencySync?.warnings && result.dependencySync.warnings.length > 0) {
for (const warning of result.dependencySync.warnings) {
console.warn(`⚠ ${warning}`);
}
}

if (options.diff) {
console.log(`\n✓ Diff complete for '${result.targetName}' (stack: ${result.stackName})`);
} else if (options.plan) {
Expand Down
9 changes: 9 additions & 0 deletions src/cli/commands/deploy/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ export async function runCliDeploy(): Promise<void> {
});
cleanup();

// Surface the managed-dependency sync outcome (#1540) — handleDeploy only writes it to the
// deploy log; this dev path passes no onNotice, so print it here.
if (result.dependencySync?.notice) {
console.log(`\n${result.dependencySync.notice}\n`);
}
for (const warning of result.dependencySync?.warnings ?? []) {
console.warn(`${ANSI.yellow}⚠ ${warning}${ANSI.reset}`);
}

if (result.success) {
console.log('Deploy complete.');
if (result.logPath) {
Expand Down
7 changes: 6 additions & 1 deletion src/cli/commands/deploy/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { DependencySyncResult } from '../../../lib/dependency-management';
import type { Result } from '../../../lib/result';

export interface DeployOptions {
Expand All @@ -17,7 +18,11 @@ export type DeployResult = Result<{
nextSteps?: string[];
notes?: string[];
postDeployWarnings?: string[];
}> & { logPath?: string };
}> & {
logPath?: string;
/** Sync outcome rides on BOTH branches so dep_sync_* telemetry survives a failed deploy. */
dependencySync?: DependencySyncResult;
};

export type PreflightResult = Result<{
stackNames?: string[];
Expand Down
15 changes: 15 additions & 0 deletions src/cli/commands/deploy/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { DependencySyncResult } from '../../../lib/dependency-management';
import type { AgentCoreProjectSpec } from '../../../schema';
import type { DeployMode } from '../../telemetry/schemas/common-shapes';

Expand All @@ -15,6 +16,20 @@ export const DEFAULT_DEPLOY_ATTRS = {
deploy_mode: 'deploy' as DeployMode,
};

/**
* Map a managed-dependency sync outcome (#1540) to its dep_sync_* telemetry attrs.
* Single source for both the CLI command (command.tsx) and the TUI flow (useDeployFlow).
*/
export function toDepSyncAttrs(sync: DependencySyncResult) {
return {
dep_sync_changed_count: sync.changes.length + sync.restored.length,
dep_sync_migrated: sync.migrated,
dep_sync_opted_out: sync.optedOut,
dep_sync_skew_warning: sync.skewWarning,
dep_sync_reinstalled: sync.reinstalled,
};
}

export function computeDeployAttrs(projectSpec: Partial<AgentCoreProjectSpec>, mode: DeployMode) {
const gateways = projectSpec.agentCoreGateways ?? [];
const policyEngines = projectSpec.policyEngines ?? [];
Expand Down
Loading
Loading