workflows: android-debug: name: Android – Debug APK max_build_duration: 45 instance_type: mac_mini_m2 environment: node: 22 java: 21 cache: cache_paths: - $HOME/.gradle/caches - $HOME/.gradle/wrapper scripts: # Template ships with optional native plugins (maps, push, firebase auth) that most apps # won't use. Keeping unused plugins in package.json adds SPM/Gradle deps and bloats the # build. Strip any that have no import in src/ so only actually-used plugins are compiled. - name: Strip unused scaffold packages script: | for pkg in "@capacitor/google-maps" "@capacitor/push-notifications" "@capacitor-firebase/authentication"; do if ! grep -rq "$pkg" src/; then node -e "const fs=require('fs'),p=JSON.parse(fs.readFileSync('package.json','utf8'));delete p.dependencies['${pkg}'];fs.writeFileSync('package.json',JSON.stringify(p,null,2));" echo "Stripped $pkg (not used in src/)" else echo "Keeping $pkg (used in src/)" fi done # Delete package-lock.json (generated if user ever ran npm locally) so it doesn't # conflict with yarn.lock. --legacy-peer-deps silences peer dep warnings from older # Ionic/React packages that haven't updated their peerDependencies for React 18/19. - name: Install dependencies script: rm -f package-lock.json && npm install --legacy-peer-deps - name: Build web assets script: npm run build # Firebase config files are injected as base64 env vars by the Appcakes backend at # build time. Decode them here so cap sync and Gradle can pick them up. Optional — # builds without Firebase still succeed; native Firebase SDK just won't initialise. - name: Write Firebase config script: | if [ -n "$GOOGLE_SERVICES_JSON" ]; then echo "$GOOGLE_SERVICES_JSON" | base64 --decode > android/app/google-services.json echo "Wrote google-services.json ($(wc -c < android/app/google-services.json) bytes)" else echo "GOOGLE_SERVICES_JSON is empty — skipping" fi if [ -n "$GOOGLE_SERVICE_INFO_PLIST" ]; then echo "$GOOGLE_SERVICE_INFO_PLIST" | base64 --decode > ios/App/App/GoogleService-Info.plist echo "Wrote GoogleService-Info.plist ($(wc -c < ios/App/App/GoogleService-Info.plist) bytes)" fi # cap sync copies the built web assets (dist/) into android/app/src/main/assets/public/ # and updates Capacitor plugin registrations in MainActivity. Must run after npm build. - name: Capacitor sync script: npx cap sync android - name: Set up debug keystore script: | if [ -n "$CM_DEBUG_KEYSTORE" ]; then echo "$CM_DEBUG_KEYSTORE" | base64 --decode > /tmp/debug-keystore.p12 fi - name: Build debug APK script: | cd android if [ -n "$CM_DEBUG_KEYSTORE" ]; then ./gradlew assembleDebug \ -Pandroid.injected.signing.store.file=/tmp/debug-keystore.p12 \ -Pandroid.injected.signing.store.password=$CM_DEBUG_KEYSTORE_PASSWORD \ -Pandroid.injected.signing.key.alias=debug \ -Pandroid.injected.signing.key.password=$CM_DEBUG_KEYSTORE_PASSWORD \ --no-daemon else ./gradlew assembleDebug --no-daemon fi artifacts: - android/app/build/outputs/apk/debug/app-debug.apk android-release-apk: name: Android – Release APK max_build_duration: 45 instance_type: mac_mini_m2 environment: node: 22 java: 21 cache: cache_paths: - $HOME/.gradle/caches - $HOME/.gradle/wrapper scripts: - name: Strip unused scaffold packages script: | for pkg in "@capacitor/google-maps" "@capacitor/push-notifications" "@capacitor-firebase/authentication"; do if ! grep -rq "$pkg" src/; then node -e "const fs=require('fs'),p=JSON.parse(fs.readFileSync('package.json','utf8'));delete p.dependencies['${pkg}'];fs.writeFileSync('package.json',JSON.stringify(p,null,2));" echo "Stripped $pkg (not used in src/)" else echo "Keeping $pkg (used in src/)" fi done - name: Install dependencies script: rm -f package-lock.json && npm install --legacy-peer-deps - name: Build web assets script: npm run build - name: Write Firebase config script: | if [ -n "$GOOGLE_SERVICES_JSON" ]; then echo "$GOOGLE_SERVICES_JSON" | base64 --decode > android/app/google-services.json fi - name: Capacitor sync script: npx cap sync android - name: Set up keystore script: echo "$CM_KEYSTORE" | base64 --decode > /tmp/keystore.jks - name: Build Release APK script: | cd android ./gradlew assembleRelease \ -Pandroid.injected.signing.store.file=/tmp/keystore.jks \ -Pandroid.injected.signing.store.password=$CM_KEYSTORE_PASSWORD \ -Pandroid.injected.signing.key.alias=$CM_KEY_ALIAS \ -Pandroid.injected.signing.key.password=$CM_KEY_PASSWORD \ --no-daemon artifacts: - android/app/build/outputs/apk/release/app-release.apk android-release-aab: name: Android – Release AAB max_build_duration: 45 instance_type: mac_mini_m2 environment: node: 22 java: 21 cache: cache_paths: - $HOME/.gradle/caches - $HOME/.gradle/wrapper scripts: - name: Strip unused scaffold packages script: | for pkg in "@capacitor/google-maps" "@capacitor/push-notifications" "@capacitor-firebase/authentication"; do if ! grep -rq "$pkg" src/; then node -e "const fs=require('fs'),p=JSON.parse(fs.readFileSync('package.json','utf8'));delete p.dependencies['${pkg}'];fs.writeFileSync('package.json',JSON.stringify(p,null,2));" echo "Stripped $pkg (not used in src/)" else echo "Keeping $pkg (used in src/)" fi done - name: Install dependencies script: rm -f package-lock.json && npm install --legacy-peer-deps - name: Build web assets script: npm run build - name: Write Firebase config script: | if [ -n "$GOOGLE_SERVICES_JSON" ]; then echo "$GOOGLE_SERVICES_JSON" | base64 --decode > android/app/google-services.json fi - name: Capacitor sync script: npx cap sync android - name: Set up keystore script: echo "$CM_KEYSTORE" | base64 --decode > /tmp/keystore.jks - name: Build Release AAB script: | cd android ./gradlew bundleRelease \ -Pandroid.injected.signing.store.file=/tmp/keystore.jks \ -Pandroid.injected.signing.store.password=$CM_KEYSTORE_PASSWORD \ -Pandroid.injected.signing.key.alias=$CM_KEY_ALIAS \ -Pandroid.injected.signing.key.password=$CM_KEY_PASSWORD \ --no-daemon artifacts: - android/app/build/outputs/bundle/release/app-release.aab android-release: name: Android – Publish to Google Play max_build_duration: 60 instance_type: mac_mini_m2 environment: node: 22 java: 21 cache: cache_paths: - $HOME/.gradle/caches - $HOME/.gradle/wrapper scripts: - name: Strip unused scaffold packages script: | for pkg in "@capacitor/google-maps" "@capacitor/push-notifications" "@capacitor-firebase/authentication"; do if ! grep -rq "$pkg" src/; then node -e "const fs=require('fs'),p=JSON.parse(fs.readFileSync('package.json','utf8'));delete p.dependencies['${pkg}'];fs.writeFileSync('package.json',JSON.stringify(p,null,2));" echo "Stripped $pkg (not used in src/)" else echo "Keeping $pkg (used in src/)" fi done - name: Install dependencies script: rm -f package-lock.json && npm install --legacy-peer-deps - name: Build web assets script: npm run build - name: Write Firebase config script: | if [ -n "$GOOGLE_SERVICES_JSON" ]; then echo "$GOOGLE_SERVICES_JSON" | base64 --decode > android/app/google-services.json fi - name: Capacitor sync script: npx cap sync android - name: Set up keystore script: echo "$CM_KEYSTORE" | base64 --decode > /tmp/keystore.jks - name: Build Release AAB script: | cd android ./gradlew bundleRelease \ -Pandroid.injected.signing.store.file=/tmp/keystore.jks \ -Pandroid.injected.signing.store.password=$CM_KEYSTORE_PASSWORD \ -Pandroid.injected.signing.key.alias=$CM_KEY_ALIAS \ -Pandroid.injected.signing.key.password=$CM_KEY_PASSWORD \ --no-daemon publishing: google_play: credentials: $GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS track: $GOOGLE_PLAY_TRACK submit_as_draft: false artifacts: - android/app/build/outputs/bundle/release/app-release.aab ios-debug: name: iOS – Debug IPA max_build_duration: 60 instance_type: mac_mini_m2 environment: node: 22 xcode: latest scripts: - name: Strip unused scaffold packages script: | for pkg in "@capacitor/google-maps" "@capacitor/push-notifications" "@capacitor-firebase/authentication"; do if ! grep -rq "$pkg" src/; then node -e "const fs=require('fs'),p=JSON.parse(fs.readFileSync('package.json','utf8'));delete p.dependencies['${pkg}'];fs.writeFileSync('package.json',JSON.stringify(p,null,2));" echo "Stripped $pkg (not used in src/)" else echo "Keeping $pkg (used in src/)" fi done - name: Install dependencies script: rm -f package-lock.json && npm install --legacy-peer-deps - name: Build web assets script: npm run build # cap sync ios must run before Firebase config is written so that CapApp-SPM/Package.swift # is regenerated with the correct plugin list first. The Firebase plist is written # afterwards and a second sync re-runs to ensure everything is consistent. # Note: this project uses Capacitor SPM (no CocoaPods/Podfile), so cap sync replaces # pod install — it regenerates CapApp-SPM/Package.swift and copies web assets. - name: Capacitor sync script: npx cap sync ios # GoogleService-Info.plist is injected as a base64 env var. The Ruby snippet adds the # file reference to the Xcode project if it isn't already there, so Xcode includes it # in the app bundle at build time. - name: Write Firebase config script: | if [ -n "$GOOGLE_SERVICE_INFO_PLIST" ]; then echo "$GOOGLE_SERVICE_INFO_PLIST" | base64 --decode > ios/App/App/GoogleService-Info.plist ruby -e " require 'xcodeproj' project = Xcodeproj::Project.open('ios/App/App.xcodeproj') target = project.targets.find { |t| t.name == 'App' } app_group = project.main_group['App'] unless app_group.files.find { |f| f.path == 'GoogleService-Info.plist' } file_ref = app_group.new_reference('GoogleService-Info.plist') target.resources_build_phase.add_file_reference(file_ref) project.save end " fi # Second sync ensures CapApp-SPM/Package.swift reflects any plugin changes made during # the Firebase config step and keeps the package graph in a consistent state. - name: Capacitor sync script: npx cap sync ios - name: Set up signing script: | keychain initialize if [ -n "$APP_STORE_CONNECT_PRIVATE_KEY" ]; then app-store-connect fetch-signing-files \ $(xcode-project detect-bundle-id) \ --type IOS_APP_DEVELOPMENT \ --certificate-key=@env:APP_STORE_CONNECT_PRIVATE_KEY \ --issuer-id=$APP_STORE_CONNECT_ISSUER_ID \ --key-id=$APP_STORE_CONNECT_KEY_IDENTIFIER \ --create xcode-project use-profiles elif [ -n "$CM_CERTIFICATE" ]; then echo "$CM_CERTIFICATE" | base64 --decode > /tmp/cert.p12 keychain add-certificates \ --certificate /tmp/cert.p12 \ --certificate-password "$CM_CERTIFICATE_PASSWORD" mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" echo "$CM_PROVISIONING_PROFILE" | base64 --decode > "$HOME/Library/MobileDevice/Provisioning Profiles/profile.mobileprovision" xcode-project use-profiles else echo "No signing credentials configured — build will fail at signing" exit 1 fi # --workspace: Capacitor SPM projects require workspace context for Xcode to resolve # the CapApp-SPM local package. Using --project causes xcodebuild to fail on # Xcode 26+ because SPM resolution requires workspace scope. App.xcworkspace is a # static file committed to the template (not generated by cap sync) that wraps # App.xcodeproj for this purpose. # --no-show-build-settings: xcode-project runs xcodebuild -showBuildSettings as a # pre-build diagnostic. Xcode 26 fails this step for SPM projects even with # --workspace, so we skip it. The actual archive step still surfaces real errors. # --disable-xcpretty: xcpretty reformats and filters xcodebuild output, swallowing # the actual error lines when a build fails. Raw output makes failures debuggable. - name: Build IPA script: | xcode-project build-ipa \ --workspace ios/App/App.xcworkspace \ --scheme App \ --no-show-build-settings \ --disable-xcpretty artifacts: - build/ios/ipa/*.ipa ios-release: name: iOS – Release IPA max_build_duration: 90 instance_type: mac_mini_m2 environment: node: 22 xcode: latest scripts: - name: Strip unused scaffold packages script: | for pkg in "@capacitor/google-maps" "@capacitor/push-notifications" "@capacitor-firebase/authentication"; do if ! grep -rq "$pkg" src/; then node -e "const fs=require('fs'),p=JSON.parse(fs.readFileSync('package.json','utf8'));delete p.dependencies['${pkg}'];fs.writeFileSync('package.json',JSON.stringify(p,null,2));" echo "Stripped $pkg (not used in src/)" else echo "Keeping $pkg (used in src/)" fi done - name: Install dependencies script: rm -f package-lock.json && npm install --legacy-peer-deps - name: Build web assets script: npm run build - name: Capacitor sync script: npx cap sync ios - name: Write Firebase config script: | if [ -n "$GOOGLE_SERVICE_INFO_PLIST" ]; then echo "$GOOGLE_SERVICE_INFO_PLIST" | base64 --decode > ios/App/App/GoogleService-Info.plist ruby -e " require 'xcodeproj' project = Xcodeproj::Project.open('ios/App/App.xcodeproj') target = project.targets.find { |t| t.name == 'App' } app_group = project.main_group['App'] unless app_group.files.find { |f| f.path == 'GoogleService-Info.plist' } file_ref = app_group.new_reference('GoogleService-Info.plist') target.resources_build_phase.add_file_reference(file_ref) project.save end " fi - name: Capacitor sync script: npx cap sync ios - name: Set up signing script: | keychain initialize if [ -n "$APP_STORE_CONNECT_PRIVATE_KEY" ]; then app-store-connect fetch-signing-files \ $(xcode-project detect-bundle-id) \ --type IOS_APP_STORE \ --certificate-key=@env:APP_STORE_CONNECT_PRIVATE_KEY \ --issuer-id=$APP_STORE_CONNECT_ISSUER_ID \ --key-id=$APP_STORE_CONNECT_KEY_IDENTIFIER \ --create xcode-project use-profiles else echo "$CM_CERTIFICATE" | base64 --decode > /tmp/cert.p12 keychain add-certificates \ --certificate /tmp/cert.p12 \ --certificate-password "$CM_CERTIFICATE_PASSWORD" mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" echo "$CM_PROVISIONING_PROFILE" | base64 --decode > "$HOME/Library/MobileDevice/Provisioning Profiles/profile.mobileprovision" xcode-project use-profiles fi # See ios-debug Build IPA comments for --workspace, --no-show-build-settings, # and --disable-xcpretty rationale — same reasons apply here. - name: Build IPA script: | xcode-project build-ipa \ --workspace ios/App/App.xcworkspace \ --scheme App \ --config Release \ --no-show-build-settings \ --disable-xcpretty - name: Publish script: | if [ "$SUBMIT_TO_TESTFLIGHT" = "true" ] || [ "$SUBMIT_TO_APP_STORE" = "true" ]; then app-store-connect publish \ --certificate-key=@env:APP_STORE_CONNECT_PRIVATE_KEY \ --issuer-id=$APP_STORE_CONNECT_ISSUER_ID \ --key-id=$APP_STORE_CONNECT_KEY_IDENTIFIER \ --submit-to-testflight=$SUBMIT_TO_TESTFLIGHT \ --submit-to-app-store=$SUBMIT_TO_APP_STORE fi artifacts: - build/ios/ipa/*.ipa