Skip to content

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:

bash
yarn ota:preview

Production 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:

EnvironmentEAS build profileEAS channelEAS update branchNormal use now
DevelopmentdevelopmentdevelopmentdevelopmentLocal dev client / Metro
Pre-productionpreviewpreviewpreviewInternal testing + preview OTA
ProductionproductionproductionproductionLaunch-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:

MechanicCurrent sourceWhat it proves
OTA scriptspackage.jsonota:preview publishes to EAS branch/environment preview
Source guardscripts/guard-ota-release.mjsOTA publish fails unless the tree is clean and HEAD is synced with upstream
Native compatibilityapps/mobile/ota-compatibility-baseline.jsonPreview OTA fails if native-sensitive files changed after the last compatible preview build/source
R1 readiness gatescripts/check-r1-release-readiness.mjsR1 acceptance fails unless static, live app-link, Kakao, OTA/native, and optional EAS gates pass
Production gatescripts/guard-ota-release.mjsProduction OTA fails unless TWOMORE_ALLOW_PRODUCTION_OTA=1 is set
Channelsapps/mobile/eas.jsondevelopment, preview, and production build profiles map to same channels
Runtime policyapps/mobile/app.json, apps/mobile/app.config.jsOTA 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:

bash
yarn check:r1-release-readiness:eas

Use the non-exiting report form for handoff or blocker triage:

bash
yarn report:r1-release-readiness

This 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:

  1. Version-control every significant change. Commit the exact source, config, migration, docs, or test changes that define the artifact.
  2. Classify before publishing. Significant means "must be traceable"; it does not automatically mean "publish an OTA."
  3. 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.
  4. 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

IDRuleWhy / sourceEnforcement today
RVO-1Every 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-2Treat 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-3Runtime-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-4With 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-5Preview 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-6Production 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-7Rollback 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-8Main-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-9Non-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-10Agent 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-11Significant 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-12A 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:

Local command evidence checked on 2026-06-28:

bash
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 --help

Installed 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 classGit actionPublish action nowRequired evidence
Docs-only or planningCommit + pushNo OTAyarn docs:check, affected doc links
Pure JS/TS client behavior or copyCommit + pushyarn ota:preview if the installed preview build can run ityarn check, update group, runtime, smoke result
Asset-only updateCommit + pushOTA only if asset is included in the update bundle and no native asset config changedyarn check, preview smoke
Supabase migration / generated typesCommit + pushApply DB change first; OTA only for compatible client follow-upmigration result, generated type check when env is available, client smoke
Edge function changeCommit + pushDeploy edge function separately; OTA only for compatible client follow-upfunction deploy command, edge smoke, client smoke if UI changed
EAS env / credential changeCommit docs/config if applicable; never commit secretsUsually rebuild or runtime smoke, not OTA-onlyEAS env evidence, target profile, smoke
Native dependency, config plugin, permission, icon/splash, app config, SDK, runtime versionCommit + push with version/runtime decisionyarn build:preview; no OTA-only shippingapp version/runtime, build ID, install smoke, rollback/fallback plan
Production hotfix after R4Commit + push from protected mainpreview verify, then launch-gated production canary/full rolloutpreview 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:

QuestionRequired 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:

  1. Verify the change against the relevant routing docs and checks.
  2. Commit only the intended paths.
  3. Push the commit before publishing a significant OTA/build.
  4. Confirm the local branch is synced with upstream before publishing/building.
  5. Record the artifact ID with the source commit.

Use this source-sync check before significant artifacts:

bash
git status --short
git rev-parse --abbrev-ref --symbolic-full-name @{u}
git fetch --prune
git status -sb
git log --oneline @{u}..HEAD

The 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 attemptDurable place to record it
Significant preview OTAdocs/release-evidence.md and the active source pack or implementation slice if it gates product acceptance
Preview builddocs/release-evidence.md; docs/guides/builds-and-deployment.md only when the workflow itself changes
Hosted Maestro or device smokedocs/release-evidence.md; docs/maestro-e2e.md when current smoke posture changes; active source pack for slice evidence
Supabase migration or edge deploydocs/release-evidence.md and the active source pack when the backend change is part of a product slice
Failed release, build, or smoke runSame place as a successful run, marked FAILURE or Blocked, with run ID, failing assertion, and next action
Docs-only release-process refinementdocs/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:

  1. Classify the change as OTA-safe.
  2. Run the required verification. Default:
    bash
    yarn check
  3. Commit the exact source that should ship.
  4. Push the commit.
  5. Confirm the tree is clean:
    bash
    git status --short
  6. Confirm the committed source is upstream-synced and OTA-compatible with the current preview native baseline:
    bash
    node scripts/guard-ota-release.mjs preview
    This should print source-sync and native-compatibility passes and stop before EAS publishing.
  7. Publish to preview:
    bash
    yarn ota:preview
  8. Record release evidence when the slice is significant or release-gated.
  9. 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:

  1. Classify why a build is required.
  2. If the native runtime changes, decide and document the app version/runtime change before building.
  3. Run preflight:
    bash
    yarn preflight
  4. Build preview:
    bash
    yarn build:preview
  5. Install and smoke-test the build.
  6. Record build evidence: profile, platform, build ID, app version, runtime version, commit SHA, and smoke result.
  7. 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:

  1. Confirm launch gates: production credentials, store listings/builds, privacy blockers, branch protection, and release-owner approval.
  2. Confirm the source commit is on protected main and all required checks passed.
  3. Verify the same runtime in preview first.
  4. Prefer promoting/republishing the tested update when possible, rather than generating a materially different bundle for production.
  5. Use canary for material risk:
    bash
    TWOMORE_ALLOW_PRODUCTION_OTA=1 yarn ota:production:canary
  6. Monitor EAS update insights, Sentry, client_errors, and the smoke path.
  7. Increase rollout or publish full production only after the evidence is green.
  8. If red, stop rollout and use the documented rollback/republish path.

Production rollback ladder after R4:

SituationPreferred action
Canary rollout is redRevert 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 goodRepublish 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 runtimeRoll back to embedded update: cd apps/mobile && eas update:roll-back-to-embedded --branch production --runtime-version <RUNTIME> --non-interactive
Operator needs guided selectionUse 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:

md
### 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:* or yarn build:*;
  • records the final update group/build ID;
  • closes the release evidence.

This keeps the release trail linear even when implementation work is parallel.

Markdown remains the source of truth. Run yarn docs:check before handoff.