From 083f8b7f2adfde011dd880a39929aae09ed05695 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 18 May 2026 20:48:44 -0400 Subject: [PATCH] CI: add Android build workflow (signed APK + AAB) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the Linux workflow's approach — uses the Cake-Labs prebuilt Docker image (Flutter 3.32.0 + NDK r28 + Go + Android SDK + JDK17 + all native build deps) and the prebuilt monero_c .so bundle, so we skip the multi-hour native cross-compile. Stages monero + wownero .so files into android/app/src/main/jniLibs/ for all 4 Android ABIs (arm64-v8a, armeabi-v7a, x86, x86_64). Zano libs in the bundle are ignored. Decodes the upload keystore from secret ANDROID_KEYSTORE_BASE64, writes android/key.properties (which build.gradle's signingConfigs already reads), builds a universal release APK for sideload + an AAB for Play Console. Both signed with the upload key; Play App Signing re-signs the AAB server-side with the production key on upload. Required Gitea Actions secrets: ANDROID_KEYSTORE_BASE64 ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD TROCADOR_API_KEY (already set, reused from Linux) TROCADOR_MONERO_API_KEY (same) TROCADOR_EXCHANGE_MARKUP (same) mwebd (Litecoin MWEB privacy) is NOT built. LTC works without it, just no MWEB feature. Add the build step + go build pipeline if we want MWEB on Android — separate effort. Artifacts: - hash-wallet-android-apk- (single universal APK, 14-day retention) - hash-wallet-android-aab- (Play Console upload, 30-day retention) --- .github/workflows/build-android.yml | 228 ++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 .github/workflows/build-android.yml diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml new file mode 100644 index 000000000..ffbca15c9 --- /dev/null +++ b/.github/workflows/build-android.yml @@ -0,0 +1,228 @@ +name: Hash Wallet Android build + +# Triggers: +# - push to dev/main (validate every commit) +# - PRs targeting dev/main (gate merges) +# - manual via workflow_dispatch +on: + push: + branches: [dev, main] + pull_request: + branches: [dev, main] + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + build: + runs-on: ubuntu-latest + container: + # Same Cake-Labs prebuilt image used for Linux. Bundles Flutter 3.32.0, + # Android NDK r28, Go 1.24.1, Android SDK, JDK 17, gradle, and the + # native build deps (boost, openssl, libsodium, libzmq, unbound, icu). + image: ghcr.io/cake-tech/cake_wallet:debian13-flutter3.32.0-ndkr28-go1.24.1-ruststablenightly + env: + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + APP_ANDROID_TYPE: cakewallet + + steps: + - name: Fix Actions HOME (writeable for ~/.cache + gradle) + run: echo "HOME=/root" >> $GITHUB_ENV + + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Configure git inside the container + run: | + git config --global --add safe.directory '*' + git config --global user.email "ci@suchsoftware.com" + git config --global user.name "Hash Wallet CI" + + # ---- External prebuilt deps (same approach as Linux workflow) -------- + - name: Fetch prebuilt torch_dart + run: | + set -x -e + pushd scripts + rm -rf torch_dart torch_dart.tar.gz + wget -q https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -O torch_dart.tar.gz + mkdir torch_dart + tar -xzf torch_dart.tar.gz -C torch_dart + rm torch_dart.tar.gz + popd + + - name: Fetch prebuilt reown_flutter + run: | + set -x -e + pushd scripts + rm -rf reown_flutter reown_flutter.tar.gz + wget -q https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz + mkdir reown_flutter + tar -xzf reown_flutter.tar.gz -C reown_flutter + rm reown_flutter.tar.gz + popd + + - name: Clone BitBox Flutter + run: | + set -x -e + pushd scripts + ./build_bitbox_flutter.sh + popd + + # ---- Native crypto cores (monero_c prebuilt bundle) ------------------ + # Same release-bundle.zip as Linux. Contains pre-cross-compiled .so + # files for monero/wownero/zano across all 4 Android ABIs. We only ship + # monero + wownero — zano libs in the bundle are ignored. + - name: Fetch prebuilt monero_c .so bundle + run: | + set -x -e + ./scripts/prepare_moneroc.sh + MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags) + echo "monero_c TAG: $MONERO_C_TAG" + mkdir -p "scripts/monero_c/release/$MONERO_C_TAG" + pushd "scripts/monero_c/release/$MONERO_C_TAG" + wget -q https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC1/release-bundle.zip + unzip -q release-bundle.zip + rm release-bundle.zip + ls + popd + + # ---- Stage native libs into android/app/src/main/jniLibs// ----- + # Flutter Android packaging auto-includes everything under jniLibs. + - name: Place Android .so files into jniLibs + run: | + set -x -e + MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags) + BUNDLE_DIR="scripts/monero_c/release/$MONERO_C_TAG" + + # monero_c target dir → Android ABI dir + declare -A ABI_MAP=( + [aarch64-linux-android]=arm64-v8a + [armv7a-linux-androideabi]=armeabi-v7a + [i686-linux-android]=x86 + [x86_64-linux-android]=x86_64 + ) + + for target in "${!ABI_MAP[@]}"; do + abi="${ABI_MAP[$target]}" + mkdir -p "android/app/src/main/jniLibs/$abi" + for coin in monero wownero; do + src="$BUNDLE_DIR/$target/lib${coin}_wallet2_api_c.so" + if [[ -f "$src" ]]; then + cp -v "$src" "android/app/src/main/jniLibs/$abi/" + else + echo "MISSING: $src — fail loud, the bundle layout may have changed" + exit 1 + fi + done + done + + echo "=== jniLibs contents ===" + find android/app/src/main/jniLibs/ -type f -name '*.so' | sort + + # mwebd (Litecoin MWEB) is NOT built here. LTC works without MWEB — + # users just don't get the privacy MWEB feature. Add a build_mwebd + # step if/when we want MWEB on Android. + + # ---- Configure: pubspec.yaml, AndroidManifest.xml, app_properties ---- + - name: Run Android configure (cakewallet profile) + run: | + set -x -e + pushd scripts/android + source ./app_env.sh cakewallet + ./app_config.sh + popd + + # ---- Secrets --------------------------------------------------------- + - name: Generate per-module secrets.g.dart files (all empty defaults) + run: dart run tool/generate_new_secrets.dart + + - name: Inject Trocador affiliate secrets into lib/.secrets.g.dart + env: + TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }} + TROCADOR_MONERO_API_KEY: ${{ secrets.TROCADOR_MONERO_API_KEY }} + TROCADOR_EXCHANGE_MARKUP: ${{ secrets.TROCADOR_EXCHANGE_MARKUP }} + run: | + sed -i \ + -e "s|const trocadorApiKey = '';|const trocadorApiKey = '${TROCADOR_API_KEY}';|" \ + -e "s|const trocadorMoneroApiKey = '';|const trocadorMoneroApiKey = '${TROCADOR_MONERO_API_KEY}';|" \ + -e "s|const trocadorExchangeMarkup = '';|const trocadorExchangeMarkup = '${TROCADOR_EXCHANGE_MARKUP:-1}';|" \ + lib/.secrets.g.dart + grep '^const trocador' lib/.secrets.g.dart + + # ---- Signing: decode upload keystore + write key.properties --------- + - name: Decode Android upload keystore + write key.properties + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + set -e + if [[ -z "$ANDROID_KEYSTORE_BASE64" ]]; then + echo "FATAL: ANDROID_KEYSTORE_BASE64 not set — configure Gitea Actions secrets first" + exit 1 + fi + # Write decoded keystore next to build.gradle (storeFile path + # in key.properties is resolved relative to android/app/). + echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/upload-keystore.jks + ls -la android/app/upload-keystore.jks + # key.properties consumed by build.gradle's signingConfigs.release. + cat > android/key.properties </dev/null | sort -V | tail -1 || which apksigner) + if [[ -n "$APKSIGNER" ]]; then + "$APKSIGNER" verify --print-certs build/app/outputs/flutter-apk/app-release.apk || true + else + echo "apksigner not on PATH; skipping sig print" + fi + + # actions/upload-artifact@v4 doesn't work on Gitea; pin to v3. + - name: Upload APK artifact (sideload-able) + uses: actions/upload-artifact@v3 + with: + name: hash-wallet-android-apk-${{ github.sha }} + path: build/app/outputs/flutter-apk/app-release.apk + retention-days: 14 + + - name: Upload AAB artifact (for Play Console) + uses: actions/upload-artifact@v3 + with: + name: hash-wallet-android-aab-${{ github.sha }} + path: build/app/outputs/bundle/release/app-release.aab + retention-days: 30