From a71801333ab7541d0c746cb47f3fd787425a9157 Mon Sep 17 00:00:00 2001 From: "Phat H. Nguyen" Date: Sun, 9 Aug 2026 23:06:15 +0200 Subject: [PATCH 1/3] ci: build signed APKs for release-labeled PRs --- .github/workflows/build.yml | 85 ++++++++++++++++++++++- gh-pages/docs/developer/1-how-to-build.md | 6 ++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e7a3c3d..98ba407 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,6 +2,16 @@ name: Build APK & AAB on: workflow_dispatch: + pull_request: + types: + - opened + - reopened + - synchronize + - labeled + - unlabeled + - ready_for_review + branches: + - main push: tags: - "v*.*.*" @@ -9,6 +19,10 @@ on: permissions: contents: write +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: CARGO_TERM_COLOR: always ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} @@ -16,7 +30,48 @@ env: ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} jobs: + classify-pr-apk: + name: Classify PR APK build + runs-on: ubuntu-latest + outputs: + should_build: ${{ steps.classify.outputs.should_build }} + + steps: + - name: Classify PR + id: classify + uses: actions/github-script@v7 + with: + script: | + if (context.eventName !== "pull_request") { + core.setOutput("should_build", "true"); + return; + } + + const pr = context.payload.pull_request; + const appReleaseLabels = new Set([ + "release:breaking", + "release:feature", + "release:bugfix", + "release:patch", + "release:improvement", + "release:optimisation", + ]); + const labels = pr.labels.map((label) => label.name); + const selected = labels.filter((label) => appReleaseLabels.has(label)); + const sameRepo = pr.head.repo.full_name === `${context.repo.owner}/${context.repo.repo}`; + const shouldBuild = + sameRepo && + selected.length === 1 && + !labels.includes("release:docs"); + + core.info(`same-repo PR: ${sameRepo}`); + core.info(`app release labels: ${selected.join(", ") || "none"}`); + core.info(`build signed APK: ${shouldBuild}`); + core.setOutput("should_build", shouldBuild ? "true" : "false"); + build: + needs: classify-pr-apk + if: github.event_name != 'pull_request' || needs.classify-pr-apk.outputs.should_build == 'true' runs-on: ubuntu-latest steps: @@ -32,6 +87,7 @@ jobs: run: echo "version=$(cargo metadata --format-version=1 --no-deps | jq -r '.packages[0].version')" >> "$GITHUB_OUTPUT" - name: Extract release notes + if: github.ref_type == 'tag' id: release_notes uses: actions/github-script@v7 with: @@ -67,7 +123,7 @@ jobs: core.setOutput("path", notesPath); - name: Fallback release notes to commit message - if: steps.release_notes.outputs.found != 'true' + if: github.ref_type == 'tag' && steps.release_notes.outputs.found != 'true' run: | git log -1 --pretty=%B > "${{ steps.release_notes.outputs.path }}" @@ -123,10 +179,37 @@ jobs: run: | mv target/x/release/android/gradle/app/build/outputs/apk/debug/app-debug.apk target/x/localdesktop-${{ steps.extract_version.outputs.version }}-debug.apk mv target/x/release/android/gradle/app/build/outputs/apk/release/app-release.apk target/x/localdesktop-${{ steps.extract_version.outputs.version }}.apk + + - name: Compute PR APK artifact name + id: pr_apk_artifact + if: github.event_name == 'pull_request' + env: + HEAD_REF: ${{ github.head_ref }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + VERSION: ${{ steps.extract_version.outputs.version }} + run: | + SAFE_REF=$(printf '%s' "$HEAD_REF" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//; s/-+/-/g' | cut -c1-60) + if [ -z "$SAFE_REF" ]; then + SAFE_REF="pr-${PR_NUMBER}" + fi + SHORT_SHA="${HEAD_SHA:0:7}" + echo "name=localdesktop-${VERSION}-${SAFE_REF}-${SHORT_SHA}-release-apk" >> "$GITHUB_OUTPUT" + + - name: Upload PR release APK + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.pr_apk_artifact.outputs.name }} + path: target/x/localdesktop-${{ steps.extract_version.outputs.version }}.apk + if-no-files-found: error + - name: Build AAB + if: github.event_name != 'pull_request' run: x build --release --platform=android --arch=arm64 --format=aab - name: Rename AAB + if: github.event_name != 'pull_request' run: | mv target/x/release/android/gradle/app/build/outputs/bundle/debug/app-debug.aab target/x/localdesktop-${{ steps.extract_version.outputs.version }}-debug.aab mv target/x/release/android/gradle/app/build/outputs/bundle/release/app-release.aab target/x/localdesktop-${{ steps.extract_version.outputs.version }}.aab diff --git a/gh-pages/docs/developer/1-how-to-build.md b/gh-pages/docs/developer/1-how-to-build.md index ccf35a2..46d2a54 100644 --- a/gh-pages/docs/developer/1-how-to-build.md +++ b/gh-pages/docs/developer/1-how-to-build.md @@ -33,6 +33,12 @@ Then you will find the APK file in `target/x/release/android/localdesktop.apk`. ## FAQ +### Can I test a release-signed APK from a PR? + +Yes. Same-repository PRs targeting `main` produce a signed release APK artifact when they have exactly one app-release label: `release:breaking`, `release:feature`, `release:bugfix`, `release:patch`, `release:improvement`, or `release:optimisation`. Download the workflow artifact from GitHub Actions and install it over the GitHub APK. + +PRs labeled `release:docs` do not build an APK. + ### Can I build on Termux? Yes. From ce3f39dbbc62d65b36022a5b81dbdafc3f0a3b03 Mon Sep 17 00:00:00 2001 From: "Phat H. Nguyen" Date: Sun, 9 Aug 2026 23:38:40 +0200 Subject: [PATCH 2/3] ci: refresh release docs PR checks From ef1ccd2176d127ce8cdc7714ae126c7778af94bd Mon Sep 17 00:00:00 2001 From: "Phat H. Nguyen" Date: Sun, 9 Aug 2026 23:41:27 +0200 Subject: [PATCH 3/3] ci: emit required release label check --- .github/workflows/build.yml | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 98ba407..5fe9232 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,8 +30,8 @@ env: ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} jobs: - classify-pr-apk: - name: Classify PR APK build + validate-release-label: + name: Validate Release Label runs-on: ubuntu-latest outputs: should_build: ${{ steps.classify.outputs.should_build }} @@ -56,22 +56,31 @@ jobs: "release:improvement", "release:optimisation", ]); + const releaseLabels = new Set([...appReleaseLabels, "release:docs"]); const labels = pr.labels.map((label) => label.name); - const selected = labels.filter((label) => appReleaseLabels.has(label)); + const selected = labels.filter((label) => releaseLabels.has(label)); const sameRepo = pr.head.repo.full_name === `${context.repo.owner}/${context.repo.repo}`; + if (selected.length !== 1) { + core.setFailed( + `PRs to main must have exactly one release label. Found: ${ + selected.join(", ") || "none" + }` + ); + return; + } + const shouldBuild = sameRepo && - selected.length === 1 && - !labels.includes("release:docs"); + appReleaseLabels.has(selected[0]); core.info(`same-repo PR: ${sameRepo}`); - core.info(`app release labels: ${selected.join(", ") || "none"}`); + core.info(`release label: ${selected[0]}`); core.info(`build signed APK: ${shouldBuild}`); core.setOutput("should_build", shouldBuild ? "true" : "false"); build: - needs: classify-pr-apk - if: github.event_name != 'pull_request' || needs.classify-pr-apk.outputs.should_build == 'true' + needs: validate-release-label + if: github.event_name != 'pull_request' || needs.validate-release-label.outputs.should_build == 'true' runs-on: ubuntu-latest steps: