@@ -154,10 +154,150 @@ jobs:
154154 if-no-files-found : error
155155 retention-days : 7
156156
157+ android-build :
158+ name : Build signed Android packages
159+ runs-on : ubuntu-latest
160+ permissions :
161+ contents : read
162+ env :
163+ ANDROID_SIGNING_ENABLED : ${{ vars.ANDROID_RELEASE_SIGNING_ENABLED }}
164+ RELEASE_TAG : ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
165+
166+ steps :
167+ - name : Report disabled Android signing
168+ if : env.ANDROID_SIGNING_ENABLED != 'true'
169+ run : echo "Android release packaging is disabled; set ANDROID_RELEASE_SIGNING_ENABLED=true after configuring the four Android signing secrets."
170+
171+ - name : Checkout
172+ if : env.ANDROID_SIGNING_ENABLED == 'true'
173+ # actions/checkout v5
174+ uses : actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
175+ with :
176+ ref : refs/tags/${{ env.RELEASE_TAG }}
177+ persist-credentials : false
178+
179+ - name : Verify checkout matches release tag
180+ if : env.ANDROID_SIGNING_ENABLED == 'true'
181+ shell : bash
182+ run : |
183+ set -euo pipefail
184+ tag_commit="$(git rev-parse "${RELEASE_TAG}^{commit}")"
185+ head_commit="$(git rev-parse HEAD)"
186+ if [ "${head_commit}" != "${tag_commit}" ]; then
187+ echo "HEAD ${head_commit} does not match ${RELEASE_TAG} commit ${tag_commit}" >&2
188+ exit 1
189+ fi
190+
191+ - name : Validate Gradle wrapper
192+ if : env.ANDROID_SIGNING_ENABLED == 'true'
193+ # gradle/actions/wrapper-validation v4.4.4
194+ uses : gradle/actions/wrapper-validation@da187c8e6ffbd3802e00f2477aa5a822b25f2dda
195+
196+ - name : Install JDK 17
197+ if : env.ANDROID_SIGNING_ENABLED == 'true'
198+ # actions/setup-java v4
199+ uses : actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9
200+ with :
201+ distribution : temurin
202+ java-version : " 17"
203+
204+ - name : Install Android SDK tools
205+ if : env.ANDROID_SIGNING_ENABLED == 'true'
206+ # android-actions/setup-android v3
207+ uses : android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407
208+
209+ - name : Install Android platform and NDK
210+ if : env.ANDROID_SIGNING_ENABLED == 'true'
211+ run : sdkmanager "platforms;android-36" "build-tools;36.0.0" "ndk;28.2.13676358"
212+
213+ - name : Install Rust Android target
214+ if : env.ANDROID_SIGNING_ENABLED == 'true'
215+ # dtolnay/rust-toolchain stable
216+ uses : dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
217+ with :
218+ targets : aarch64-linux-android
219+
220+ - name : Install cargo-ndk
221+ if : env.ANDROID_SIGNING_ENABLED == 'true'
222+ # taiki-e/install-action v2
223+ uses : taiki-e/install-action@2ca9b94c269419b7b0c711c09d0b21c4e1d51145
224+ with :
225+ tool : cargo-ndk
226+
227+ - name : Restore Android signing key
228+ if : env.ANDROID_SIGNING_ENABLED == 'true'
229+ env :
230+ ANDROID_KEYSTORE_BASE64 : ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
231+ MDO_ANDROID_KEYSTORE_PASSWORD : ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
232+ MDO_ANDROID_KEY_ALIAS : ${{ secrets.ANDROID_KEY_ALIAS }}
233+ MDO_ANDROID_KEY_PASSWORD : ${{ secrets.ANDROID_KEY_PASSWORD }}
234+ run : |
235+ set -euo pipefail
236+ for name in \
237+ ANDROID_KEYSTORE_BASE64 \
238+ MDO_ANDROID_KEYSTORE_PASSWORD \
239+ MDO_ANDROID_KEY_ALIAS \
240+ MDO_ANDROID_KEY_PASSWORD; do
241+ if [[ -z "${!name}" ]]; then
242+ echo "${name} is not configured" >&2
243+ exit 1
244+ fi
245+ done
246+ printf '%s' "${ANDROID_KEYSTORE_BASE64}" | base64 --decode > "${RUNNER_TEMP}/mdo-upload.jks"
247+
248+ - name : Verify Android version matches release tag
249+ if : env.ANDROID_SIGNING_ENABLED == 'true'
250+ run : |
251+ set -euo pipefail
252+ tag_version="${RELEASE_TAG#v}"
253+ app_version="$(sed -nE 's/^[[:space:]]*versionName "([^"]+)"/\1/p' android/app/build.gradle)"
254+ if [[ "${app_version}" != "${tag_version}" ]]; then
255+ echo "Android versionName ${app_version} does not match release tag ${RELEASE_TAG}" >&2
256+ exit 1
257+ fi
258+
259+ - name : Build signed APK and app bundle
260+ if : env.ANDROID_SIGNING_ENABLED == 'true'
261+ working-directory : android
262+ env :
263+ MDO_ANDROID_KEYSTORE : ${{ runner.temp }}/mdo-upload.jks
264+ MDO_ANDROID_KEYSTORE_PASSWORD : ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
265+ MDO_ANDROID_KEY_ALIAS : ${{ secrets.ANDROID_KEY_ALIAS }}
266+ MDO_ANDROID_KEY_PASSWORD : ${{ secrets.ANDROID_KEY_PASSWORD }}
267+ run : ./gradlew assembleRelease bundleRelease --stacktrace
268+
269+ - name : Verify and stage Android packages
270+ if : env.ANDROID_SIGNING_ENABLED == 'true'
271+ run : |
272+ set -euo pipefail
273+ "${ANDROID_HOME}/build-tools/36.0.0/apksigner" verify --verbose --print-certs android/app/build/outputs/apk/release/app-release.apk
274+ # Android upload keys are self-signed, which jarsigner -strict
275+ # rejects. Plain -verify exits 0 even for an unsigned jar, so
276+ # assert the "jar verified" verdict explicitly.
277+ jarsigner -verify android/app/build/outputs/bundle/release/app-release.aab | tee /dev/stderr | grep -q '^jar verified'
278+ mkdir -p target/dist
279+ cp android/app/build/outputs/apk/release/app-release.apk target/dist/mdo-android-arm64.apk
280+ cp android/app/build/outputs/bundle/release/app-release.aab target/dist/mdo-android-arm64.aab
281+
282+ - name : Upload Android release artifacts
283+ if : env.ANDROID_SIGNING_ENABLED == 'true'
284+ # actions/upload-artifact v4
285+ uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
286+ with :
287+ name : mdo-android-arm64
288+ path : |
289+ target/dist/mdo-android-arm64.apk
290+ target/dist/mdo-android-arm64.aab
291+ if-no-files-found : error
292+ retention-days : 7
293+
157294 publish :
158295 name : Publish GitHub Release
159296 runs-on : ubuntu-latest
160- needs : build
297+ needs : [build, android-build]
298+ # An Android failure must not block the desktop archives that released
299+ # independently before android-build existed; it fails loudly on its own.
300+ if : always() && needs.build.result == 'success'
161301 permissions :
162302 contents : write
163303
0 commit comments