Release Versioning And OTA Workflow
Status: Active Last reviewed: 2026-06-28 Scope: Canonical workflow for Git version control, app/runtime versioning, EAS Build, EAS Update, and release evidence.
Use this doc before publishing an OTA update, cutting a preview build, changing apps/mobile/app.json, changing apps/mobile/eas.json, or deciding whether a significant change needs a build instead of an update.
Record the result in Release Evidence Ledger. This workflow classifies the change; the ledger records the artifact or explicit no-artifact decision.
Current TwoMore Policy
Until R4 Launch Readiness Final Gate is accepted, the only normal OTA target is preview:
yarn ota:previewProduction OTA commands exist only so the release path is ready. They are mechanically gated by scripts/guard-ota-release.mjs and require TWOMORE_ALLOW_PRODUCTION_OTA=1 after launch readiness is explicitly accepted.
This repo uses the simple EAS convention that EAS branch names match EAS channel names:
| Environment | EAS build profile | EAS channel | EAS update branch | Normal use now |
|---|---|---|---|---|
| Development | development | development | development | Local dev client / Metro |
| Pre-production | preview | preview | preview | Internal testing + preview OTA |
| Production | production | production | production | Launch-gated, not normal today |
The mobile app currently uses runtimeVersion.policy = "appVersion" in apps/mobile/app.json. App version 0.6.1 maps to runtime 0.6.1.
Current local release mechanics:
| Mechanic | Current source | What it proves |
|---|---|---|
| OTA scripts | package.json | ota:preview publishes to EAS branch/environment preview |
| Source guard | scripts/guard-ota-release.mjs | OTA publish fails unless the tree is clean and HEAD is synced with upstream |
| Native compatibility | apps/mobile/ota-compatibility-baseline.json | Preview OTA fails if native-sensitive files changed after the last compatible preview build/source |
| R1 readiness gate | scripts/check-r1-release-readiness.mjs | R1 acceptance fails unless static, live app-link, Kakao, OTA/native, and optional EAS gates pass |
| Production gate | scripts/guard-ota-release.mjs | Production OTA fails unless TWOMORE_ALLOW_PRODUCTION_OTA=1 is set |
| Channels | apps/mobile/eas.json | development, preview, and production build profiles map to same channels |
| Runtime policy | apps/mobile/app.json, apps/mobile/app.config.js | OTA compatibility is controlled by app version/runtime and the native compatibility baseline |
The script gate proves the tree is clean, the branch has an upstream, upstream state can be fetched, local HEAD has no ahead/behind drift from that upstream, and preview OTA does not cross a native-sensitive change after the last compatible preview build/source baseline. Release owners must still classify the change and record evidence, but the preview/production OTA commands no longer rely on manual memory for source sync or native drift state.
For R1 acceptance work, yarn check is not enough by design. Run the explicit release gate:
yarn check:r1-release-readiness:easUse the non-exiting report form for handoff or blocker triage:
yarn report:r1-release-readinessThis command intentionally inspects live app-link state and EAS preview metadata, so it is not part of the normal offline development check.
The release-control model is therefore:
- Version-control every significant change. Commit the exact source, config, migration, docs, or test changes that define the artifact.
- Classify before publishing. Significant means "must be traceable"; it does not automatically mean "publish an OTA."
- Publish only the compatible artifact. OTA-safe JS goes to preview OTA; native/runtime changes go through a preview build; edge functions and database changes deploy through their own backend commands.
- Record source + artifact together. Future agents must be able to answer which commit produced which update group, build ID, function deployment, or "no publish needed" decision.
Evidence-Backed Rules
| ID | Rule | Why / source | Enforcement today |
|---|---|---|---|
| RVO-1 | Every OTA publish must come from a clean Git worktree whose HEAD is synced with upstream. | EAS Update publishes the local bundle; Git is the only durable source pointer for agents, CI, and rollback review. | scripts/guard-ota-release.mjs blocks dirty, no-upstream, ahead, and behind states. |
| RVO-2 | Treat EAS channel + runtime version as the deployment boundary, not chat memory or a branch name alone. | Expo's deployment model uses channels, branches, and runtime versions to target app versions and keep deployment bookkeeping clear. | apps/mobile/eas.json channel names; this doc; root ota:* scripts. |
| RVO-3 | Runtime-affecting native changes require a new build before updates can target that runtime. | Expo runtime versions represent compatibility between native code and an update; updates must be compatible with the build's native code. | scripts/guard-ota-release.mjs compares current source against apps/mobile/ota-compatibility-baseline.json; yarn build:preview for native/runtime changes. |
| RVO-4 | With appVersion runtime policy, native/runtime releases must deliberately bump the app version/runtime target. | Expo recommends appVersion as the default runtime policy and uses the app version as the runtime target for each release. | apps/mobile/app.json; release reviewer must verify version/runtime before build. |
| RVO-5 | Preview verification comes before production promotion. | Expo recommends verifying an update in a staging/preview target before production and republishing/promoting the same tested commit when possible. | Current policy is preview-only until R4; production guard blocks accidental publish. |
| RVO-6 | Production updates after launch use canary or staged rollout when risk is material. | EAS supports per-update rollouts with --rollout-percentage and later edit/revert commands. | yarn ota:production:canary is available but launch-gated. |
| RVO-7 | Rollback must be planned before production publish, not invented during an incident. | EAS supports rollback/republish paths; the operator must know the previous good update group/runtime. | Release evidence template requires rollback pointer. |
| RVO-8 | Main-branch protection and required checks are part of release safety, not optional process overhead. | GitHub protected branches can require status checks, linear history, block force pushes, and prevent deletion. | Pre-launch blocker until branch protection is enabled; local hooks and yarn check are baseline. |
| RVO-9 | Non-interactive rollback uses explicit EAS commands, not ad hoc rollout edits. | Installed EAS CLI 18.7.0 supports update:revert-update-rollout, update:republish, and update:roll-back-to-embedded; its update:rollback help explicitly points non-interactive use to those commands. | Documented rollback ladder; release owner verifies eas <command> --help before production use. |
| RVO-10 | Agent release ownership is linear: one release owner classifies, publishes, records artifact IDs, and closes evidence. | Parallel agents increase drift risk unless artifact ownership is singular. | Multi-agent release ownership section below. |
| RVO-11 | Significant release attempts, including failed OTAs, builds, and hosted smokes, must be recorded in committed docs. | EAS artifact URLs and CLI output are ephemeral; committed release evidence is the durable recovery path for future agents. | Release Evidence Ledger; yarn docs:check verifies links and generated state freshness. |
| RVO-12 | A significant change may explicitly close as "no OTA/build run" when the artifact is docs-only, planning-only, or backend-only. | Treating every significant change as OTA-worthy creates false evidence; classification must preserve the distinction between source control and app distribution. | Significant change matrix + release evidence template. |
Research basis:
- Expo EAS Update: Deploy updates
- Expo EAS Update: Runtime versions and updates
- Expo EAS Update: Rollouts
- Expo EAS Update: Rollbacks
- GitHub Docs: About protected branches
Local command evidence checked on 2026-06-28:
cd apps/mobile
eas --version
eas update --help
eas update:rollback --help
eas update:republish --help
eas update:roll-back-to-embedded --help
eas update:revert-update-rollout --helpInstalled CLI evidence on 2026-06-28: eas-cli/18.7.0 in this workspace, with the CLI warning that 20.4.0 is available. Follow the installed-project help when operating from this repo, and rerun the help commands before any production rollback if the CLI version changes.
Significant Change Classification
"Significant" means the change is user-visible, data-shape-affecting, release-risky, or part of a launch gate. Significant does not automatically mean "publish OTA." Classify the change first:
| Change class | Git action | Publish action now | Required evidence |
|---|---|---|---|
| Docs-only or planning | Commit + push | No OTA | yarn docs:check, affected doc links |
| Pure JS/TS client behavior or copy | Commit + push | yarn ota:preview if the installed preview build can run it | yarn check, update group, runtime, smoke result |
| Asset-only update | Commit + push | OTA only if asset is included in the update bundle and no native asset config changed | yarn check, preview smoke |
| Supabase migration / generated types | Commit + push | Apply DB change first; OTA only for compatible client follow-up | migration result, generated type check when env is available, client smoke |
| Edge function change | Commit + push | Deploy edge function separately; OTA only for compatible client follow-up | function deploy command, edge smoke, client smoke if UI changed |
| EAS env / credential change | Commit docs/config if applicable; never commit secrets | Usually rebuild or runtime smoke, not OTA-only | EAS env evidence, target profile, smoke |
| Native dependency, config plugin, permission, icon/splash, app config, SDK, runtime version | Commit + push with version/runtime decision | yarn build:preview; no OTA-only shipping | app version/runtime, build ID, install smoke, rollback/fallback plan |
| Production hotfix after R4 | Commit + push from protected main | preview verify, then launch-gated production canary/full rollout | preview update group, production update group, monitoring, rollback pointer |
If classification is unclear, choose the build path or stop for a release-owner decision. The unsafe failure mode is publishing an OTA bundle that calls native code absent from the installed binary.
Release Decision Gate
Before any significant artifact attempt, answer these in the evidence note:
| Question | Required answer before proceeding |
|---|---|
| Is the change significant? | If yes, it needs commit + push + evidence. If no, normal local development is enough. |
| Is the mobile JS bundle affected? | If no, do not run OTA merely because the change is important. |
| Is the change OTA-safe for the installed preview build? | If yes, preview OTA is allowed after verification. If no, use preview build or backend deploy flow. |
| Is the source synced with upstream? | scripts/guard-ota-release.mjs enforces this for OTA. For builds, git log --oneline @{u}..HEAD should be empty and git status -sb should show no ahead/behind drift. |
| Where will the artifact evidence live? | Pick docs/rebuild-log.md, the active source pack, docs/maestro-e2e.md, or the release note before running commands. |
| What is the rollback or mitigation pointer? | Previous known-good update group/build/runtime, backend revert path, or "not applicable: docs-only/no artifact." |
This is the canonical answer to "does the app version-control and OTA update whenever it has undergone significant changes?":
- Version control: yes, every significant change must be committed, pushed, and upstream-synced before a release artifact is claimed.
- OTA update: only when the change affects the OTA bundle and is compatible with the installed runtime. Before launch, that target is
preview. - Build: required for native/runtime/app-config changes.
- Backend deploy: required for edge function or database changes; OTA is only a compatible client follow-up.
- Docs-only: commit + push +
yarn docs:check; record that no OTA/build was run when the doc change affects release, roadmap, or launch guidance.
Version-Control Contract
Every significant change must leave a source trail before an artifact trail. Use this order:
- Verify the change against the relevant routing docs and checks.
- Commit only the intended paths.
- Push the commit before publishing a significant OTA/build.
- Confirm the local branch is synced with upstream before publishing/building.
- Record the artifact ID with the source commit.
Use this source-sync check before significant artifacts:
git status --short
git rev-parse --abbrev-ref --symbolic-full-name @{u}
git fetch --prune
git status -sb
git log --oneline @{u}..HEADThe first command must be empty. The second command must resolve an upstream. The third command refreshes upstream state. The fourth command should not show ahead/behind drift. The fifth command must be empty; if it prints commits, push before publishing.
For OTA, the root yarn ota:* commands run this source-sync gate through scripts/guard-ota-release.mjs. For preview builds, run the check explicitly before yarn build:preview.
For docs-only changes, the artifact trail is usually "no OTA/build run." That is still evidence and should be written explicitly when the change touches release, launch, or roadmap guidance.
Do not publish a significant OTA from:
- uncommitted changes;
- a local-only commit that no other agent can fetch;
- a commit whose checks are unknown or red;
- a tree with unrelated staged changes;
- a source state different from the one named in the evidence note.
Artifact Evidence Routing
Volatile release facts do not belong in generated state pages. Keep generated docs for repository facts, and keep artifact history in the active slice or release evidence docs:
| Artifact or attempt | Durable place to record it |
|---|---|
| Significant preview OTA | docs/release-evidence.md and the active source pack or implementation slice if it gates product acceptance |
| Preview build | docs/release-evidence.md; docs/guides/builds-and-deployment.md only when the workflow itself changes |
| Hosted Maestro or device smoke | docs/release-evidence.md; docs/maestro-e2e.md when current smoke posture changes; active source pack for slice evidence |
| Supabase migration or edge deploy | docs/release-evidence.md and the active source pack when the backend change is part of a product slice |
| Failed release, build, or smoke run | Same place as a successful run, marked FAILURE or Blocked, with run ID, failing assertion, and next action |
| Docs-only release-process refinement | docs/release-evidence.md entry saying no OTA/build was run, plus yarn docs:generate and yarn docs:check proof. If the change is the ledger itself, use the commit and final handoff instead of a self-referential ledger entry. |
Do not replace a failed artifact with a later successful one without preserving the failed run ID. Failed runs are part of the evidence chain because they show which hypothesis was tested and what changed next.
Canonical Pre-Production OTA Flow
Use this sequence for JS-only changes while the app is pre-production:
- Classify the change as OTA-safe.
- Run the required verification. Default:bash
yarn check - Commit the exact source that should ship.
- Push the commit.
- Confirm the tree is clean:bash
git status --short - Confirm the committed source is upstream-synced and OTA-compatible with the current preview native baseline:bashThis should print source-sync and native-compatibility passes and stop before EAS publishing.
node scripts/guard-ota-release.mjs preview - Publish to preview:bash
yarn ota:preview - Record release evidence when the slice is significant or release-gated.
- Smoke-test the installed preview build.
The OTA message comes from the latest commit subject, so write commit subjects as release labels a human can understand.
If yarn ota:preview fails the guard, stop and inspect the status. Dirty or ahead/behind failures are source-sync problems. Native-sensitive path failures mean the current source requires a compatible preview build before OTA can resume. Do not stash, discard unrelated work, or publish around the guard.
Preview Build Flow
Use this sequence when the change is not OTA-safe:
- Classify why a build is required.
- If the native runtime changes, decide and document the app version/runtime change before building.
- Run preflight:bash
yarn preflight - Build preview:bash
yarn build:preview - Install and smoke-test the build.
- Record build evidence: profile, platform, build ID, app version, runtime version, commit SHA, and smoke result.
- If the build proves the current source is the new OTA-compatible baseline, update apps/mobile/ota-compatibility-baseline.json with the source commit, build ID(s), runtime, and evidence pointer before any later
yarn ota:preview.
Do not follow a runtime-affecting preview build with an OTA publish until the installed build's runtime matches the intended update runtime and the mobile OTA compatibility baseline names that proven build/source state.
Production Flow After R4
Production OTA remains blocked until R4 is accepted. After R4:
- Confirm launch gates: production credentials, store listings/builds, privacy blockers, branch protection, and release-owner approval.
- Confirm the source commit is on protected
mainand all required checks passed. - Verify the same runtime in preview first.
- Prefer promoting/republishing the tested update when possible, rather than generating a materially different bundle for production.
- Use canary for material risk:bash
TWOMORE_ALLOW_PRODUCTION_OTA=1 yarn ota:production:canary - Monitor EAS update insights, Sentry,
client_errors, and the smoke path. - Increase rollout or publish full production only after the evidence is green.
- If red, stop rollout and use the documented rollback/republish path.
Production rollback ladder after R4:
| Situation | Preferred action |
|---|---|
| Canary rollout is red | Revert the rollout update: cd apps/mobile && eas update:revert-update-rollout --group <UPDATE_GROUP_ID> --non-interactive |
| Full production OTA is red and previous OTA is known good | Republish the known-good group: cd apps/mobile && eas update:republish --group <GOOD_GROUP_ID> --destination-branch production --non-interactive |
| No previous OTA is safe for the runtime | Roll back to embedded update: cd apps/mobile && eas update:roll-back-to-embedded --branch production --runtime-version <RUNTIME> --non-interactive |
| Operator needs guided selection | Use cd apps/mobile && eas update:rollback, but treat it as interactive and record the selected rollback target manually |
Before using any rollback command in production, run the command with --help and record the CLI version because EAS CLI behavior changes over time.
Release Evidence Template
Use this in Release Evidence Ledger, a handoff, or the release note whenever the change is significant:
### Release Evidence - YYYY-MM-DD
- Change:
- Classification: docs-only | OTA-safe | preview-build | production-hotfix
- Source commit:
- Source sync: upstream-synced | not applicable
- Source sync proof: `node scripts/guard-ota-release.mjs <target>` passed | `git log --oneline @{u}..HEAD` empty | not applicable
- Target: preview OTA | preview build | backend deploy | production canary | production full | no artifact
- App version:
- Runtime version:
- EAS artifact: update group / build ID / rollout percentage
- Verification:
- Smoke evidence:
- Monitoring checked:
- Rollback pointer:
- Known gaps:Agents should not claim a change shipped unless the evidence includes the artifact ID or explicitly says publish/build was not run.
For failed release attempts, keep the same template and mark the target as failed or blocked. Failed artifact IDs are still evidence because future agents need to avoid repeating the same failure path.
Multi-Agent Release Ownership
For multiple agents, use one release owner. Other agents may implement or verify slices, but they report classification and evidence back to the owner. The release owner is the only actor who:
- decides OTA-safe vs build-required when classification is ambiguous;
- runs
yarn ota:*oryarn build:*; - records the final update group/build ID;
- closes the release evidence.
This keeps the release trail linear even when implementation work is parallel.