From 39777e876e7179a1bc69ac824c5c34cd64aa53ad Mon Sep 17 00:00:00 2001 From: notgitika Date: Wed, 20 May 2026 00:46:55 -0400 Subject: [PATCH] fix(cdk): replace async entry point with synchronous reads CDK CLI's autoSynth fires on process 'beforeExit' before async main() resolves, causing cdk ls/synth/deploy to produce zero stacks with exit code 0. All config reads are now synchronous (fs.readFileSync) so stacks are registered on the App before the CDK CLI reads the cloud assembly. Closes #821 --- .../assets.snapshot.test.ts.snap | 206 +++++++++--------- src/assets/cdk/bin/cdk.ts | 196 ++++++++--------- 2 files changed, 201 insertions(+), 201 deletions(-) diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 520b4a57d..c1070bf22 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -45,11 +45,15 @@ agentcore status # checks deployment status exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/bin/cdk.ts should match snapshot 1`] = ` "#!/usr/bin/env node import { AgentCoreStack } from '../lib/cdk-stack'; -import { ConfigIO, type AwsDeploymentTarget } from '@aws/agentcore-cdk'; +import type { AgentCoreProjectSpec, AwsDeploymentTarget } from '@aws/agentcore-cdk'; import { App, type Environment } from 'aws-cdk-lib'; import * as path from 'path'; import * as fs from 'fs'; +// CDK CLI's autoSynth uses process.on('beforeExit') which fires before async +// functions resolve. All reads must be synchronous to ensure stacks are +// registered on the App before the CDK CLI reads the cloud assembly. + function toEnvironment(target: AwsDeploymentTarget): Environment { return { account: target.account, @@ -65,115 +69,111 @@ function toStackName(projectName: string, targetName: string): string { return \`AgentCore-\${sanitize(projectName)}-\${sanitize(targetName)}\`; } -async function main() { - // Config root is parent of cdk/ directory. The CLI sets process.cwd() to agentcore/cdk/. - const configRoot = path.resolve(process.cwd(), '..'); - const configIO = new ConfigIO({ baseDir: configRoot }); - - const spec = await configIO.readProjectSpec(); - const targets = await configIO.readAWSDeploymentTargets(); - - // Extract MCP configuration from project spec. - // Gateway fields are stored in agentcore.json but may not yet be on the - // AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them - // dynamically and cast the resulting object. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const specAny = spec as any; - const mcpSpec = specAny.agentCoreGateways?.length - ? { - agentCoreGateways: specAny.agentCoreGateways, - mcpRuntimeTools: specAny.mcpRuntimeTools, - unassignedTargets: specAny.unassignedTargets, - } - : undefined; - - // Read deployed state for credential ARNs (populated by pre-deploy identity setup) - let deployedState: Record | undefined; - try { - deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8')); - } catch { - // Deployed state may not exist on first deploy - } - - if (targets.length === 0) { - throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json'); - } - - // Read harness configs for role creation. - // Harness fields may not yet be on the AgentCoreProjectSpec type from @aws/agentcore-cdk, - // so we read them dynamically via specAny (same pattern as gateways above). - // Harness paths in agentcore.json are relative to the project root (parent of agentcore/). - const projectRoot = path.resolve(configRoot, '..'); - const harnessConfigs: { - name: string; - executionRoleArn?: string; - memoryName?: string; - containerUri?: string; - hasDockerfile?: boolean; - dockerfile?: string; - codeLocation?: string; - tools?: { type: string; name: string }[]; - apiKeyArn?: string; - }[] = []; - for (const entry of specAny.harnesses ?? []) { - const harnessDir = path.resolve(projectRoot, entry.path); - const harnessPath = path.resolve(harnessDir, 'harness.json'); - try { - const harnessSpec = JSON.parse(fs.readFileSync(harnessPath, 'utf-8')); - harnessConfigs.push({ - name: entry.name, - executionRoleArn: harnessSpec.executionRoleArn, - memoryName: harnessSpec.memory?.name, - containerUri: harnessSpec.containerUri, - hasDockerfile: !!harnessSpec.dockerfile, - dockerfile: harnessSpec.dockerfile, - codeLocation: harnessSpec.dockerfile ? harnessDir : undefined, - tools: harnessSpec.tools, - apiKeyArn: harnessSpec.model?.apiKeyArn, - }); - } catch (err) { - throw new Error( - \`Could not read harness.json for "\${entry.name}" at \${harnessPath}: \${err instanceof Error ? err.message : err}\` - ); +// Config root is parent of cdk/ directory. The CLI sets process.cwd() to agentcore/cdk/. +const configRoot = path.resolve(process.cwd(), '..'); +const projectRoot = path.resolve(configRoot, '..'); + +const spec: AgentCoreProjectSpec = JSON.parse( + fs.readFileSync(path.join(configRoot, 'agentcore.json'), 'utf-8') +); +const targets: AwsDeploymentTarget[] = JSON.parse( + fs.readFileSync(path.join(configRoot, 'aws-targets.json'), 'utf-8') +); + +// Extract MCP configuration from project spec. +// Gateway fields are stored in agentcore.json but may not yet be on the +// AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them +// dynamically and cast the resulting object. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const specAny = spec as any; +const mcpSpec = specAny.agentCoreGateways?.length + ? { + agentCoreGateways: specAny.agentCoreGateways, + mcpRuntimeTools: specAny.mcpRuntimeTools, + unassignedTargets: specAny.unassignedTargets, } - } + : undefined; + +// Read deployed state for credential ARNs (populated by pre-deploy identity setup) +let deployedState: Record | undefined; +try { + deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8')); +} catch { + // Deployed state may not exist on first deploy +} - const app = new App(); - - for (const target of targets) { - const env = toEnvironment(target); - const stackName = toStackName(spec.name, target.name); - - // Extract credentials from deployed state for this target - const targetState = (deployedState as Record)?.targets as - | Record> - | undefined; - const targetResources = targetState?.[target.name]?.resources as Record | undefined; - const credentials = targetResources?.credentials as - | Record - | undefined; - - new AgentCoreStack(app, stackName, { - spec, - mcpSpec, - credentials, - harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined, - env, - description: \`AgentCore stack for \${spec.name} deployed to \${target.name} (\${target.region})\`, - tags: { - 'agentcore:project-name': spec.name, - 'agentcore:target-name': target.name, - }, +if (targets.length === 0) { + throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json'); +} + +// Read harness configs for role creation. +// Harness fields may not yet be on the AgentCoreProjectSpec type from @aws/agentcore-cdk, +// so we read them dynamically via specAny (same pattern as gateways above). +// Harness paths in agentcore.json are relative to the project root (parent of agentcore/). +const harnessConfigs: { + name: string; + executionRoleArn?: string; + memoryName?: string; + containerUri?: string; + hasDockerfile?: boolean; + dockerfile?: string; + codeLocation?: string; + tools?: { type: string; name: string }[]; + apiKeyArn?: string; +}[] = []; +for (const entry of specAny.harnesses ?? []) { + const harnessDir = path.resolve(projectRoot, entry.path); + const harnessPath = path.resolve(harnessDir, 'harness.json'); + try { + const harnessSpec = JSON.parse(fs.readFileSync(harnessPath, 'utf-8')); + harnessConfigs.push({ + name: entry.name, + executionRoleArn: harnessSpec.executionRoleArn, + memoryName: harnessSpec.memory?.name, + containerUri: harnessSpec.containerUri, + hasDockerfile: !!harnessSpec.dockerfile, + dockerfile: harnessSpec.dockerfile, + codeLocation: harnessSpec.dockerfile ? harnessDir : undefined, + tools: harnessSpec.tools, + apiKeyArn: harnessSpec.model?.apiKeyArn, }); + } catch (err) { + throw new Error( + \`Could not read harness.json for "\${entry.name}" at \${harnessPath}: \${err instanceof Error ? err.message : err}\` + ); } +} - app.synth(); +const app = new App(); + +for (const target of targets) { + const env = toEnvironment(target); + const stackName = toStackName(spec.name, target.name); + + // Extract credentials from deployed state for this target + const targetState = (deployedState as Record)?.targets as + | Record> + | undefined; + const targetResources = targetState?.[target.name]?.resources as Record | undefined; + const credentials = targetResources?.credentials as + | Record + | undefined; + + new AgentCoreStack(app, stackName, { + spec, + mcpSpec, + credentials, + harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined, + env, + description: \`AgentCore stack for \${spec.name} deployed to \${target.name} (\${target.region})\`, + tags: { + 'agentcore:project-name': spec.name, + 'agentcore:target-name': target.name, + }, + }); } -main().catch((error: unknown) => { - console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error); - process.exitCode = 1; -}); +app.synth(); " `; diff --git a/src/assets/cdk/bin/cdk.ts b/src/assets/cdk/bin/cdk.ts index 1c010e19b..9234861b7 100644 --- a/src/assets/cdk/bin/cdk.ts +++ b/src/assets/cdk/bin/cdk.ts @@ -1,10 +1,14 @@ #!/usr/bin/env node import { AgentCoreStack } from '../lib/cdk-stack'; -import { ConfigIO, type AwsDeploymentTarget } from '@aws/agentcore-cdk'; +import type { AgentCoreProjectSpec, AwsDeploymentTarget } from '@aws/agentcore-cdk'; import { App, type Environment } from 'aws-cdk-lib'; import * as path from 'path'; import * as fs from 'fs'; +// CDK CLI's autoSynth uses process.on('beforeExit') which fires before async +// functions resolve. All reads must be synchronous to ensure stacks are +// registered on the App before the CDK CLI reads the cloud assembly. + function toEnvironment(target: AwsDeploymentTarget): Environment { return { account: target.account, @@ -20,112 +24,108 @@ function toStackName(projectName: string, targetName: string): string { return `AgentCore-${sanitize(projectName)}-${sanitize(targetName)}`; } -async function main() { - // Config root is parent of cdk/ directory. The CLI sets process.cwd() to agentcore/cdk/. - const configRoot = path.resolve(process.cwd(), '..'); - const configIO = new ConfigIO({ baseDir: configRoot }); +// Config root is parent of cdk/ directory. The CLI sets process.cwd() to agentcore/cdk/. +const configRoot = path.resolve(process.cwd(), '..'); +const projectRoot = path.resolve(configRoot, '..'); - const spec = await configIO.readProjectSpec(); - const targets = await configIO.readAWSDeploymentTargets(); +const spec: AgentCoreProjectSpec = JSON.parse( + fs.readFileSync(path.join(configRoot, 'agentcore.json'), 'utf-8') +); +const targets: AwsDeploymentTarget[] = JSON.parse( + fs.readFileSync(path.join(configRoot, 'aws-targets.json'), 'utf-8') +); - // Extract MCP configuration from project spec. - // Gateway fields are stored in agentcore.json but may not yet be on the - // AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them - // dynamically and cast the resulting object. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const specAny = spec as any; - const mcpSpec = specAny.agentCoreGateways?.length - ? { - agentCoreGateways: specAny.agentCoreGateways, - mcpRuntimeTools: specAny.mcpRuntimeTools, - unassignedTargets: specAny.unassignedTargets, - } - : undefined; +// Extract MCP configuration from project spec. +// Gateway fields are stored in agentcore.json but may not yet be on the +// AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them +// dynamically and cast the resulting object. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const specAny = spec as any; +const mcpSpec = specAny.agentCoreGateways?.length + ? { + agentCoreGateways: specAny.agentCoreGateways, + mcpRuntimeTools: specAny.mcpRuntimeTools, + unassignedTargets: specAny.unassignedTargets, + } + : undefined; - // Read deployed state for credential ARNs (populated by pre-deploy identity setup) - let deployedState: Record | undefined; - try { - deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8')); - } catch { - // Deployed state may not exist on first deploy - } +// Read deployed state for credential ARNs (populated by pre-deploy identity setup) +let deployedState: Record | undefined; +try { + deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8')); +} catch { + // Deployed state may not exist on first deploy +} - if (targets.length === 0) { - throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json'); - } +if (targets.length === 0) { + throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json'); +} - // Read harness configs for role creation. - // Harness fields may not yet be on the AgentCoreProjectSpec type from @aws/agentcore-cdk, - // so we read them dynamically via specAny (same pattern as gateways above). - // Harness paths in agentcore.json are relative to the project root (parent of agentcore/). - const projectRoot = path.resolve(configRoot, '..'); - const harnessConfigs: { - name: string; - executionRoleArn?: string; - memoryName?: string; - containerUri?: string; - hasDockerfile?: boolean; - dockerfile?: string; - codeLocation?: string; - tools?: { type: string; name: string }[]; - apiKeyArn?: string; - }[] = []; - for (const entry of specAny.harnesses ?? []) { - const harnessDir = path.resolve(projectRoot, entry.path); - const harnessPath = path.resolve(harnessDir, 'harness.json'); - try { - const harnessSpec = JSON.parse(fs.readFileSync(harnessPath, 'utf-8')); - harnessConfigs.push({ - name: entry.name, - executionRoleArn: harnessSpec.executionRoleArn, - memoryName: harnessSpec.memory?.name, - containerUri: harnessSpec.containerUri, - hasDockerfile: !!harnessSpec.dockerfile, - dockerfile: harnessSpec.dockerfile, - codeLocation: harnessSpec.dockerfile ? harnessDir : undefined, - tools: harnessSpec.tools, - apiKeyArn: harnessSpec.model?.apiKeyArn, - }); - } catch (err) { - throw new Error( - `Could not read harness.json for "${entry.name}" at ${harnessPath}: ${err instanceof Error ? err.message : err}` - ); - } +// Read harness configs for role creation. +// Harness fields may not yet be on the AgentCoreProjectSpec type from @aws/agentcore-cdk, +// so we read them dynamically via specAny (same pattern as gateways above). +// Harness paths in agentcore.json are relative to the project root (parent of agentcore/). +const harnessConfigs: { + name: string; + executionRoleArn?: string; + memoryName?: string; + containerUri?: string; + hasDockerfile?: boolean; + dockerfile?: string; + codeLocation?: string; + tools?: { type: string; name: string }[]; + apiKeyArn?: string; +}[] = []; +for (const entry of specAny.harnesses ?? []) { + const harnessDir = path.resolve(projectRoot, entry.path); + const harnessPath = path.resolve(harnessDir, 'harness.json'); + try { + const harnessSpec = JSON.parse(fs.readFileSync(harnessPath, 'utf-8')); + harnessConfigs.push({ + name: entry.name, + executionRoleArn: harnessSpec.executionRoleArn, + memoryName: harnessSpec.memory?.name, + containerUri: harnessSpec.containerUri, + hasDockerfile: !!harnessSpec.dockerfile, + dockerfile: harnessSpec.dockerfile, + codeLocation: harnessSpec.dockerfile ? harnessDir : undefined, + tools: harnessSpec.tools, + apiKeyArn: harnessSpec.model?.apiKeyArn, + }); + } catch (err) { + throw new Error( + `Could not read harness.json for "${entry.name}" at ${harnessPath}: ${err instanceof Error ? err.message : err}` + ); } +} - const app = new App(); - - for (const target of targets) { - const env = toEnvironment(target); - const stackName = toStackName(spec.name, target.name); +const app = new App(); - // Extract credentials from deployed state for this target - const targetState = (deployedState as Record)?.targets as - | Record> - | undefined; - const targetResources = targetState?.[target.name]?.resources as Record | undefined; - const credentials = targetResources?.credentials as - | Record - | undefined; +for (const target of targets) { + const env = toEnvironment(target); + const stackName = toStackName(spec.name, target.name); - new AgentCoreStack(app, stackName, { - spec, - mcpSpec, - credentials, - harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined, - env, - description: `AgentCore stack for ${spec.name} deployed to ${target.name} (${target.region})`, - tags: { - 'agentcore:project-name': spec.name, - 'agentcore:target-name': target.name, - }, - }); - } + // Extract credentials from deployed state for this target + const targetState = (deployedState as Record)?.targets as + | Record> + | undefined; + const targetResources = targetState?.[target.name]?.resources as Record | undefined; + const credentials = targetResources?.credentials as + | Record + | undefined; - app.synth(); + new AgentCoreStack(app, stackName, { + spec, + mcpSpec, + credentials, + harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined, + env, + description: `AgentCore stack for ${spec.name} deployed to ${target.name} (${target.region})`, + tags: { + 'agentcore:project-name': spec.name, + 'agentcore:target-name': target.name, + }, + }); } -main().catch((error: unknown) => { - console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error); - process.exitCode = 1; -}); +app.synth();