Releasing a New Version β
How a new build of the mobile app goes from a merged commit to users' devices β through store release (native builds) or OTA update (JS-only changes).
Two delivery paths, pick by what changed:
| What changed | Path |
|---|---|
| JS/TS, styles, translations, Metro-bundled assets, feature-flag features | OTA (EAS Update) |
| Any native dependency, Expo SDK bump, config plugin, permission, Firebase blob, entitlement / Info.plist / AndroidManifest | Store build |
Rule of thumb: if it changes the native fingerprint, it needs a store build. If unsure, publish OTA anyway β a fingerprint mismatch means old binaries simply don't receive it (see What is OTA-shippable).
TIP
For the raw eas build / eas submit mechanics, profiles, and Play service-account setup, see Build & Submit. This page is the end-to-end workflow.
Version knobs β
Two independent version numbers. Don't conflate them.
| Knob | iOS | Android | Source | When to bump |
|---|---|---|---|---|
| Marketing version | CFBundleShortVersionString | versionName | app.config.ts version β reads package.json version (single source) | Once per public release |
| Build number | CFBundleVersion | versionCode | Auto-incremented remotely (eas.json appVersionSource: "remote") | Every build (automatic) |
IMPORTANT
Bump marketing version only per public release, not per build. Apple closes a pre-release train once its marketing version is released (Invalid Pre-Release Train β¦ is closed). Multiple TestFlight builds share one marketing version β the automatic build-number bump is enough until it goes public.
Store release (native build) β
1. Cut from the right branch β
Store builds embed the JS at build time. Always build from the branch matching the target environment, never a stale local checkout β otherwise fresh installs launch on old code.
stagingbranch β staging backend, internal testersproductionbranch β production backend, TestFlight + public
Promotion is merge-forward: main β staging β production (never skip an env).
2. Staging build (internal testing first) β
# Build both platforms + auto-submit to internal tracks
pnpm build:submit:staging # eas build --platform all --profile staging --auto-submit --non-interactive
pnpm build:submit:staging:ios # iOS onlyLands on Play internal track / TestFlight within minutes-to-review. Smoke-test here before promoting.
3. Production release β
Use the release script β it bumps the marketing version, commits package.json, then builds + auto-submits in one shot:
pnpm release:production # version:patch β commit β build:submit:production (all platforms)
pnpm release:production:ios # iOS onlyWhat it runs under the hood:
version:patch # pnpm version patch --no-git-tag-version (marketing version, e.g. 1.0.1 β 1.0.2)
git commit package.json -m "π chore(mobile): bump marketing version to X.Y.Z"
build:submit:production # eas build --platform all --profile production --auto-submitNOTE
The version bump lives in release:production, not in build:submit:production β that script stays pure and re-runnable. If you need to rebuild the same version without bumping, call build:submit:production directly.
4. Store-side release (the human step) β
--auto-submit uploads the build to the store; it does not make it public.
- Google Play: build lands on the
productiontrack. First submission of an app requires review; the very first release of a brand-new app cannot be created byeas submitβ upload that first AAB via the Play Console UI. Release #2+ works througheas submit. - Apple: build lands in App Store Connect / TestFlight. "Promote to public" is done inside the production ASC app (TestFlight β public) β the backend URL is baked at build time, so there is no rebuild.
Store apps / bundles:
| Env | Bundle | Backend | Distribution |
|---|---|---|---|
| Staging | com.tellia.mobile.staging | Staging | Internal testers |
| Prod | com.tellia.mobile | Production | TestFlight beta + public |
5. Store listing metadata (App Store only) β
If the release changes the "What's New" text, description, screenshots, or any other App Store Connect listing field, update apps/mobile/store.config.js and push it β don't hand-edit the ASC portal, it'll drift from the repo again.
pnpm --filter @tellia-solutions/mobile-app metadata:pull # check live ASC state first β see caveat below
pnpm --filter @tellia-solutions/mobile-app metadata:push # needs a submitted binary firstWARNING
metadata:push overwrites the live listing with whatever's in store.config.js β always metadata:pull and diff before editing, and never push right after a schema/config change without a fresh pull. See apps/mobile/marketing/README.md for the full setup (env vars for the demo password / review phone, why it's a .js not .json config).
EAS Metadata is Apple-only (preview feature, no Play Store equivalent). Google Play listing text is still copy-pasted into Play Console by hand from apps/mobile/marketing/play-store/copy/*.md.
OTA updates (EAS Update) β
JS-only changes ship over the air β no store round-trip. Full runbook lives in the app repo at apps/mobile/EAS_UPDATE.md.
Publish β
# From apps/mobile β prompts for an update message
pnpm update:staging # channel "staging", EAS env "preview"
pnpm update:production # channel "production", EAS env "production"Publishing is manual-only, on demand β no CI path by design.
Requirements:
SENTRY_AUTH_TOKENin the shell (scripts upload source maps fromdist/after publish).- Authenticated EAS CLI (
eas whoami). - Publish from a clean checkout of the branch matching the channel β the bundle is whatever your working tree contains.
WARNING
Never publish with local EXPO_PUBLIC_* overrides exported. Both scripts pin bundle env via eas update --environment <preview|production> (server-side EAS env, same values store builds bake). A bundle published with the wrong API URL repoints every installed app.
Runtime behavior β
Native check at cold launch + foreground checks (hooks/use-ota-updates.ts). Download is silent and applies on the next cold launch β never blocks startup; a failed download keeps the current bundle.
How the fingerprint gates OTA β
runtimeVersion: { policy: 'fingerprint' } hashes the native project. An update only reaches binaries with an identical native runtime. Any native change produces a new fingerprint β old binaries are structurally unreachable by the new JS. No manual version discipline needed.
The publish-side fingerprint (your machine) and build-side fingerprint (EAS builder) must be identical or EAS fails the build with "Runtime version mismatch". Stabilizers that keep them equal (don't remove without understanding): .fingerprintignore (Firebase blobs), fingerprint.config.js (skips package.json scripts), and inline APP_VARIANT pinning in the update scripts. Truth for debugging drift:
APP_VARIANT=preview GOOGLE_SERVICES_JSON=./firebase-configs/staging/google-services.json \
eas fingerprint:compare --build-id <id>What is OTA-shippable β
- OTA-safe: JS/TS, styles, translations, Metro-bundled assets, feature-flag-guarded features.
- Store build required: any native-code dependency, Expo SDK bump,
app.config.tsnative config (plugins, permissions,expo-build-properties, Firebase blobs), anything touching entitlements / Info.plist / AndroidManifest.
Gradual rollout β
eas update --channel production --environment production --rollout-percentage 20
# then widen:
eas update:editRollback β
eas update:republish --channel production # republish a known-good group (interactive)
eas update:roll-back-to-embedded --channel production # revert to JS embedded in the binaryDevices pick up the rollback on their next check (foreground or cold launch).
Verify an update landed β
eas update:list --branch productionβ published update groups.- EAS Update insights (expo.dev β Updates): embedded-vs-OTA adoption per channel, crash correlation.
- Sentry events carry
expo-update-id(orexpo-is-embedded-update: truefor binary JS).
Decision cheat-sheet β
Merged a change β need it on devices?
ββ Only JS / styles / translations / assets / flags?
β ββ pnpm update:staging β verify β pnpm update:production
ββ Touched anything native (deps, SDK, plugins, permissions, blobs)?
ββ pnpm build:submit:staging β smoke test
β pnpm release:production β release in store console