HotCodePush

Test with preview channels

Create an expiring channel per pull request from CI, release the build to it and hand testers a QR code that points their phone at it.

A preview channel is an ordinary channel with an expiry. The pipeline creates one per pull request, releases the pull request's build to it and deletes it when the pull request closes. A tester's phone follows it after one scan of a QR code, so a preview costs no store build and no TestFlight round.

Switch channels by link owns the URL scheme and the handler that make the QR code work.

Prerequisites

  • Quickstart has run, so the SDK is installed and a first release is live.
  • Ship every commit sets up a pipeline that builds the app and runs the CLI.
  • An API token is stored as the CI secret HOTCODEPUSH_TOKEN.

1. Create a channel per pull request

If the pipeline already creates the channel, skip to step 2. Name the channel after the pull request and give it an expiry, so a forgotten one goes away on its own.

npx hotcodepush channel create --name pr-42 --expires-in 14d --if-not-exists --json

With --if-not-exists the next push creates nothing twice: the command answers with the existing channel when the name is taken. With --json it prints the channel with the id the next steps use. channel create lists the expiry forms.

2. Release the pull request's build to it

If the pipeline already releases to the channel, skip to step 3. The flag --yes answers the confirmation a pipeline cannot answer, and --bundle-version names the bundle by its commit:

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

The release applies within about a minute. A pipeline that must not publish runs --dry-run, which uploads nothing and needs no key. A signed app's job also holds the signing secret.

In GitHub Actions the commands fit one workflow:

.github/workflows/preview.yml
name: Preview
on:
  pull_request:
    types: [opened, synchronize, reopened, closed]

env:
  HOTCODEPUSH_TOKEN: ${{ secrets.HOTCODEPUSH_TOKEN }}
  CHANNEL_NAME: pr-${{ github.event.pull_request.number }}

jobs:
  release:
    if: github.event.action != 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm run build
      - name: Release to the preview channel
        run: |
          CHANNEL_ID=$(npx hotcodepush channel create --name "$CHANNEL_NAME" --expires-in 14d --if-not-exists --json | jq -r '.id')
          npx hotcodepush release create --channel "$CHANNEL_ID" --bundle-version "$(git rev-parse --short HEAD)" --yes
          echo "CHANNEL_ID=$CHANNEL_ID" >> "$GITHUB_ENV"
      - name: Comment the QR code
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          APP_ID=$(jq -r '.appId' hotcodepush.json)
          gh pr comment "${{ github.event.pull_request.number }}" --body "![Preview channel QR code](https://api.hotcodepush.com/v1/apps/$APP_ID/channels/$CHANNEL_ID/qr-code?format=png)"

  cleanup:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx hotcodepush channel delete --channel "$CHANNEL_NAME" --ignore-not-found --yes

On React Native and Expo the CLI runs the framework's bundling itself, so the pipeline skips npm run build. On Capacitor and Cordova that line is the web build.

In GitHub Actions a pull request checkout labels the bundle with the merge commit's short SHA, not the branch head's. The Azure DevOps, Bitbucket, Bitrise, Capawesome Cloud, Codemagic, EAS Build, Gitea Actions, GitHub Actions and GitLab recipes have each provider's token and trigger.

3. Hand it to testers

If the app's link template is set and the app handles the link, skip to step 4. The QR code encodes the channel's deep link, and the app's handler turns the link into a setChannel({ id }) call. Switch channels by link sets the template, registers the scheme and adds the handler and shows the QR route the workflow embeds.

A channel id in a public repository is public

Whoever reads the comment can point a build of the app at the channel and read everything released to it. Comment it in a private repository, or hand it over from the channel's Install dialog.

Which builds take a preview depends on the framework:

  • React Native and Expo: a debug build served by the development server embeds no bundle and takes no update. Hand testers a release build or a TestFlight or internal-track build.
  • Capacitor and Cordova: a debug build takes over-the-air (OTA) updates by default, so the developer's build on the phone follows the preview.

Once the tester scans the QR code, the app calls setChannel({ id }) and syncs. The release applies the way the app's apply strategy says, at the next start by default. If nothing happens, see Troubleshooting.

4. Clean up

Delete the channel when the pull request closes.

npx hotcodepush channel delete --channel pr-42 --ignore-not-found --yes

--ignore-not-found succeeds when the channel is already gone, so a re-run deletes nothing twice.

A deleted or expired channel serves no index: a device on it keeps the bundle it runs and its channel, and every check fails with channel_unknown. The handler in Switch channels by link returns the tester's phone to the build's channel, and so does Return to configured channel on the SDK's debug screen.

Next steps

Last updated on

On this page