Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 222 additions & 0 deletions .github/workflows/release-mobile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
name: Release Mobile App

# Triggered alongside desktop releases — same version tag, same
# workflow_dispatch inputs. Builds signed iOS (.ipa) and Android
# (.aab) bundles and uploads them to TestFlight and Play Console
# Internal Testing respectively.
#
# PREREQUISITES: before this workflow can run, you must complete the
# one-time setup steps in docs/RELEASE_MOBILE.md and store the
# required secrets in GitHub Settings → Secrets and variables → Actions.

on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g. 0.3.0). Must match a tag or the desktop release workflow.'
required: true
branch:
description: 'Branch to release from'
required: false
default: 'main'

concurrency:
group: release-mobile-${{ github.event.inputs.version || github.ref_name }}
cancel-in-progress: false

permissions:
contents: read

jobs:
# ── iOS: build signed IPA → upload to TestFlight ──────────────────

ios-release:
name: iOS (TestFlight)
runs-on: macos-26
timeout-minutes: 30

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch || github.ref }}
lfs: true

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install Rust stable with iOS targets
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-ios

- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-cargo-ios-release-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-ios-release-

- run: npm ci

# Import the Apple distribution certificate into a temporary keychain
# so xcodebuild can sign the app. The certificate and provisioning
# profile are stored as base64-encoded GitHub secrets.
- name: Import code signing certificate
env:
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_PROVISIONING_PROFILE_BASE64 }}
run: |
# Create a temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain

# Import the certificate
CERT_PATH=$RUNNER_TEMP/certificate.p12
echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > "$CERT_PATH"
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

# Install the provisioning profile
PROFILE_PATH=$RUNNER_TEMP/profile.mobileprovision
echo "$APPLE_PROVISIONING_PROFILE_BASE64" | base64 --decode > "$PROFILE_PATH"
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "$PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/

- name: Bootstrap iOS scaffold
run: |
if [ ! -d "src-tauri/gen/apple" ]; then
npx tauri ios init
fi

- name: Build iOS app (release, device)
env:
APPLE_DEVELOPMENT_TEAM: ${{ secrets.APPLE_DEVELOPMENT_TEAM }}
run: npx tauri ios build --export-method app-store-connect

# Upload to TestFlight via the App Store Connect API key
- name: Upload to TestFlight
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
run: |
# Decode the API key
API_KEY_PATH=$RUNNER_TEMP/AuthKey.p8
echo "$APP_STORE_CONNECT_API_KEY_BASE64" | base64 --decode > "$API_KEY_PATH"

# Find the IPA
IPA=$(find src-tauri/gen/apple/build -name '*.ipa' | head -1)
if [ -z "$IPA" ]; then
echo "::error::No IPA found after build"
exit 1
fi

# Upload via xcrun altool (or notarytool for newer Xcode)
xcrun altool --upload-app \
--file "$IPA" \
--type ios \
--apiKey "$APP_STORE_CONNECT_API_KEY_ID" \
--apiIssuer "$APP_STORE_CONNECT_API_ISSUER_ID" \
--apiKeyPath "$API_KEY_PATH"

- name: Clean up keychain
if: always()
run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db 2>/dev/null || true

# ── Android: build signed AAB → upload to Play Console ────────────

android-release:
name: Android (Play Console)
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch || github.ref }}
lfs: true

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- name: Set up Android SDK
uses: android-actions/setup-android@v3
with:
packages: 'platform-tools platforms;android-36 build-tools;36.0.0 ndk;28.2.13676358'

- name: Install Rust stable with Android targets
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android,armv7-linux-androideabi

- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-cargo-android-release-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-android-release-

- run: npm ci

- name: Export NDK_HOME
run: echo "NDK_HOME=$ANDROID_HOME/ndk/28.2.13676358" >> $GITHUB_ENV

# Decode the upload keystore from a GitHub secret
- name: Decode Android signing keystore
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > $RUNNER_TEMP/upload-keystore.jks

# Write the signing config for Gradle
- name: Write key.properties
env:
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
run: |
cat > src-tauri/gen/android/key.properties << EOF
storeFile=$RUNNER_TEMP/upload-keystore.jks
storePassword=$ANDROID_STORE_PASSWORD
keyAlias=$ANDROID_KEY_ALIAS
keyPassword=$ANDROID_KEY_PASSWORD
EOF

- name: Build Android app (release, AAB)
run: npx tauri android build --aab

# Upload AAB to Play Console Internal Testing track
- name: Upload to Play Console
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
packageName: org.zyra_project.interactive_sphere
releaseFiles: src-tauri/gen/android/app/build/outputs/bundle/release/*.aab
track: internal
status: completed
Loading
Loading