Skip to content

Release Automation Runbook

This runbook documents the Metricis GitHub Actions + Fastlane release workflow for web artifacts, Android builds, Google Play uploads, iOS builds, TestFlight uploads, and App Store Connect uploads.

The workflow is intentionally semi-automated. It can build, package, retain artifacts, and upload to store testing tracks, but production promotion should stay behind explicit GitHub Environment approvals and store-console review.

Source Files

File Purpose
.github/workflows/mobile-release.yml Manual GitHub Actions workflow for web, Android, and iOS release jobs
fastlane/Fastfile Android and iOS build/upload lanes
fastlane/Gemfile Fastlane/CocoaPods Ruby dependencies, isolated from normal Capacitor sync
scripts/bump-mobile-version.mjs Version bump helper for npm, Android, and iOS native metadata
.nvmrc Node version pin for local release work

Normal Capacitor development still uses npm run build:android --workspace=client and npm run build:ios --workspace=client. Fastlane dependencies are isolated under fastlane/Gemfile so a root Gemfile does not interfere with CocoaPods during cap sync ios.

What This Automates

The manual workflow supports these release targets:

  • Build all web frontends and retain their dist directories as a GitHub artifact.
  • Optionally deploy the landing site through the existing Cloudflare Wrangler path.
  • Build a signed Android App Bundle (app-release.aab).
  • Optionally upload the Android App Bundle to a Google Play track.
  • Build a signed iOS App Store IPA (Metricis.ipa).
  • Optionally upload the iOS IPA to TestFlight.
  • Optionally upload the iOS IPA to App Store Connect without automatic submission or release.

It does not automate:

  • App Store review submission.
  • Google Play production rollout completion.
  • Creation of the first Google Play app listing / first manual binary.
  • Apple Developer account provisioning setup.
  • Sponsor, regulatory, or production cutover approval.

Toolchain Baseline

The release workflow is aligned to the Capacitor 8 upgrade baseline:

  • Node 24.16.0
  • npm from the selected Node toolchain
  • Ruby 3.3 on GitHub Actions
  • Fastlane >= 2.228, < 3.0
  • CocoaPods >= 1.16, < 2.0
  • Java 21
  • Android SDK 36
  • Android Gradle Plugin 8.13.0
  • Gradle wrapper 8.14.3
  • Xcode 26.x with iOS 26.5 simulator/runtime support for current local validation

GitHub Environments

Create these environments in GitHub repository settings:

Environment Intended Use Recommended Protection
release-build Artifact-only web builds No required reviewers
mobile-beta TestFlight and Google Play internal/alpha/beta uploads Required reviewer if external testers can access builds
mobile-production App Store Connect production candidate and Google Play production track uploads Required reviewers, prevent self-review, restrict to main
web-production Cloudflare landing deployment Required reviewers if the landing site points at production

The workflow maps jobs to environments based on dispatch inputs. Production-like store destinations should pause for environment approval before credentials are used.

Required Secrets

Web / Cloudflare

Required only when deploy_landing=true:

Secret Purpose
CLOUDFLARE_API_TOKEN Wrangler deploy token
CLOUDFLARE_ACCOUNT_ID Cloudflare account ID

Android / Google Play

Secret Purpose
ANDROID_GOOGLE_SERVICES_JSON_BASE64 Base64-encoded Firebase google-services.json for package app.metricis.app
ANDROID_KEYSTORE_BASE64 Base64-encoded Android upload keystore
ANDROID_KEYSTORE_PASSWORD Keystore password
ANDROID_KEY_ALIAS Upload key alias
ANDROID_KEY_PASSWORD Upload key password
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON Raw Google Play service account JSON

Create the base64 keystore value locally:

base64 -i upload-keystore.jks | pbcopy

Create the base64 Firebase config value locally:

base64 -i client/android/app/google-services.json | pbcopy

The real google-services.json is deployment-specific and ignored locally. Use client/android/app/google-services.json.example only as a shape reference.

GOOGLE_PLAY_SERVICE_ACCOUNT_JSON should be the raw JSON text for a service account with access to the app in Google Play Console. Google Play usually requires first-time app setup and at least one manual initial binary/listing step before API uploads can manage tracks.

iOS / App Store Connect

Secret Purpose
APPLE_TEAM_ID Apple Developer team ID
IOS_CERTIFICATE_P12_BASE64 Base64-encoded Apple distribution certificate
IOS_CERTIFICATE_PASSWORD Certificate password
IOS_PROVISIONING_PROFILE_BASE64 Base64-encoded App Store provisioning profile
IOS_PROVISIONING_PROFILE_NAME Provisioning profile name from Apple Developer portal
IOS_KEYCHAIN_PASSWORD Temporary CI keychain password
APP_STORE_CONNECT_KEY_ID App Store Connect API key ID
APP_STORE_CONNECT_ISSUER_ID App Store Connect issuer ID
APP_STORE_CONNECT_API_KEY_P8_BASE64 Base64-encoded App Store Connect .p8 API key

Encode signing assets locally:

base64 -i distribution-certificate.p12 | pbcopy
base64 -i Metricis_App_Store.mobileprovision | pbcopy
base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy

IOS_PROVISIONING_PROFILE_NAME must match the App Store profile name for bundle ID app.metricis.app.

Version Bump Runbook

Create a version-bump branch/PR before running a release workflow from main.

Dry-run first:

npm run release:bump -- --version=1.2.3 --build=123 --dry-run

Apply the bump:

npm run release:bump -- --version=1.2.3 --build=123

The helper updates:

  • package.json
  • client/package.json
  • client/android/app/build.gradle
  • client/ios/App/App.xcodeproj/project.pbxproj

Rules:

  • --version must be numeric SemVer: major.minor.patch.
  • --build must be a positive integer.
  • Use a monotonically increasing build number. The GitHub run number is a good default for CI-driven build numbers, but store uploads require each platform's submitted build number to be higher than the previous accepted upload.

After applying the bump:

npm install
npm run build --workspace=client
npm run build:android --workspace=client
npm run build:ios --workspace=client

Open a PR, let CI pass, then merge to main.

GitHub Actions Workflow

Run the workflow from GitHub:

  1. Open Actions.
  2. Select Mobile and web release.
  3. Select Run workflow.
  4. Choose inputs.
  5. Review any environment approval prompts.
  6. Download retained artifacts if needed.

Inputs:

Input Values Meaning
platform all, android, ios, web Jobs to run
upload_to_stores true, false Whether native jobs upload to store services
android_track internal, alpha, beta, production Google Play target track
ios_destination testflight, appstore iOS upload destination
deploy_landing true, false Whether to deploy the landing site to Cloudflare

Build Artifacts Only

Use for release-candidate validation without touching store services.

Input Value
platform all
upload_to_stores false
deploy_landing false

Expected outputs:

  • web-dist artifact
  • metricis-android-aab artifact, if Android signing and Firebase config secrets are configured
  • metricis-ios-ipa artifact, if iOS signing secrets are configured

Android Internal Testing

Input Value
platform android
upload_to_stores true
android_track internal

Fastlane uploads the AAB as a draft release to the internal track.

iOS TestFlight

Input Value
platform ios
upload_to_stores true
ios_destination testflight

Fastlane uploads the IPA to TestFlight and skips waiting for build processing.

Production Candidate

Use only after sponsor/regulatory approval.

Input Value
platform all or target platform
upload_to_stores true
android_track production
ios_destination appstore
deploy_landing true only when intended

Production jobs should require GitHub Environment approval. Android uploads are configured as draft releases. iOS App Store uploads do not submit for review or release automatically.

Local Fastlane Commands

Install Fastlane dependencies:

BUNDLE_GEMFILE=fastlane/Gemfile bundle install

Build Android release App Bundle:

test -s client/android/app/google-services.json
BUNDLE_GEMFILE=fastlane/Gemfile bundle exec fastlane android build

Upload Android to Google Play internal track:

SUPPLY_JSON_KEY_DATA="$(cat google-play-service-account.json)" \
BUNDLE_GEMFILE=fastlane/Gemfile bundle exec fastlane android play track:internal release_status:draft

Build iOS IPA after exporting required signing variables:

BUNDLE_GEMFILE=fastlane/Gemfile bundle exec fastlane ios build

Upload iOS to TestFlight:

BUNDLE_GEMFILE=fastlane/Gemfile bundle exec fastlane ios testflight

Upload iOS binary to App Store Connect without automatic submission:

BUNDLE_GEMFILE=fastlane/Gemfile bundle exec fastlane ios appstore

Local Validation Commands

Run these before merging release automation or version-bump changes:

ruby -c fastlane/Fastfile
ruby -e 'require "yaml"; Dir[".github/workflows/*.yml"].each { |f| YAML.load_file(f); puts "OK #{f}" }'
node --check scripts/bump-mobile-version.mjs
npm run build:all
npm run build:android --workspace=client
npm run build:ios --workspace=client
cd client/android && ./gradlew bundleRelease --stacktrace

For iOS simulator validation:

cd client/ios/App
xcodebuild -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -sdk iphonesimulator \
  -destination 'generic/platform=iOS Simulator' \
  CODE_SIGNING_ALLOWED=NO \
  build

Store Promotion Policy

Recommended policy for Metricis:

  1. Build artifacts from main.
  2. Upload Android to internal track and iOS to TestFlight.
  3. Complete smoke testing and sponsor/regulatory sign-off.
  4. Promote through store consoles or production-dispatch workflow with mobile-production approval.
  5. Record release version, build number, commit SHA, workflow run URL, store destination, and approver in the operational release log.

Do not enable fully automatic production release until store-readiness findings are closed and the release-signoff process is proven.

Troubleshooting

cap sync ios tries to resolve Fastlane gems

There should not be a root Gemfile. Fastlane dependencies live under fastlane/Gemfile. Run Fastlane with:

BUNDLE_GEMFILE=fastlane/Gemfile bundle exec fastlane ...

Android build cannot find Java 21

CI uses actions/setup-java@v4 with Java 21. Locally, either export JAVA_HOME or set Gradle's Java home:

export JAVA_HOME=$(/usr/libexec/java_home -v 21)

or:

org.gradle.java.home=/Library/Java/JavaVirtualMachines/amazon-corretto-21.jdk/Contents/Home

Android upload fails on first app release

Finish the first app/listing setup manually in Google Play Console. Google Play API uploads are reliable after Play Console has the app, package name, signing setup, and first track/listing state established.

iOS signing fails in CI

Check:

  • APPLE_TEAM_ID matches the signing certificate/profile.
  • IOS_PROVISIONING_PROFILE_NAME exactly matches the installed profile name.
  • The provisioning profile bundle ID is app.metricis.app.
  • The certificate is an Apple distribution certificate, not a development certificate.
  • The .p8 API key is base64-encoded and belongs to the correct App Store Connect issuer.

App Store upload succeeds but no release happens

That is expected. The lane uses submit_for_review:false and automatic_release:false. Complete review submission and phased release decisions in App Store Connect unless this policy is intentionally changed.

Appflow Position

Appflow is not required. The GitHub Actions + Fastlane path gives Metricis:

  • Release workflow ownership in the repo.
  • GitHub Environment approvals.
  • Artifact retention.
  • Store upload paths without vendor lock-in.
  • A clear audit trail tied to commit SHA and workflow run.

Appflow can still be adopted later if the team wants a managed Capacitor dashboard, hosted native build service, or Appflow-specific live update workflow.