Skip to content

feat(project): implement project build - #1970

Merged
tejaskash merged 3 commits into
refactorfrom
feat/project-build-synth
Aug 13, 2026
Merged

feat(project): implement project build#1970
tejaskash merged 3 commits into
refactorfrom
feat/project-build-synth

Conversation

@notgitika

@notgitika notgitika commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

agentcore project build compiles the project's CDK app and synthesizes its CloudFormation templates, so the deployable artifacts exist before deploy.

The generated package.json defines cdk as "npm run build && cdk", so that one command covers both compile and synthesis — no need to construct a node_modules/.bin/cdk path or branch on win32.

Synthesis is credential-free, by design

An earlier draft of this called STS GetCallerIdentity and wrote agentcore/aws-targets.json as a side effect of build. That isn't needed: each stack takes an explicit env: { account, region } from aws-targets.json and nothing in the app calls fromLookup, so synth never talks to AWS. The account is only load-bearing at deploy.

So build stays offline and does not write config. When aws-targets.json is still [], the CDK app raises its own error, which already says what to do:

AgentCore CDK synthesis failed: No deployment targets configured.
Please define targets in agentcore/aws-targets.json

wrapped by ProcessFailedError with Fix the issue and run 'cd .../agentcore/cdk && npm run cdk -- synth --quiet' to retry.

Deliberately not guessing the account also avoids a trap the earlier draft had: it wrote region unvalidated, but AgentCoreRegionSchema is an enum of 9 regions. --region eu-west-2 would have written a region synth then rejects, and because the CLI's own re-read used z.array(z.unknown()) it would see a non-empty list and never repair the file.

withProject goes into service

src/middleware/withProject.tsx already existed but was never exported and never used. It now resolves the enclosing project and hands it to the handler via ProjectKey, so the handler body is just:

const project = ctx.require(ProjectKey);
for await (const event of config.projectManager.build(project)) {
  config.io.stderr.write(`${event.message}\n`);
}

Two changes to it:

  • cwd is optional and resolved per invocation (config.cwd ?? process.cwd()) rather than captured at wiring time, so the directory the user actually ran in is the one searched.
  • Failure is a ProjectStateError naming the path searched and the file looked for, and pointing at agentcore project create.

It wraps only build, not the whole router — create refuses to nest inside an existing project, so requiring one would break it.

Second commit: runtimeVersion on the CodeZip template

Included here rather than as a follow-up, so build is never merged in a state where it fails on the config the CLI itself just scaffolded.

The CDK construct library's schema refines build !== 'Container' && !runtimeVersion into runtimeVersion is required for CodeZip builds, and our hello-world-python template didn't set it. Five lines: runtimeVersion: "PYTHON_3_14" on the template runtime plus the matching test assertion. Container builds take their version from the image, so the container template is untouched.

A follow-up will mirror that refinement into our own AgentEnvSpecSchema, which currently marks runtimeVersion plain .optional() — that way the CLI catches this class of error itself instead of surfacing CDK's zod path error.

Verification

Beyond unit tests, I ran this against a real scaffolded project with a real npm install:

  1. Empty aws-targets.json → the CDK app's "No deployment targets configured" error, wrapped with the retry hint.
  2. One target filled in, with AWS_ACCESS_KEY_ID/SECRET/SESSION_TOKEN blanked and AWS_PROFILE pointed at a nonexistent profile → Built project 'Demo', emitting cdk.out/AgentCore-Demo-dev.template.json. Confirms synth needs no credentials.
  3. Stripping runtimeVersion back out of that same project → reproduces the CodeZip failure, which is what motivated the second commit.

Unit coverage: the exact synth command and cwd, the missing-node_modules error, subprocess failure propagation, the progress event, resolution from a nested directory, and failing outside a project.

  • bun test — 1061 pass, 0 fail
  • tsc --noEmit clean, oxlint clean

@github-actions github-actions Bot added agentcore-harness-reviewing AgentCore Harness review in progress and removed agentcore-harness-reviewing AgentCore Harness review in progress labels Aug 11, 2026
@codecov-commenter

codecov-commenter commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.82%. Comparing base (0e33398) to head (05c244e).

Additional details and impacted files
@@             Coverage Diff              @@
##           refactor    #1970      +/-   ##
============================================
+ Coverage     96.81%   96.82%   +0.01%     
============================================
  Files           329      329              
  Lines         18754    18794      +40     
============================================
+ Hits          18157    18198      +41     
+ Misses          597      596       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

// single command compiles the app and then synthesizes it. Synthesis needs no
// AWS credentials: each stack's environment comes from aws-targets.json.
yield { message: "Synthesizing CloudFormation templates" };
await this.run(["npm", "run", "cdk", "--", "synth", "--quiet"], cdkDir);

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.

Are we not using @aws-cdk/toolkit-lib anymore?

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.

For now, I went with a subprocess to keep the PR small. using toolkit-lib means porting the wrapper and the schema pinning first, and deploy is what actually needs those. So I will introduce it when I introduce deploy. It should be easy to switch later, build() already yields events so it's a 2-way door decision

return project;
}

public async *build(project: Project): AsyncGenerator<ProjectEvent, void> {

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.

How does this build impl handle alternative project backends like terraform and SDK if we chose to implement those in the future?

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.

In the commit I just pushed, we use the managedBy field in agentcore.json

Comment on lines +14 to +24
handle: async (ctx) => {
// withProject has already resolved the enclosing project.
const project = ctx.require(ProjectKey);

// Progress goes to stderr, keeping stdout for machine output. Subprocess
// output goes to the debug log; on failure ProcessFailedError carries it.
for await (const event of config.projectManager.build(project)) {
config.io.stderr.write(`${event.message}\n`);
}

config.io.stderr.write(`Built project '${project.name}'\n`);

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 like that this is kept clean. It doesn't need to be aware of the build backend or any of the other steps.

Comment on lines +132 to +138
await this.checkTool("npm", "Install Node.js: https://nodejs.org/");

// The generated package.json defines `cdk` as "npm run build && cdk", so this
// single command compiles the app and then synthesizes it. Synthesis needs no
// AWS credentials: each stack's environment comes from aws-targets.json.
yield { message: "Synthesizing CloudFormation templates" };
await this.run(["npm", "run", "cdk", "--", "synth", "--quiet"], cdkDir);

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 think we had discussed that build would:

  • Validate Schemas
  • Generate ZIP artifacts for CodeZIP Agents
  • Run CDK Synth

I'm not sure if Generate ZIP artifacts for CodeZIP Agents is happening here or if we decided to move that to a different PR.

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.

All three are happening. schema validation is up front, withProject resolves the project through ProjectSpecSchema, so build can't run on an invalid agentcore.json (#1972 makes that catch the CodeZip runtimeVersion rule and name the field).

the ZIP is generated by synth: the construct library's packager runs uv pip install and stages the asset, so cdk.out gets the zip and the template's CodeConfiguration.Code.S3 points at it.

I validated and checked a real synth to be sure. so there's no separate ZIP step to write, doing our own would duplicate the packager deploy relies on. LMK what you think

Base automatically changed from chore/project-event-shape to refactor August 11, 2026 19:03
@notgitika
notgitika force-pushed the feat/project-build-synth branch from 305bf56 to 639227f Compare August 11, 2026 19:03
@tejaskash
tejaskash force-pushed the feat/project-build-synth branch from 78097a8 to 0b2da99 Compare August 12, 2026 20:24
tejaskash
tejaskash previously approved these changes Aug 12, 2026
aidandaly24
aidandaly24 previously approved these changes Aug 12, 2026

@aidandaly24 aidandaly24 left a comment

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.

This looks good to me one really minor comment:


// The generated package.json defines `cdk` as "npm run build && cdk", so this
// single command compiles the app and then synthesizes it. Synthesis needs no
// AWS credentials: each stack's environment comes from aws-targets.json.

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.

Non-blocking and fine as a follow-up: synthesis succeeds without credentials, but AWS_PROFILE currently causes the pinned ConfigIO.readAWSDeploymentTargets() to call STS even when every target already has an account. I confirmed this by redirecting STS locally. The build still succeeded, but made six GetCallerIdentity attempts and inherited the retry latency. Could we avoid that fallback when account values are already present so build is fully offline?

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.

creating a follow up issue for this. thanks for catching this!

`agentcore project build` compiles the project's CDK app and synthesizes its
CloudFormation templates, so the deployable artifacts exist before deploy.

Synthesis runs offline: each stack's environment comes from aws-targets.json,
so no credentials are needed. An empty targets file makes the CDK app fail with
its own actionable message rather than the CLI guessing an account.

The generated package.json defines `cdk` as "npm run build && cdk", so one
`npm run cdk -- synth --quiet` covers both compile and synthesis.

Also puts withProject to work for the first time: it resolves the enclosing
project and hands it to the handler through ProjectKey, and it wraps only build
so that `create` (which refuses to nest inside a project) stays unaffected. Its
cwd is now resolved per invocation instead of at wiring time, so the directory
the user actually ran in is the one searched.
The CDK construct library rejects a CodeZip runtime that declares no
runtimeVersion ("runtimeVersion is required for CodeZip builds"), and it is the
field that selects the packager. Without it, synthesizing a freshly created
python project fails on its own scaffolded config.

Container builds take their version from the image, so the container template
is unaffected.
agentcore.json already records `managedBy` (CDK is the only value today), but
nothing read it: build() hardcoded the CDK path, so adding a terraform or
no-IaC backend later would have meant editing that path instead of adding
alongside it.

Carry managedBy on Project and switch on it in build(), delegating the CDK
work to a private buildWithCdk(). The default arm assigns to `never`, so a new
ManagedBySchema member fails to compile until it has an arm here.

@tejaskash tejaskash left a comment

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.

LGTM

@tejaskash
tejaskash merged commit 25d59fb into refactor Aug 13, 2026
13 checks passed
@tejaskash
tejaskash deleted the feat/project-build-synth branch August 13, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants