Guides

React Native OTA CI/CD: Publish, Test, and Promote

Publish React Native OTA updates from CI with publishing tokens, matching source maps, compatible runtimes, and a controlled production rollout.

5 min read

Written by Founder and maintainer of Bundle Drop

A green upload job proves that an OTA bundle reached the service. A useful React Native OTA pipeline also retains the exact bundle and source map, identifies what QA tested, and leaves production exposure under your control.

The examples below publish a bare Android app from GitHub Actions, Azure Pipelines, or CircleCI. Each uses the same small publishing script. Commit that script and the YAML for your provider; you do not need all three configurations. The CI/CD documentation owns the full command reference.

Prepare the project once

Complete and review Bundle Drop integration locally before adding unattended publication. Commit bundle.drop.config.js, the intended native configuration, package.json, and package-lock.json. Keep credentials out of those files. Create the develop channel in the configured project first: uploading does not create channels.

These examples assume:

  • The locked project dependencies include @gfean/react-native-bundle-drop and its CLI.
  • Android's android/app/build.gradle supplies the app version matching the installed native build.
  • Node 24.15.0 is suitable for your app; change the runner version if your application requires another supported version.
  • Your app has lint and Jest-based test scripts. Replace the check step with your real checks if its test runner uses different options.
  • A Bundle Drop Personal Access Token can publish to this organization and project.

npm ci installs the committed dependency graph; npx --no-install uses its CLI rather than downloading a different release. Keep the native runtime compatible with the installed binary. Native fingerprinting also needs consistent build inputs; strict Expo build authority needs its matching receipt. A successful job cannot make a native dependency change work in an older binary.

Keep publication and artifact checks together

Save this as scripts/publish-ota.sh in your app repository. It generates release notes from the source revision, requests a matching source map, and retains the result under artifacts/bundledrop/android.

scripts/publish-ota.sh
#!/usr/bin/env bash
set -euo pipefail
: "${BUNDLE_DROP_PAT:?Set BUNDLE_DROP_PAT in the CI secret store}"

artifact_dir=./artifacts/bundledrop/android
mkdir -p "$artifact_dir"
git rev-parse HEAD > "$artifact_dir/source-revision.txt"
git log -1 --format='Release %h' > "$artifact_dir/release-notes.txt"

npx --no-install bundle-drop upload android \
  --buildgradle-path android/app/build.gradle \
  --channel develop \
  --token "$BUNDLE_DROP_PAT" \
  --author "CI Pipeline" \
  --release-notes-file "$artifact_dir/release-notes.txt" \
  --sourcemap \
  --artifact-dir "$artifact_dir"

# Upload success alone does not guarantee retained local artifacts.
node <<'NODE'
const fs = require('node:fs');
const result = JSON.parse(fs.readFileSync(
  './artifacts/bundledrop/android/bundle-drop-result.json', 'utf8'
));
if (result.success === false || result.status === 'failed' ||
    !/^[a-f0-9]{64}$/i.test(result.hash ?? '') ||
    !Number.isInteger(result.bundleVersion) || result.bundleVersion < 1) {
  throw new Error('Missing successful publication identity');
}
for (const field of ['bundlePath', 'sourceMapPath']) {
  const file = result[field];
  if (typeof file !== 'string' || !file) {
    throw new Error('Missing retained artifact: ' + field);
  }
  const stat = fs.statSync(file);
  if (!stat.isFile() || stat.size === 0) {
    throw new Error('Empty or invalid retained artifact: ' + field);
  }
}
console.log('Retained bundle v' + result.bundleVersion + ': ' + result.hash);
NODE

The script stops on upload failure. It then requires a published hash, bundle version, and nonempty retained bundle and source-map files. That second check matters: the CLI can warn about a local artifact-copy or result-writing failure after the server accepted the upload. An exit code alone does not prove that debugging artifacts were saved.

If artifact validation fails after publication, inspect the dashboard before rerunning the upload. Repeating a successful publication can create another release. Preserve the available diagnostics and repair the artifact handoff first.

Choose your CI provider

Copy the matching YAML into the filename shown. GitHub and Azure examples are manually queued on main; CircleCI runs after commits to main. Adapt the branch and trigger to your team's release process. Restrict publishing secrets to trusted jobs, and keep pull-request checks separate from publication.

The provider configurations check out the repository, install locked dependencies, run application checks, publish through the shared script, and save release artifacts. Artifact collection also runs after an ordinary step failure so available diagnostics can be retained; cancellation or runner loss can still prevent collection.

Create the BUNDLE_DROP_PAT repository secret. Dispatch this workflow from the reviewed main branch. The GitHub token only reads your repository; the Bundle Drop token publishes OTA.

.github/workflows/ota.yml
name: Publish Android OTA
on:
  workflow_dispatch:
permissions:
  contents: read
concurrency:
  group: bundledrop-develop-android
  cancel-in-progress: false
jobs:
  publish:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - uses: actions/setup-node@v7
        with:
          node-version: '24.15.0'
          cache: npm
      - run: npm ci
      - run: npm run lint && npm test -- --runInBand
      - name: Publish and verify retained artifacts
        run: bash scripts/publish-ota.sh
        env:
          BUNDLE_DROP_PAT: ${{ secrets.BUNDLE_DROP_PAT }}
      - name: Save release artifacts and diagnostics
        if: ${{ always() }}
        uses: actions/upload-artifact@v7
        with:
          name: bundledrop-android-${{ github.run_id }}
          path: artifacts/bundledrop/android/
          if-no-files-found: error
          retention-days: 30
Provider documentation ↗

Use a secret named BUNDLE_DROP_PAT in the selected provider. Azure requires explicit environment mapping; CircleCI's named context must exist and be authorized for the project. Do not run interactive login or AI setup inside routine publication jobs. Never enable shell tracing around the token or archive credential files.

The GitHub example uses the current checkout, Node setup, and artifact upload actions. Pin reviewed action revisions according to your team's dependency policy. Azure's Node task and pipeline artifacts example targets Azure DevOps Services; on-premises Azure DevOps Server needs its supported build-artifact task. CircleCI's configuration reference documents branch filters and artifact storage. Check each provider's retention and artifact-access settings for your source maps.

Adapt the platform without changing the release contract

For bare iOS, use upload ios with the appropriate --plist-file, a separate artifact directory, and the same validation. For Expo, use the option-free platform upload in Uploading, preserving token, channel, notes, source-map, and artifact options. Validate those variants against your own app before enabling publication.

These are JavaScript-and-asset publication jobs, not native binary builds. Keep store builds and any required compatibility receipts in your existing native build workflow.

Test the identified bundle before widening delivery

Record the retained hash, platform, runtime, source revision, and CI run. QA should validate that exact candidate on a compatible release build. “The develop channel passed” becomes ambiguous if another job publishes there during testing.

After acceptance, use the dashboard's channel promotion flow to move the tested bundle into the intended release track. Promotion preserves its identity without rebuilding JavaScript. Confirm the destination's history, managed publishing, audience rules, and rollout settings: promotion alone does not guarantee selection. Staged rollouts and rollback explains that decision.

Upload completion is neither device installation nor patch readiness. A compatible device can receive the complete bundle while a suitable patch is unavailable. Activation follows the app's configured policy.

Send the retained map to your monitoring tool and identify the active OTA hash in runtime events. The source-map guide explains how to keep those identities aligned. A complete CI handoff ends with evidence for the next release decision, not just a green upload step.

Common questions

About the author

Founder and maintainer of Bundle Drop

George builds Bundle Drop and writes about the compatibility, delivery, rollout, and recovery boundaries behind React Native OTA updates.

GitHub profile

Related resources

Interested in safer OTA deployments?

Read the implementation guides or connect a React Native project when you are ready to test the release workflow.