Files
pezkuwi-wallet-android/.github/workflows/android_build.yml
T
pezkuwichain d8b31739e7 ci: fail the build when a 64-bit native library is not 16 KB aligned (#20)
* ci: fail the build when a 64-bit native library is not 16 KB aligned

Play rejects updates whose 64-bit native libraries are not aligned for 16 KB memory
pages, but nothing in the build says so. A misaligned .so surfaces only as a policy
issue in Play Console, days after a release has gone out — which is how one reached
production here unnoticed.

The check reads what the build actually produced rather than trusting the dependency
list, because a single transitive dependency shipping an unaligned library is enough
to fail the whole bundle. It walks every .aab and .apk under app/build/outputs, pulls
each arm64-v8a and x86_64 .so, and compares its largest LOAD segment alignment against
16384. 32-bit ABIs are skipped: those devices use 4 KB pages.

Verified against the v1.2.0 release bundle before it was cleaned up: 14 libraries
scanned, and it caught libpl_droidsonroids_gif.so at 0x1000 — the one library Play
flagged. A check that cannot fail on known-bad input is not a check.

* test: make the tamper-detection test actually tamper

checkTamperedDataFails wrote to encoded[lastIndex - 1] but computed the new value from
encoded.last(). Whenever those two bytes already differed by exactly 0x01 the write was
a no-op, the ciphertext stayed valid, decryption succeeded, and the expected
AEADBadTagException never arrived — so the test failed for reasons unrelated to what it
checks. One run in 256.

Flip a bit in the byte being written instead, which changes the ciphertext every time.
2026-08-03 07:25:46 -07:00

319 lines
12 KiB
YAML

name: Reusable workflow for build Android
on:
workflow_call:
inputs:
branch:
required: true
default: main
type: string
gradlew-command:
required: false
type: string
default: "false"
run-tests:
required: false
type: boolean
default: true
keystore-file-name:
required: false
type: string
default: "false"
keystore-file-base64:
required: false
type: string
default: "false"
upload-name:
required: false
type: string
default: "apk"
build-debug-tests:
required: false
type: boolean
default: false
free-disk-space:
required: false
type: boolean
default: false
secrets:
# Crowdloan secrets - NOT NEEDED for Pezkuwi (own blockchain)
ACALA_PROD_AUTH_TOKEN:
required: false
ACALA_TEST_AUTH_TOKEN:
required: false
MOONBEAM_PROD_AUTH_TOKEN:
required: false
MOONBEAM_TEST_AUTH_TOKEN:
required: false
# Fiat on-ramp - OPTIONAL (future update)
MOONPAY_PRODUCTION_SECRET:
required: false
MOONPAY_TEST_SECRET:
required: false
# EVM chain support - REQUIRED for cross-chain
EHTERSCAN_API_KEY_MOONBEAM:
required: true
EHTERSCAN_API_KEY_MOONRIVER:
required: true
EHTERSCAN_API_KEY_ETHEREUM:
required: true
INFURA_API_KEY:
required: true
# RPC provider - use own nodes or Dwellir
DWELLIR_API_KEY:
required: false
# Tron - raises TronGrid's aggressive anonymous rate limit
TRONGRID_API_KEY:
required: false
# WalletConnect - REQUIRED for dApp connections
WALLET_CONNECT_PROJECT_ID:
required: true
# Google OAuth - REQUIRED for cloud backup
DEBUG_GOOGLE_OAUTH_ID:
required: true
RELEASE_GOOGLE_OAUTH_ID:
required: true
# Special secrets for signing:
CI_MARKET_KEYSTORE_PASS:
required: false
CI_MARKET_KEYSTORE_KEY_ALIAS:
required: false
CI_MARKET_KEYSTORE_KEY_PASS:
required: false
CI_MARKET_KEY_FILE:
required: false
CI_KEYSTORE_PASS:
required: false
CI_KEYSTORE_KEY_ALIAS:
required: false
CI_KEYSTORE_KEY_PASS:
required: false
CI_GITHUB_KEYSTORE_PASS:
required: false
CI_GITHUB_KEYSTORE_KEY_ALIAS:
required: false
CI_GITHUB_KEYSTORE_KEY_PASS:
required: false
# Secrets for google-services:
CI_DEVELOP_GOOGLE_SERVICES:
required: true
CI_PRODUCTION_GOOGLE_SERVICES:
required: true
env:
ACALA_PROD_AUTH_TOKEN: ${{ secrets.ACALA_PROD_AUTH_TOKEN }}
ACALA_TEST_AUTH_TOKEN: ${{ secrets.ACALA_TEST_AUTH_TOKEN }}
MOONBEAM_PROD_AUTH_TOKEN: ${{ secrets.MOONBEAM_PROD_AUTH_TOKEN }}
MOONBEAM_TEST_AUTH_TOKEN: ${{ secrets.MOONBEAM_TEST_AUTH_TOKEN }}
MOONPAY_PRODUCTION_SECRET: ${{ secrets.MOONPAY_PRODUCTION_SECRET }}
MOONPAY_TEST_SECRET: ${{ secrets.MOONPAY_TEST_SECRET }}
MERCURYO_PRODUCTION_SECRET: ${{ secrets.MERCURYO_PRODUCTION_SECRET }}
MERCURYO_TEST_SECRET: ${{ secrets.MERCURYO_TEST_SECRET }}
EHTERSCAN_API_KEY_MOONBEAM: ${{ secrets.EHTERSCAN_API_KEY_MOONBEAM }}
EHTERSCAN_API_KEY_MOONRIVER: ${{ secrets.EHTERSCAN_API_KEY_MOONRIVER }}
EHTERSCAN_API_KEY_ETHEREUM: ${{ secrets.EHTERSCAN_API_KEY_ETHEREUM }}
INFURA_API_KEY: ${{ secrets.INFURA_API_KEY }}
DWELLIR_API_KEY: ${{ secrets.DWELLIR_API_KEY }}
TRONGRID_API_KEY: ${{ secrets.TRONGRID_API_KEY }}
WALLET_CONNECT_PROJECT_ID: ${{ secrets.WALLET_CONNECT_PROJECT_ID }}
DEBUG_GOOGLE_OAUTH_ID: ${{ secrets.DEBUG_GOOGLE_OAUTH_ID }}
RELEASE_GOOGLE_OAUTH_ID: ${{ secrets.RELEASE_GOOGLE_OAUTH_ID }}
CI_MARKET_KEYSTORE_PASS: ${{ secrets.CI_MARKET_KEYSTORE_PASS }}
CI_MARKET_KEYSTORE_KEY_ALIAS: ${{ secrets.CI_MARKET_KEYSTORE_KEY_ALIAS }}
CI_MARKET_KEYSTORE_KEY_PASS: ${{ secrets.CI_MARKET_KEYSTORE_KEY_PASS }}
CI_MARKET_KEY_FILE: ${{ secrets.RELEASE_MARKET_KEY_FILE }}
CI_DEV_KEY_FILE: ${{ secrets.BASE64_DEV_KEYSTORE_FILE }}
CI_KEYSTORE_PASS: ${{ secrets.CI_KEYSTORE_PASS }}
CI_KEYSTORE_KEY_ALIAS: ${{ secrets.CI_KEYSTORE_KEY_ALIAS }}
CI_KEYSTORE_KEY_PASS: ${{ secrets.CI_KEYSTORE_KEY_PASS }}
CI_GITHUB_KEYSTORE_PASS: ${{ secrets.CI_GITHUB_KEYSTORE_PASS }}
CI_GITHUB_KEYSTORE_KEY_ALIAS: ${{ secrets.CI_GITHUB_KEYSTORE_KEY_ALIAS }}
CI_GITHUB_KEYSTORE_KEY_PASS: ${{ secrets.CI_GITHUB_KEYSTORE_KEY_PASS }}
CI_GITHUB_KEYSTORE_KEY_FILE: ${{ secrets.BASE64_GITHUB_KEYSTORE_FILE }}
CI_DEVELOP_GOOGLE_SERVICES_FILE: ${{ secrets.CI_DEVELOP_GOOGLE_SERVICES }}
CI_PRODUCTION_GOOGLE_SERVICES_FILE: ${{ secrets.CI_PRODUCTION_GOOGLE_SERVICES }}
POLKASSEMBLY_SUMMARY_API_KEY: ${{ secrets.POLKASSEMBLY_SUMMARY_API_KEY }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{inputs.upload-name}}
cancel-in-progress: true
jobs:
build-app:
name: Build app and test
runs-on: ubuntu-24.04
timeout-minutes: 90
steps:
- name: 🧹 Free disk space
if: ${{ inputs.free-disk-space }}
run: |
# The androidTest build of all modules fills the ~14 GB free on
# GitHub-hosted runners; drop preinstalled toolchains we never use.
sudo rm -rf /usr/share/dotnet /usr/share/swift /opt/ghc /usr/local/.ghcup \
/opt/hostedtoolcache/CodeQL /usr/local/share/boost /usr/local/share/powershell \
/usr/local/lib/node_modules /usr/local/julia* /opt/microsoft
sudo docker image prune -af > /dev/null 2>&1 || true
df -h /
- name: Checkout particular branch
uses: actions/checkout@v4
with:
ref: ${{ inputs.branch }}
fetch-depth: 0
- name: Compute version code from commit count
run: |
COMMIT_COUNT=$(git rev-list --count HEAD)
VERSION_CODE=$((COMMIT_COUNT + 100))
echo "CI_BUILD_ID=$VERSION_CODE" >> $GITHUB_ENV
echo "Version code: $VERSION_CODE (commits: $COMMIT_COUNT + offset 100)"
- name: 📂 Set up DEV Google Services
uses: davidSchuppa/base64Secret-toFile-action@v3
with:
secret: ${{ env.CI_DEVELOP_GOOGLE_SERVICES_FILE }}
filename: google-services.json
destination-path: ./app/
- name: 📂 Set up PROD Google Services
uses: davidSchuppa/base64Secret-toFile-action@v3
with:
secret: ${{ env.CI_PRODUCTION_GOOGLE_SERVICES_FILE }}
filename: google-services.json
destination-path: ./app/src/release/
- name: 📂 Copy PROD Google Services to releaseMarket
run: |
mkdir -p ./app/src/releaseMarket
cp ./app/src/release/google-services.json ./app/src/releaseMarket/google-services.json
- name: 📂 Copy PROD Google Services to releaseGithub
run: |
mkdir -p ./app/src/releaseGithub
cp ./app/src/release/google-services.json ./app/src/releaseGithub/google-services.json
- name: 🔧 Install dependencies
uses: ./.github/workflows/install/
- name: 🧪 Run tests
if: ${{ inputs.run-tests }}
run: ./gradlew runTest
- name: 🔐 Getting github sign key
if: ${{ startsWith(inputs.keystore-file-name, 'github_key.jks') }}
uses: timheuer/base64-to-file@v1.1
with:
fileName: ${{ inputs.keystore-file-name }}
fileDir: './app/'
encodedString: ${{ env.CI_GITHUB_KEYSTORE_KEY_FILE }}
- name: 🔐 Getting market sign key
if: ${{ startsWith(inputs.keystore-file-name, 'market_key.jks') }}
uses: timheuer/base64-to-file@v1.1
with:
fileName: ${{ inputs.keystore-file-name }}
fileDir: './app/'
encodedString: ${{ env.CI_MARKET_KEY_FILE }}
- name: 🔐 Getting develop sign key
if: ${{ startsWith(inputs.keystore-file-name, 'develop_key.jks') }}
uses: timheuer/base64-to-file@v1.1
with:
fileName: ${{ inputs.keystore-file-name }}
fileDir: './app/'
encodedString: ${{ env.CI_DEV_KEY_FILE }}
- name: 🏗 Build app
if: ${{ !startsWith(inputs.gradlew-command, 'false') }}
run: ./gradlew ${{ inputs.gradlew-command }}
- name: 🏗 Build debug tests
if: ${{ inputs.build-debug-tests }}
run: ./gradlew assembleDebugAndroidTest
# Play rejects updates whose 64-bit native libraries are not aligned for 16 KB
# memory pages. Nothing in the build says so — a misaligned .so only surfaces as a
# policy issue in Play Console, days later, after a release has already gone out.
# One dependency shipping an unaligned library is enough, so check what was
# actually produced rather than trusting the dependency list.
#
# 32-bit ABIs are exempt: those devices use 4 KB pages.
- name: 🔍 Verify 16 KB native alignment
if: ${{ !startsWith(inputs.gradlew-command, 'false') }}
run: |
set -euo pipefail
work=$(mktemp -d)
found=0
bad=0
for archive in $(find app/build/outputs -name '*.aab' -o -name '*.apk' | sort); do
for entry in $(unzip -Z1 "$archive" 2>/dev/null | grep -E 'lib/(arm64-v8a|x86_64)/.*\.so$' || true); do
found=$((found + 1))
unzip -o -q -j "$archive" "$entry" -d "$work"
lib="$work/$(basename "$entry")"
# Largest LOAD segment alignment; 16384 (0x4000) is the requirement.
align=$(readelf -lW "$lib" 2>/dev/null | awk '/LOAD/ {print $NF}' | sort -u | tail -1)
align_dec=$((align))
if [ "$align_dec" -lt 16384 ]; then
echo "::error::$(basename "$archive") $entry aligned at $align, needs 0x4000 (16384)"
bad=$((bad + 1))
else
echo " ok $entry $align"
fi
rm -f "$lib"
done
done
rm -rf "$work"
if [ "$found" -eq 0 ]; then
echo "No 64-bit native libraries in the build output — nothing to check."
exit 0
fi
echo "Checked $found 64-bit native libraries, $bad misaligned."
[ "$bad" -eq 0 ]
- name: 🧹 Delete key after building
if: ${{ !startsWith(inputs.keystore-file-name, 'false') }}
run: rm ./app/${{ inputs.keystore-file-name }}
- name: ➡️ Upload build artifacts
if: ${{ !startsWith(inputs.gradlew-command, 'false') }}
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.upload-name }}
path: app/build/outputs/apk/
if-no-files-found: ignore
- name: ➡️ Upload bundle artifacts
if: ${{ !startsWith(inputs.gradlew-command, 'false') }}
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.upload-name }}-bundle
path: app/build/outputs/bundle/
if-no-files-found: ignore
- name: ➡️ Upload mapping file
if: ${{ !startsWith(inputs.gradlew-command, 'false') }}
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.upload-name }}-mapping
path: app/build/outputs/mapping/
if-no-files-found: ignore
- name: ➡️ Upload native debug symbols
if: ${{ !startsWith(inputs.gradlew-command, 'false') }}
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.upload-name }}-native-debug-symbols
path: app/build/outputs/native-debug-symbols/
if-no-files-found: ignore