# 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 # 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 build on: pull_request: branches: - dev push: branches: - dev workflow_dispatch: inputs: build_release: description: Build signed/optimized release APK artifacts required: true type: boolean default: true run_tests: description: Run Android unit tests required: true type: boolean default: true permissions: contents: read actions: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true NODE_OPTIONS: --max-old-space-size=8192 NODE_VERSION: "24" PNPM_VERSION: "10.32.1" PYTHON_VERSION: "3.11" JAVA_VERSION: "17" CHAQUOPY_REF: "9f563f45108a873d7feb363e1f754c0173f1114e" jobs: frontend: name: Build frontend artifact uses: ./.github/workflows/frontend-build.yml permissions: contents: read with: artifact_name: meshchatx-frontend-android-${{ github.run_id }}-${{ github.run_attempt }} retention_days: 1 # Android wheels run on Python 3.11; using the same here avoids # spurious version drift in the docs-bundling step. python_version: "3.11" android: name: Android test/build runs-on: ubuntu-latest needs: frontend timeout-minutes: 90 permissions: contents: write actions: write defaults: run: shell: bash env: FRONTEND_ARTIFACT_NAME: ${{ needs.frontend.outputs.artifact_name }} steps: - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 with: fetch-depth: 0 - name: Resolve tag release track id: track if: github.ref_type == 'tag' 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: ${{ github.ref_type == 'tag' && 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 workflow 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: ${{ github.event_name != 'workflow_dispatch' || inputs.run_tests }} working-directory: android run: | chmod +x gradlew ./gradlew --no-daemon :app:testDebugUnitTest - name: Run Android Lint if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_tests }} working-directory: android run: | chmod +x gradlew ./gradlew --no-daemon :app:lintDebug - name: Build debug APK if: ${{ github.ref_type != 'tag' || steps.track.outputs.track != 'master' }} working-directory: android run: | chmod +x gradlew ./gradlew --no-daemon :app:assembleDebug - name: Build release APK if: ${{ (github.ref_type == 'tag' && steps.track.outputs.track == 'master') || (github.ref_type != 'tag' && (github.event_name != 'workflow_dispatch' || inputs.build_release)) }} working-directory: android run: | chmod +x gradlew ./gradlew --no-daemon :app:assembleRelease - name: Sign release APKs if: ${{ github.ref_type == 'tag' && 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: ${{ github.ref_type != 'tag' || 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: ${{ github.event_name != 'workflow_dispatch' || inputs.run_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: ${{ (github.ref_type == 'tag' && steps.track.outputs.track == 'master') || (github.ref_type != 'tag' && (github.event_name != 'workflow_dispatch' || inputs.build_release)) }} 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