Skip to content

Release Management System — High-Level Design (HLD)

Status: 🟢 active · Governs: every production change · Depends on: ADR-0011 · Related: LLD · Process · Promotion runbook

The end-to-end view of how a change reaches production: components, governance, roles, gates, and integrations. Implementation detail — data model, state machine, automation, audit — is in the LLD.

Why this exists

Every production change before 2026-08 was applied by hand, and 2026-08-01 alone produced five proofs that nothing could see the result: Dev had zero Edge Functions while Prod had 21 (QRS-283); Prod runs manage-reminders while the repo says manage-reminder (QRS-286); config.toml disagreed with Prod on all eleven functions (QRS-284); Prod's SMTP pointed at the wrong provider (QRS-285); MCP apply_migration left four orphan rows in schema_migrations (QRS-267).

The mechanics to prevent each of those now exist. This system is the spine that binds them: defined scope, change control, dependency and risk modelling, named gates, and a record tying every production change to an approved release.

The one idea that makes it work

Release documentation is load-bearing. deploy-prod.yml reads release.json and refuses to deploy a production change that is not declared in it.

Documentation stops being a chore done afterwards and becomes the thing without which the deploy does not run. Everything else follows from that inversion.

The design principle behind it: a process step that isn't gated does not exist. This repo has a measured ~0 completion rate on deferred reconciliation (QRS-180), a lint gate that once passed as a green no-op (QRS-013), and a SonarQube standard documented for months and never implemented (QRS-246). Ceremony decays; gates do not.

C4 — Level 1: System context

Push versus submit-and-wait is the distinction that shapes the whole design. Supabase and Cloudflare are push — we control the timing. Play and the App Store are submit-and-wait — a third party gates them and can reject. A release is therefore not atomic, which is why it has per-surface state rather than one status.

Major components

ComponentRoleWhere
Release recordMachine SSOT for one releasereleases/<version>/release.json
Release artifactsScope, changes, deployment, rollback, evidence, approvals, post-releasereleases/<version>/*.md
check:releaseRefuses undeclared changes, cycles, unsafe contractions, void approvalstools/check-release.js
deploy-prodThe only sanctioned path to Prod; reads the release record.github/workflows/deploy-prod.yml
check:env-driftDev↔Prod parity across schema, RLS, grants, EFs, extensionsdaily + preflight
check:fn-configRefuses a deploy that removes an EF's gateway JWT checkpreflight
check:versionVersion/build-number integrity per platformpre-push + CI
Production state ledgerWhat is actually live per surfacereleases/production-state.md
TrackerPermanent QRS-### ids for defects, debt, decisionsdev-tracker/tracker.md

Governance model

Release types and gate profiles

TypeCadenceFreezeEnvironmentsProfile
train_monthlymonthlyT-4dDEV → UAT → PRODfull
train_biweekly2-weeklyT-2dDEV → UAT → PRODfull
train_weeklyweeklyT-1dDEV → PRODstandard
hotfixon demandnoneDEV → PRODexpedited
patchon demandnoneDEV → UAT → PRODstandard

Expedited is not "fewer rules" — it is a named, recorded subset. Any gate skipped is written to gates[].waived_by with a reason. A waiver is legitimate; an unrecorded skip is not, and an undocumented bypass is how controls die (QRS-204).

QRSETU default: train_monthly + hotfix on demand. Weekly is supported by the schema but not adopted while there is one maintainer, because a 1-day freeze leaves no window for a three-surface parity check.

Decision gates

GateEntry criteriaExit
G0objectives, dates, ≥1 scope item, acceptance criteria eachscoped
G1freeze date reached; scope-change log currentscope_frozen
G2scope done; CI green; every prod change has a CR; dependencies acyclicrelease_candidate
G3deployed to UAT; evidence per CR; parity evidence for all three surfaces; risks revieweddev_verified
G4G3 exit + approval bound to a commit SHA, recorded in a later sessionapproved
G5post-deploy checks pass; observation window elapsed; metrics recordedclosed

Scope change control

High-impact triggers, each drawn from something that has already cost this project time: needs a new migration · touches packages/tokens/** or apps/*/src/ui/** (ADR-0015 design pull) · changes an Edge Function contract (multiple app versions are live) · needs new i18n keys (×3 languages) · a console-only change class · behaves differently per platform · adds a dependency or native module (app-size budget) · requires a data migration.

Zero triggers ⇒ accept without ceremony. A process that treats a copy fix like a schema change gets bypassed for both.

Roles and responsibilities

These are hats, not headcount

All five currently map to the single maintainer. Stating otherwise would imply a team that does not exist — the same dishonesty as calling a self-dispatch "peer review".

RoleOwnsToday
Release ManagerScope, gates, go/no-go, the registermaintainer
EngineerChanges and their Change Recordsmaintainer
QAEvidence per CR, three-surface paritymaintainer
DevOpsPipeline, environments, driftmaintainer
ApproverG4maintainer

Scalability triggers — when each hat separates, stated so the process does not silently stay solo-shaped:

TriggerChange
A second engineerG4 approver must differ from the author
A paid GitHub planEnvironment required-reviewers replace the honour system
>1 release/weekAdopt train_weekly; split the register per quarter

Component interaction

Approval workflow

The approval binds to content, not to a date. If the manifest changes after approval, the gate voids it. Otherwise "approved on the 14th" pins nothing and cannot answer the only question an audit asks: what exactly was approved?

Integrations

SystemDirectionPurpose
GitHub Actionsread/writeGates, deploy pipeline, 90-day artifacts
Supabase Management APIreadDrift facts, EF inventory, migration list
Supabase CLIwritedb push, functions deploynever MCP (QRS-267)
Cloudflare PageswriteWeb PWA deploy and instant rollback
Play / App StorewriteBuild submission — outcomes recorded by hand today

Design invariants

  1. No production change without a Change Record. Enforced, not requested.
  2. release.json is the only machine SSOT. Markdown carries narrative keyed by id.
  3. Approvals bind to a commit SHA and a manifest hash.
  4. A release is not atomic — per-surface targets, derived status.
  5. Contraction is bounded by the oldest live app build.
  6. All three surfaces ship in sync. No exception path exists.
  7. Irreversible changes get a forward fix, never a fictional rollback.
  8. Every gate fails closed, and "could not run" (exit 2) is distinct from "found a problem" (exit 1).