@deployfoundation/foundation-deploy
The CDK app that deploys one Foundation instance, and foundation-deploy, the
tool an instance's pipeline runs.
Foundation ships no instance file and no company configuration. Everything that identifies a deployment — account, region, Slack workspace, GitHub org, naming prefixes, which integrations are provisioned — lives in an instance YAML the deployment's own repository owns. Both halves of this package take the path to that file and derive the rest.
The CDK app
cd packages/infra
bunx cdk synth --all -c instanceFile=../../../my-instance/instance.yaml
bunx cdk deploy --all -c instanceFile=/abs/path/instance.yaml -c agentImageTag=abc1234
Context keys:
| Key | Meaning |
|---|---|
instanceFile | Required. Path to the instance YAML, absolute or relative to the working directory. |
admins | CSV of Slack admin ids. Defaults to admins in the instance's runtime config. |
alarmEmail | DLQ alarm subscriber. Defaults to none. |
agentImageTag | Required when deploying the AgentCore runtime from a local build; a release names the image instead. |
release | Deploy a published release: vX.Y.Z. The manifest is read from $FOUNDATION_RELEASE_DIR or the per-version cache directory foundation-deploy writes. |
releaseManifest | Path to a verified release manifest, which is what foundation-deploy --release passes. |
releaseBucket | The Foundation release bucket the manifest's keys live in. Required in release mode (or FOUNDATION_RELEASE_BUCKET). |
deployRuntime=false | Bootstrap phase: every stack except the runtime. |
pipelineInstanceFile | Where the instance file sits inside the repo a CodePipeline sources, when that differs from the path given here. |
vpcId, createVpcEndpoints, skipEndpoints, agentSubnetIds | Network escape hatches, unchanged. |
instance.config is resolved relative to the instance file's own directory,
so an instance repository whose layout is .foundation/{instance.yaml,config.yaml}
writes config: config.yaml and nothing else.
FOUNDATION_SKIP_BUNDLE=1 swaps every Lambda bundle for an inline stub. It is what the
tests use and what a synth check on a host without Docker or Bun should use.
The deploy tool
bun run packages/infra/bin/foundation-deploy.ts <command> --instance <path>
| Command | What it does |
|---|---|
deploy | Build and push the agent image, cdk deploy --all, refresh the runtime secret (and cdk deploy once more when that changed it, so no container boots on the old one), sync config. |
post-deploy | Smoke DEFAULT, probe the persistent mount, promote live, smoke live. |
config:sync | Upload the runtime config (for the deployed Foundation version, which must be this tool's) and Foundation's product skills to S3. |
setup | First-deploy orchestration, idempotent. |
github-app-create | Create the instance's GitHub App from the shipped manifest template. |
slack-manifest | Print the instance's Slack app manifest as JSON (the Slack CLI's get-manifest hook contract). |
stage-customization | Validate and stage a tenant-owned runtime config. |
crm-key | Write the company CRM's crm/1 key, piped in with --stdin, into <secretPrefix>/crm/http (crm.provider kind http). |
crm-check | Run the crm/1 conformance suite: the read cases against the instance's CRM with its stored key, or, with --base-url --tenant --key-stdin and no instance, any server, write cases included, every case strict given the server's harness endpoint (--harness-url) and the deployment's inputs (docs/crm-provider-contract.md#crm-check). A CRM can also import the suite: @deployfoundation/foundation-deploy/crm-conformance. |
crm-replay | Replay the agent's CRM writes (JSONL) into the instance's own table after a rollback to the DynamoDB provider. |
--instance <path> is required for every command (or FOUNDATION_INSTANCE_FILE
in a pipeline), except crm-check --base-url, which checks a server rather than a deployment. There is no instance name, no instances/ directory and no
default: a deploy that guessed its target would be a deploy into the wrong
account.
Deploying a release
# an instance repo's whole build step
npx @deployfoundation/foundation-deploy@$(cat foundation.version) deploy \
--instance .foundation/instance.yaml
A copy of the tool installed from npm has no Foundation workspace to build
from, so it deploys the release matching its own version: it fetches
releases/vX.Y.Z/manifest.json from the Foundation release bucket, verifies
the KMS signature and every artifact's sha256, and only then deploys. In a
Foundation checkout the same command builds locally, as it always did.
--release <vX.Y.Z>, --manifest <path|s3://…> and --release-bucket <name>
override the choice.
Until the release account exists, set FOUNDATION_RELEASE_KMS_KEY_ARN to the
published signing key ARN: the tool ships no default, because trusting the
wrong key is worse than checking nothing.
The license check
deploy verifies the instance's license.key against
https://license.foundry41.com/v1/verify (override with
FOUNDATION_LICENSE_ENDPOINT) before it deploys, sending the key, the
instance id and the version — nothing else. An instance with no key is
skipped with a warning. A definite refusal stops the deploy; an unreachable
endpoint does not, for 30 days, on the verification cached in the instance's
own <prefix>/license/last-verified secret.
The tool never imports the agent package. It runs in a pipeline, not in the agent container.
Artifacts
src/artifacts.ts is the single place that decides where the deployed bytes
come from, and it has two modes:
- local build (the default). Every Lambda bundle is built from this
checkout at synth with the pinned Bun, and
deploybuilds and pushes the agent image to the instance's own ECR first. A contributor deploys their working tree this way. - release.
-c release=vX.Y.Z(or-c releaseManifest=<path>) plus-c releaseBucket=<name>: each Lambda's code isCode.fromBucket(...)on the key the signed manifest names, and the agent image is Foundation's own repository pinned by digest. Nothing is built, so the pipeline needs no Docker, no Bun and no Foundation checkout.
src/release/manifest.ts is the manifest itself — schema, builder, the
canonical bytes a signature covers, and verifyManifest, which checks the KMS
signature before any artifact is read and then every artifact's sha256.
src/release/kms.ts is the AWS-facing half. A manifest reaching a synth has
already been verified by foundation-deploy.
The release account (Foundry 41's own)
src/stacks/release-account-stack.ts is the only stack here that is not an
instance's. It holds what a release is published from: the release bucket,
the foundation/agent image repository, the KMS signing key, and the OIDC
role .github/workflows/release.yml assumes on a tag push. Customers appear
in exactly one place — the account-id list that opens the bucket, the
repository and the key.
cd packages/infra
bunx cdk --app 'bun run bin/release-account.ts' diff FoundationRelease \
-c customerAccountIds=111122223333,444455556666
bunx cdk --app 'bun run bin/release-account.ts' deploy FoundationRelease \
-c customerAccountIds=111122223333,444455556666
Deployed once, by hand, into the release account. Its outputs are the four
repository variables the release workflow needs (FOUNDATION_RELEASE_BUCKET,
FOUNDATION_RELEASE_KMS_KEY_ARN, FOUNDATION_RELEASE_ROLE_ARN, and the image
repository), plus the signing key ARN customers verify against. Adding a
customer is a redeploy with a longer customerAccountIds.
Publishing
bun run --cwd packages/infra build # dist/, Node target, ESM
cd packages/infra && npm pack --dry-run
The build bundles the three entry points — the CLI, the CDK app and the
release-account app — into dist/, inlining the workspace packages (which are
not published) and leaving every declared dependency external, so npm installs
one aws-cdk-lib and both halves of the tool see it. aws-cdk itself is a
dependency, not a devDependency: the tool shells out to the CDK CLI and
resolves it through this package rather than through PATH.
The tarball carries dist/, src/ (the manifest templates are read from
there at runtime), the agent image's Dockerfile, the reference buildspec and
cdk.json. It does NOT carry bin/: those TypeScript entry points need Bun
and the workspace, and shipping an app entry that cannot run is how a deploy
picks the wrong one. node dist/bin/foundation-deploy.js --help works from an
extracted tarball with only the runtime dependencies installed — no Bun, no
Foundation checkout — which is exactly what an instance's pipeline has.
Tests
bun test packages/infra
They run under bun test with the rest of the repo — no jest, no ts-node. The
snapshot test shells out to cdk synth for both fixtures, so it needs the
workspace installed but no AWS credentials and no Docker.
Fixtures are synthetic (test/fixtures/): acme turns every integration on,
beta is a minimal instance deployed by CodePipeline. No real company appears
anywhere in this package.