Skip to main content

ADR-0001 — Gated CI/CD deploy pipeline for the backend

  • Status: Accepted
  • Date: 2026-08-23
  • Deciders: Wes (engineering), Kenji (owner, KCB1099)

Context

The Drape backend (backend/server.py, FastAPI) runs on Render as the service drape-backend. Before this ADR:

  • CI already existed and was solid. Two GitHub Actions workflows run on every push/PR to main: backend-checks.yml (syntax, imports, secret scan, .env-not-tracked, flake8, a startup smoke test hitting /api/health) and predeploy.yml (backend syntax/imports/env + frontend tsc/ESLint).
  • Deployment to production was manual by policy. render.yaml sets autoDeploy: false, and the prior runbook required a human to click the Render deploy hook after running predeploy_check.sh, validate_env.py, and applying Supabase migrations first (schema leads code).

That manual posture is correct for this app — it is a payment path (Stripe Connect) handling BIPA-regulated biometric data with real users — but it had three gaps:

  1. CI success neither triggered nor blocked a deploy. The deploy was a separate manual action, so a red check could not stop a human, and a green one shipped nothing. The two were disconnected.
  2. No verified staging CD. Project notes described "staging auto-deploys," but render.yaml defined only one service and no staging pipeline was codified.
  3. Health checks gave false confidence. /api/health reported liveness only, so polling it after a deploy could pass against the old instance still serving traffic during the rollover.

This ADR revisits and supersedes the earlier "production deploy is manual, full stop" policy. It keeps the safety intent (no blind production deploys) while closing the gaps above.

Decision

GitHub Actions is the single control point for backend deploys, using Render deploy hooks. render.yaml remains autoDeploy: false so Render never deploys on its own — a red check or a missing approval means nothing ships.

Two workflows implement this:

  • deploy-staging.yml — on push to staging (paths backend/**, render.yaml, the workflow itself): run pre-deploy checks → on green, automatically fire the staging Render deploy hook → poll staging /api/health until the new commit is live. This is "auto-deploy, but only if the checks pass" — strictly safer than Render's native branch auto-deploy, which ships regardless of CI.

  • deploy-production.yml — a manual run (workflow_dispatch) on main: run the same checks → fire the production Render deploy hook → poll /api/health until the live commit matches the deployed SHA. The manual trigger is the gate (see the plan constraint below): only a user with write access can start it, and the checks always run before anything deploys.

To make the post-deploy verification trustworthy, /api/health now returns a commit field sourced from Render's injected RENDER_GIT_COMMIT. The workflows wait until status ∈ {healthy, degraded} and commit == <pushed short SHA>, so a "pass" means the new code is actually live — not the old instance.

main branch — manual

staging branch

green

green

promote after smoke test

push backend/**

checks: syntax / imports / env

fire staging deploy hook

poll /api/health until commit matches

manual run: workflow_dispatch

checks: syntax / imports / env

fire prod deploy hook

poll /api/health until commit matches SHA

Migrations remain human-ordered. Supabase migrations still run before the production deploy; the person triggering the manual run confirms they were applied. Automating migrations inside the pipeline is deliberately left as a future ADR.

Plan constraint (why the gate is a manual trigger, not reviewer approval). The original intent was a push-triggered prod deploy that pauses on the production GitHub Environment for required-reviewer approval. That protection rule is not available on this account's plan for a private repo — it requires GitHub Team/Enterprise (attempting to set it returns HTTP 422: "billing plan"). Rather than block on a billing change, production is gated by a manual workflow_dispatch trigger: a deploy only happens when a write-access user deliberately runs the workflow, and the checks run inside that run. Staging still auto-deploys on push (no protection rule needed).

Upgrade path. On GitHub Team/Enterprise, add required reviewers to the production environment and re-add a push trigger to deploy-production.yml; the deploy job will then pause for reviewer approval automatically. No other change is needed — the deploy/verify steps are identical.

Consequences

Easier / better

  • Deploys are gated on green CI automatically; the two are no longer disconnected.
  • Every production release has an audit trail (who approved, which SHA, health result).
  • Rollback is "re-run the deploy hook on the previous good commit" — the pipeline verifies the live commit either way.
  • Staging gets true CD, so changes are exercised in a real environment before prod.
  • No blind production deploys survive: payment/BIPA risk stays behind a human gate.

New obligations (one-time setup, then ongoing)

  • Kenji must generate the Render deploy hook URL(s) and store them as repo secrets (RENDER_DEPLOY_HOOK_URL, and RENDER_STAGING_DEPLOY_HOOK_URL once a staging service exists). PROD_BACKEND_URL is already set as a repo variable.
  • A dedicated drape-backend-staging Render service (tracking staging, own DB) must exist for staging CD to be meaningful.
  • The person running the production deploy must confirm migrations were applied first.
  • Production has no reviewer-approval gate on the current plan — it relies on the manual trigger. Revisit if/when the org moves to GitHub Team (see upgrade path).

Full step-by-step setup and the rollback procedure live in the operational runbook: Backend deploys.

Alternatives considered

  • Turn on Render native autoDeploy for main. Rejected: ships regardless of CI status, cannot order Supabase migrations before code, and removes the human gate on a payment/biometric service.
  • Keep deploys purely manual (prior policy). Rejected: CI and deploy stayed disconnected, the manual steps were error-prone and under-documented, and staging had no CD.
  • Auto-deploy production on green with no approval. Rejected: for this app the blast radius (money movement, biometric data) warrants a human checkpoint.
  • Full GitOps (ArgoCD/Flux) or a Render Blueprint-only flow. Rejected as over-engineered for a single Render service and a solo-founder team; deploy hooks + Actions give the needed control with far less machinery.