Migrate from Stallion
Move a React Native or Expo app from Stallion to HotCodePush, with channels for projects, the fingerprint for app versions and useUpdates() for the hooks.
A React Native or Expo app leaves Stallion at its next store release: the build that carries @hotcodepush/react-native-code-push is the switch. Stallion's projects become channels, its exact app version becomes the fingerprint, and the signing key carries over. What differs, side by side, is on the site's Stallion alternative page.
The builds before the switch keep checking Stallion for their app version, so both run until their users move. An Expo app imports from @hotcodepush/expo-ota-updates instead, and everything else on this page reads the same.
AI-assisted migration (recommended)
Install the HotCodePush skills:
npx skills add hotcodepush-team/skillsThen paste the prompt into your coding agent:
Migrate this app from Stallion to HotCodePush: install the skills with `npx skills add hotcodepush-team/skills`, then run the hotcodepush-migrate skill, following https://hotcodepush.com/docs/migrate/stallion.The agent removes Stallion, runs init, maps channels, the signing key and the binary ranges, and reports a checklist for the store release. It never touches Stallion, and the first release waits for the store build. Agent skills explains what the agent does.
Prerequisites
- You have a HotCodePush account and an organization.
npx hotcodepush initcreates both, as the quickstart shows. - The app is a React Native app on the New Architecture, or an Expo app that runs in a development build. The minimum versions are on Supported versions. Stallion ran on older versions, so upgrade first.
- You have
stallion/secrets/private-key.pem, if the app signed. A fresh key frominitworks too. - A store release with a new app version lies ahead: Stallion matches the exact version, so the old releases stay with the old builds.
- Node.js is on every machine that makes a native build, since the build step runs the CLI from an Xcode phase and a Gradle task.
Steps
If you already removed Stallion and ran init, skip to step 4.
1. Freeze a branch for the old builds
git branch stallion-fleetA bundle built from the migrated code imports a native module the old binaries lack. Hotfixes for them come from a branch that keeps the old client.
2. Remove Stallion
npm uninstall react-native-stallionThe CI job's npm install -g stallion-cli line goes with it. An Expo app also uninstalls expo-stallion-plugin and removes its entry from app.json. Remove withStallion, the hooks, sync(), restart() and the listeners per the SDK calls below. Then delete what the native projects carried:
StallionProjectId,StallionAppTokenandStallionPublicSigningKeyinInfo.plistand in every flavour'sstrings.xml, with the.xcconfigvariables behind them.Stallion.getJSBundleFile()inMainApplication.ktandStallionModule.getBundleURL()in the SwiftbundleURL(): put both files back to the template, whichinitrecognises (Installation).
3. Run init
npx hotcodepush initinit signs you in, creates the app with its production channel, installs the package and wires the build step. Answer no to the signing key if you bring Stallion's, and decline the first release. An Expo app gets @hotcodepush/expo-ota-updates and its config plugin. The prebuild refuses while expo-updates is installed without updates.enabled set to false (Expo installation).
Release after the store build, not before
A release created before a binary was built is never served to that binary. The release that reaches your new store builds is the one you create in step 7, after step 6 built them.
4. Add the signing key
npx hotcodepush signing-key add --private-key-path stallion/secrets/private-key.pemSkip this step if the app did not sign. Stallion's key is RSA-2048 in PKCS #8, which signing-key add takes. It fills publicKeys in hotcodepush.json, which replaces StallionPublicSigningKey. The private key stays where it is, and CI holds its content as HOTCODEPUSH_SIGNING_KEY in place of --private-key (Sign your bundles).
5. Create a channel per project
npx hotcodepush channel create --name staging --discoverableStallion gave each environment a project with its own id and app token. Here one app holds one channel per environment, with production from the start.
A flavour build sets HOTCODEPUSH_CHANNEL=staging in its build environment instead of a per-flavour strings.xml (Set up environments and tenants). Protect production with channel update --channel production --protected, so only Owners and Admins release to it, as Stallion's console promotion did.
6. Ship the store release that carries the SDK
Build the release as before, with the app version bumped. The build step registers the build as a binary with its fingerprint, so the machine needs npx hotcodepush login or HOTCODEPUSH_TOKEN. Under CI a store build without a token fails, and npx hotcodepush binary list shows the registered builds you submit to the stores.
7. Release from your pipeline
npx hotcodepush release create --bundle-version "$(git rev-parse --short HEAD)" --yesThis line replaces publish-bundle per platform, the grep for the hash, release-bundle and update-release. In the environment, HOTCODEPUSH_TOKEN replaces --ci-token on every command, and HOTCODEPUSH_SIGNING_KEY the key file (Ship every commit).
Add --rollout-percent 0 to start dark as Stallion did, then widen with release rollout. On a device running the new build the release applies within about a minute (How an update reaches a device). If nothing happens, see Troubleshooting.
8. Run both until the fleet moves
Old builds keep checking Stallion for their app version, so keep the plan and the projects alive while they have users. Their releases still come from release-bundle --app-version <old>, from the branch of step 1 only.
Stallion's adoption dashboard and binary list show the shift. Stop the old pipeline once the old builds are below the share you will leave behind.
What maps to what
| Stallion | HotCodePush | Note |
|---|---|---|
| Project, one per environment | one app, a channel per environment | appId sits in hotcodepush.json, and HOTCODEPUSH_CHANNEL picks the channel per flavour build. |
| Bucket | nothing | Bundles belong to the app, labelled by --bundle-version, and bundle list shows them. |
| Release for one exact app version | release on a channel, numbered per channel | The fingerprint gates every release, and --binary-version "1.0.1" pins a version. |
| Promote in the console, admins only | release create --from-channel staging --channel production | channel update --protected restricts a channel to Owners and Admins. |
| Rollout from 0 percent, rising only | 100 percent by default, moved in either direction with release rollout, and --progression widens on a schedule | A device keeps its release when the percentage falls. See Rollouts and auto-pause. |
| The previous release, served outside the newest's rollout only while it is at 100 percent | the newest release a device qualifies for, the older ones for the rest | |
| Pause | release pause, release resume | |
| Rollback, reversible | release rollback publishes the previous bundle as a new release, mandatory by default, and release revoke ends a release for good. | See Roll back a release. |
| Rollback on a crash before mount | the readiness gate: the first render within readyTimeoutSeconds, then auto-pause over the fleet | |
| A 0 percent release for testers with the pin | a beta channel, or release create --device <id> | See Target devices with attributes. |
| Patches from the second release on | a delta per base, the embedded bundle included | The first update after a store build is a delta. |
StallionProjectId | appId in hotcodepush.json | It is public. |
StallionAppToken in the binary | nothing | No token ships in the binary. |
StallionPublicSigningKey | publicKeys, written by signing-key add | |
expo-stallion-plugin with projectId, appToken and publicSigningKey | @hotcodepush/expo-ota-updates in plugins, the values in hotcodepush.json |
CLI commands
| Stallion | HotCodePush |
|---|---|
--ci-token=stl_… on every command | HOTCODEPUSH_TOKEN in the environment |
use --org-id --project-id, context | hotcodepush.json, or --organization and --app |
publish-bundle --upload-path … --platform android | release create uploads both platforms itself, or bundle upload uploads a build released later. |
--release-note, --private-key | --notes, --private-key-path or HOTCODEPUSH_SIGNING_KEY |
--custom-bundle-path after npx expo export:embed, --hermes-disabled, --entry-file | Nothing. The upload runs the bundler itself, and --path <dir> takes a bundle made by hand with your own flags. |
--sourcemap, --keep-artifacts | Nothing. Source maps stay out of the upload. |
release-bundle --hash <hash> --app-version 1.0.1 | release create --bundle <n>, with --binary-version "1.0.1" to pin the version |
--is-mandatory=true | --mandatory |
update-release --rollout-percent 50 | release rollout --release <n> --rollout-percent 50 |
update-release --is-paused, --is-rolled-back | release pause, release revoke |
update-release --release-note, --is-mandatory | release update --notes, --mandatory |
list-projects, list-bundles, list-releases, release-info | app list, bundle list, release list, release get |
list-buckets, list-patches | nothing |
generate-key-pair | signing-key create |
login, logout, whoami, --json | They are the same. |
The CLI reference lists every command. release and its flags are on the release page.
SDK calls
| Stallion | HotCodePush | Note |
|---|---|---|
withStallion(App) | Nothing. Export App plain. | checkStrategy: auto checks at start, on resume and every checkIntervalSeconds, 900 by default. |
sync() | HotCodePush.sync() | It resolves with a typed result. |
restart() | applyUpdate() | It reloads only when an update waits. |
useStallionUpdate().isRestartRequired | useUpdates().state.nextRelease !== null | |
currentlyRunningBundle, newReleaseBundle | state.currentRelease, state.nextRelease | Each is a Release with number, bundleVersion and isMandatory. |
newReleaseBundle.releaseNote | notes on the sync() result and on updateAvailable | It is not on Release. |
ACTIVE_RELEASE_HASH | getState().currentRelease?.bundleId | It is an id, not a hash. |
useStallionModal().showModal | showDebugScreen() | It shows device state and Check now, with no bundle browsing and no pin. Testers switch with setChannel({ name }). |
useStallionConfig() | getChannel(), getDevice() | |
addEventListener, removeEventListener | addListener(name, fn), resolving a handle with remove(), and removeAllListeners() | |
DOWNLOAD_STARTED and the download's progress and completion events | updateAvailable, downloadProgress, updateDownloaded | progress runs from 0 to 1 in both. |
DOWNLOAD_ERROR, SYNC_ERROR_PROD, CORRUPTED_FILE_ERROR, SIGNATURE_VERIFICATION_FAILED | updateFailed with a reason | |
ROLLED_BACK_PROD, AUTO_ROLLED_BACK_PROD | updateRolledBack | |
INSTALLED_PROD, STABILIZED_PROD, EXCEPTION_PROD, the _STAGE events | There is no event. notifyReady() under readySignal: manual confirms a release by hand. | |
| a restart modal of your own | applyStrategy: manual, useUpdates() and applyUpdate() | See Before and after. |
| a mandatory modal of your own | mandatoryApplyStrategy: immediate reloads at once, and manual keeps the modal. |
The React Native SDK's API page has every method, event and type.
Before and after
The withStallion wrapper goes and the component is exported plain: the SDK checks at start, on resume and every 900 seconds on its own. A restart prompt reads useUpdates() where it read useStallionUpdate(), and applyUpdate() restarts where restart() did:
import { HotCodePush, useUpdates } from '@hotcodepush/react-native-code-push';
import { Pressable, Text, View } from 'react-native';
export function RestartPrompt() {
const { lastSync, state } = useUpdates();
if (state.nextRelease === null) {
return null;
}
const notes = lastSync?.status === 'downloaded' ? lastSync.notes : null;
return (
<View>
<Text>{notes ?? `Version ${state.nextRelease.bundleVersion} is ready`}</Text>
<Pressable onPress={() => void HotCodePush.applyUpdate()}>
<Text>Restart now</Text>
</Pressable>
</View>
);
}The prompt needs "applyStrategy": "manual" in hotcodepush.json. Without the key a downloaded update applies at the next start, as Stallion did. A mandatory release follows mandatoryApplyStrategy, immediate by default: set it to manual to keep a modal that cannot be dismissed, and read isMandatory on state.nextRelease (Choose an apply strategy).
What differs
A rollout starts at 100 percent
Stallion started every release at 0 percent and let the percentage rise only. Here release create reaches every qualifying device unless --rollout-percent says otherwise, and release rollout moves the number in either direction. A ported pipeline that leaves the flag out ships to everyone.
Mandatory reloads at once
Stallion's sample pipeline set --is-mandatory=true and left the restart to a modal of your own. Here --mandatory reloads devices as soon as the download is verified, under the default mandatoryApplyStrategy: immediate. Set it to manual to keep the modal.
The readiness gate has a timer and no off switch
Stallion rolled a release back on a crash before the app mounted. Here a release that has not rendered within readyTimeoutSeconds, 10 by default, is rolled back and marked failed on that device. Raise the number for a slow first start.
Next steps
- Choose an apply strategy decides when a downloaded update takes effect, and who restarts the app after a mandatory release.
- Roll out gradually: start a release at a small percentage, let it progress and let auto-pause stop a bad one.
- Ship every commit has the pipeline that releases every merge with the one CLI line.
- React Native SDK reference documents every
hotcodepush.jsonkey, method, event and type of the SDK you installed.
Last updated on