Automating React Native OTA updates in CI/CD
Manual laptop releases work for a first test, but production React Native teams need repeatable pipelines: native store builds register their baseline binary automatically, and JavaScript merges ship staged delta patches without human copy-paste errors.
This guide covers end-to-end CI/CD setups for GitHub Actions, Fastlane, and GitLab CI using react-native-update-cli and scoped Cresc API Keys.
Golden rule: separate native changes from JS OTA jobs
An over-the-air update can ship JavaScript, TypeScript, styling, pure-JS npm packages, and Metro-bundled assets — never changes to ios/, android/, harmony/, CocoaPods, or Gradle dependencies (full boundary table).
Structure your CI around two distinct workflows:
- Native store pipeline (triggered on release tags like
v2.4.0or native directory changes): compiles the signed.ipaand.aab/.apk, uploads that exact binary to Cresc (cresc uploadIpa/uploadAab) as the diff baseline, and submits to App Store Connect / Google Play. - OTA hotfix pipeline (triggered on
ota/*tags,workflow_dispatch, or JS-only commits): bundles JavaScript, publishes a Hermes-aware delta update, and binds it to compatible native versions at a safe initial rollout percentage (for example--rollout 10).
Prerequisites for any CI runner
- Commit
update.jsonto git. Created bycresc createApp, it holds your public per-platformappKeys so CI knows which iOS and Android apps to target. - Create a Write-scoped API Key in the Cresc console and store it in your CI secret manager as
CRESC_API_TOKEN. - Pass explicit publish and bind flags. When no interactive TTY is attached,
cresc bundlenever prompts:- Without
--name, it builds the.ppklocally and exits without uploading. - Without a target flag (
--packageVersionRange,--packageVersion,--packageId,--minPackageVersion, or--maxPackageVersion), it uploads the version unbound so zero devices receive it.
- Without
GitHub Actions: JS path guard + staged OTA rollout
This workflow runs on manual dispatch or pushes to release/ota, blocks accidental OTA deploys when native directories changed since the last native tag, and publishes to both iOS and Android at a configurable canary percentage:
Notice that publishing an OTA update does not require macOS runners or Xcode — ubuntu-latest bundles both iOS and Android JavaScript and Hermes bytecode in under two minutes.
Fastlane: upload native baselines automatically on store builds
Every time Fastlane archives a new store release, upload the exact .ipa or .aab artifact to Cresc in the same lane so nobody forgets to register the baseline:
Run assembleRelease and bundleRelease in one Gradle command (tasks: ["clean", "assembleRelease", "bundleRelease"]), not two separate gradle(...) calls. Separate invocations inject different build timestamps, reducing diff reuse across channels. See Best Practices.
GitLab CI example
Promoting a staged rollout or rolling back from CI
Once Analytics and JS Error Monitoring confirm healthy adoption and zero regressions on your 10% canary, promote that versionId to 100% without rebuilding the bundle:
If an incident surfaces instead, follow the OTA rollback & recovery playbook to pause or re-bind the previous known-good versionId immediately.
FAQ
Does publishing an iOS OTA update in CI require a macOS runner?
No. cresc bundle --platform ios runs Metro and the Hermes bytecode compiler (hermesc), both of which ship cross-platform binaries for Linux. Standard Linux runners (ubuntu-latest or node:22-bookworm) can bundle and publish both iOS and Android OTA updates. You only need macOS when archiving a new native .ipa.
Why did cresc bundle succeed in CI without delivering the update to users?
In non-interactive CI environments, cresc bundle only builds locally unless --name is provided, and only binds to devices when a target flag (--packageVersion, --packageVersionRange, --packageId, --minPackageVersion, or --maxPackageVersion) is passed alongside --name.
How do I prevent engineers from shipping native changes through the OTA pipeline?
Add a git diff guard step comparing HEAD against your last native release tag on ios/, android/, harmony/, and native lockfiles, failing the OTA workflow whenever native files have changed.
Are source maps uploaded automatically during CI builds?
Yes. With react-native-update-cli v2.23.0+, --sourcemap is enabled by default. The CLI strips node_modules sources, compresses the map, and archives it server-side so JS Error Monitoring can symbolicate production crashes without shipping source maps to client devices.