From 8b3c9bf9c306a400c565f0884078495b8da740fa Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 9 May 2026 07:46:40 -0500 Subject: [PATCH] feat(ci): publish PR Android APK to comment (#28283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ci): publish PR Android APK to R2 with installable links Adds a universal debug APK to PR builds and uploads it to a public R2 bucket alongside the existing GitHub Actions artifact. Posts a sticky PR comment with tap-to-install links and a QR code so testers can install directly on their device without unzipping artifacts. Required setup: - Secrets: R2_APK_ACCESS_KEY_ID, R2_APK_SECRET_ACCESS_KEY, R2_APK_ACCOUNT_ID, R2_APK_BUCKET - Optional repo variable: APK_PUBLIC_HOST (defaults to apk.immich.app) - R2 bucket configured with a public custom domain matching APK_PUBLIC_HOST * chore(ci): drop R2 upload, link directly to GitHub artifact Surfaces the existing release-apk-signed artifact in a sticky PR comment with a QR code. Avoids new infra and secrets — the trade-off is GitHub login and a zip wrapper instead of tap-to-install. * feat(ci): build PR APK as release and publish to GitHub Release PR builds now produce a release APK signed with the release keystore. The universal APK is published as a GitHub Release asset under tag 'pr-' (prerelease), giving testers a direct, unzipped, tap-to- install URL plus a QR code in the PR comment. The release-apk-signed artifact is unchanged. * chore(ci): drop GitHub Release, publish universal APK as own artifact Reverts the prerelease publish. Uploads the universal release APK as a separate single-file artifact so its download URL gives a zip containing only that APK — no extra files to dig through. The QR in the PR comment points at this universal-only artifact. * chore(ci): build only universal APK for PR, drop split artifact PR builds skip the arm64-only split — release-apk-signed now contains just the universal app-release.apk, so the download zip is a single file. Removes the redundant separate universal artifact and points the PR comment QR at the main artifact URL. * feat(mobile): suffix PR APK applicationId so it installs alongside production Each PR build now becomes app.alextran.immich.pr via PR_NUMBER env read in build.gradle, so testers can install multiple PR builds and the Play Store version on the same device without uninstalling. Also tags the version with -pr for visibility. * feat(ci): allow PR APK build to run on forks Forks can now run the Android build job. Steps that need repo secrets (create-workflow-token, Create Keystore) are skipped when the PR is from a fork, the checkout falls back to GITHUB_TOKEN, and build.gradle falls back to debug signing if the release keystore isn't materialised. The PR comment still requires write access, so it's gated to non-fork PRs — fork APKs are reachable from the workflow run's artifact tab. --- .github/workflows/build-mobile.yml | 33 +++++++++++++++++++++++++----- mobile/android/app/build.gradle | 9 +++++++- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 0dfe67401f..a5d1fd358e 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -51,6 +51,7 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token + if: ${{ !github.event.pull_request.head.repo.fork }} 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 }} @@ -60,7 +61,7 @@ jobs: id: check uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 with: - github-token: ${{ steps.token.outputs.token }} + github-token: ${{ steps.token.outputs.token || github.token }} filters: | mobile: - 'mobile/**' @@ -73,12 +74,13 @@ jobs: 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 }} + pull-requests: write + if: ${{ github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }} runs-on: mich steps: - id: token + if: ${{ !github.event.pull_request.head.repo.fork }} 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 }} @@ -88,9 +90,10 @@ jobs: with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false - token: ${{ steps.token.outputs.token }} + token: ${{ steps.token.outputs.token || github.token }} - name: Create the Keystore + if: ${{ !github.event.pull_request.head.repo.fork }} env: KEY_JKS: ${{ secrets.KEY_JKS }} working-directory: ./mobile @@ -144,20 +147,40 @@ jobs: 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 --debug --split-per-abi --target-platform android-arm64 + 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 + + GitHub login required. Downloads as a zip containing a single `app-release.apk` — extract and install. 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 diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index e879b54ae5..7e3d67fa81 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -64,8 +64,15 @@ android { } release { - signingConfig signingConfigs.release + def hasKeystore = file("../key.jks").exists() && file("../key.jks").length() > 0 + signingConfig hasKeystore ? signingConfigs.release : signingConfigs.debug proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + + def prNumber = System.getenv("PR_NUMBER") + if (prNumber) { + applicationIdSuffix ".pr${prNumber}" + versionNameSuffix "-pr${prNumber}" + } } } namespace 'app.alextran.immich'