skills/ballee/mobile-deployment/SKILL.md
# Mobile Deployment Skill Comprehensive guide for deploying the Ballee mobile app to iOS (TestFlight/App Store) and Android (Play Store). ## Overview | Platform | CI/CD | Local Deploy | Distribution | |----------|-------|--------------|--------------| | **iOS** | Xcode Cloud | Fastlane / xcodebuild | TestFlight, App Store | | **Android** | GitHub Actions | Fastlane / Gradle | Firebase App Distribution, Play Store | ## Prerequisites ### iOS Prerequisites 1. **Apple Developer Account** with
npx skillsauth add javeedishaq/ai-workflow-orchestrator skills/ballee/mobile-deploymentInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Comprehensive guide for deploying the Ballee mobile app to iOS (TestFlight/App Store) and Android (Play Store).
| Platform | CI/CD | Local Deploy | Distribution | |----------|-------|--------------|--------------| | iOS | Xcode Cloud | Fastlane / xcodebuild | TestFlight, App Store | | Android | GitHub Actions | Fastlane / Gradle | Firebase App Distribution, Play Store |
~/.appstoreconnect/private_keys/AuthKey_LGU934Y2XR.p8Key Identifiers:
| Item | Value |
|------|-------|
| Bundle ID | co.ballee |
| Team ID | A86CXY8H75 |
| API Key ID | LGU934Y2XR |
| API Issuer ID | 69a6de96-5d75-47e3-e053-5b8c7c11a4d1 |
| Apple ID | [email protected] |
apps/mobile/android/key.propertiesKey Identifiers:
| Item | Value |
|------|-------|
| Package Name | co.ballee |
| Min SDK | 24 |
Xcode Cloud automatically builds and deploys to TestFlight on push to main.
Trigger a build manually:
cd apps/mobile/ios/scripts
python3 xcode_cloud_cli.py trigger
Check build status:
python3 xcode_cloud_cli.py list
Environment variables (set in Xcode Cloud):
| Variable | Description |
|----------|-------------|
| SUPABASE_URL | Production Supabase URL |
| SUPABASE_ANON_KEY | Production anon key |
| SENTRY_DSN | Sentry DSN for crash reporting |
| MIXPANEL_TOKEN | Analytics token |
| APP_STORE_CONNECT_API_KEY_ID | API key ID |
| APP_STORE_CONNECT_API_ISSUER_ID | Issuer ID |
| APP_STORE_CONNECT_API_KEY_CONTENT | Base64-encoded .p8 key |
CI Scripts:
ci_post_clone.sh - Installs Flutter, builds iOS app, runs code generationci_post_xcodebuild.sh - Uploads to TestFlight, uploads dSYMs to SentrySetup (first time):
cd apps/mobile/ios
bundle install
Deploy to TestFlight:
cd apps/mobile/ios
# Option A: Manual signing (uses match provisioning profiles)
bundle exec fastlane beta
# Option B: Automatic signing (uses Xcode managed signing)
bundle exec fastlane beta_auto
# Option C: Build only (no upload)
bundle exec fastlane build_only
Available Lanes:
| Lane | Description |
|------|-------------|
| beta | Build with manual signing + upload to TestFlight |
| beta_auto | Build with automatic signing + upload to TestFlight |
| build_only | Build IPA without uploading |
Increment build number:
bundle exec fastlane beta increment_build:true
Step 1: Build Flutter app
cd apps/mobile
flutter pub get
dart run build_runner build --delete-conflicting-outputs
flutter build ios --release --no-codesign \
--dart-define=ENV=prod \
--dart-define=BACKEND_URL="https://csjruhqyqzzqxnfeyiaf.supabase.co" \
--dart-define=SUPABASE_TOKEN="$SUPABASE_ANON_KEY"
Step 2: Install pods
cd ios
pod install --repo-update
Step 3: Archive
xcodebuild -workspace Ballee.xcworkspace \
-scheme Ballee \
-sdk iphoneos \
-configuration Release \
-archivePath build/Ballee.xcarchive \
archive
Step 4: Export and upload
xcodebuild -exportArchive \
-archivePath build/Ballee.xcarchive \
-exportOptionsPlist ExportOptions.plist \
-exportPath build/export \
-allowProvisioningUpdates \
-authenticationKeyPath ~/.appstoreconnect/private_keys/AuthKey_LGU934Y2XR.p8 \
-authenticationKeyID LGU934Y2XR \
-authenticationKeyIssuerID 69a6de96-5d75-47e3-e053-5b8c7c11a4d1
Use the provided script for one-command deployment:
./.claude/skills/mobile-deployment/scripts/deploy-ios.sh
Options:
--increment - Increment build number--skip-upload - Build only, don't uploadStep 1: Create keystore (if needed)
./.claude/skills/mobile-deployment/scripts/setup-android-signing.sh
Or manually:
keytool -genkey -v \
-keystore ~/ballee-release-key.jks \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-alias ballee
Step 2: Create key.properties
Create apps/mobile/android/key.properties:
storePassword=<keystore password>
keyPassword=<key password>
keyAlias=ballee
storeFile=/path/to/ballee-release-key.jks
Step 3: Enable release signing
Uncomment the signing config in apps/mobile/android/app/build.gradle.kts:
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
Build AAB (for Play Store):
cd apps/mobile
flutter build appbundle --release \
--dart-define=ENV=prod \
--dart-define=BACKEND_URL="https://csjruhqyqzzqxnfeyiaf.supabase.co" \
--dart-define=SUPABASE_TOKEN="$SUPABASE_ANON_KEY"
Output: build/app/outputs/bundle/release/app-release.aab
Build APK (for direct install/testing):
flutter build apk --release \
--dart-define=ENV=prod \
--dart-define=BACKEND_URL="https://csjruhqyqzzqxnfeyiaf.supabase.co" \
--dart-define=SUPABASE_TOKEN="$SUPABASE_ANON_KEY"
Output: build/app/outputs/flutter-apk/app-release.apk
Setup (first time):
cd apps/mobile/android
bundle install
Available Lanes:
| Lane | Description |
|------|-------------|
| build_apk | Build release APK |
| build_aab | Build release AAB |
| firebase | Build and deploy to Firebase App Distribution |
| playstore | Build and upload to Play Store (internal track) |
Deploy to Firebase App Distribution:
bundle exec fastlane firebase
Deploy to Play Store:
bundle exec fastlane playstore
build/app/outputs/bundle/release/app-release.aabUse the provided script for one-command deployment:
./.claude/skills/mobile-deployment/scripts/deploy-android.sh
Options:
--apk - Build APK instead of AAB--firebase - Deploy to Firebase App Distribution--playstore - Deploy to Play StoreAndroid deployments are fully automated via GitHub Actions with:
main and dev| Trigger | Action |
|---------|--------|
| Push to dev | Build and validate |
| Push to main | Build + Deploy to Firebase |
| PR to main/dev | Build and validate |
| Tag v*.*.* | Build + Deploy to Play Store (internal) |
| Tag release/* | Promote to Production (10% rollout) |
| Manual dispatch | Configurable deployment |
Run these scripts to set up Android CI/CD:
# 1. Create release keystore and local key.properties
./scripts/setup-android-keystore.sh
# 2. Set up Workload Identity Federation (requires gcloud CLI)
./scripts/setup-gcloud-wif.sh
# 3. Configure GitHub repository secrets
./scripts/setup-android-secrets.sh
| Secret | Description | How to Obtain |
|--------|-------------|---------------|
| ANDROID_KEYSTORE_BASE64 | Base64-encoded keystore | openssl base64 < keystore.jks |
| ANDROID_KEYSTORE_PASSWORD | Keystore password | Set during keystore creation |
| ANDROID_KEY_ALIAS | Key alias | Usually ballee |
| ANDROID_KEY_PASSWORD | Key password | Set during keystore creation |
| SUPABASE_URL | Production Supabase URL | .env.local |
| SUPABASE_ANON_KEY | Production anon key | .env.local |
| FIREBASE_APP_ID_ANDROID | Firebase App ID | Firebase Console |
| FIREBASE_SERVICE_ACCOUNT | Firebase SA JSON | Firebase Console |
| PLAY_STORE_SERVICE_ACCOUNT | Play Store SA JSON | Google Play Console |
Instead of storing long-lived service account JSON keys, we use OIDC-based keyless authentication:
The setup-gcloud-wif.sh script automates the WIF setup:
Trigger a deployment manually via GitHub CLI:
# Deploy to Firebase
gh workflow run android-deploy.yml -f deploy_target=firebase
# Deploy to Play Store (internal)
gh workflow run android-deploy.yml -f deploy_target=playstore-internal
# Promote to production
gh workflow run android-deploy.yml -f deploy_target=playstore-production
Production releases use staged rollouts for safety:
# Promote with 10% rollout (default)
fastlane promote_to_production
# Increase to 50%
fastlane increase_rollout rollout:0.5
# Full rollout
fastlane increase_rollout rollout:1.0
| File | Purpose |
|------|---------|
| .github/workflows/android-deploy.yml | Main CI/CD workflow |
| scripts/setup-android-keystore.sh | Keystore creation script |
| scripts/setup-gcloud-wif.sh | Workload Identity Federation setup |
| scripts/setup-android-secrets.sh | GitHub secrets configuration |
./scripts/setup-android-keystore.sh to create keystore./scripts/setup-gcloud-wif.sh (requires gcloud CLI)./scripts/setup-android-secrets.sh to configure GitHubAll environment variables are passed via --dart-define at build time:
| Variable | Description | iOS | Android |
|----------|-------------|-----|---------|
| ENV | Environment (prod, staging, dev) | Yes | Yes |
| BACKEND_URL | Supabase URL | Yes | Yes |
| SUPABASE_TOKEN | Supabase anon key | Yes | Yes |
| SENTRY_DSN | Sentry DSN | Yes | Yes |
| MIXPANEL_TOKEN | Mixpanel token | Yes | Yes |
| APP_STORE_ID | App Store ID (iOS only) | Yes | No |
SUPABASE_URL="https://csjruhqyqzzqxnfeyiaf.supabase.co"
SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNzanJ1aHF5cXp6cXhuZmV5aWFmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzNDkxOTQsImV4cCI6MjA3MzkyNTE5NH0.wyDYf6UpJr2trbGOzWx-Sf0p5pSbnTWcQOCFnvtN5lI"
| File | Purpose |
|------|---------|
| apps/mobile/ios/fastlane/Fastfile | Fastlane lane definitions |
| apps/mobile/ios/fastlane/Appfile | App Store Connect config |
| apps/mobile/ios/Gemfile | Ruby dependencies |
| apps/mobile/ios/ci_scripts/ci_post_clone.sh | Xcode Cloud setup script |
| apps/mobile/ios/ci_scripts/ci_post_xcodebuild.sh | Post-build upload script |
| apps/mobile/ios/scripts/xcode_cloud_cli.py | CLI for Xcode Cloud API |
| apps/mobile/ios/ExportOptions.plist | Export configuration |
| apps/mobile/ios/Ballee/Info.plist | App configuration |
| File | Purpose |
|------|---------|
| apps/mobile/android/app/build.gradle.kts | Gradle build config |
| apps/mobile/android/key.properties | Signing credentials (not in git) |
| apps/mobile/android/fastlane/Fastfile | Fastlane lane definitions |
| apps/mobile/android/fastlane/Appfile | Play Store config |
"Preparing build for App Store Connect failed" (Xcode Cloud)
"No signing certificate found" / "Revoked certificate"
"Missing purpose string in Info.plist"
NS*UsageDescription keys to Info.plistNSPhotoLibraryUsageDescription, NSCameraUsageDescriptionPodfile.lock out of sync
cd apps/mobile/ios
rm -rf Pods Podfile.lock
pod install --repo-update
Build number already exists
# Increment via Fastlane
bundle exec fastlane beta increment_build:true
# Or manually in Xcode
# Ballee.xcodeproj > Build Settings > Current Project Version
"Keystore was tampered with, or password was incorrect"
key.properties match keystore"No key with alias 'ballee' found in keystore"
keyAlias in key.properties matches alias used during keystore creationRelease build not signed
key.properties exists and paths are correctbuild.gradle.ktsApp crashes on release but not debug
"pub get failed"
flutter clean
flutter pub get
"build_runner failed"
dart run build_runner clean
dart run build_runner build --delete-conflicting-outputs
xcode-cloud-cicd - Detailed Xcode Cloud CI/CD documentationflutter-development - Flutter development patternsflutter-testing - Testing patterns for mobilecd apps/mobile/ios && bundle exec fastlane beta_auto
cd apps/mobile/android && bundle exec fastlane playstore
cd apps/mobile/ios/scripts && python3 xcode_cloud_cli.py trigger
tools
# Test Patterns Testing patterns for reliable, maintainable, and fast tests. > **Template Usage:** Customize for your test framework (Vitest, Jest, Playwright, etc.) and assertion library. ## Test Structure ```typescript // user.test.ts import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { userService } from '@/services/user.service'; import { createTestUser, cleanupTestData } from '@/tests/helpers'; describe('UserService', () => { let testUserId: string; befor
tools
# State Management Patterns Client-side state management patterns for modern applications. > **Template Usage:** Customize for your state library (React Query, Zustand, Jotai, Redux, etc.). ## State Categories | Type | Description | Solution | |------|-------------|----------| | **Server State** | Data from API/database | React Query, SWR | | **Client State** | UI state, user preferences | Zustand, Jotai, useState | | **Form State** | Form inputs, validation | React Hook Form, Formik | | **U
development
# Service Patterns Service layer patterns for clean architecture with proper error handling, logging, and type safety. > **Template Usage:** Customize for your ORM (Prisma, Drizzle, TypeORM, etc.) and logging solution. ## Result Type Pattern Never throw exceptions from services. Always return a Result type. ```typescript // lib/result.ts export type Result<T, E = Error> = | { success: true; data: T } | { success: false; error: E }; export function ok<T>(data: T): Result<T, never> { r
testing
# Row-Level Security Patterns Database security patterns for multi-tenant and user-scoped data. > **Template Usage:** Customize for your database (PostgreSQL, Supabase, etc.) and auth system. ## RLS Fundamentals ### Enable RLS on Tables ```sql -- Enable RLS (required before policies take effect) ALTER TABLE users ENABLE ROW LEVEL SECURITY; ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ALTER TABLE comments ENABLE ROW LEVEL SECURITY; -- Force RLS for table owners too (recommended) ALTER TABLE