Automate TestFlight & Play Store delivery from GitHub
.ipa or .aab out of GitHub Actions is the easy half. The hard half is the handoff: App Store Connect API keys, Play Console service accounts, release tracks, expedited reviews, and a Fastlane config that nobody on the team wants to own. This guide walks the full automated path — from a green build to a TestFlight tester and a Play Internal Track release — and shows where Corenna removes the parts most teams get stuck on.What the handoff actually requires
- iOS — TestFlight: an App Store Connect API key (
.p8), key ID, issuer ID, and a signed.ipabuilt with a distribution certificate and an App Store provisioning profile. - Android — Play Store: a Google Play service-account JSON with the "Release manager" role, the app's package name listed in Play Console, and a signed
.aab. - Both: a versioning scheme that increments on every upload — TestFlight rejects duplicate
CFBundleVersions and Play rejects duplicateversionCodes.
TestFlight upload from GitHub Actions
The modern path is the official app-store-connect API via Fastlane's pilot or Apple's altool/notarytool. Both need the same three secrets stored as GitHub Actions secrets: APP_STORE_CONNECT_KEY_ID, APP_STORE_CONNECT_ISSUER_ID, and the base64-encoded .p8 as APP_STORE_CONNECT_KEY_CONTENT.
- name: Upload to TestFlight
env:
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }}
run: |
echo "$APP_STORE_CONNECT_KEY_CONTENT" | base64 -d > AuthKey.p8
xcrun altool --upload-app -f build/App.ipa -t ios \
--apiKey "$APP_STORE_CONNECT_KEY_ID" \
--apiIssuer "$APP_STORE_CONNECT_ISSUER_ID"The painful part is not the upload — it's the certificate plumbing that produces a valid distribution .ipa in the first place. See iOS code signing explained for the full breakdown.
Play Store upload from GitHub Actions
The community-standard action is r0adkll/upload-google-play. It takes the service-account JSON and pushes the .aab to a named track. Internal → Closed → Production is the safe promotion path; internal is the right default for CI.
- name: Upload to Play Console
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.example.app
releaseFiles: build/app-release.aab
track: internal
status: completedThe most common failure here is the service account not being invited to the Play Console app. Add the service-account email under Users and permissions in Play Console before the first run.
Version bumping that won't get rejected
TestFlight and Play both reject re-uploads of the same version. The simplest reliable scheme is to use the GitHub Actions run number as the build number:
# iOS
agvtool new-version -all $GITHUB_RUN_NUMBER
# Android (Gradle)
def vCode = System.getenv("GITHUB_RUN_NUMBER")?.toInteger() ?: 1
defaultConfig { versionCode vCode }Pair this with a semantic versionName / CFBundleShortVersionString tied to git tags, and you get human-readable release notes plus a strictly monotonic build number for the stores.
Where Fastlane stops paying off
Fastlane works, but the typical Fastfile for a mobile team grows to 400–800 lines once you add match, pilot, supply, version bumping, changelog generation, and Slack notifications. That config becomes load-bearing and quietly owned by one person. For most teams, the better trade-off is to keep the signing and store-delivery glue out of your repo entirely and treat it as managed infrastructure — which is exactly the gap Corenna fills.
How Corenna handles the handoff
- Encrypted keystore and Apple certificate storage, with one-click rotation.
- Signed
.aaband.ipastraight from a GitHub repo — no Gradle or Xcode locally. - Automatic
versionCode/CFBundleVersionbumps so uploads never collide. - Google Play delivery built in: connect a service account once and send the signed
.aabto the internal, closed, open, or production track with release notes. The.ipais uploaded to TestFlight by you. - Self-host bundle export — if you outgrow the hosted flow, you keep the workflows and run the same pipeline on your own infrastructure. See security for the threat model.
TL;DR
TestFlight uploads need an App Store Connect API key and a distribution-signed .ipa; Play Store uploads need a service-account JSON and a signed .aab. The plumbing is well-trodden but time-consuming, and version-number collisions are the single most common cause of failed automated releases. If you don't want to own that surface area, let Corenna handle the signing and the Google Play delivery and keep your repo focused on app code.