Delivery and Releases

Pipelines

flowchart LR
  MAIN[push to main] --> CI["ci.yml<br/>lint · typecheck · tests<br/>content validate + build with a throwaway key<br/>web build · Playwright render-loop test<br/>API bundle"]
  CI -- passes --> D["deploy.yml"]
  D --> API["API + jobs<br/>remote Docker build<br/>release command: migrations<br/>check /statusz"]
  API --> WEB["website + Worker<br/>source maps → Sentry<br/>wrangler deploy"]
  WEB --> CON["content<br/>sign with the trusted key<br/>upload changed files · switch manifest"]
  TAG["app-v* tag"] --> MOB["mobile.yml<br/>iOS + Android builds"]
  MOB --> TF[TestFlight]
  MOB --> FAD[Firebase App Distribution]
TargetTriggerStepsRollback
API and jobsCI passes on main (or run by hand)Docker build, migrations as release command, deploy, then /statusz must answerRedeploy the previous image (migrations are backward compatible)
Website and WorkerAfter the APIBuild, upload source maps (fails if the token is missing), wrangler deployRedeploy the previous version
ContentAfter the websiteBuild, sign, upload changed files, switch manifestrollback switches the manifest back instantly
Androidapp-v* tag or by handSigned build, then Firebase App Distribution to the founder or friends groupPrevious build stays installable; min can be held back
iOSapp-v* tag or by handArchive with cloud-managed signing, upload to TestFlightAs above

Deploys run one at a time and only when a repository switch enables them, so the pipeline never half-runs for want of secrets.

Versions

  • App builds are numbered (build 17 at the time of writing). The web build carries the same number plus a commit id.
  • The signed release info in each content publish carries latest and min per platform and release notes in English and Hinglish, so update prompts change without an app release.
timeline
  title Release flow of one app build
  Build tagged : CI builds iOS and Android
  Testers : TestFlight and Firebase groups
  Content publish : release info latest = N
  Old builds : banner below latest
  Breaking change : min raised, blocking screen below min

decision Deploys are boring on purpose. Every deploy is the same pipeline, every migration works with the previous build, and content can roll back in seconds. Shipping several times a day is safe because undoing is cheap.

Health checks during deploys

Fly moves traffic to a new machine only after its health check passes, and a failed check stops the rollout with the old machines still serving. Today /readyz checks only that the database answers; after the deploy, CI calls /statusz, which also checks jobs and partitions.

${sch.nav()}