name: Build Mobile on: workflow_dispatch: workflow_call: inputs: ref: required: false type: string secrets: KEY_JKS: required: true ALIAS: required: true ANDROID_KEY_PASSWORD: required: true ANDROID_STORE_PASSWORD: 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 }} if: ${{ false }} 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 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@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 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 if: ${{ !github.event.pull_request.head.repo.fork && fromJSON(needs.pre-job.outputs.should_run).mobile == true && github.ref == 'refs/heads/main' }} runs-on: macos-latest steps: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false - name: Setup Flutter SDK uses: subosito/flutter-action@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.2' working-directory: ./mobile/ios - name: Install Fastlane run: | cd mobile/ios gem install bundler bundle config set --local path 'vendor/bundle' bundle install - name: Create API Key JSON 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 cat > api_key.json << EOF { "key_id": "${API_KEY_ID}", "issuer_id": "${API_KEY_ISSUER_ID}", "key": "$(cat ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8)", "duration": 1200, "in_house": false } EOF - name: Import Certificate and Provisioning Profile env: IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }} IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} IOS_PROVISIONING_PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }} working-directory: ./mobile/ios run: | echo "$IOS_CERTIFICATE_P12" | base64 --decode > certificate.p12 echo "$IOS_PROVISIONING_PROFILE" | base64 --decode > profile.mobileprovision - name: Create keychain env: KEYCHAIN_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} run: | 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 - 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 }} working-directory: ./mobile/ios run: bundle exec fastlane release_ci - name: Clean up keychain if: always() run: | security delete-keychain build.keychain || true - name: Upload IPA artifact uses: actions/upload-artifact@v4 with: name: ios-release-ipa path: mobile/ios/Runner.ipa