Appearance
Deploy pipeline
How code reaches staging/prod. Source of truth: .github/workflows/ci.yml. For the runtime shape (edge, ECS, CDN) see Infrastructure.
How a deploy happens
Triggers: push to main only (+ workflow_dispatch). PR runs are disabled to save Actions minutes — validate locally with bun run typecheck && lint && format:check && test.
The jobs — .github/workflows/ci.yml
| Job | Gate | What it does |
|---|---|---|
build | always | Postgres 17 service → bun install --frozen-lockfile, typecheck, lint, format:check, test. Guards the edge worker stays Node-free (wrangler deploy --dry-run + grep for node:*/pino). |
deploy | main | Migrate Neon → deploy the Worker (wrangler deploy) → set Worker DATABASE_URL secret. (Path-B step sets ORIGIN_URL+EDGE_SECRET when PATH_B=true.) |
origin-image | DEPLOY_AWS=true | Build the origin Docker image → push to ECR ratio-3-0-origin (:sha, :latest). A nested ECS-Express roll (when ECS_DEPLOY=true) re-registers the task def with the new image + env and canaries it in. |
admin-ui | always (deploy gated) | Build/test apps/admin-web; deploy to Cloudflare Pages (ratio-admin) when DEPLOY_ADMIN_UI=true. |
purge | PURGE_CACHE=true | Purge the ratiodev.in Cloudflare zone. |
Gates live in GitHub, not the repo
What actually deploys is controlled by GitHub repo-variable gates (DEPLOY_AWS, ECS_DEPLOY, PATH_B, PURGE_CACHE, DEPLOY_ADMIN_UI) — their values live in GitHub settings, not the repo, so read the gate column as "runs when its variable is set", not as a statement of which are on.
Origin rollout — canary + auto-rollback
When ECS_DEPLOY=true, the new origin image doesn't go straight to 100%. It rolls out gradually through ECS Express's managed canary, and rolls back on its own if health regresses:
- Uses the Express-native
update-express-gateway-service(not the classicupdate-service, which tangles Express). - Origin + admin-api are one image — the role is chosen at runtime by
RATIO_SERVICE(unset → origin;admin-api→ control plane). - Two rollback kinds: code (redeploy, minutes) vs the content/live-theme pointer (a flip, seconds).
Adding a workspace? Update the Dockerfile.
The origin image hand-copies each workspace's package.json before bun install --frozen-lockfile. A new workspace not in that copy-list breaks the origin build. (This is why apps/docs is deliberately excluded from the workspace set — "!apps/docs" in the root package.json — so its VitePress deps never touch the app lockfile or the origin image.)