Publish a React Native OTA Release

Build a JavaScript bundle, publish it to a deployment, and verify that installed apps actually picked the update up.

Publish

Publish a release

Run this from your React Native project root. It detects Expo vs bare React Native, runs the right bundler for each configured platform, zips the output with its assets, and uploads it:

terminal
npm run ota:staging                       # ota release --channel staging
npm run ota:prod                          # ota release --channel production
ota release --channel production --rollout 20 --platform ios

Nothing is positional — no app name, no key, no version. The project comes from ota.config.js, the runtime version from your app version, and the release note, commit, branch and author from git. Every flag is optional:

FlagDefaultMeaning
-c, --channelstagingWhich channel to publish to.
-p, --platformbothLimit to android or ios.
-m, --messagelast commit subjectRelease note shown in the dashboard and to the SDK.
-r, --rollout100Percentage of devices served, 1–100.
--mandatoryoffDevices install before continuing, using IMMEDIATE by default.
--runtime-versionyour app versionWhich native builds this bundle is compatible with.
--allow-dirtyoffPublish with uncommitted changes in the working tree.
--allow-duplicateoffPublish even though the bundle is byte-identical to the current release.

What a publish prints:

terminal
$ ota release --channel production -m "Fix checkout crash"
✔ Released #14 to production
  android  4.2 MB
  ios      4.4 MB
  commit 9f3c1ab (main)

#14 is the release label — the same value the SDK reports as getCurrentPackage().label, so a device can always be matched to a release.

A dirty working tree is refused

A release built from uncommitted changes contains code that exists on no branch, and nobody can reproduce what shipped. Commit first, or pass --allow-dirty and accept that the release is recorded as dirty.

Get the runtime version right

A release is only served to devices whose native build it declares compatibility with — not your JS version. The CLI derives it from your app version, so you rarely set it by hand; when you do, it is --runtime-version (the device-side SDK reports the same value as targetBinaryVersion). Get it wrong and no device ever receives the release, which is the most common cause of "I published but nothing happened".

Three commands cover the whole job

ota init wrote the scripts so nothing here has to be memorised: npm run ota:prod ships, npm run ota:doctor says why it didn't, and npm run ota:rollback undoes it. Flags go after --(npm run ota:prod -- --rollout 10), and yarn and pnpm need no -- at all. The raw commands in the CLI reference are what these scripts run.

Promote instead of rebuilding

Once staging looks healthy, serve the exact same bytes from production. The stored bundle is reused, so there is no bundler run and no upload:

terminal
npm run ota:promote                              # staging → production
ota promote --from staging --to production

Confirm

Verify it worked

Change something visible, publish, then background and cold-start the app. The default install mode is ON_NEXT_RESTART, so a foreground refresh will not apply it.

terminal
# Android — watch the native state machine
adb logcat | grep OtaUpdate

# Expected on a successful update:
#   I OtaUpdate: booting into pending update <sha256>
#   I OtaUpdate: update <sha256> confirmed healthy

On iOS, open Console.app and filter on OtaUpdate.

Server side, confirm the device reported in:

terminal
npm run ota:doctor            # when nothing arrives at all
ota status                    # the last ten releases and what each is doing

Or open the channel in the dashboard, where each release shows how many devices are running it, the install and failure counts, and what share of that platform’s active fleet has moved — Android and iOS separately, so a healthy number on one cannot hide a stalled one on the other.

Testing rollback without writing a crash

Remove withOtaUpdate (or simply never call notifyAppReady()), publish, let the device install it, then relaunch twice. The second launch reverts to the previous bundle and blacklists the bad hash — you will see failed to become ready — rolling back in the log and a rolled_back report in the dashboard.