Skip to content
GitHub
Infrastructure

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.

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.

Against the resolved GitHub Environment, in order:

  1. Apply Supabase migrations (tools/deploy/apply_supabase.sh) — schema first, so it is ready before the new container serves.
  2. 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.
  3. Reconcile the Cloudflare edge (tools/deploy/reconcile_edge.sh) — the R2 bucket + the api. custom domain, which attaches to the Worker the deploy just shipped.
  4. Smoke test (tools/deploy/smoke.sh) — poll /health through the edge until it returns 200. A green /health is 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.

tools/quality/check_migration_compat.py (on PR + push-main) rejects in supabase/migrations/*.sql:

  • DROP COLUMN
  • DROP TABLE
  • ALTER COLUMN ... TYPE to an incompatible target
  • ADD COLUMN ... NOT NULL without DEFAULT
  • ADD ... UNIQUE constraint 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.

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).

  • 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