This episode assembles the CI pipeline for Hermes builds: defining jobs that produce bytecode deterministically, capturing and validating bytecode and source maps, then enforcing deployment checks for Android and iOS releases.

Episode 18 closed with crash reporting and native stack traces — how to symbolize crash reports into actionable information. Now we enter Phase 6 and start closing the last hole before production: trust in the build. If every developer builds differently, what reaches users isn't the artifact you validated.
Episode 19's roadmap: CI pipelines for Hermes builds, capturing consistent artifacts, validating bytecode and source maps, then deployment checks for Android and iOS releases.
The CI pipeline for a Hermes app must produce the same artifact as a validated local build. Every push to the main branch (or a PR) should trigger a job that: checks out the source, installs dependencies, compiles Hermes bytecode, then builds the release. An example GitHub Actions pipeline:
name: hermes-release
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
build-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Generate bytecode Hermes
run: |
npx react-native bundle --platform android --dev false \
--entry-file index.js --bundle-output index.android.bundle
hermesc -O -emit-binary -out index.android.hbc index.android.bundle
- name: Build release APK
working-directory: android
run: ./gradlew assembleRelease
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: android-release
path: android/app/build/outputs/apk/release/Info
CI needs a deterministic environment. Use separate runners between Android (Ubuntu) and iOS (macOS), and make sure the JDK, Node, and Hermes versions are pinned — never let CI pick up a wild "latest version".
The pipeline above produces bytecode explicitly with hermesc. In modern React Native, Gradle does this step automatically during assembleRelease; producing it explicitly in CI gives you control and an artifact that can be validated before it goes inside the APK.
Validation is useless if the artifacts disappear. For a Hermes build, at minimum store four artifacts indexed by version:
.hbc bytecode (or the Hermes-output .bundle)..hbc.map source map for stack trace deobfuscation..so libraries on Android or dSYM on iOS).Consistency starts with the same compiler flags for all developers and CI. Lock the flags in Gradle so they can't be silently changed:
react {
hermesFlags = ["-O", "-output-source-map"]
}With -output-source-map, Gradle produces a .hbc.map file paired with the bytecode. Archive both with names containing the release version, for example app-1.4.0.hbc and app-1.4.0.hbc.map — because a production stack trace without a matching source map is just a puzzle.
CI must fail if the bytecode is odd. hbcdump can inspect the contents of a .hbc file — function count, bytecode version, even disassembly if needed:
hbcdump -function-sources index.android.hbc
shasum -a 256 index.android.hbc index.android.hbc.map
node scripts/verify-sourcemap.mjs index.android.hbc.mapAnother mandatory step: verify the source map. Take a few addresses from an example stack trace, map them through a symbolication tool, and make sure the results point to the correct lines in source files. A source map that "doesn't connect" makes production errors untraceable. To run this check automatically, add the node scripts/verify-sourcemap.mjs step to the pipeline as a gate before artifact upload.
Warning
Hermes bytecode is locked to the engine version. If the Hermes version in Gradle differs from the hermesc version used to make the .hbc in CI, the result can be unexecutable bytecode or mysterious runtime errors. Always use the same version in every step.
Before the APK is released, run automatic verification:
cd android && ./gradlew assembleRelease
apksigner verify --print-certs app/build/outputs/apk/release/app-release.apk
unzip -p app/build/outputs/apk/release/app-release.apk \
assets/index.android.bundle | head -c 4 | xxdThree things are checked: the build succeeds, the APK is signed with the correct certificate, and the bundle asset starts with Hermes bytecode magic bytes (not plain JavaScript). Magic bytes showing up as Hermes opcodes make sure the Hermes runtime will execute, not a slow JavaScript interpreter. This verification also protects you from builds that accidentally disable Hermes.
On macOS, use xcodebuild to archive and verify the signature and contents:
xcodebuild archive -workspace App.xcworkspace -scheme App -configuration Release
codesign --verify --deep --strict build/Release-iphoneos/App.app
file build/Release-iphoneos/App.app/main.jsbundlefile on iOS's main.jsbundle must also report Hermes bytecode. If the report shows text (aka plain JavaScript), Hermes compilation didn't run and your startup performance will disappoint in production.
A reproducible build is the foundation of all the observability and release automation discussions in the last two episodes. CI isn't the place to "discover" problems — it should be a gate that rejects artifacts that don't meet the standard before they reach users.
The essentials to take home:
hbcdump and verify the source map with example symbolication.apksigner and codesign) before uploading to the store.In episode 20, we continue with Observability & Performance Monitoring: monitoring startup, memory, and JavaScript execution runtime metrics, then setting enforceable performance budgets. See you there!