ADR-0004: Per-Lambda PR preview deploys + promote-on-merge
- Status: Accepted (infrastructure shipped — Phase 3)
- Date proposed: 2026-04-10
- Date accepted: 2026-04-17
- Supersedes: none (first versioned Lambda deploy policy)
- Related: deployment-cleanup Phase 3 (#150), Phase 4 shared-lib extraction
Context
Vercel gives the Next.js app a preview deploy per PR, so reviewers can click a
URL and see the change before it lands on main. Our Lambdas have no such
workflow — the old lambda-deploy.yml only ran on push to main, which
meant:
- Code review decisions had to be made blind to the packaged bundle.
- No smoke-invocation against real Lambda before the change reached prod.
- Bundle bloat was invisible until someone happened to deploy locally.
- Rollback required manually running
lambda/<name>/rollback.shagainst an alias, which only existed forpropflow-inbound-processor.
We have four Lambdas today:
| Lambda | Trigger | Has :live alias |
|---|---|---|
propflow-inbound-processor (Clara) |
SQS FIFO | yes |
propflow-tour-reminder |
EventBridge 1h | no |
propflow-outlook-subscription-renewer |
EventBridge 30m | no |
ses-email-forwarder |
SES rule | no |
propflow-inbox-pollerwas retired in ADR-0011.
Decision
Add a per-Lambda GitHub Actions workflow that runs in two stages:
pr-previewjob onpull_requestwhenever a file the Lambda bundles has changed:- Build the bundle, zip it.
- Ensure a
propflow-<name>-previewLambda exists (clone config from the prod function on first run — same role / runtime / memory / env vars). The preview function has no trigger (no SQS, no EventBridge, no SES), so invoking it cannot touch production data paths. aws lambda publish-version— captures an immutable preview version.- Smoke-invoke
lambda/<name>/fixtures/*.jsonagainst the preview version. - Post a sticky PR comment with ARN, version, bundle size, and per-fixture smoke results.
prod-promotejob onpushtomain:- Rebuild (see Known gap below).
update-function-codeon the prod function.publish-version— immutable prod version.- Smoke-invoke the new version before flipping the alias.
- If smoke passes AND a
:livealias exists, flip it. If no alias, theupdate-function-codeabove is already "live" (the EventBridge / SES trigger targets$LATEST). This is a soft migration path for adding aliases to the other Lambdas later. - Post-promote smoke against the alias (or the new version).
- On any failure, emit a workflow annotation and do NOT flip the alias.
Known gaps (post-Phase-3)
- Promote rebuilds instead of reusing the PR version. The tested preview
bundle is not carried through merge, so the promote job rebuilds the same
commit. Phase 4 will stamp the green preview version on the PR (commit
status or label) and make promote a
aws lambda update-aliasagainst the already-published version. TaggedTODO(phase4)in each workflow. - Only
propflow-inbound-processorhas a:livealias. The workflows handle both cases, so we can add aliases to the other Lambdas incrementally without a workflow change. - Fixtures for smoke.
lambda/inbound-processor/fixtures/exists with an empty-SQS event as the baseline. The other Lambdas need fixture packs (empty event, one-property event, error-path event). Tracked in Phase 3 follow-up cards. The composite action treats a missing fixtures dir as "skipped" so PRs don't fail while fixtures are being written. - No Telegram notification on prod-promote failure yet. The existing
src/lib/messaging/telegram-dispatch.tsis wired for product alerts, not CI. Workflow annotations are the primary surface today; adding a Telegram secret + job step is a Phase 3.1 follow-up. - Path filters are broad. Until the Phase 4 shared-lib extraction lands,
deploy-clara.ymltriggers on most ofsrc/lib/**. After the extraction toagents/clara/**, tighten these.
Consequences
- Every PR that touches Lambda-bundled code now publishes a real Lambda version that a reviewer can invoke manually (via AWS console or CLI) if they want to probe behavior beyond the fixtures.
- Prod deploys fail closed: a bad bundle fails the pre-flip smoke and the alias is never pointed at it. The previous version keeps serving.
- Five new AWS Lambda functions (
propflow-<name>-preview) will be created on first PR run per Lambda. They carry the tagpropflow-purpose=pr-previewso we can bulk-cleanup later. - The legacy
.github/workflows/lambda-deploy.ymlstays one week as a break-glass manual fallback (workflow_dispatchonly — theon: pushtrigger was removed so it doesn't double-deploy).
Rollback plan
If the new workflows misbehave:
- Delete or disable the per-Lambda workflows (
gh workflow disable). - Re-enable the
on: pushtrigger inlambda-deploy.yml. - Manually run
bash lambda/<name>/deploy.shfrom a clean main checkout for the other Lambdas (these scripts still exist — we intentionally did not delete them in Phase 3).
References
.github/workflows/deploy-clara.ymland siblings.github/actions/lambda-smoke/action.ymldocs/architecture/lambda-ci-cd.md— operator-facing doc- deployment-cleanup Phase 3 tracking issue #150