Hangar
In-house mobile CI/CD that replaced Bitrise
The build-and-release pipeline for the OddsTrader iOS and Android app. It replaced a paid Bitrise plan with services the team already paid for, cutting the yearly CI bill from $1,068 to about $10, and went from design to production in about two weeks.
- CI cost, down from $1,068 a year on Bitrise
- ~$10/yr
- CI cost, down from $1,068 a year on Bitrise
- Less spent on mobile CI
- ~99%
- Less spent on mobile CI
- From first design draft to Bitrise cancelled
- ~2 weeks
- From first design draft to Bitrise cancelled
- One-click pipelines for test and store builds
- 5
- One-click pipelines for test and store builds
// how it works
The core loop
A release used to mean starting builds by hand on a paid CI. Now everything starts from one place, and nobody touches a Mac or a build number.
Bump
One npm script bumps the version name on a release branch. Build numbers are never edited by hand.
Run
Pick a pipeline in Bitbucket on any branch: test builds, or a store release per platform.
Build
Android builds on Bitbucket's Linux runners; iOS builds on Xcode Cloud, started through the App Store Connect API.
Publish
Each CI uploads its build and rewrites a static builds page. Runs show as In progress, then become the build or Failed.
Install
QA opens one stable link: Install on iPhone (ad-hoc), Download on Android. Test builds expire after 30 days.
Ship
Store builds land on Play closed testing and TestFlight. Promoting to production stays a deliberate, manual step.
// screenshots
What it looks like


// architecture
Two CIs we already paid for, one static page
The goal was the lowest running cost, with builds centralized instead of tied to whoever has a Mac. iOS needs macOS, so it runs on Xcode Cloud, whose hours come with the Apple Developer membership. Android runs on Bitbucket Pipelines, where the code already lives. Bitbucket starts everything; both CIs publish to a private S3 bucket behind CloudFront, and the builds page is a static file they regenerate. No server, nothing running between builds.
Pipelines & lanes
Bitbucket Pipelines · fastlane
- Five custom pipelines: test builds for both platforms, one per platform, and one store release per platform.
- fastlane lanes shared by CI and local runs: internal APK/IPA, store AAB/IPA, publish, trigger Xcode Cloud.
- Android signing keys from secrets at build time; nothing is stored on a laptop.
- Gradle builds only the app targets, with keyed caches.
iOS on Xcode Cloud
Xcode Cloud · ci_scripts
- Two workflows: internal (ad-hoc) and store (archive to TestFlight plus an installable ad-hoc twin).
- Post-clone script installs the pinned Node and Ruby, app config and pods, and switches to cloud-managed signing.
- Post-build script publishes the IPA and its install manifest to the builds page.
- About 25 minutes per build, well inside the hours included with the Apple membership.
Builds page
S3 · CloudFront · Ruby/ERB
- A static page regenerated from the bucket's contents by one Ruby module after every upload.
- Private bucket; only CloudFront can read it (origin access control), and HTTPS makes iPhone installs work.
- Cards for runs In progress, Failed, or No news after 90 minutes; refreshes itself while a run is going.
- Lifecycle rules expire test builds at 30 days and store builds at 90. The CI key can write but never delete.
Versioning
Automatic build numbers
- Android: highest versionCode on Google Play + 1, read from the Play API at build time.
- iOS: Xcode Cloud's own build counter, started above every build already in App Store Connect.
- The version name stays in the repo, shared by both platforms.
- Each run is tracked by its CI run id, so a card always matches its build.
// design principles
Cheap, centralized, reproducible
Build time was not a constraint; cost was. Every option was scored on what it costs to run, who has to own it, and whether any developer can ship a release.
Cheap
$89/month → ~$1/month
No new subscription: Xcode Cloud hours come with the Apple membership and Android minutes with the Bitbucket plan. What is left is a few cents of S3 and CloudFront.
Centralized
no laptops in the loop
Builds, logs and history live in the CIs, not on someone's machine. Store credentials stay in CI secrets, and a second developer can ship a release without touching a Mac.
Reproducible
pinned, clean, automatic
Tool versions are pinned in the repo, builds come from a clean clone of a branch or tag, and build numbers come from the stores themselves instead of a hand-edited file.
// engineering decisions
How it was done
A small migration run like a big one: costed options, a written design, a spike with pass criteria, a parallel run, then the cut-over.
Six options, costed
Xcode Cloud + Bitbucket, a local release script, an on-demand AWS Mac, a Mac mini runner, GitHub Actions and an always-on cloud Mac, each with a 12-month running cost. The winner costs about $10 a year.
Design doc, five reviews
One document covered the Bitrise inventory, architecture, signing, secrets, failure modes, cost/ROI and the migration roadmap, and went through five review rounds with the engineering leads.
A spike with seven pass criteria
Before committing, a short spike had to prove each risk: signing, ad-hoc and store exports from Xcode Cloud, Android on Linux, build numbers, publishing and installs. The fallback plan stayed ready.
Built with AI, owned by me
I used Claude Code as a pair throughout: drafting the design doc, writing implementation plans and verification scripts, and writing the pipelines and lanes. Every change went through my own review and the team's.
Store twins for QA
Every build sent to a store gets an installable twin on the builds page with the same version and build number, so QA tests exactly what reviewers will see.
Zero-downtime cut-over
Signing kept a fallback to the old setup so both systems ran side by side. Once a real release went through the new path to both stores, Bitrise was cancelled.
// what's in it
Feature set
- One place to start every build: Bitbucket → Run pipeline, on any branch
- Test builds for both platforms in parallel from one pipeline
- Store releases per platform: Play closed testing and TestFlight
- Builds page with Test builds and Store releases tabs
- In progress, Failed and No news states, with live refresh
- Platform and branch filters; Latest marks the newest develop build
- iPhone install over the air (ad-hoc), Android APK download
- Automatic, per-platform build numbers from the stores
- Expiry by lifecycle rules: 30 days for test builds, 90 for store builds
- Local runs of the same lanes for emergencies
// tech stack
Built with
Mobile app
CI/CD
Cloud
Stores
Tooling
My role
Sole engineer: cost analysis, design doc, architecture, pipelines, builds page, migration and cut-over. Built with Claude Code as a pair and reviewed with the engineering leads.