Why We Ditched Git-Flow for Trunk-Based Mobile Releases
If you've shipped a React Native (or any mobile) app on git-flow, you know the tax. develop, release/x.y.z, a hotfix branch nobody remembers to merge back, a changelog someone writes by hand at 6pm the night before submission, and a release manager whose entire job is keeping four branches from drifting apart. It works. It's also slow, and slowness compounds — every extra day between "code written" and "code in front of a user" is a day a bug sits undiscovered.

We moved our React Native app to trunk-based development. Everything lives on main. There's no develop, no long-lived release branch, no merge-back ceremony. And counterintuitively, it's safer than git-flow was — not despite skipping the release branches, but because we replaced "a human remembers to be careful" with a machine that actually checks.
The one idea that makes this work
Mobile has a problem web doesn't: most of your code can update instantly over-the-air, but some of it is physically baked into a binary sitting in the App Store, and there is no way to OTA your way out of that. So the whole system rests on one question, asked by machine, not by a human's judgment call:
Does this code produce the same native fingerprint as what's already in the store?
Every native-relevant input — native dependencies, build config, platform config plugins — gets hashed into a single fingerprint. If your commit's fingerprint matches the currently-shipped binary, you're clear to ship instantly over the air. If it doesn't, no amount of hoping fixes that — you need an actual App Store build. The system doesn't ask anyone to remember which category a change falls into. It computes the answer:
$ npx expo-fingerprint compare --against store-build ✓ native fingerprint matches — OTA is safe $ npx expo-fingerprint compare --against store-build ✗ native fingerprint changed + node_modules/some-native-module ~ ios podfile lockfile a full store release is required before the next OTA
That diff naming exactly what changed — not just "hashes don't match, good luck" — is the difference between a five-minute "oh, it's this dependency" and an hour of guessing. That single mechanism is what makes trunk-based development safe for mobile in a way it wouldn't be otherwise. You're not trusting a person to correctly judge "is this OTA-safe?" under deadline pressure. You're trusting a hash comparison that can't be talked into a wrong answer.

OTA vs. full release, decided for you
A release is just a manually dispatched workflow with a choice input — no fixed train:
on: workflow_dispatch: inputs: action: type: choice options: [ota, full-release]
OTA (minutes): JS-only changes — UI, business logic, copy, most bug fixes. Dispatch the workflow, the fingerprint gate confirms nothing native changed, the update ships straight to users' existing app installs. No App Store review queue, no waiting on Apple.
Full release (days): anything that touches native code — a new native module, a build config change, a native SDK bump. The fingerprint gate catches this automatically and blocks the OTA path outright.

What actually happens on a release
Nobody hand-writes a version number or a changelog. When a release is triggered, the pipeline:
- Bumps the version and generates the changelog automatically from commit history, opens it as a PR for a quick human glance.
- On merge, tags the release, builds, and submits to the stores.
- Pins a protected snapshot branch at that exact commit — the "what's actually in the store" record — separate from
main, which keeps moving.
That last part matters more than it sounds like it should, and it's the hinge for how hotfixes work — which is where the real safety story is, and where Part 2 picks up: tests, feature flags, and how a hotfix finds its way to production in minutes without ever touching main directly.

Next: how we keep a pipeline this fast from being reckless — the test gate, feature-flag discipline, and the hotfix routing logic that decides exactly which branch a fix belongs on.