WhollySoftware
Back to blogMobile

Mobile CI/CD: Automating Builds, Signing, and Store Submission

Wholly Software TeamSeptember 9, 20247 min read
Mobile CI/CD: Automating Builds, Signing, and Store Submission

Before we standardized our CI/CD approach, releasing a build to TestFlight on one client project meant an engineer manually archiving in Xcode, uploading through Transporter, and updating a changelog by hand — roughly half a day of work per release when you counted the inevitable provisioning profile issue. We now run that entire flow through fastlane triggered from GitHub Actions, and a release build with changelog and TestFlight distribution takes about 20 minutes end to end with zero manual steps.

Code signing is where most teams' CI setups break first, usually around certificate expiry or a developer's local keychain having a certificate CI doesn't. We moved every client project to fastlane match, storing encrypted certificates and provisioning profiles in a private git repo, so any CI runner or developer machine can pull the exact same signing identity rather than each machine generating its own. This alone eliminated a recurring 'works on my machine, fails in CI' signing failure that used to cost us a couple hours per occurrence.

On Android, we standardized on storing the upload keystore as a base64-encoded GitHub Actions secret, decoded at build time, with Play App Signing enabled on every project so the actual release-signing key never leaves Google's infrastructure. We also automate versionCode incrementing based on the CI run number rather than a manually bumped constant, which used to cause rejected uploads when two developers bumped the version in parallel branches.

Store submission automation has diminishing returns past a point — we automate the build, signing, and upload to TestFlight/Internal Testing tracks fully, but we deliberately keep the final 'submit for review' action as a manual step triggered by a person, because we've been burned by an automated pipeline submitting a build with a broken feature flag configuration straight to review with no human check.

Build time itself mattered enough to optimize directly — a client's React Native app's CI build had crept to 35 minutes due to an unoptimized Gradle configuration and no dependency caching. Enabling Gradle build cache and configuring GitHub Actions' cache action for both Gradle and CocoaPods dependencies cut that to about 11 minutes, which matters more than it sounds like once you're running it 15-20 times a day across a team.

The overall principle we apply now: any release step performed manually more than twice becomes a scripted fastlane lane within the same sprint. That discipline is what turned release day from a stressful half-day event into something a junior engineer can trigger with confidence.

CI/CDFastlaneiOSAndroidDevOps
Mobile CI/CD: Automating Builds, Signing, and Store Submission — Wholly Software