Skip to content

feat(project): tear down the stack on a deploy of an empty project, behind --yes - #2089

Open
notgitika wants to merge 2 commits into
refactorfrom
feat/project-deploy-credentials
Open

feat(project): tear down the stack on a deploy of an empty project, behind --yes#2089
notgitika wants to merge 2 commits into
refactorfrom
feat/project-deploy-credentials

Conversation

@notgitika

@notgitika notgitika commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Deploying a project that declares no resources tore down its stack silently. This makes that outcome explicit, gated, and reported — and fixes the stack-selection gap found in the same review.

A deploy of nothing was a silent delete. The CDK Toolkit reads a template with no resources as an instruction to delete an existing stack of that name, and reports the run as an ordinary successful deploy. #2058 caught it after the fact, by which point the stack was gone.

The first attempt at a guard here was dead code. CDK writes an AWS::CDK::Metadata resource into every stack unless version reporting is disabled, so a project whose spec declares nothing still synthesizes a template with one resource in it — Object.keys(Resources).length === 0 is unreachable through the CLI. Verified against eight real synthesized assemblies. Counting only the resources the project asked for makes the check fire.

Making it fire needs somewhere for that deploy to go. main supports this: empty the project, deploy, and the stack is destroyed. The refactor dropped it, so refusing outright would trade a silent delete for a regression. Instead the deploy routes to an explicit toolkit.destroy(), gated on --yes:

$ agentcore project deploy
Project 'orders' declares no resources to deploy, so deploying to target 'default'
would delete stack 'AgentCore-orders-default' and every resource in it.
Re-run with --yes to confirm, or restore the resources the project should have.

$ agentcore project deploy --yes
Removing stack AgentCore-orders-default
Removed project 'orders' from target 'default'

Three states are distinguished, all before anything is destroyed:

Project declares Stack exists --yes Outcome
resources deploy
nothing yes no refuse, naming the stack that would be deleted
nothing yes yes destroy, reported as Removed
nothing no refuse: add a resource, e.g. project add runtime

Destroying explicitly rather than deploying the empty template is what makes the outcome reportable: destroy fails loudly when the stack cannot be removed, where a deploy of an empty template succeeds either way. The empty-outputs guard in performCdkOperation stays as the backstop for reaching that state some other way.

Detection reads the synthesized template rather than counting spec collections the way main does, so a resource type added to the spec later is covered without anyone remembering to extend a list.

Stack selection also matched on the target-name tag alone, never on the account and region the artifact was synthesized for. Those derive from the same target today and so cannot disagree, but nothing enforced it, and the Toolkit deploys where the artifact's environment points rather than where the tag says. Both fields were being stripped on read, since the manifest schema declared neither.

stackArtifactIdForTarget becomes stackArtifactForTarget, returning the template path and the deployed stack name alongside the id. The stack name is derived as properties.stackName ?? artifactId, which is CDK's own derivation — so the name handed to CloudFormation is the one the Toolkit would have used.

Testing

bun test src/ — 1985 pass, 0 fail against these files. Unit coverage for each state in the table, for the metadata-only template that made the original guard dead, and for the ordering that makes the guards meaningful: every check runs before the Toolkit is called, so a bad target or an unconfirmed teardown fails without touching AWS. probeStack is covered for a present stack, a stack mid-rollback, an absent stack, an empty response, and a permissions failure — the last matters because reporting "no stack" on an AccessDeniedException would turn a confirmed teardown into an unexplained "add a resource" error.

Notes

main deletes orphaned harnesses and config bundles before destroying the stack, because it creates some of them imperatively. This branch creates everything through CloudFormation, so the stack deletion covers them; nothing is intentionally left behind, but flagging it in case a later imperative resource needs the same treatment.

@github-actions github-actions Bot added the size/xl PR size: XL label Aug 24, 2026
@github-actions github-actions Bot added agentcore-harness-reviewing AgentCore Harness review in progress size/xl PR size: XL and removed agentcore-harness-reviewing AgentCore Harness review in progress size/xl PR size: XL labels Aug 24, 2026
Base automatically changed from feat/project-deploy-implementation to refactor August 24, 2026 19:39
@notgitika
notgitika force-pushed the feat/project-deploy-credentials branch from a1b6cbf to c255cdd Compare August 24, 2026 19:50
@notgitika
notgitika marked this pull request as draft August 24, 2026 19:50
@notgitika notgitika changed the title feat(project): create credential providers before synthesizing a deploy fix(project): stop deploy silently dropping credentials, deleting stacks, and trusting a tag Aug 24, 2026
@notgitika notgitika changed the title fix(project): stop deploy silently dropping credentials, deleting stacks, and trusting a tag fix(project): tighten agentcore project deploy command Aug 24, 2026
@codecov-commenter

codecov-commenter commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.90446% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.34%. Comparing base (e51a676) to head (1763df3).
⚠️ Report is 3 commits behind head on refactor.

Files with missing lines Patch % Lines
src/core/project/backends/cdk/environment.ts 63.63% 8 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           refactor    #2089      +/-   ##
============================================
- Coverage     97.36%   97.34%   -0.02%     
============================================
  Files           424      424              
  Lines         25571    25706     +135     
============================================
+ Hits          24897    25024     +127     
- Misses          674      682       +8     

☔ 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.

@notgitika
notgitika force-pushed the feat/project-deploy-credentials branch from 1a13ff1 to dfb7780 Compare August 24, 2026 20:38
@notgitika notgitika changed the title fix(project): tighten agentcore project deploy command fix(project): stop deploy deleting stacks and trusting a tag Aug 24, 2026
@notgitika notgitika changed the title fix(project): stop deploy deleting stacks and trusting a tag fix(project): deploy credentials through CloudFormation, and stop deleting stacks Aug 24, 2026
Two review findings from #2058, both cases of deploy trusting something it
had not checked.

A synthesized template with no resources makes the CDK Toolkit *delete* an
existing stack of that name and return as though it deployed. #2058 caught
that after the fact, by which point the stack was already gone. Check the
resource count before handing the assembly to the Toolkit instead.

Stack selection matched on the target-name tag alone, never on the account
and region the artifact was synthesized for. Those derive from the same
target today and so cannot disagree, but nothing enforced it, and the
Toolkit deploys where the artifact's environment points rather than where
the tag says. Both fields were also being stripped on read, since the
manifest schema declared neither.

stackArtifactIdForTarget becomes stackArtifactForTarget, returning the
template path alongside the id so the resource check needs no second read
of the manifest.
@notgitika
notgitika force-pushed the feat/project-deploy-credentials branch from 378208a to f20c060 Compare August 25, 2026 19:59
@notgitika notgitika changed the title fix(project): deploy credentials through CloudFormation, and stop deleting stacks fix(project): check the template before letting the Toolkit deploy it Aug 25, 2026
@github-actions github-actions Bot added size/m PR size: M and removed size/xl PR size: XL size/m PR size: M labels Aug 25, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label Aug 25, 2026
@notgitika
notgitika marked this pull request as ready for review August 25, 2026 20:01
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label Aug 25, 2026
): void {
// An artifact with no environment is environment-agnostic: it deploys into
// whatever the credentials resolve to, which the account preflight checked.
if (environment === undefined) return;

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.

For my own understanding: wouldn't CDK write unknown-account and unknown-region if env-agnostic? The comment on L9 says thats the case, but this comment says no env = env-agnostic.

@Hweinstock

Hweinstock commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Rejects a synthesized stack that declares no resources.

I kind of think we want the opposite. If I clear all of my resources, then deploy, I'd expect that to delete the stack. Without this functionality, how do I destroy the AWS resources I've created with the CLI?

I could see an alternative where we offer an explicit destroy, but that feels overly complex.

@notgitika

Copy link
Copy Markdown
Contributor Author

Rejects a synthesized stack that declares no resources.

I kind of think we want the opposite. If I clear all of my resources, then deploy, I'd expect that to delete the stack. Without this functionality, how do I destroy the AWS resources I've created with the CLI?

I could see an alternative where we offer an explicit destroy, but that feels overly complex.

Discussed with folks offline we are going with a confirmation + flag once the condition is user runs deploy when spec is empty. this will just inform the user that we are also going to delete their stack. (-y)

…ith --yes

The resource check added in the previous commit never fired. CDK writes an
AWS::CDK::Metadata resource into every stack unless version reporting is
disabled, so a project whose spec declares nothing still synthesizes a
template with one resource in it: Object.keys(Resources).length === 0 is
unreachable through the CLI. Verified against eight synthesized assemblies.
Count only the resources the project asked for, and the check becomes real.

Making it real needs somewhere for that deploy to go. main handles it -- an
empty project plus deploy destroys the stack -- and refactor dropped that
along the way, so refusing outright would trade a silent delete for a
regression. Route it to an explicit toolkit.destroy() instead, gated on
--yes, and report it as "Removed" rather than "Deployed" since the stack no
longer exists.

Destroying explicitly rather than deploying the empty template is what makes
the outcome reportable: destroy fails loudly when the stack cannot be
removed, where a deploy of an empty template succeeds either way. The
existing guard in performCdkOperation stays as the backstop for reaching
that state some other way.

Two states are distinguished before anything is destroyed, both by probing
CloudFormation for the stack: nothing to deploy and no stack to remove is a
project that needs a resource added, not a teardown. Detection reads the
synthesized template rather than counting spec collections the way main
does, so a resource type added to the spec later is covered without anyone
remembering to extend a list.

Also corrects the comment nico flagged: an artifact with no environment is
not the env-agnostic case -- CDK spells that out as
aws://unknown-account/unknown-region -- it is a manifest field the cloud
assembly schema leaves optional.
@notgitika notgitika changed the title fix(project): check the template before letting the Toolkit deploy it feat(project): tear down the stack on a deploy of an empty project, behind --yes Aug 25, 2026
@github-actions github-actions Bot added size/l PR size: L and removed size/m PR size: M labels Aug 25, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label Aug 25, 2026
@github-actions github-actions Bot added size/l PR size: L and removed size/l PR size: L labels Aug 25, 2026
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label Aug 25, 2026

@Hweinstock Hweinstock 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.

one question about the flag naming, and some internal questions about toolkit lib behavior for my own understanding.

flags: [
flag("target", "name of the aws-targets.json entry to deploy", z.string().default("default")),
flag(
"yes",

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.

to me --yes implies, answer yes to the prompt, but here there is no prompt and its instead signaling a bypassing of safety measures (kind of like git push --force).

What do you think of naming this to force?

// with nothing in it as an instruction to delete the stack, and reports that
// as an ordinary successful deploy.
if ((await countDeployableResources(this.json, assemblyDirectory, artifact)) === 0) {
return yield* this.teardown({ project, artifact, input, options });

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.

where does the need for an explicit teardown come in? I thought the behavior of toolkit lib was that it already deleted the stack if it was empty.

// the synthesized app, so today they cannot disagree. Checking anyway keeps a
// correct tag from carrying a stack into the wrong account or region: the Toolkit
// deploys where the artifact's environment points, not where the tag says.
function assertEnvironmentMatches(

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.

the stack's environment derive from the same target in the synthesized app, so today they cannot disagree.

What could change that would make them disagree? I'm wondering if we can avoid this complexity, or if there is real concern we could deploy to the wrong environment.

@nborges-aws nborges-aws 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. Teardown looks sound; I'm indifferent on --force vs --yes naming convention Harrison raised.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l PR size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants