HotCodePush

Migrate from Codemagic Patch

Move a React Native, Expo or Capacitor app from self-hosted Codemagic Patch to HotCodePush, with every command, key and call mapped, and retire the server.

A Codemagic Patch app moves to HotCodePush at its next store release and stops running its Patch server, storage and CDN. The store build that carries the new SDK is the switch, and the builds before it keep reading your Patch storage until their users move. What differs, side by side, is on the site's Codemagic Patch alternative page.

Patch runs on a stack you host, or one Codemagic hosts on request. The steps below assume your own stack. HotCodePush is hosted alone: there is no self-hosted edition, and the Patch SDK cannot be pointed at it. This guide covers React Native and Expo apps on @codemagic/react-native-patch and Capacitor apps on @codemagic/capacitor-patch.

Codemagic's legacy CodePush service runs @code-push-next/react-native-code-push, a CodePush client, which the CodePush guide covers.

Install the HotCodePush skills:

npx skills add hotcodepush-team/skills

Then paste the prompt into your coding agent:

Migrate this app from Codemagic Patch to HotCodePush: install the skills with `npx skills add hotcodepush-team/skills`, then run the hotcodepush-migrate skill, following https://hotcodepush.com/docs/migrate/codemagic-patch.

The agent removes the Patch SDK, runs init, maps deployments, the signing key and the target versions, and reports a checklist for the store release. It never touches your Patch server, 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 init creates both, as the quickstart shows.
  • The app is a React Native app on the New Architecture, an Expo app in a development build, or a Capacitor app on a supported major. The minimum versions are on Supported versions. Patch runs on older React Native versions, so upgrade first.
  • You have the private key your Patch releases were signed with, if the app required code signing. Sign your bundles explains what the key does here.
  • A store release lies ahead, and Node.js is on every build machine, since the build step runs the CLI from an Xcode phase and a Gradle task.

Steps

If you already removed Patch and ran init, skip to step 4.

1. Freeze a branch for the old builds

git branch codemagic-patch-fleet

A bundle built from the migrated code lacks Patch's JavaScript, and the old binaries lack the HotCodePush native module. Hotfixes for them come from this branch, with cmpatch or cmpatch-capacitor as before.

2. Remove the Patch SDK

npm uninstall @codemagic/react-native-patch

Export the root component plain instead of Patch.wrap(App, options) and replace every call per the SDK calls below. Delete codemagic-patch.config.json and the codemagicPatch key in package.json, then clean the native projects:

  • the CodemagicPatch* keys in Info.plist and strings.xml, and the $(PATCH_IOS_DEPLOYMENT_KEY) build setting or .xcconfig line.
  • the resValue block in android/app/build.gradle that reads PATCH_ANDROID_DEPLOYMENT_KEY: delete it before the CI variable goes, since the block throws without the variable.
  • import io.codemagic.patch.CodemagicPatch and the jsBundleFilePath line, or the getJSBundleFile() override, in MainApplication.kt, and the Swift host file as the callout says, then pod install.
  • in an Expo app, the @codemagic/react-native-patch entry in plugins and the PATCH_*_DEPLOYMENT_KEY variables in eas.json. The next prebuild regenerates the native projects.

Put the Swift host file back before you run init

Patch wraps the template's call: CodemagicPatch.bundleURL() ?? Bundle.main.url(forResource: "main", withExtension: "jsbundle"). init swaps the inner call for HotCodePush.bundleURL() and leaves the rest, so the file stops compiling once the Patch pod is gone. Restore the template's line and drop import CodemagicPatchClient first, in AppDelegate.swift or whichever Swift file overrides bundleURL().

A Capacitor app runs npm uninstall @codemagic/capacitor-patch and deletes the CodemagicPatch block from plugins in capacitor.config.ts, with any CodemagicPatch* override in Info.plist and strings.xml. It replaces start() and every other call per the SDK calls below, drops npx codemagic-patch-check-config from the native build job and runs npx cap sync.

3. Run init

npx hotcodepush init

init signs you in, creates the organization and one app for both platforms with its production channel, installs the package and wires the build step. Answer no to the signing key if you bring yours, and decline the first release.

A React Native app gets @hotcodepush/react-native-code-push, with an import and the HotCodePush.bundleURL() call in the Swift file and a swapped import in MainApplication.kt. An Expo app gets @hotcodepush/expo-ota-updates in plugins, a Capacitor app @hotcodepush/capacitor-live-updates. The React Native, Expo and Capacitor installation pages show the manual steps for a project init could not edit.

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.

A Capacitor app then imports the package in its entry file, where start() was, even when no call follows:

src/main.ts
import '@hotcodepush/capacitor-live-updates';

The import reports the first render, which is what start() did through notifyAppReady(). An app that imports the package late, or not at all, confirms no update, and every one is rolled back after readyTimeoutSeconds. A React Native app needs no call: the first render confirms the release.

hotcodepush.json replaces the deployment key and the two URLs. The build step writes it into the app on every build, and nothing is pasted into the native projects.

4. Add your signing key

Skip this step if your Patch app did not require code signing. HotCodePush signs with the same kind of RSA key pair as openssl genrsa made for Patch, so the pair carries over:

npx hotcodepush signing-key add --private-key-path patch-private-key.pem

signing-key add reads the PEM on your machine, registers the public key with the app and appends it to publicKeys in hotcodepush.json, which replaces CodemagicPatchPublicKey. The private key stays where it is: every upload names it with --private-key-path, or CI sets the file's content as HOTCODEPUSH_SIGNING_KEY.

5. Create the channels your deployments had

production exists and takes the place of Production on both platform apps. Each further deployment becomes one channel for both platforms:

npx hotcodepush channel create --name staging --discoverable

--discoverable lets the app switch to the channel by name with setChannel({ name }), as Channels describes. Patch had no runtime switch, so leave the flag out unless you add one. A build that carried the Staging key sets HOTCODEPUSH_CHANNEL=staging in its build environment instead of the PATCH_*_DEPLOYMENT_KEY variables, as Set up environments and tenants shows.

6. Ship the store release that carries the SDK

Build the release as before. The build step runs binary create inside the build and registers the build with its fingerprint, replacing the cmpatch release-react --disabled baseline per store build. 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.

Metro serves a React Native debug build's JavaScript, so it takes no updates: test the next step in a release build.

7. Release from your pipeline

npx hotcodepush release create --bundle-version "$(git rev-parse --short HEAD)" --yes

This line replaces both cmpatch release-react --platform runs. A Capacitor app puts npm run build && in front, in place of cmpatch-capacitor release create --bundle-path www per platform app. The upload reads webDir from capacitor.config.

HOTCODEPUSH_TOKEN replaces CODEMAGIC_PATCH_TOKEN and CODEMAGIC_PATCH_SERVER_URL, and HOTCODEPUSH_SIGNING_KEY the key file in a signing app. In an Expo app, the upload bundles in the job's shell, so give the job the EXPO_PUBLIC_* values the store build had. Ship every commit is the pipeline this line sits in, and the Codemagic recipe shows it in codemagic.yaml.

Open the app on a device that runs the new store 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

The old builds keep reading manifests from your Patch storage or CDN. Keep the server, the storage and the CDN running while they have users. Their hotfixes come from the branch of step 1, with --target-binary-version on their store versions.

cmpatch deployment timeseries or the Patch dashboard shows the old share and binary list the new one. Once the old builds are below the share you leave behind, stop the Patch pipeline and take the stack down. Nothing is imported from its metrics or release history.

Never publish a bundle from the migrated code to Patch

It carries no Patch JavaScript, so notifyAppReady() never runs: an old build rolls it back after maxLaunchAttempts launches, 3 by default, and reports it failed. The branch of step 1 keeps the Patch SDK.

CLI commands

cmpatch serves React Native and Expo apps and cmpatch-capacitor Capacitor apps, each against an app per platform. One hotcodepush CLI serves every framework and one app.

Codemagic PatchHotCodePush
npm install -g @codemagic/patch-cli, @codemagic/capacitor-patch-clinpm install --save-dev hotcodepush
cmpatch login, --token, CODEMAGIC_PATCH_TOKEN, CODEMAGIC_PATCH_SERVER_URLhotcodepush login, HOTCODEPUSH_TOKEN, with no server URL
cmpatch token create --name ciAn API token comes from the console's API tokens page.
cmpatch init, cmpatch wire, codemagic-patch.config.jsonhotcodepush init, hotcodepush.json
app create --name MyApp-iOS, --name MyApp-AndroidOne app serves both platforms, and init creates it.
deployment create, list, rename, removechannel create, list, update, delete
deployment clearrelease revoke --all revokes every release, and devices return to the embedded bundle.
cmpatch release-react --platform ios --deployment Staging --yesrelease create --channel staging --yes releases both platforms in one call.
cmpatch-capacitor release create --bundle-path www --app MyApp-iOS --deployment Staging --target-binary-version 1.2.3release create --channel staging releases, and the upload reads webDir.
--target-binary-version 1.2.0--binary-version "1.2.0" pins one store version. Without it the fingerprint alone decides.
--rollout-percentage 25, --mandatory, --release-notes "Fixes the cart"--rollout-percent 25, --mandatory, --notes "Fixes the cart"
--disabled--rollout-percent 0, then release rollout when ready. Nothing replaces the fingerprint baseline.
--private-key-path patch-private-key.pem--private-key-path patch-private-key.pem, or the key's text in HOTCODEPUSH_SIGNING_KEY
--allow-fingerprint-mismatchNothing. A release reaches matching binaries alone, and the upload warns when none matches.
--dry-run, --format json, --non-interactive, --no-duplicate-release-error--dry-run, --json, --yes. A rerun of release create fails nothing.
cmpatch bundle, then release create with the .cmpatch filebundle upload, then release create --bundle <n>
release promote --source-deployment Staging --dest-deployment Production --label v4release create --from-channel staging --channel production
release patch --label v5 --rollout-percentage 50release rollout --release 5 --rollout-percent 50. The percentage moves either way.
release patch --mandatory, --not-mandatory, --release-notes "..."release update --mandatory true, --mandatory false, --notes "..."
release patch --target-binary-versionNothing. Conditions are fixed at creation, so create a new release and revoke the old one.
release disable, release enablerelease revoke, then release create --bundle <n>. release pause keeps devices on the release.
release rollback --label v3release rollback --to-release 3, mandatory by default
release inspect --waitNothing to wait for. release create waits until the release is live, up to a minute.
deployment metrics, deployment timeseries, release metrics, failuresthe console, and statistics get
doctor, fingerprint --platform iosdoctor, fingerprint
npx codemagic-patch-check-configdoctor
selfhost install, upgrade, backup, restore, local-evalNothing. There is no server to run.

A release is numbered per channel, #4 of production, where Patch labelled it v4 per deployment. --bundle-version gives a bundle a free-text label beside it. The CLI reference lists every command. release and its flags are on the release page.

What maps to what

The native keys, the config plugin's options and the options of Patch.wrap(), start() and sync() move into hotcodepush.json:

Codemagic PatchHotCodePushNote
CodemagicPatchDeploymentKey, plugins.CodemagicPatch.<platform>.deploymentKey, the plugin's deploymentKeyappId and channel in hotcodepush.jsonappId is public, and the build step resolves the channel to its id.
CodemagicPatchApiUrl, CodemagicPatchDownloadBaseUrl, apiUrl, downloadBaseUrlnoneThere is no server to name.
CodemagicPatchPublicKey, publicKeypublicKeyssigning-key add or signing-key create fills it.
CodemagicPatchMaxLaunchAttempts, maxLaunchAttempts, 3 launchesreadyTimeoutSeconds, 10 secondsIt is a window per start instead of a count of launches, with no off switch.
$(PATCH_IOS_DEPLOYMENT_KEY), the resValue block, PATCH_*_DEPLOYMENT_KEY in eas.json or capacitor.config.tsHOTCODEPUSH_CHANNEL in the build's environment
import CodemagicPatchClient, CodemagicPatch.bundleURL() ?? Bundle.main.url(...)HotCodePush.bundleURL()init writes it into the template's line.
import io.codemagic.patch.CodemagicPatch, jsBundleFilePath = CodemagicPatch.getJSBundleFile(applicationContext) or the getJSBundleFile() overridethe HotCodePushReactHost.getDefaultReactHost importinit swaps the import, and the call stays.
the @codemagic/react-native-patch entry in plugins, with its ios and android blocks@hotcodepush/expo-ota-updates in plugins, without options
checkFrequency: ON_APP_START, the default, and ON_APP_RESUMEcheckStrategy: autoIt checks at start, on resume and every checkIntervalSeconds, 900 by default.
installMode: ON_NEXT_RESTART, the default, ON_NEXT_RESUME, IMMEDIATEapplyStrategy: next_start, the default, next_resume, immediate
installMode: ON_NEXT_SUSPENDnonenext_resume is the closest.
minimumBackgroundDuration: 60_000, milliseconds, 0 by defaultnextResumeMinimumBackgroundSeconds: 60, seconds, 300 by defaultDivide by 1,000, and set 0 to keep Patch's default.
mandatoryInstallMode, IMMEDIATE by defaultmandatoryApplyStrategy: immediate, the default, or manual

The React Native SDK's Configuration page lists every key with its type and default. The Capacitor SDK's Configuration page is the same list.

SDK calls

Codemagic PatchHotCodePushNote
export default Patch.wrap(App, options)export default AppThe options move to hotcodepush.json.
start(options) after the first screenthe import in the entry filenotifyReady() under readySignal: manual replaces a late start.
sync(options, onProgress)sync(options?) and the downloadProgress eventIt resolves with a typed result.
"up-to-date", "update-installed", "error"up_to_date, then downloaded or applied, then failed with a reason
"sync-in-progress"noneA second call joins the running cycle.
checkForUpdate(), action: "ota-update" or "up-to-date"checkForUpdate(): available or up_to_dateNothing is downloaded.
action: "embedded-revert", "embedded-revert-applied"None. After release revoke the device steps back to an older release or the embedded bundle.It has no status of its own.
isStoreUpdateAvailable, latestBinaryVersionnonesync() resolves skipped with device_incompatible when the newest release's conditions do not fit the binary.
downloadUpdate(remotePackage, onProgress)downloadUpdate({ applyStrategy }) and downloadProgressprogress runs from 0 to 1.
installUpdate(local, { installMode }), restartApp(true)applyUpdate()It reloads only when an update waits, else answers nothing_to_apply.
notifyAppReady()nothing, or notifyReady() under readySignal: manualThe first render confirms the release.
disallowRestart(), allowRestart()setRestartAllowed({ isAllowed: false }), setRestartAllowed({ isAllowed: true })
getRunningBundleUpdateMetadata()getState().currentReleaseIt is null on the embedded bundle in both.
isNextVersionReady()getState().nextRelease
remotePackage.previouslyFailedgetState().failedBundleIdsA failed bundle is never retried on that device in both.
hydrate()nothing
CodemagicPatchError with codea result with status and reasonA method rejects for a programming mistake or an unconfigured build.

The React Native SDK's API page and the Capacitor SDK's API page have every method, event and type. On the web the Capacitor package rejects with Capacitor's UNIMPLEMENTED error, where Patch threw UNSUPPORTED_ON_WEB, so guard the calls with Capacitor.isNativePlatform().

Before and after

The Patch.wrap() export goes, and the component is exported plain:

App.tsx
import * as Patch from '@codemagic/react-native-patch'; 
import { Root } from './Root';

function App() {
  return <Root />;
}

export default Patch.wrap(App, { 
  checkFrequency: Patch.CheckFrequency.ON_APP_RESUME, 
  installMode: Patch.InstallMode.ON_NEXT_RESUME, 
  minimumBackgroundDuration: 60_000, 
}); 
export default App; 

The options move into hotcodepush.json, the duration in seconds:

hotcodepush.json
{
  "appId": "<your app id>",
  "channel": "production",
  "applyStrategy": "next_resume",
  "nextResumeMinimumBackgroundSeconds": 60
}

In a Capacitor app start() goes, and the import takes its place in the entry file:

src/main.ts
import { CheckFrequency, InstallMode, start } from '@codemagic/capacitor-patch'; 
import '@hotcodepush/capacitor-live-updates'; 

start({ 
  checkFrequency: CheckFrequency.ON_APP_RESUME, 
  installMode: InstallMode.ON_NEXT_RESTART, 
}); 

What differs

  • Hosted, not self-hosted: no server, storage or CDN to run. Privacy and device data says where data is stored.
  • The fingerprint replaces the exact target version: a release reaches every store version that shares the fingerprint. Patch served the target version and, on React Native, the versions whose recorded fingerprint matched. Pass --binary-version "1.2.3" to keep the old behaviour. A Capacitor app gets the fingerprint too, see Compatibility.
  • Readiness is a window, not a launch count: a release that has not rendered within readyTimeoutSeconds, 10 by default, is rolled back. It is never tried there again. An app that called sync() late on purpose sets readySignal: manual and calls notifyReady().
  • release disable is release revoke, not release pause: Patch's disable moves devices back to the previous release. Here release revoke does that, and release pause keeps devices on the release. See Roll back a release.
  • Rollouts overlap and move either way: Patch allows one partial rollout per deployment at a time. Here releases never wait, and release rollout lowers a percentage too, and a device keeps its release when the percentage falls. Auto-pause halts a release whose failure rate climbs. See Roll out gradually.

Next steps

  • Roll out gradually: start a release at a small percentage, let it progress and let auto-pause stop a bad one.
  • Choose an apply strategy: pick when a downloaded update takes effect, the decision installMode used to make.
  • React Native SDK reference documents every hotcodepush.json key, method, event, type and reason of the SDK you installed.
  • Capacitor SDK reference documents the same for a Capacitor app, with the entry-file import that reports readiness.

Last updated on

On this page