Files
hash-wallet/.github/workflows/build-android.yml
jwinterm 9f832633b9 CI: skip TLS verify on iOS artifact upload + verbose AAB build
iOS:
- Build itself succeeds (Runner.app sits in build/ios/iphonesimulator/).
- actions/upload-artifact's Node.js HTTP client hits git.such.software
  via LAN (NAT hairpin), bypassing the public NPM/Let's Encrypt cert
  and landing on Gitea's internal self-signed cert. Node refuses with
  DEPTH_ZERO_SELF_SIGNED_CERT.
- Setting NODE_TLS_REJECT_UNAUTHORIZED=0 at job env disables verification
  for all Node-based actions in the job. Acceptable here because we're
  talking to our own server on our own LAN — no external MITM surface.
- Long-term cleaner fix: install Gitea's CA cert into the runner's
  system trust store, or have the runner reach Gitea via the public
  hostname so NPM's Let's Encrypt cert is presented.

Android AAB:
- APK built + signed + uploaded fine (~14 min). AAB build then failed
  at FinalizeBundleTask$BundleToolRunnable / :app:signReleaseBundle
  with no visible error.
- Added --verbose to 'flutter build appbundle' so the next run prints
  the actual gradle stacktrace. Once we see WHY signReleaseBundle is
  failing, we can target the real fix.
2026-05-19 12:29:53 -04:00

272 lines
11 KiB
YAML

name: Hash Bags Android build
# Triggers:
# - push to dev/main (validate every commit)
# - PRs targeting dev/main (gate merges)
# - manual via workflow_dispatch
on:
# Manual-only for now. Trigger via Actions → "Hash Bags Android build"
# → Run workflow when you want a build. workflow_dispatch runs as the
# triggering user, so secrets are always available (unlike PR triggers).
workflow_dispatch:
concurrency:
group: android-${{ github.ref }}
cancel-in-progress: true
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 Bags 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/<ABI>/ -----
# 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. i686 (32-bit x86) is
# essentially dead on real Android devices; the monero_c bundle
# doesn't ship it. Missing target = log + skip, not fatal. The
# required modern ABIs (arm64-v8a, x86_64) MUST exist or we fail.
declare -A ABI_MAP=(
[aarch64-linux-android]=arm64-v8a
[armv7a-linux-androideabi]=armeabi-v7a
[i686-linux-android]=x86
[x86_64-linux-android]=x86_64
)
REQUIRED_ABIS=(arm64-v8a x86_64)
declare -A FOUND_ABI
for target in "${!ABI_MAP[@]}"; do
abi="${ABI_MAP[$target]}"
if [[ ! -d "$BUNDLE_DIR/$target" ]]; then
echo "SKIP: $target not in bundle (Android ABI $abi will not be shipped)"
continue
fi
mkdir -p "android/app/src/main/jniLibs/$abi"
ok=true
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 "SKIP: $src missing (skipping $abi)"
ok=false
break
fi
done
if $ok; then
FOUND_ABI[$abi]=1
else
rm -rf "android/app/src/main/jniLibs/$abi"
fi
done
# Fail loudly only if a required ABI is missing.
for required in "${REQUIRED_ABIS[@]}"; do
if [[ -z "${FOUND_ABI[$required]:-}" ]]; then
echo "FATAL: required ABI $required is missing — monero_c bundle layout has changed"
exit 1
fi
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
# Debug visibility — prints lengths only, never values. If a secret
# length is 0, Gitea Actions is not passing it to this run (most
# commonly because the run was triggered by a from-fork PR).
echo "ANDROID_KEYSTORE_BASE64 length: ${#ANDROID_KEYSTORE_BASE64}"
echo "ANDROID_KEYSTORE_PASSWORD length: ${#ANDROID_KEYSTORE_PASSWORD}"
echo "ANDROID_KEY_ALIAS length: ${#ANDROID_KEY_ALIAS}"
echo "ANDROID_KEY_PASSWORD length: ${#ANDROID_KEY_PASSWORD}"
if [[ -z "$ANDROID_KEYSTORE_BASE64" ]]; then
echo "FATAL: ANDROID_KEYSTORE_BASE64 not reaching the runner."
echo "Check: (1) secret is in repo Settings → Actions → Secrets;"
echo " (2) workflow was triggered by 'push' or 'workflow_dispatch'"
echo " (PR triggers from a fork strip secrets);"
echo " (3) Gitea's runner is configured to pass secrets."
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 <<EOF
storePassword=$ANDROID_KEYSTORE_PASSWORD
keyPassword=$ANDROID_KEY_PASSWORD
keyAlias=$ANDROID_KEY_ALIAS
storeFile=upload-keystore.jks
EOF
chmod 600 android/key.properties
# ---- Flutter SDK init + codegen --------------------------------------
- name: Initialize Flutter SDK (Android precache)
run: |
flutter --version
flutter precache --android --no-linux --no-ios --no-macos --no-windows --no-fuchsia --no-web || true
- name: Build generated code (mobx + hive adapters)
run: bash model_generator.sh
- name: Generate localization
run: dart run tool/generate_localization.dart
- name: Compile SVG assets (res/pictures/*.svg → assets/new-ui/*.svg.vec)
run: ./compile_graphics.sh
# ---- Compile + sign --------------------------------------------------
# Universal APK (single binary covers all 4 ABIs, easy to sideload).
- name: Build release APK (universal, all ABIs)
run: flutter build apk --dart-define-from-file=env.json --release
# AAB for Play Console upload (Google generates per-ABI APKs server-side
# via Play App Signing). --verbose surfaces the full gradle stacktrace
# when FinalizeBundleTask / signReleaseBundle fails — without it the
# error is opaque ('FinalizeBundleTask$BundleToolRunnable failed').
- name: Build release AAB
run: flutter build appbundle --dart-define-from-file=env.json --release --verbose
- name: Sanity-check signature on APK
run: |
set -x
ls -la build/app/outputs/flutter-apk/
# apksigner is in build-tools; locate dynamically.
APKSIGNER=$(find $ANDROID_HOME/build-tools -name apksigner -type f 2>/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