Appearance
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.ymlreadsrelease.jsonand 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
| Component | Role | Where |
|---|---|---|
| Release record | Machine SSOT for one release | releases/<version>/release.json |
| Release artifacts | Scope, changes, deployment, rollback, evidence, approvals, post-release | releases/<version>/*.md |
check:release | Refuses undeclared changes, cycles, unsafe contractions, void approvals | tools/check-release.js |
deploy-prod | The only sanctioned path to Prod; reads the release record | .github/workflows/deploy-prod.yml |
check:env-drift | Dev↔Prod parity across schema, RLS, grants, EFs, extensions | daily + preflight |
check:fn-config | Refuses a deploy that removes an EF's gateway JWT check | preflight |
check:version | Version/build-number integrity per platform | pre-push + CI |
| Production state ledger | What is actually live per surface | releases/production-state.md |
| Tracker | Permanent QRS-### ids for defects, debt, decisions | dev-tracker/tracker.md |
Governance model
Release types and gate profiles
| Type | Cadence | Freeze | Environments | Profile |
|---|---|---|---|---|
train_monthly | monthly | T-4d | DEV → UAT → PROD | full |
train_biweekly | 2-weekly | T-2d | DEV → UAT → PROD | full |
train_weekly | weekly | T-1d | DEV → PROD | standard |
hotfix | on demand | none | DEV → PROD | expedited |
patch | on demand | none | DEV → UAT → PROD | standard |
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
| Gate | Entry criteria | Exit |
|---|---|---|
| G0 | objectives, dates, ≥1 scope item, acceptance criteria each | scoped |
| G1 | freeze date reached; scope-change log current | scope_frozen |
| G2 | scope done; CI green; every prod change has a CR; dependencies acyclic | release_candidate |
| G3 | deployed to UAT; evidence per CR; parity evidence for all three surfaces; risks reviewed | dev_verified |
| G4 | G3 exit + approval bound to a commit SHA, recorded in a later session | approved |
| G5 | post-deploy checks pass; observation window elapsed; metrics recorded | closed |
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".
| Role | Owns | Today |
|---|---|---|
| Release Manager | Scope, gates, go/no-go, the register | maintainer |
| Engineer | Changes and their Change Records | maintainer |
| QA | Evidence per CR, three-surface parity | maintainer |
| DevOps | Pipeline, environments, drift | maintainer |
| Approver | G4 | maintainer |
Scalability triggers — when each hat separates, stated so the process does not silently stay solo-shaped:
| Trigger | Change |
|---|---|
| A second engineer | G4 approver must differ from the author |
| A paid GitHub plan | Environment required-reviewers replace the honour system |
| >1 release/week | Adopt 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
| System | Direction | Purpose |
|---|---|---|
| GitHub Actions | read/write | Gates, deploy pipeline, 90-day artifacts |
| Supabase Management API | read | Drift facts, EF inventory, migration list |
| Supabase CLI | write | db push, functions deploy — never MCP (QRS-267) |
| Cloudflare Pages | write | Web PWA deploy and instant rollback |
| Play / App Store | write | Build submission — outcomes recorded by hand today |
Design invariants
- No production change without a Change Record. Enforced, not requested.
release.jsonis the only machine SSOT. Markdown carries narrative keyed by id.- Approvals bind to a commit SHA and a manifest hash.
- A release is not atomic — per-surface targets, derived status.
- Contraction is bounded by the oldest live app build.
- All three surfaces ship in sync. No exception path exists.
- Irreversible changes get a forward fix, never a fictional rollback.
- Every gate fails closed, and "could not run" (exit 2) is distinct from "found a problem" (exit 1).