From 15a88928ddf0baa52f320c9b4563803cf8348005 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Apr 2026 03:35:14 +0000 Subject: [PATCH] mobile(phase 6): release workflow scaffold + setup guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 6 scaffolding — the CI workflow and human setup documentation needed to ship signed iOS and Android builds to TestFlight and Play Console. The workflow is complete but requires secrets to be stored in GitHub before it can run. New files: .github/workflows/release-mobile.yml: Two parallel jobs triggered on version tags or manual dispatch: iOS (TestFlight): - Runs on macos-26 with Xcode 26 - Imports Apple distribution certificate + provisioning profile from base64-encoded GitHub secrets into a temporary keychain - Builds signed IPA via `tauri ios build --export-method app-store-connect` - Uploads to TestFlight via xcrun altool with App Store Connect API key - Cleans up keychain on completion Android (Play Console): - Runs on ubuntu-latest with JDK 17 + Android SDK 36 - Decodes upload keystore from base64 GitHub secret - Writes key.properties for Gradle signing config - Builds signed AAB via `tauri android build --aab` - Uploads to Play Console Internal Testing via r0adkll/upload-google-play docs/RELEASE_MOBILE.md: Step-by-step human setup guide covering: Part 1 — iOS (7 steps): - Create App ID in Apple Developer Portal - Create distribution certificate (.p12 export) - Create App Store provisioning profile - Create App Store Connect API key (.p8) - Create the app in App Store Connect - Store 7 secrets in GitHub - Test the workflow Part 2 — Android (8 steps): - Create app in Play Console - Generate upload keystore via keytool - Enroll in Google Play App Signing - Create Google Cloud service account - Grant service account access in Play Console - Create Internal Testing track - Store 5 secrets in GitHub - Test the workflow Plus a first-release checklist and troubleshooting section. WHAT'S NEEDED FROM THE HUMAN: - Apple: distribution cert, provisioning profile, API key, Team ID - Google: upload keystore, Play Console service account JSON - Both: store secrets in GitHub repo settings The workflow will NOT run until these secrets are provided. Refs: docs/MOBILE_APP_PLAN.md Phase 6 Signed-off-by: Claude --- .github/workflows/release-mobile.yml | 222 +++++++++++++++++++++++++ docs/RELEASE_MOBILE.md | 233 +++++++++++++++++++++++++++ 2 files changed, 455 insertions(+) create mode 100644 .github/workflows/release-mobile.yml create mode 100644 docs/RELEASE_MOBILE.md diff --git a/.github/workflows/release-mobile.yml b/.github/workflows/release-mobile.yml new file mode 100644 index 000000000..896e6934b --- /dev/null +++ b/.github/workflows/release-mobile.yml @@ -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 diff --git a/docs/RELEASE_MOBILE.md b/docs/RELEASE_MOBILE.md new file mode 100644 index 000000000..c6a358437 --- /dev/null +++ b/docs/RELEASE_MOBILE.md @@ -0,0 +1,233 @@ +# Mobile Release Setup — TestFlight & Play Console + +This guide walks through the one-time setup required before the +`release-mobile.yml` GitHub Actions workflow can build and distribute +signed iOS and Android apps. + +**Time estimate**: ~1 hour total (30 min Apple, 30 min Google). + +--- + +## Part 1: iOS — Apple Developer → TestFlight + +### Prerequisites + +- Apple Developer Program membership ($99/year) — you already have this +- Access to [App Store Connect](https://appstoreconnect.apple.com) +- Access to [Apple Developer Portal](https://developer.apple.com/account) + +### Step 1: Create an App ID + +1. Go to **Certificates, Identifiers & Profiles** → **Identifiers** +2. Click **+** → **App IDs** → **App** +3. Set: + - Description: `Interactive Sphere` + - Bundle ID: `org.zyra-project.interactive-sphere` (Explicit) +4. Enable capabilities as needed (none required for v1) +5. Click **Register** + +### Step 2: Create a Distribution Certificate + +1. Go to **Certificates** → click **+** +2. Select **Apple Distribution** +3. Follow the CSR (Certificate Signing Request) flow: + - Open Keychain Access on any Mac (or use the CI runner) + - Keychain Access → Certificate Assistant → Request a Certificate + - Save the CSR file, upload it to Apple +4. Download the `.cer` file +5. Double-click to install in Keychain Access +6. In Keychain Access, right-click the certificate → **Export** as `.p12` + - Set a strong password — you'll need it for the GitHub secret + +### Step 3: Create a Provisioning Profile + +1. Go to **Profiles** → click **+** +2. Select **App Store Connect** (under Distribution) +3. Select the App ID from Step 1 +4. Select the Distribution Certificate from Step 2 +5. Name it: `Interactive Sphere App Store` +6. Download the `.mobileprovision` file + +### Step 4: Create an App Store Connect API Key + +1. Go to [App Store Connect](https://appstoreconnect.apple.com) → + **Users and Access** → **Integrations** → **App Store Connect API** +2. Click **+** → **Generate API Key** + - Name: `GitHub Actions` + - Access: **App Manager** (minimum for TestFlight uploads) +3. Note the **Key ID** and **Issuer ID** shown on the page +4. Download the **AuthKey_XXXXXX.p8** file (you can only download once!) + +### Step 5: Create the App in App Store Connect + +1. Go to **My Apps** → click **+** → **New App** +2. Fill in: + - Platform: **iOS** + - Name: `Interactive Sphere` + - Primary Language: English + - Bundle ID: select `org.zyra-project.interactive-sphere` + - SKU: `interactive-sphere` +3. Click **Create** + +This app record is required before TestFlight uploads will work. + +### Step 6: Store Secrets in GitHub + +Go to your repo → **Settings** → **Secrets and variables** → **Actions** +→ **New repository secret** for each: + +| Secret name | Value | How to get it | +|---|---|---| +| `APPLE_CERTIFICATE_BASE64` | Base64-encoded `.p12` file | `base64 -i certificate.p12 \| pbcopy` | +| `APPLE_CERTIFICATE_PASSWORD` | The password you set when exporting the .p12 | You chose this in Step 2 | +| `APPLE_PROVISIONING_PROFILE_BASE64` | Base64-encoded `.mobileprovision` file | `base64 -i profile.mobileprovision \| pbcopy` | +| `APPLE_DEVELOPMENT_TEAM` | Your 10-character Team ID | Visible in the Developer Portal top-right, or in the certificate details | +| `APP_STORE_CONNECT_API_KEY_ID` | The Key ID from Step 4 | Shown on the API Keys page | +| `APP_STORE_CONNECT_API_ISSUER_ID` | The Issuer ID from Step 4 | Shown at the top of the API Keys page | +| `APP_STORE_CONNECT_API_KEY_BASE64` | Base64-encoded `.p8` file | `base64 -i AuthKey_XXXXX.p8 \| pbcopy` | + +### Step 7: Test + +Run the workflow manually: +**Actions** → **Release Mobile App** → **Run workflow** → enter version +→ pick branch → **Run** + +The iOS job should build a signed `.ipa`, upload it to TestFlight, and +within ~15 minutes you'll receive a TestFlight email to install on your +iPhone. + +--- + +## Part 2: Android — Google Play Console + +### Prerequisites + +- Google Play Console account ($25 one-time) — create at + [play.google.com/console](https://play.google.com/console) +- A Google Cloud service account with Play Console API access + +### Step 1: Create the App in Play Console + +1. Go to **All apps** → **Create app** +2. Fill in: + - App name: `Interactive Sphere` + - Default language: English + - App or game: **App** + - Free or paid: **Free** +3. Complete the **Dashboard setup** checklist (privacy policy URL, + content rating questionnaire, target audience, etc.) + +### Step 2: Create an Upload Keystore + +On any machine with Java installed: + +```bash +keytool -genkeypair \ + -alias upload \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 \ + -keystore upload-keystore.jks \ + -storepass YOUR_STORE_PASSWORD \ + -keypass YOUR_KEY_PASSWORD \ + -dname "CN=Zyra Project, O=Zyra Project" +``` + +Keep `upload-keystore.jks` safe — if you lose it, you'll need to +contact Google Play support to reset your upload key. + +### Step 3: Enroll in Google Play App Signing + +1. In Play Console → your app → **Setup** → **App signing** +2. Choose **Use Google-generated key** (recommended) +3. Upload your `upload-keystore.jks` certificate (the public part) + following the instructions on-screen + +Google will use their own key to sign the final APK/AAB delivered to +users. Your upload keystore just authenticates that the build came +from you. + +### Step 4: Create a Service Account for CI + +1. Go to [Google Cloud Console](https://console.cloud.google.com) +2. Select or create a project linked to your Play Console +3. **IAM & Admin** → **Service Accounts** → **Create Service Account** + - Name: `github-actions-play-upload` + - Role: none (we'll grant access in Play Console) +4. Click the service account → **Keys** → **Add Key** → **JSON** +5. Download the JSON key file + +### Step 5: Grant Service Account Access in Play Console + +1. Play Console → **Users and permissions** → **Invite new users** +2. Enter the service account email (from Step 4) +3. Set permissions: + - **App access**: your app only + - **Permissions**: **Release to production, exclude devices, and + use Play App Signing** (or at minimum, release to testing tracks) +4. Click **Invite user** + +### Step 6: Create an Internal Testing Track + +1. Play Console → your app → **Testing** → **Internal testing** +2. Click **Create new release** (you don't need to upload anything yet) +3. Add testers: create an email list with your email and any beta + testers +4. Save and activate the track + +### Step 7: Store Secrets in GitHub + +| Secret name | Value | How to get it | +|---|---|---| +| `ANDROID_KEYSTORE_BASE64` | Base64-encoded `upload-keystore.jks` | `base64 -i upload-keystore.jks \| pbcopy` (or `base64 upload-keystore.jks` on Linux) | +| `ANDROID_KEY_ALIAS` | `upload` (or whatever you used in `keytool -alias`) | You chose this in Step 2 | +| `ANDROID_KEY_PASSWORD` | The key password from Step 2 | You chose this | +| `ANDROID_STORE_PASSWORD` | The store password from Step 2 | You chose this | +| `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` | The entire contents of the JSON key file | Copy-paste the JSON from Step 4 | + +### Step 8: Test + +Run the workflow manually: +**Actions** → **Release Mobile App** → **Run workflow** → enter version +→ pick branch → **Run** + +The Android job should build a signed `.aab`, upload it to the Internal +Testing track, and within ~30 minutes you'll see it available in the +Play Console for testers to install. + +--- + +## First Full Release Checklist + +Once both platforms are working on Internal/TestFlight: + +- [ ] iOS secrets stored and TestFlight upload works +- [ ] Android secrets stored and Internal Testing upload works +- [ ] Smoke test on a real iPhone via TestFlight +- [ ] Smoke test on a real Android device via Internal Testing +- [ ] Promote Android from Internal → Closed Testing → Open Testing → Production +- [ ] Submit iOS for App Store Review +- [ ] Update `docs/MOBILE_APP_PLAN.md` Phase 6 items to ✅ + +## Troubleshooting + +**iOS: "No matching provisioning profile"** +→ Ensure the Bundle ID in the profile matches `org.zyra-project.interactive-sphere` +exactly, and that the profile includes the distribution certificate you exported. + +**iOS: "Unable to authenticate with App Store Connect"** +→ Check that `APP_STORE_CONNECT_API_KEY_ID`, `API_ISSUER_ID`, and the `.p8` key +are all from the same API key. The `.p8` can only be downloaded once — if you lost +it, revoke and recreate the key. + +**Android: "Upload failed — package name mismatch"** +→ The `applicationId` in `src-tauri/gen/android/app/build.gradle.kts` must match +the package name you registered in Play Console (`org.zyra_project.interactive_sphere`). + +**Android: "Keystore was tampered with"** +→ Re-export the keystore and re-encode as base64. Ensure no trailing newline +was added during copy-paste. + +**Either platform: "Version code already used"** +→ Bump the version in `package.json` / `tauri.conf.json` before re-running. +Each upload must have a unique version code.