Skip to content

Release process

The operating procedure. Architecture is in the HLD; mechanics of how each component works are in the LLD. This page is what you follow while running a release.

Start a release

bash
cp -r documentation/portal/releases/_template documentation/portal/releases/26.1.0

Fill release.json: version, type, cadence.target_date, cadence.scope_freeze, and base_ref (the previous release's tag or cut SHA — it defines the change range the gate diffs).

Then run npm run check:release. A draft with empty scope passes; the gate tightens as the release advances.

G0 — Scope approval

Fill 00-scope.md: objectives, scope items (each an existing QRS-###, never a new id), acceptance criteria, milestones, and measurable release-level success criteria.

Set status: "scoped". From here the gate requires a target date and at least one scope item, and it verifies every scope id exists in the tracker.

The divergence-seam question — ask it here, not at G3 (QRS-297)

If a scope item touches a divergence seam — camera/QR scan, push and notifications, storage or filesystem, share, clipboard, deep links, offline — its acceptance criteria must name the implementation or an approved fallback, per surface, before work starts.

This costs one line at G0 and it is the cheapest control in the whole process, because the platform-exception path is retired: a feature that turns out to be unbuildable on one surface now blocks the entire release rather than shipping on two. Asking at G0 means such a feature is never built to that point — it simply does not enter scope.

Scope is the pressure valve, not an exception. Blocking a feature is cheap; blocking a release is not. A feature that cannot reach all three surfaces slips to the next release, and that is a normal outcome rather than a failure.

During development

Whenever you change a production-affecting file — supabase/migrations/**, supabase/functions/**, supabase/config.toml — add a Change Record in the same commit:

  1. An entry in release.jsonchanges[] (id, class, target, reversible, blast radius).
  2. A matching row in 01-change-log.md.

The gate asserts the two id sets match exactly, in both directions. From release_candidate onward it also fails on any changed production file with no Change Record.

Console-only changes need a Change Record too — auth settings, SMTP, DNS, Cloudflare, secrets, storage, cron. No gate can see them, so their evidence must be a live functional probe, never a claim that a value was set.

Handling an ad-hoc request

SituationAction
Defect in already-accepted scope, P0/P1Accept
Defect, P2 or lowerDefer
New work, after scope freezeDefer — no exceptions
New work, before freeze, no high-impact triggerAccept, no ceremony
New work, before freeze, any trigger firesWritten impact assessment, then an explicit decision to accept the date risk or defer

Record every request in 00-scope.md's scope-change log, accepted or not. An empty table after a real cycle means it is not being filled in.

Triggers: new migration · packages/tokens/** or apps/*/src/ui/** · Edge Function contract change · new i18n keys · console-only class · per-platform behaviour · new dependency or native module · data migration.

G1 — Scope freeze

On the freeze date set status: "scope_frozen". Only P0/P1 defects in accepted scope may enter after this; anything else goes to the next release.

G2 — Code complete

All scope items done, CI green, every production change declared, dependencies acyclic. Set status: "release_candidate".

G3 — UAT and readiness review

Deploy to Dev/UAT. Capture in 04-test-evidence.md:

  • gate results
  • parity evidence for all three surfaces — Android native, iOS native, Web PWA. There is no exception field; parity is unconditional
  • an evidence anchor per Change Record, with a probe for every console-only class
  • risks reviewed and current

Set status: "dev_verified".

G3 collects evidence — it does not generate it (QRS-297)

The Definition of Done already requires each feature to be parity-verified when it is built, so by G3 the proof should already exist and this gate is assembling it.

If G3 is the first three-surface pass, a whole release's worth of risk lands on one gate and any single failure blocks everything — with no exception path to absorb it. That is the failure mode this note exists to prevent, and it is a process smell rather than a gate problem: the fix is upstream, at the feature, not here.

G4 — Go / no-go

Record the approval in a separate session from the request

There is one maintainer and no required-reviewers on a private Free repo, so this is a guarded self-dispatch, not peer review. Time separation is the substitute for a second pair of eyes — do not do this in the same sitting as the work, and never describe it as peer approval.

In 05-approvals.md record the date, the commit SHA, the manifest hash, and the explicit list of what you checked. Set status: "approved".

If the manifest changes afterwards, the gate reports the approval VOID and the deploy is refused. That is the mechanism, not a formality.

Deploy

Actions → deploy-prod → Run workflow
  release:         26.1.0
  release_version: 26.1.0
  confirm:         26.1.0
  components:      both
  dry_run:         false        ← defaults to true; turn it off deliberately

A real deploy runs from main only. Preflight re-runs every gate before the deploy job is reachable, so what you are approving is already printed in the run log.

Deployment order is fixed: backend (expand only) → web → stores. Update targets[] as each lands, and production-state.md when a surface goes live.

Store submissions

Record each build in 01-change-log.md's build ledger with its commit_sha.

If a platform is rejected: resubmit as a new build (npm run version:set -- <version> --build-ios N), add a new build row with supersedes, and leave the version name alone. If the resubmission is from the same commit, the gate accepts functional_delta: null by comparing SHAs. If you had to change code, describe the delta and assess the other platforms.

While one platform is live and another is not, the release is partially_deployed. That is a fact, not a failure.

G5 — Closure

After the observation window elapses: complete 06-post-release.md, record metrics, close any divergence with a named follow-up release, and set status: "closed" (or closed_with_divergence).

Append a delivery log entry — that is a standing instruction for every request, and the release's own retrospective is the natural place to satisfy it.

Rollback

Only if the trigger recorded in 03-rollback-plan.md is breached. Backend rolls back by compensating migration, Edge Functions by git revert + redeploy, web by Cloudflare instant rollback. Stores cannot be rolled back — halt the rollout and ship a forward fix.

Set status: "rolled_back"; the release returns to in_development.