DEV Community

Soham Mondal
Soham Mondal

Posted on Originally published at sohammondal.com

How We Keep a Trunk-Based Pipeline From Being Reckless

Part 1 covered the mechanism: a fingerprint gate decides whether a change ships in minutes over-the-air or needs a full store release. But a gate that only checks "is this native-safe" says nothing about whether the change is good. If every merge to main can reach production within minutes, your safety net can't be a release train that gives everyone time to notice a problem before it ships — it has to be built into the pipeline itself, because there's no train to catch it on the way out.

Someone walking a tightrope where the safety net is woven into the rope itself

The PR gate

Every pull request into main runs through the same automated gate before it's mergeable: a type check, a lint pass, an automated test suite, and end-to-end checks against a real device build. None of that is negotiable — it's the floor, not a nice-to-have.

E2E is a big enough topic on its own — closing the loop between what a unit test can see and what actually happens on a phone in someone's hand — that it deserves its own dedicated post rather than a paragraph here.

jobs:
  typecheck:
    run: npm run typecheck
  lint:
    run: npm run lint
  test:
    run: npm test
  e2e:
    run: npm run e2e
Enter fullscreen mode Exit fullscreen mode

Nothing exotic under the hood — ESLint for the lint pass, Husky for local pre-commit/pre-push hooks so the same checks catch you before CI even runs, Jest as the test runner, and React Native Testing Library for component-level tests. Popular, boring, well-documented tooling on purpose — the pipeline's value is in how these are wired together and gated, not in any one tool being clever.

A checkpoint gate with three checked posts and one still under construction

Feature flags are the real safety valve

Here's the entry condition that makes OTA-from-main safe at all: shipping code and releasing a feature are two different actions. A merge can put new code on every user's device within minutes — that's deploy. Whether that code actually does anything visible is a separate switch, controlled by a remote feature flag, not by whether the code merged.

That decoupling is what makes trunk-based development survivable. Nobody has to get the timing of a merge exactly right, because "merged" doesn't mean "live to users" — it means "available, dark, until someone flips the switch."

if (remoteConfig.getBoolean('new_checkout_flow')) {
  return <NewCheckoutFlow />;
}
return <LegacyCheckoutFlow />;
Enter fullscreen mode Exit fullscreen mode

Anything not ready to be seen — a half-finished feature, a risky rewrite, a change you want to roll out to 5% of users first — goes behind a flag before it goes near main. That's a team discipline, not a code review checkbox, and it's the one non-negotiable rule underneath everything else in this pipeline.

I've used both Firebase Remote Config and Flagsmith for this in production — different feature sets, same underlying idea. Firebase leans simpler (boolean/value flags, decent default targeting) if you're already in that ecosystem; Flagsmith gives you more control over rollout percentages and environments if flags are a bigger part of how your team ships. Either is fine. What matters is picking one and actually using it as the gate, not treating it as optional.

A wall light switch with a blank tag hanging off it

The same fingerprint gate, wearing a different hat

The fingerprint check from Part 1 isn't just a build-vs-OTA router — it's also what makes a hotfix routing decision possible at all. Because the gate always compares against the exact binary currently in the store (or the last successful OTA), the system always knows one thing with certainty: whether main right now could safely reach the devices that are live, or whether it's drifted natively since. That single fact is the entire input to what happens next during an incident.

Hotfixing without touching main

A production fire needs a fix routed to wherever users actually are — and after a few routine OTAs, that is very often not the same commit as the last full store release. Two protected, idle snapshots exist for exactly this:

  • One pinned at the last full store release.
  • One pinned at the last successful OTA, if a newer one shipped since.

A hotfix branches off whichever of those two is actually live, ships from there, and only afterward gets cherry-picked back into main. Neither snapshot branch is ever merged into — they're frozen markers, not moving targets, precisely so a hotfix never accidentally drags in unrelated work that happened to land on main in the meantime.

A decision diamond routing into whichever pinned snapshot is currently live, looping back to main

The whole shape of it

None of these four pieces — the PR gate, feature flags, the fingerprint check, the hotfix routing — does much on its own. Together, they replace what a release manager and a release train used to do by hand: catch bad code before it ships, decouple "shipped" from "visible," know instantly whether main can reach production safely, and know exactly where a hotfix belongs during an incident. That's the whole trade this series has been describing — slower-feeling ceremony traded for faster, more honest feedback loops.


There's a fifth piece coming eventually — testing an OTA update against the real production binary before any real user sees it, without a second build. That's a post of its own once it's actually shipped.

Top comments (0)