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 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@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 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 pull-requests: write if: ${{ 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@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref }} persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup Mise uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1 with: github_token: ${{ steps.token.outputs.token }} - name: Create the Keystore if: ${{ !github.event.pull_request.head.repo.fork }} env: KEY_JKS: ${{ secrets.KEY_JKS }} working-directory: ./mobile run: printf "%s" $KEY_JKS | base64 -d > android/key.jks - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: distribution: 'zulu' java-version: '17' - name: Restore Gradle Cache id: cache-gradle-restore uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | ~/.gradle/caches ~/.gradle/wrapper ~/.android/sdk mobile/android/.gradle key: build-mobile-gradle-${{ runner.os }}-main - name: Setup Android SDK uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 with: packages: '' - name: Get Packages working-directory: ./mobile run: flutter pub get - name: Generate translation file run: mise //mobile:codegen:translation - name: Generate platform APIs run: mise //mobile:codegen: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' }} PR_NUMBER: ${{ github.event.pull_request.number }} 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 --release fi - name: Publish Android Artifact id: upload-apk uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-apk-signed path: mobile/build/app/outputs/flutter-apk/*.apk - name: Comment APK download link on PR if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0 env: HEAD_SHA: ${{ github.event.pull_request.head.sha }} APK_URL: ${{ steps.upload-apk.outputs.artifact-url }} with: github-token: ${{ steps.token.outputs.token }} message-id: 'mobile-android-apk' message: | 📱 **Android release APK (universal)** — `${{ env.HEAD_SHA }}` Download: ${{ env.APK_URL }}
QR code QR code
Installs as a separate app (applicationId `app.alextran.immich.pr${{ github.event.pull_request.number }}`), so it coexists with the Play Store version and any other PR builds. - name: Save Gradle Cache id: cache-gradle-save uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 if: github.ref == 'refs/heads/main' with: path: | ~/.gradle/caches ~/.gradle/wrapper ~/.android/sdk mobile/android/.gradle 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-15 steps: - id: token uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 with: client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Select Xcode 26 run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false - name: Setup Mise uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1 with: github_token: ${{ steps.token.outputs.token }} - name: Install Flutter dependencies working-directory: ./mobile run: flutter pub get - name: Generate translation files run: mise //mobile:codegen:translation - name: Generate platform APIs run: mise //mobile:codegen:pigeon - name: Setup Ruby uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0 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 env: IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }} working-directory: ./mobile/ios run: | # Decode certificate echo "$IOS_CERTIFICATE_P12" | base64 --decode > certificate.p12 - 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 }} FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120 FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ios-release-ipa path: mobile/ios/Runner.ipa