staging-deploy.md3.7 KBView on GitHub # Staging Deploy
## Goal
Deploy the AWS staging environment for app, API, chat, and worker services.
## Steps
1. Ensure images for `api-service`, `chat-service`, and `worker-service` are published.
2. Sync staging secrets into AWS Secrets Manager.
3. Run the staging synth and deploy commands.
4. Verify health endpoints and frontend load.
5. Run the staging validation checklist.
## Deploy Speed Optimizations
Staging deploys use several optimizations to keep cycle times short:
### CDK Hotswap (`CEDAR_AWS_CDK_HOTSWAP=true`)
Staging uses `cdk deploy --hotswap-fallback`, which bypasses CloudFormation for
supported resource types (ECS task definitions, Step Functions, CloudFront
Functions) and falls back to a normal CloudFormation deploy for everything else.
This is **not enabled for production** — hotswap creates drift between
CloudFormation state and actual resources.
### Docker Layer Caching
Image builds use ECR as a buildx cache backend (`--cache-from`/`--cache-to`
with `type=registry`). The `npm install` layer is cached across builds so only
the `COPY dist/` layer rebuilds when code changes.
### Smart Change Detection
`plan-surface-scope.sh` determines what needs deploying based on which files
changed:
| Files changed | What runs |
|---------------|-----------|
| `aws/bin/*`, `aws/lib/*`, `aws/cdk.json`, `aws/package.json`, `aws/tsconfig.json` | Full CDK stack deploy |
| `aws/scripts/*` | Service image republish only (no CDK deploy) |
| `apps/server/*` | Service image republish + ECS rollout |
| `apps/mail/*` | Frontend deploy only |
The baseline it diffs against is **the commit each surface was last deployed
from**, not the previous push — `last-deployed-sha.sh` asks the Actions API which
commit `deploy-frontend` / `deploy-backend` last succeeded on. Staging runs with
`cancel-in-progress: true`, so two merges landing seconds apart cancel the first
run; diffing from the pushed baseline would then treat that cancelled run's
changes as already deployed and strand them until something else touched the same
surface. Each surface carries its own baseline, so a stranded frontend change is
picked up without forcing a redundant backend republish. When the lookup cannot find a deploy to anchor to — the surface has not been
deployed within the ~200 runs it scans — it does not fall back to the pushed
baseline, because doing so would diff away every undeployed commit behind it and
reintroduce the same bug. It deploys the surface instead, and the next run finds
that deploy and resumes scoping normally. When the question cannot be asked at all
— no token, an API error, or a job name that never appears in the history, which
means a rename rather than a stale surface — it keeps the pushed baseline and logs
why.
### ECS Tuning (staging only)
- `minHealthyPercent: 50` (vs 100 in prod) — allows faster rolling deploys
- `healthCheckGracePeriod: 30s` (same as prod) — containers start in ~10-15s
- `imageScanOnPush: false` on ECR repos (enabled in prod)
### Preflight Parallelization
TypeScript typechecking and CDK synthesis run in parallel in CI, and only when
`deploy_stack=true`. The deploy script skips the duplicate typecheck
(`CEDAR_AWS_RUN_TYPECHECK=false`).
## Environment Variables
| Variable | Effect |
|----------|--------|
| `CEDAR_AWS_CDK_HOTSWAP` | Set to `true` to use `--hotswap-fallback` (staging only) |
| `CEDAR_AWS_RUN_TYPECHECK` | Set to `false` to skip the typecheck in `deploy-surface.sh` |
| `CEDAR_AWS_DEPLOY_STACK` | Set to `false` to skip CDK deploy and only rollout images |
## Verification
- frontend responds on the staging app URL
- API health endpoint returns `200`
- chat service responds on `/health`
- worker service responds on `/health`