Fastlane for iOS: half my commits were build numbers
Rereading my 2014 and 2015 app repos, half the commits were release chores. What automating iOS build numbers and signing with fastlane looked like.
From 2015 · 2 min read · Published September 3, 2026
In 2014 and 2015 I built a string of small apps: an illustrated eBook app with stickers and sound effects, a social app with a Node server and iOS and Android clients, an emoji keyboard for Android, a few client web projects. Rereading those repos for this blog, one number jumped out, and it explains why I adopted fastlane for iOS.
In the eBook app, 34 of my 65 commits were release chores: "bumping build num," "updating profile for apple," "uploading to App Store," "reseting build num for App Store." In the social app it was 36 of 154, with gems like "debugging keyboard and provisioning profile."
That's not engineering. That's clerical work, done by hand, committed to git as a record that it happened.
What it cost
The real cost wasn't the minutes each bump took. It was that every release depended on me remembering the steps in the right order: bump the build number, pick the right provisioning profile, archive, upload, wait, and if Apple rejected the profile, start over. Anyone else on the project couldn't ship without me, and I couldn't ship without a clear afternoon.
2016: automating the release with fastlane
By mid 2016, on a larger iOS project, I finally sat down and set up fastlane. The commit log for July 8th, 2016 tells the story: "fastlane init," "adding fastlane CrashlyticsBeta lane," "updating fastlane CrashlyticsBuild" twice, and then, that evening, "1st successful fastlane build." Three days later: "life in the fastlane."
The lane that replaced all those commits was short:
lane :beta do
increment_build_number
gym(scheme: "App", clean: true)
crashlytics(groups: ["testers"])
slack(message: "Beta build released")
end
error do |lane, exception|
slack(message: exception.message, success: false)
endincrement_build_number alone would have eliminated half of my 2015 commit history. CircleCI ran the lane automatically on every push to the dev branch, so a beta build reached testers without anyone touching Xcode.
The error block mattered as much as the happy path. A failed build posted the exception to Slack, so a broken release was visible to the whole team instead of sitting in one person's terminal.
Then signing
Build numbers were the easy part. Code signing was the part that ate afternoons. In November 2016 I set up a private fastlane match repository: certificates and provisioning profiles, encrypted, stored in git, shared across machines and CI. Its history is nothing but automated commits like "[fastlane] Updated appstore and platform ios," which is exactly the point. The machine does the clerical work and records it.
Why this still matters
Every team eventually learns this, usually around the third painful release. Today's tools (EAS Build, Xcode Cloud, GitHub Actions) are far better than 2016's, but the principle is identical: if a release step can be forgotten, it will be. Put it in a script, run the script in CI, and make failures loud.
Related: React Native CI/CD that only builds the platform that changed · Hash the native layer: skipping React Native builds before Expo Fingerprint