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
distdirectories 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.3on 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:
Create the base64 Firebase config value locally:
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:
Apply the bump:
The helper updates:
package.jsonclient/package.jsonclient/android/app/build.gradleclient/ios/App/App.xcodeproj/project.pbxproj
Rules:
--versionmust be numeric SemVer:major.minor.patch.--buildmust 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:
- Open Actions.
- Select Mobile and web release.
- Select Run workflow.
- Choose inputs.
- Review any environment approval prompts.
- 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 |
Recommended Dispatch Presets¶
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-distartifactmetricis-android-aabartifact, if Android signing and Firebase config secrets are configuredmetricis-ios-ipaartifact, 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:
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:
Upload iOS to TestFlight:
Upload iOS binary to App Store Connect without automatic submission:
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:
- Build artifacts from
main. - Upload Android to internal track and iOS to TestFlight.
- Complete smoke testing and sponsor/regulatory sign-off.
- Promote through store consoles or production-dispatch workflow with
mobile-productionapproval. - 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:
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:
or:
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_IDmatches the signing certificate/profile.IOS_PROVISIONING_PROFILE_NAMEexactly 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
.p8API 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.