CD Pipeline Overview
The CD pipeline is a reusable GitHub Actions deploy engine fired by a fast-forward push of the production branch. One container deploy, gated by a /health smoke check through the edge; rollback is forward-fix. Decision lineage in ADR-053 and ADR-110; operational runbooks at docs/runbooks/deployment.md and docs/runbooks/rollback.md.
Trigger: a fast-forward of production
Section titled “Trigger: a fast-forward of production”main is integration — pushing main runs CI but does not deploy. A deploy is an explicit fast-forward push of the production branch, which fires deploy-production.yml. The production-ff-only repository ruleset enforces fast-forward-only + no-delete on that branch, and the production GitHub Environment’s protection rule gates the run. The advance is git merge --ff-only main on production, then push — so remote main is always safe to be at, and deploying is a separate, deliberate act.
One reusable engine, thin per-environment callers
Section titled “One reusable engine, thin per-environment callers”deploy.yml is the deploy engine — a workflow_call reusable workflow parameterized by an environment input. deploy-production.yml is a thin caller that invokes it with environment: production. A staging deploy lands as a second thin caller when a Supabase stage project exists; the engine is already environment-parameterized. The secret contract is enumerated explicitly (not inherit) and each secret is scoped to the step that needs it, so credentials are never in scope during pnpm install or other package-lifecycle steps.
Deploy steps
Section titled “Deploy steps”Against the resolved GitHub Environment, in order:
- Apply Supabase migrations (
tools/deploy/apply_supabase.sh) — schema first, so it is ready before the new container serves. - Deploy the app container via wrangler (
tools/deploy/deploy_container.sh) — sets the container runtime secrets + non-secret vars on the Worker (the secret hop), which the edge Worker forwards into the container at start. - Reconcile the Cloudflare edge (
tools/deploy/reconcile_edge.sh) — the R2 bucket + theapi.custom domain, which attaches to the Worker the deploy just shipped. - Smoke test (
tools/deploy/smoke.sh) — poll/healththrough the edge until it returns 200. A green/healthis the deploy’s go/no-go; never reaching 200 fails the deploy.
One deploy runs at a time per environment (concurrency: { group: deploy-<environment>, cancel-in-progress: false }) — a live deploy is never cancelled mid-flight.
Migration-compat lint
Section titled “Migration-compat lint”tools/quality/check_migration_compat.py (on PR + push-main) rejects in supabase/migrations/*.sql:
DROP COLUMNDROP TABLEALTER COLUMN ... TYPEto an incompatible targetADD COLUMN ... NOT NULLwithoutDEFAULTADD ... UNIQUEconstraint on a populated column
Override: a -- compat: breaking (reason: <reason>) marker on the file forces explicit human review; an unannotated breaking change blocks the PR. The V1-against-V2-schema corner is caught at PR time, not deploy time.
Rollback is forward-fix
Section titled “Rollback is forward-fix”There is no color swap-back and no drain service. A bad deploy is rolled forward: fix on main, fast-forward production, redeploy. The container comes up at a new generation; the prior generation’s stranded core.outbox rows stop being claimed once it is no longer deployed. Because migrations are forward-only + expand/contract, the prior code already runs against the new schema, so a code-level forward deploy suffices. A loss past Cloudflare/Supabase retention, or an upstream-yanked dependency, escalates to DR per ADR-040 (Supabase-native managed backups + PITR).
See also
Section titled “See also”- ADR-053 — CD pipeline
- ADR-110 — provisioning + the GitHub Environment
- Deployment topology — the one-container runtime + generation cutover
- ADR-049 — container build strategy
- Secrets management — environment placement
docs/runbooks/deployment.md,docs/runbooks/rollback.md,docs/runbooks/ci-secrets.md