Skip to content

Instantly share code, notes, and snippets.

@slavingia
Created April 6, 2026 14:53
Show Gist options
  • Select an option

  • Save slavingia/9360cb77f5440bff581963ae27fb57c2 to your computer and use it in GitHub Desktop.

Select an option

Save slavingia/9360cb77f5440bff581963ae27fb57c2 to your computer and use it in GitHub Desktop.

Build and ship a new version to TestFlight (iOS) and Firebase App Distribution (Android).

Each build produces TWO variants per platform: a TEST ENV build (pointing to test API) and a PROD ENV build (pointing to production API), so testers can validate both environments.

Steps

  1. Bump build number in all three places (must stay in sync):

    • app.jsonexpo.android.versionCode
    • android/app/build.gradleversionCode
    • ios/<AppName>/Info.plistCFBundleVersion Increment by 2 from the current value (PROD gets the even number, TEST gets the next odd number). IMPORTANT: PROD and TEST builds MUST use separate build numbers — TestFlight rejects duplicate CFBundleVersion uploads. Convention: PROD = N, TEST = N+1.
  2. Compile release notes from git log since the last build tag or recent commits.

  3. Commit and push the build number bump to main.

  4. Build and distribute PROD ENV variant first: a. Verify services/api.ts has USE_TEST_ENV = false (should already be the default). b. Build both platforms in parallel:

    • iOS: xcodebuild archive then xcodebuild -exportArchive
    • Android: ./gradlew assembleRelease c. Build web dist: npx expo export --platform web d. IMPORTANT: Save PROD artifacts before any TEST build overwrites them:
    cp android/app/build/outputs/apk/release/app-release.apk /tmp/MyApp-prod-buildXX.apk
    cp /tmp/MyApp-export/MyApp.ipa /tmp/MyApp-prod-buildXX.ipa

    e. Upload all in parallel:

    • iOS → TestFlight
    • iOS → Firebase with release notes prefixed: 🟢 PROD ENV (Build XX) — <release notes>
    • Android → Firebase with release notes prefixed: 🟢 PROD ENV (Build XX) — <release notes>
    • Android → Google Play (if service account key exists)
  5. Build and distribute TEST ENV variant: a. Set USE_TEST_ENV = true in services/api.ts. b. Bump CFBundleVersion in Info.plist to N+1 (TestFlight requires unique build numbers per upload). c. Swap ALL app icons to test variant (e.g. inverted colors):

    • Android: Copy *-test.png over the prod versions, regenerate mipmap WebP files.
    • iOS: Copy icon-test.pngicon.png AND → iOS asset catalog (MUST be RGB with no alpha channel — Apple rejects icons with transparency)
    • Web: Copy favicon-test.pngfavicon.png d. Build both platforms in parallel (same commands as step 4b). e. Upload all in parallel:
    • iOS → TestFlight
    • iOS → Firebase with release notes prefixed: 🟡 TEST ENV (Build XX) — <release notes>
    • Android → Firebase with release notes prefixed: 🟡 TEST ENV (Build XX) — <release notes>
  6. Restore PROD as default: a. Set USE_TEST_ENV = false back in services/api.ts. b. Restore all prod icons:

    git checkout assets/images/android-icon-*.png assets/images/icon.png assets/images/favicon.png ios/<AppName>/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png
    

    c. Regenerate Android mipmap WebP files from the prod source images. d. Do NOT commit the toggle/icon changes — they're only temporary for the test build.

  7. Commit and push the rebuilt web dist (from the PROD build).

  8. Create GitHub Release for the PROD build. CRITICAL: Use the saved PROD artifacts from step 4d, NOT the live build output paths (which will have been overwritten by the TEST build):

    gh release create vX.X.X-buildXX \
      /tmp/MyApp-prod-buildXX.apk#MyApp-buildXX.apk \
      /tmp/MyApp-prod-buildXX.ipa#MyApp-buildXX.ipa \
      --title "Build XX — PROD (vX.X.X)" \
      --notes "<release notes>"
  9. Report the build version, upload status for both platforms × both environments, and release notes.

Release notes format

For Firebase, clearly label each build:

PROD build:

🟢 PROD ENV (Build XX)
Points to: production API

Changes:
- <bullet points from git log>

TEST build:

🟡 TEST ENV (Build XX)
Points to: test API

Changes:
- <same bullet points>

Note: This build uses the TEST API environment for QA testing.

Build commands reference

iOS Archive

xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Release -archivePath /tmp/MyApp.xcarchive archive CODE_SIGN_STYLE=Manual DEVELOPMENT_TEAM=XXXXXXXXXX PROVISIONING_PROFILE_SPECIFIER="My Profile" "CODE_SIGN_IDENTITY=Apple Distribution"

iOS Export

xcodebuild -exportArchive -archivePath /tmp/MyApp.xcarchive -exportOptionsPlist scripts/ExportOptions.plist -exportPath /tmp/MyApp-export -allowProvisioningUpdates

iOS Upload (TestFlight)

xcrun altool --upload-app -f /tmp/MyApp-export/MyApp.ipa -t ios -u "developer@example.com" -p "@keychain:APP_SPECIFIC_PASSWORD"

iOS Upload (Firebase)

firebase appdistribution:distribute /tmp/MyApp-export/MyApp.ipa --app "<FIREBASE_IOS_APP_ID>" --groups my-team --release-notes "<release notes>"

Android Build

The release build is signed with the release keystore (configured in android/app/build.gradle).

export JAVA_HOME=$(/usr/libexec/java_home -v 17)
export ANDROID_HOME=~/Library/Android/sdk
cd android && ./gradlew assembleRelease && cd ..

Android Upload (Firebase)

firebase appdistribution:distribute android/app/build/outputs/apk/release/app-release.apk --app "<FIREBASE_ANDROID_APP_ID>" --groups my-team --release-notes "<release notes>"

Android Upload (Google Play)

Requires service account key. Only upload the PROD APK to Google Play.

google-play-cli apk upload \
  --config-file .certs/google-play-key.json \
  --package-name com.example.myapp \
  --apk android/app/build/outputs/apk/release/app-release.apk

$ARGUMENTS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment