Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3253ab0
feat(project): implement project deploy
notgitika Aug 13, 2026
5ae34c8
feat(project): deploy one target per invocation
notgitika Aug 14, 2026
78348f6
refactor(project): load the CDK toolkit lazily and test the adapter
notgitika Aug 14, 2026
5e56870
fix(project): ship the toolkit's bootstrap template with the artifacts
notgitika Aug 17, 2026
5bcd05f
feat(project): log deploy's detailed output to the shared log
notgitika Aug 17, 2026
7e328ea
docs(project): shorten the comments deploy's plumbing carries
notgitika Aug 17, 2026
e822485
refactor(project): move CDK behind a project backend
notgitika Aug 17, 2026
5008322
feat(logging): file each run's log under the command that wrote it
notgitika Aug 17, 2026
aeba2db
Merge upstream/refactor into feat/project-deploy
notgitika Aug 17, 2026
d682e87
fix(logging): write one file per run, named for the run and nothing t…
notgitika Aug 17, 2026
8910f5d
fix(build): name the Windows executable what Bun writes
notgitika Aug 17, 2026
d1c1fb2
feat(project): report the outputs of the stack a deploy created
notgitika Aug 17, 2026
85ebc15
fix(logging): keep the log where the CLI's own state is
notgitika Aug 17, 2026
5412990
fix(logging): bound the log directory now that runs no longer share a…
notgitika Aug 18, 2026
9bdfeb6
revert(logging): keep the rotating log every command has written to
notgitika Aug 18, 2026
e9365ba
feat(project): write what a deploy shows to the log it points at
notgitika Aug 18, 2026
4cca59c
fix(project): bootstrap only an environment that needs it
notgitika Aug 18, 2026
569c562
fix(project): survive an unmapped toolkit log level, and catch a bad …
notgitika Aug 18, 2026
bb0ba2e
fix(project): only treat a usable bootstrap stack as bootstrapped
notgitika Aug 18, 2026
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
1,226 changes: 1,158 additions & 68 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@
"typescript": "^5"
},
"dependencies": {
"@aws-cdk/toolkit-lib": "^1.38.2",
"@aws-sdk/client-bedrock-agentcore": "^3.1092.0",
"@aws-sdk/client-bedrock-agentcore-control": "^3.1102.0",
"@aws-sdk/client-cloudformation": "^3.1092.0",
"@aws-sdk/client-cloudwatch-logs": "^3.1092.0",
"@aws-sdk/client-iam": "^3.1080.0",
"@opentelemetry/api": "^1.9.1",
Expand Down
78 changes: 71 additions & 7 deletions scripts/build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env bun

import { $ } from "bun";
import { existsSync } from "node:fs";
import { join, resolve } from "node:path";
import { runWithExitCode } from "../src/runnable";

Expand All @@ -11,6 +12,16 @@ const DIST = join(REPO_ROOT, "dist");

const ASSET_NAMING = "agentcore-assets/[dir]/[name].[ext]";

// Kept out of the bundle so the toolkit stays a real directory in node_modules at
// runtime: it reads its bootstrap template from its own package directory, which a
// bundle would rewrite to this machine's absolute path. The npm package declares it
// as a dependency, so installing the CLI installs it.
const EXTERNAL = ["@aws-cdk/toolkit-lib"];

// A compiled executable has no node_modules, so the template the toolkit would read
// from its package directory is embedded instead. See loadBootstrapTemplate.
const BOOTSTRAP_TEMPLATE = ["lib", "api", "bootstrap", "bootstrap-template.yaml"];

// Shrink whitespace/syntax but keep identifiers: minified names make stack
// traces unreadable and erase error names telemetry keys on.
const MINIFY = { whitespace: true, syntax: true, identifiers: false } as const;
Expand All @@ -26,14 +37,54 @@ function assetLoaderPlugin(): Bun.BunPlugin {
return {
name: "asset-file-loader",
setup(build) {
build.onLoad({ filter: /src[/\\]assets[/\\]/ }, async ({ path }) => ({
contents: await Bun.file(path).bytes(),
loader: "file",
}));
build.onLoad(
{ filter: /src[/\\]assets[/\\]|bootstrap-template\.yaml$/ },
async ({ path }) => ({
contents: await Bun.file(path).bytes(),
loader: "file",
}),
);
},
};
}

/**
* Absolute path of the toolkit's own bootstrap template.
*
* Resolved from the installed package rather than a copy in this repo, so the
* embedded template is always the one the toolkit being compiled in expects.
*/
function bootstrapTemplate(): string {
const manifest = Bun.resolveSync("@aws-cdk/toolkit-lib/package.json", REPO_ROOT);
const template = join(resolve(manifest, ".."), ...BOOTSTRAP_TEMPLATE);
if (!existsSync(template)) {
throw new Error(
`@aws-cdk/toolkit-lib no longer ships ${BOOTSTRAP_TEMPLATE.join("/")}; ` +
`bootstrap in a compiled executable reads the embedded copy, so this must be found. Looked in ${template}`,
);
}
return template;
}

/**
* Fail loudly unless the compiled executable carries the bootstrap template's bytes.
*
* Nothing reads that template until someone bootstraps an AWS account, so an
* executable that lost it looks healthy in every build check and fails in a user's
* first deploy instead.
*/
async function assertTemplateIsEmbedded(outfile: string, template: string): Promise<void> {
const [executable, bytes] = await Promise.all([
Bun.file(outfile).bytes(),
Bun.file(template).bytes(),
]);
if (!Buffer.from(executable).includes(bytes)) {
throw new Error(
`${outfile} does not carry ${BOOTSTRAP_TEMPLATE.join("/")}, so bootstrap would fail wherever it runs`,
);
}
}

/** Fail loudly on a non-UTF-8 asset — the source reads every asset as text. */
async function assertAssetsAreText(assets: string[]): Promise<void> {
const decoder = new TextDecoder("utf-8", { fatal: true });
Expand All @@ -54,6 +105,7 @@ async function bundle(): Promise<void> {
outdir: DIST,
target: "node",
minify: MINIFY,
external: EXTERNAL,
});

// Mirror assets beside the emitted module for resolveAssetsRoot().
Expand All @@ -67,18 +119,30 @@ async function compile(target: string): Promise<void> {
const assets = discoverAssets();
await assertAssetsAreText(assets);

const outfile = join(DIST, "bin", `agentcore-${target.replace(/^bun-/, "")}`);
// Bun appends .exe to a Windows executable whatever it is asked for, so the extension
// is asked for: the name below is also the one this script then reads back and the one
// the build workflow smoke tests.
const platform = target.replace(/^bun-/, "");
const outfile = join(
DIST,
"bin",
`agentcore-${platform}${platform.startsWith("windows") ? ".exe" : ""}`,
);
await $`mkdir -p ${join(DIST, "bin")}`;

const template = bootstrapTemplate();
await Bun.build({
entrypoints: [ENTRYPOINT, ...assets],
entrypoints: [ENTRYPOINT, ...assets, template],
compile: { target: target as Bun.Build.CompileTarget, outfile },
minify: MINIFY,
root: REPO_ROOT,
naming: { asset: ASSET_NAMING },
plugins: [assetLoaderPlugin()],
});
console.log(`Compiled ${target} → ${outfile} (${assets.length} assets embedded)`);
await assertTemplateIsEmbedded(outfile, template);
console.log(
`Compiled ${target} → ${outfile} (${assets.length} assets embedded, plus the bootstrap template)`,
);
}

process.exit(
Expand Down
54 changes: 54 additions & 0 deletions src/core/project/assembly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { existsSync } from "node:fs";
import { join } from "node:path";
import z from "zod";
import { ProjectStateError } from "../../errors/errors";
import type { ReadWriteJson } from "../../io";

// The generated app tags every stack with the target it was synthesized for. Selecting
// on the tag keeps the CLI from reproducing the app's stack-naming convention.
const TARGET_TAG = "agentcore:target-name";

const STACK_ARTIFACT = "aws:cloudformation:stack";

// Artifacts are keyed by the hierarchical id the toolkit matches stack patterns against.
const AssemblyManifestSchema = z.object({
artifacts: z
.record(
z.string(),
z.object({
type: z.string(),
properties: z.object({ tags: z.record(z.string(), z.string()).optional() }).optional(),
}),
)
.default({}),
});

/**
* Asks the synthesized manifest which stack belongs to `target`, rather than deriving
* the name and hoping it matches what synth chose.
*/
export async function stackForTarget(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't the synth output the stack name? Is there a reason we need to re-derive it here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nothing is re-derived. stackForTarget reads synth's manifest.json and selects the stack whose agentcore:target-name tag matches, precisely so the CLI never reproduces the app's naming convention

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess re-derive is the wrong word, but I'm wondering why the build doesn't output the stack name allowing us to avoid inspecting the assembly directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept the manifest lookup in #2058 because generic build has no deployment target and may synthesize multiple stacks; the cloud assembly is CDK synth’s structured output, so returning one stack from build would still require this lookup while leaking CDK-specific semantics into the build contract. Clarified the implementation in c76b638 by renaming stackForTarget/stackName to stackArtifactIdForTarget/stackArtifactId.

json: ReadWriteJson,
assemblyDirectory: string,
target: string,
): Promise<string> {
const path = join(assemblyDirectory, "manifest.json");
// deploy synthesizes immediately before this, so a missing manifest means synth wrote
// somewhere else rather than that the user skipped a step.
if (!existsSync(path)) {
throw new ProjectStateError(`No synthesized cloud assembly was found at ${path}.`);
}

const manifest = await json.read(path, AssemblyManifestSchema);
const stacks = Object.entries(manifest.artifacts).filter(
([, artifact]) => artifact.type === STACK_ARTIFACT,
);
const match = stacks.find(([, artifact]) => artifact.properties?.tags?.[TARGET_TAG] === target);
if (!match) {
throw new ProjectStateError(
`The synthesized cloud assembly has no stack for deployment target '${target}'. ` +
`${path} defines ${stacks.length} stack(s), none tagged ${TARGET_TAG}='${target}'.`,
);
}
return match[0];
}
Loading
Loading