From 6cd56168f0ced896fe96c7e557a0deae4519844e Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 28 Apr 2026 03:21:20 -0500 Subject: [PATCH] chore(workflows): add android-apk-tag.yml for building APKs on tag releases and update build-release.yml to integrate Android APK builds --- .github/workflows/android-apk-tag.yml | 299 ++++++++++++++++++++++++++ .github/workflows/android-build.yml | 28 +-- .github/workflows/build-release.yml | 36 +++- 3 files changed, 335 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/android-apk-tag.yml diff --git a/.github/workflows/android-apk-tag.yml b/.github/workflows/android-apk-tag.yml new file mode 100644 index 0000000..f50e16f --- /dev/null +++ b/.github/workflows/android-apk-tag.yml @@ -0,0 +1,299 @@ +# Android APK build for Git tags only. Invoked from build-release.yml so APKs ship in the +# same workflow run and draft release as Linux/desktop/Flatpak (immutable per tag). +# +# Pinned first-party actions (keep in sync with android-build.yml): +# actions/checkout@v6.0.1 8e8c483db84b4bee98b60c0593521ed34d9990e8 +# actions/setup-python@v6.2.0 a309ff8b426b58ec0e2a45f0f869d46889d02405 +# actions/setup-java@v4.7.1 c5195efecf7bdfc987ee8bae7a71cb8b11521c00 +# actions/upload-artifact@v5.0.0 330a01c490aca151604b8cf639adc76d48f6c5d4 +# actions/download-artifact@v5.0.0 634f93cb2916e3fdff6788551b99b062d0335ce0 + +name: Android APK (tag) + +on: + workflow_call: + inputs: + frontend_artifact_name: + description: Name of the frontend artifact from frontend-build (Python 3.11). + required: true + type: string + run_unit_tests: + description: Run Gradle unit tests and lint. + required: false + type: boolean + default: true + +permissions: + contents: read + actions: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + NODE_OPTIONS: --max-old-space-size=8192 + NODE_VERSION: "24" + PNPM_VERSION: "10.33.0" + PYTHON_VERSION: "3.11" + JAVA_VERSION: "17" + CHAQUOPY_REF: "9f563f45108a873d7feb363e1f754c0173f1114e" + +jobs: + android: + name: Android APK (tag) + runs-on: ubuntu-latest + timeout-minutes: 90 + defaults: + run: + shell: bash + env: + FRONTEND_ARTIFACT_NAME: ${{ inputs.frontend_artifact_name }} + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + with: + fetch-depth: 0 + + - name: Resolve tag release track + id: track + run: | + set -euo pipefail + git fetch --no-tags origin dev master + sha="$(git rev-parse HEAD)" + on_m=false + on_d=false + while IFS= read -r line; do + ref="$(printf '%s' "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + case "$ref" in + origin/master) on_m=true ;; + origin/dev) on_d=true ;; + esac + done < <(git branch -r --contains "${sha}") + if [[ "${on_m}" == true ]]; then + track=master + elif [[ "${on_d}" == true ]]; then + track=dev + else + track=none + fi + echo "track=${track}" >> "${GITHUB_OUTPUT}" + echo "Resolved tag ${GITHUB_REF_NAME} -> track=${track}" + + - name: Detect Android release signing secrets + id: android_signing + env: + KS_B64: ${{ secrets.ANDROID_SIGNING_KEYSTORE_BASE64 }} + KS_PASS: ${{ secrets.ANDROID_SIGNING_KEYSTORE_PASSWORD }} + KS_ALIAS: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }} + run: | + set -euo pipefail + if [[ -n "${KS_B64}" && -n "${KS_PASS}" && -n "${KS_ALIAS}" ]]; then + echo "ready=true" >> "${GITHUB_OUTPUT}" + else + echo "ready=false" >> "${GITHUB_OUTPUT}" + fi + + - name: Require signing secrets for master release tags + if: ${{ steps.track.outputs.track == 'master' && steps.android_signing.outputs.ready != 'true' }} + run: | + echo "::error::Tagged master build needs release signing. Set secrets ANDROID_SIGNING_KEYSTORE_BASE64, ANDROID_SIGNING_KEYSTORE_PASSWORD, and ANDROID_SIGNING_KEY_ALIAS (see android-build.yml header)." + exit 1 + + - name: Set up Java + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + cache: gradle + cache-dependency-path: | + android/build.gradle + android/settings.gradle + android/app/build.gradle + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Download frontend artifact + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: ${{ env.FRONTEND_ARTIFACT_NAME }} + path: meshchatx/public + + - name: Verify frontend artifact contents + run: | + set -euo pipefail + test -f meshchatx/public/index.html + test -d meshchatx/public/assets + test -d meshchatx/public/reticulum-docs-bundled/current + + - name: Install Android wheel build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake pkg-config patchelf + + - name: Install Rust toolchain + run: | + curl -sSfL https://sh.rustup.rs -o rustup-init.sh + chmod +x rustup-init.sh + ./rustup-init.sh -y --profile minimal --default-toolchain stable + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Install Android NDK for Chaquopy native wheels + run: | + set -euo pipefail + SDK_ROOT="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-/usr/local/lib/android/sdk}}" + SDKMANAGER="${SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager" + if [[ ! -x "${SDKMANAGER}" ]]; then + echo "Expected preinstalled sdkmanager at ${SDKMANAGER}" >&2 + exit 1 + fi + NDK_VERSION="27.3.13750724" + yes_n() { for _ in $(seq 1 "$1"); do echo y; done; } + if [[ ! -d "${SDK_ROOT}/ndk/${NDK_VERSION}" ]]; then + yes_n 200 | "${SDKMANAGER}" --sdk_root="${SDK_ROOT}" --licenses >/dev/null + yes_n 50 | "${SDKMANAGER}" --sdk_root="${SDK_ROOT}" "ndk;${NDK_VERSION}" >/dev/null + fi + { + echo "ANDROID_HOME=${SDK_ROOT}" + echo "ANDROID_SDK_ROOT=${SDK_ROOT}" + } >> "${GITHUB_ENV}" + + - name: Build Android wheels + run: bash scripts/build-android-wheels-local.sh --python-minor "${PYTHON_VERSION}" --chaquopy-ref "${CHAQUOPY_REF}" --abis arm64-v8a,x86_64,armeabi-v7a + + - name: Build MeshChatX wheel for repository bundle + run: | + set -euo pipefail + python -m pip install -U pip build + python -m build --wheel -o dist . + + - name: Verify required Android wheels are present + run: | + set -euo pipefail + required=( + "miniaudio-1.70-*-cp311-cp311-android_24_arm64_v8a.whl" + "miniaudio-1.70-*-cp311-cp311-android_24_x86_64.whl" + "miniaudio-1.70-*-cp311-cp311-android_24_armeabi_v7a.whl" + "pycodec2-*-cp311-cp311-android_24_arm64_v8a.whl" + "pycodec2-*-cp311-cp311-android_24_x86_64.whl" + "pycodec2-*-cp311-cp311-android_24_armeabi_v7a.whl" + "lxst-*-py3-none-any.whl" + ) + missing=0 + for pattern in "${required[@]}"; do + if ! ls android/vendor/${pattern} >/dev/null 2>&1; then + echo "::error::Missing wheel matching android/vendor/${pattern}" + missing=1 + fi + done + if [[ "${missing}" -ne 0 ]]; then + echo "Built wheels:" + ls -la android/vendor/ || true + exit 1 + fi + echo "All required Android wheels present:" + ls -1 android/vendor/ + + - name: Run unit tests + if: ${{ inputs.run_unit_tests }} + working-directory: android + run: | + chmod +x gradlew + ./gradlew --no-daemon :app:testDebugUnitTest + + - name: Run Android Lint + if: ${{ inputs.run_unit_tests }} + working-directory: android + run: | + chmod +x gradlew + ./gradlew --no-daemon :app:lintDebug + + - name: Build debug APK + if: ${{ steps.track.outputs.track != 'master' }} + working-directory: android + run: | + chmod +x gradlew + ./gradlew --no-daemon :app:assembleDebug + + - name: Build release APK + if: ${{ steps.track.outputs.track == 'master' }} + working-directory: android + run: | + chmod +x gradlew + ./gradlew --no-daemon :app:assembleRelease + + - name: Sign release APKs + if: ${{ steps.track.outputs.track == 'master' && steps.android_signing.outputs.ready == 'true' }} + env: + KS_B64: ${{ secrets.ANDROID_SIGNING_KEYSTORE_BASE64 }} + SIGNING_KEYSTORE_PATH: ${{ runner.temp }}/meshchatx-release.jks + SIGNING_KEY_ALIAS: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }} + SIGNING_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_SIGNING_KEYSTORE_PASSWORD }} + SIGNING_KEY_PASSWORD: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }} + run: | + set -euo pipefail + printf '%s' "${KS_B64}" | base64 -d > "${SIGNING_KEYSTORE_PATH}" + export SIGNING_KEYSTORE_PATH SIGNING_KEY_ALIAS SIGNING_KEYSTORE_PASSWORD + export SIGNING_KEY_PASSWORD="${SIGNING_KEY_PASSWORD:-${SIGNING_KEYSTORE_PASSWORD}}" + bash scripts/sign-android-apks.sh + + - name: Upload wheels + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + with: + name: android-wheels-${{ github.ref_name }}-${{ github.run_id }} + path: android/vendor/*.whl + if-no-files-found: error + + - name: Upload debug APK + if: ${{ steps.track.outputs.track != 'master' }} + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + with: + name: meshchatx-android-debug-${{ github.ref_name }}-${{ github.run_id }} + path: android/app/build/outputs/apk/debug/*.apk + if-no-files-found: error + + - name: Upload test reports + if: ${{ inputs.run_unit_tests }} + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + with: + name: meshchatx-android-tests-${{ github.ref_name }}-${{ github.run_id }} + path: | + android/app/build/reports/tests/ + android/app/build/test-results/ + android/app/build/reports/lint-results-debug.html + android/app/build/reports/lint-results-debug.xml + if-no-files-found: warn + + - name: Upload release APK + if: ${{ steps.track.outputs.track == 'master' }} + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + with: + name: meshchatx-android-release-${{ github.ref_name }}-${{ github.run_id }} + path: android/app/build/outputs/apk/release/*.apk + if-no-files-found: error + + - name: Stage APKs for unified draft release + if: ${{ steps.track.outputs.track == 'dev' || steps.track.outputs.track == 'master' }} + run: | + set -euo pipefail + mkdir -p android-apks-for-draft + if [[ "${{ steps.track.outputs.track }}" == "dev" ]]; then + cp -v android/app/build/outputs/apk/debug/*.apk android-apks-for-draft/ + else + shopt -s nullglob + signed=(android/app/build/outputs/apk/release/*-signed.apk) + shopt -u nullglob + if [[ ${#signed[@]} -eq 0 ]]; then + echo "::error::Expected *-signed.apk under android/app/build/outputs/apk/release/" >&2 + exit 1 + fi + cp -v "${signed[@]}" android-apks-for-draft/ + fi + + - name: Upload Android APK bundle for draft + if: ${{ steps.track.outputs.track == 'dev' || steps.track.outputs.track == 'master' }} + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + with: + name: meshchatx-android-apks-${{ github.ref_name }}-${{ github.run_id }} + path: android-apks-for-draft/*.apk + if-no-files-found: error diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index ab47a6d..480394c 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -1,4 +1,5 @@ -# Android APK build (debug + optional release). +# Android APK build (debug + optional release) for branches, PRs, and manual runs. +# Tagged APKs attached to GitHub Releases are built in build-release.yml (android-apk-tag.yml). # # Pinned first-party actions (bump tag and SHA together when upgrading): # actions/checkout@v6.0.1 8e8c483db84b4bee98b60c0593521ed34d9990e8 @@ -16,8 +17,6 @@ on: push: branches: - dev - tags: - - "*" workflow_dispatch: inputs: build_release: @@ -300,26 +299,3 @@ jobs: name: meshchatx-android-release-${{ github.ref_name }}-${{ github.run_id }} path: android/app/build/outputs/apk/release/*.apk if-no-files-found: error - - - name: Upload APKs to GitHub release - if: ${{ github.ref_type == 'tag' && (steps.track.outputs.track == 'dev' || steps.track.outputs.track == 'master') }} - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - dir="${RUNNER_TEMP}/android-gh-release" - rm -rf "${dir}" - mkdir -p "${dir}" - if [[ "${{ steps.track.outputs.track }}" == "dev" ]]; then - cp -v android/app/build/outputs/apk/debug/*.apk "${dir}/" - else - shopt -s nullglob - signed=(android/app/build/outputs/apk/release/*-signed.apk) - shopt -u nullglob - if [[ ${#signed[@]} -eq 0 ]]; then - echo "::error::Expected *-signed.apk under android/app/build/outputs/apk/release/" >&2 - exit 1 - fi - cp -v "${signed[@]}" "${dir}/" - fi - bash scripts/ci/github-draft-release-upload-assets.sh "${dir}" diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index e2dffbb..a0ad20c 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -1,6 +1,7 @@ # Single tagged-release pipeline: Linux release assets, Windows + macOS Electron -# builds, Flatpak, SLSA provenance (generic generator), optional cosign bundles, and one -# draft GitHub release. One workflow run per tag keeps the release graph immutable. +# builds, Flatpak, Android APKs (dev/master track tags via android-apk-tag.yml), SLSA +# provenance (generic generator), optional cosign bundles, and one draft GitHub release. +# One workflow run per tag keeps the release graph immutable. # # Pinned first-party actions (bump tag and SHA together when upgrading): # actions/checkout@v6.0.1 8e8c483db84b4bee98b60c0593521ed34d9990e8 @@ -9,6 +10,7 @@ # actions/upload-artifact@v5.0.0 330a01c490aca151604b8cf639adc76d48f6c5d4 # actions/download-artifact@v5.0.0 634f93cb2916e3fdff6788551b99b062d0335ce0 # actions/cache@v4.2.0 1bd1e32a3bdc45362d1e726936510720a7c30a57 +# actions/setup-java@v4.7.1 c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # # SLSA generator (must stay @vX.Y.Z semver per upstream): # slsa-framework/slsa-github-generator/generator_generic_slsa3.yml@v2.1.0 @@ -52,6 +54,28 @@ jobs: retention_days: 7 pnpm_version: "10.33.0" + frontend-android: + name: Build frontend (Android / Python 3.11) + if: startsWith(github.ref, 'refs/tags/') + uses: ./.github/workflows/frontend-build.yml + permissions: + contents: read + with: + artifact_name: meshchatx-frontend-android-rel-${{ github.run_id }}-${{ github.run_attempt }} + retention_days: 7 + python_version: "3.11" + pnpm_version: "10.33.0" + + android-release: + name: Android APKs (tag) + if: startsWith(github.ref, 'refs/tags/') + needs: [frontend-android] + secrets: inherit + uses: ./.github/workflows/android-apk-tag.yml + with: + frontend_artifact_name: ${{ needs.frontend-android.outputs.artifact_name }} + run_unit_tests: true + linux-release: name: Linux release assets needs: frontend @@ -434,6 +458,7 @@ jobs: - build-release - slsa-provenance-desktop - flatpak + - android-release if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest timeout-minutes: 45 @@ -468,6 +493,13 @@ jobs: name: meshchatx-linux-flatpak-${{ github.ref_name }}-${{ github.run_id }} path: upload/flatpak + - name: Download Android APK bundle + continue-on-error: true + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: meshchatx-android-apks-${{ github.ref_name }}-${{ github.run_id }} + path: upload/android + - name: Download SLSA provenance (Linux) uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 with: