name: Build Mobile on: workflow_call: inputs: ref: required: false type: string environment: description: 'Target environment' required: true default: 'development' type: string secrets: KEY_JKS: required: true ALIAS: required: true ANDROID_KEY_PASSWORD: required: true ANDROID_STORE_PASSWORD: required: true APP_STORE_CONNECT_API_KEY_ID: required: true APP_STORE_CONNECT_API_KEY_ISSUER_ID: required: true APP_STORE_CONNECT_API_KEY: required: true IOS_CERTIFICATE_P12: required: true IOS_CERTIFICATE_PASSWORD: required: true IOS_PROVISIONING_PROFILE: required: true IOS_PROVISIONING_PROFILE_SHARE_EXTENSION: required: true IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION: required: true IOS_DEVELOPMENT_PROVISIONING_PROFILE: required: true IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION: required: true IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION: required: true FASTLANE_TEAM_ID: required: true pull_request: push: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: {} jobs: pre-job: runs-on: ubuntu-latest permissions: contents: read outputs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check uses: immich-app/devtools/actions/pre-job@08bac802a312fc89808e0dd589271ca0974087b5 # pre-job-action-v2.0.0 with: github-token: ${{ steps.token.outputs.token }} filters: | mobile: - 'mobile/**' force-filters: | - '.github/workflows/build-mobile.yml' force-events: 'workflow_call,workflow_dispatch' build-sign-android: name: Build and sign Android needs: pre-job permissions: contents: read # Skip when PR from a fork if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }} runs-on: mich steps: - id: token uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Create the Keystore env: KEY_JKS: ${{ secrets.KEY_JKS }} working-directory: ./mobile run: printf "%s" $KEY_JKS | base64 -d > android/key.jks - uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0 with: distribution: 'zulu' java-version: '17' - name: Restore Gradle Cache id: cache-gradle-restore uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper ~/.android/sdk mobile/android/.gradle mobile/.dart_tool key: build-mobile-gradle-${{ runner.os }}-main - name: Setup Flutter SDK uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0 with: channel: 'stable' flutter-version-file: ./mobile/pubspec.yaml cache: true - name: Setup Android SDK uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 with: packages: '' - name: Get Packages working-directory: ./mobile run: flutter pub get - name: Generate translation file run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart working-directory: ./mobile - name: Generate platform APIs run: make pigeon working-directory: ./mobile - name: Build Android App Bundle working-directory: ./mobile env: ALIAS: ${{ secrets.ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} IS_MAIN: ${{ github.ref == 'refs/heads/main' }} run: | if [[ $IS_MAIN == 'true' ]]; then flutter build apk --release flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64 else flutter build apk --debug --split-per-abi --target-platform android-arm64 fi - name: Publish Android Artifact uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: release-apk-signed path: mobile/build/app/outputs/flutter-apk/*.apk - name: Save Gradle Cache id: cache-gradle-save uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 if: github.ref == 'refs/heads/main' with: path: | ~/.gradle/caches ~/.gradle/wrapper ~/.android/sdk mobile/android/.gradle mobile/.dart_tool key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }} build-sign-ios: name: Build and sign iOS needs: pre-job permissions: contents: read # Run on main branch or workflow_dispatch, or on PRs/other branches (build only, no upload) if: ${{ !github.event.pull_request.head.repo.fork && fromJSON(needs.pre-job.outputs.should_run).mobile == true }} runs-on: macos-latest steps: - name: Checkout code uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false - name: Setup Flutter SDK uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2 with: channel: 'stable' flutter-version-file: ./mobile/pubspec.yaml cache: true - name: Install Flutter dependencies working-directory: ./mobile run: flutter pub get - name: Generate translation files run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart working-directory: ./mobile - name: Generate platform APIs run: make pigeon working-directory: ./mobile - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: '3.3' bundler-cache: true working-directory: ./mobile/ios - name: Install CocoaPods dependencies working-directory: ./mobile/ios run: | pod install - name: Create API Key env: API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY }} working-directory: ./mobile/ios run: | mkdir -p ~/.appstoreconnect/private_keys echo "$API_KEY_CONTENT" | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8 - name: Import Certificate and Provisioning Profiles env: IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }} IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} IOS_PROVISIONING_PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }} IOS_PROVISIONING_PROFILE_SHARE_EXTENSION: ${{ secrets.IOS_PROVISIONING_PROFILE_SHARE_EXTENSION }} IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION: ${{ secrets.IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION }} IOS_DEVELOPMENT_PROVISIONING_PROFILE: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE }} IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION }} IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION }} ENVIRONMENT: ${{ inputs.environment || 'development' }} working-directory: ./mobile/ios run: | # Decode certificate echo "$IOS_CERTIFICATE_P12" | base64 --decode > certificate.p12 # Decode provisioning profiles based on environment if [[ "$ENVIRONMENT" == "development" ]]; then echo "$IOS_DEVELOPMENT_PROVISIONING_PROFILE" | base64 --decode > profile_dev.mobileprovision echo "$IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION" | base64 --decode > profile_dev_share.mobileprovision echo "$IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION" | base64 --decode > profile_dev_widget.mobileprovision ls -lh profile_dev*.mobileprovision else echo "$IOS_PROVISIONING_PROFILE" | base64 --decode > profile.mobileprovision echo "$IOS_PROVISIONING_PROFILE_SHARE_EXTENSION" | base64 --decode > profile_share.mobileprovision echo "$IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION" | base64 --decode > profile_widget.mobileprovision ls -lh profile*.mobileprovision fi - name: Create keychain and import certificate env: KEYCHAIN_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} working-directory: ./mobile/ios run: | # Create keychain security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security set-keychain-settings -t 3600 -u build.keychain # Import certificate security import certificate.p12 -k build.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain # Verify certificate was imported security find-identity -v -p codesigning build.keychain - name: Build and deploy to TestFlight env: FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }} IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} KEYCHAIN_NAME: build.keychain KEYCHAIN_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} ENVIRONMENT: ${{ inputs.environment || 'development' }} BUNDLE_ID_SUFFIX: ${{ inputs.environment == 'production' && '' || 'development' }} GITHUB_REF: ${{ github.ref }} working-directory: ./mobile/ios run: | # Only upload to TestFlight on main branch if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then if [[ "$ENVIRONMENT" == "development" ]]; then bundle exec fastlane gha_testflight_dev else bundle exec fastlane gha_release_prod fi else # Build only, no TestFlight upload for non-main branches bundle exec fastlane gha_build_only fi - name: Clean up keychain if: always() run: | security delete-keychain build.keychain || true - name: Upload IPA artifact uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: ios-release-ipa path: mobile/ios/Runner.ipa