Ship every commit

Every merge to main becomes an over-the-air (OTA) update on your users' devices — one workflow file, a staged rollout, auto-pause watching.

.github/workflows/release.yml

name: Release to production
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
env:
HOTCODEPUSH_TOKEN: ${{ secrets.HOTCODEPUSH_TOKEN }}
HOTCODEPUSH_SIGNING_KEY: ${{ secrets.HOTCODEPUSH_SIGNING_KEY }}
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- run: npx hotcodepush release create --channel production --path dist --yes

The whole pipeline: the merge builds, uploads, signs and releases to production; devices take it on their next check.

Release to production

▶ push to main · ab12c3f fix: cart crash
$ npm ci && npm run build
$ npx hotcodepush release create --channel production --path dist --rollout-percentage 10 --progression --yes
✓ 214 files packaged · manifest signed · commit ab12c3f recorded
✓ Fingerprint computed · 3 store builds in the field match
✓ Delta computed · devices download 42 KB, not 18.4 MB
✓ Released bundle #38 to production as release #43 at 10 percent
▸ Rolling out 10% → 50% → 100% · pauses itself at 5% failures
⚡ Devices update on their next check. No store review.

Two workflows: merge to main and it releases to production; open a pull request and it gets a preview channel that cleans up after itself.

Discipline one · data

Forward-compatible data, because devices move backwards

A rollback, a revocation or a failed start runs older code over data the newer code wrote.

  • A schema change adds; it never renames, removes or retypes

  • Migrations run forward only; a store release retires old shapes

  • Code keeps the fields it does not know

Diagram: three releases with an arrow from the newest back to the oldest, and the device's local data beneath — an added field the older release keeps unread, a renamed field it cannot read

Discipline two · native

Native changes wait for the store

An OTA update replaces the JavaScript or web layer, never the native one. A device takes a release only when its store build matches.

  • No channel per native version, no version to type

  • Compatibility is worked out at upload, not typed

  • The dry run shows the honest zero before the merge

Diagram: the audience count three times — the pull request's dry run after a native change reaches 0 devices with a warning that the build is not known yet; the merge's release stays at zero; the first merge after the store build ships reaches the devices on it

Discipline three · rollout

Rollout progression: auto-pause, run in reverse

One line in the workflow stages every merge: the release starts small and widens on its own while failures stay low.

  • The release starts small and widens in steps

  • Auto-pause stops the rollout when failures climb

  • It waits visibly until enough devices have tried

Diagram: a step chart widening in stages to a hundred percent; a dashed line from a middle step down marks an auto-pause

CI providers

Runs in your pipeline

One CLI line after your build. Recipes for every provider below, and a preview channel per pull request.

  • GitHub Actions
  • GitLab CI/CD
  • Bitbucket Pipelines
  • Azure Pipelines
  • Bitrise
  • Codemagic
  • EAS Build
  • Gitea Actions
  • Capawesome Cloud

Provenance

Which commit is live? The release log says

Every bundle records the commit it was built from — read by the CLI at upload, no repository connection, nothing to type.

At upload the CLI reads the commit behind the build. The release log shows it, linked to the commit: the merge is the release, and the release names the merge. A bundle uploaded from a tree with uncommitted changes is marked as such, since it may not match its commit. Pipelines that build in one job and deploy in another hand the commit over.

Ship from CI

Diagram: the production release log with a commit column — release #43 from ab12c3f "fix: cart crash", #42 from 9e4d1f0 "feat: address form", #41 from 7d2e9a4 "chore: bump deps", marked dirty

Frameworks

The same file for every framework

React Native and Expo bring source and end with one release per platform. Capacitor and Cordova build first and release once for both.

  • React Native

    Brings source. The CLI bundles your JavaScript per platform inside the upload, so the workflow needs no build line of its own and ends with one release per platform. The same file also opens a preview channel for every pull request.

    React Native code push
  • Expo

    Brings source too. The CLI bundles inside the upload, and compatibility with each store build is worked out for you, with no version to bump. The workflow ends with one release per platform, and a deep link opens each preview on a real phone.

    Expo OTA updates
  • Capacitor

    Brings its built web directory, the one your project already configures. The pipeline runs your own web build first, then one release line serves both platforms, and a deep link switches a test device to a pull request’s preview channel.

    Capacitor live updates
  • Cordova

    Brings www, its built web directory. Build first, release once for both platforms, on tooling that stays maintained. The workflow is the same file as Capacitor’s, the web build step included, and a deep link opens each pull request’s preview channel on a test device.

    Cordova code push

The pipeline is one file.

Your next merge ships itself.