Skip to content

Build Signed Android APK/AAB #26

Build Signed Android APK/AAB

Build Signed Android APK/AAB #26

Workflow file for this run

name: Build Signed Android APK/AAB
on:
workflow_dispatch:
inputs:
build_type:
description: "Build type"
required: true
default: "release"
type: choice
options:
- debug
- release
push:
tags:
- "v*.*.*"
jobs:
build-android:
runs-on: ubuntu-latest
env:
# Make the keystore path explicit – we keep it in the repo root.
KEYSTORE_PATH: ${{ github.workspace }}/release.keystore
steps:
# -------------------------------------------------
# 0️⃣ Checkout & basic tooling
# -------------------------------------------------
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: Setup Android SDK
uses: android-actions/setup-android@v3
# -------------------------------------------------
# 1️⃣ Install npm deps
# -------------------------------------------------
- name: Install dependencies
run: npm ci --legacy-peer-deps # faster & reproducible
# -------------------------------------------------
# 2️⃣ Expo pre‑build (generates the Android project)
# -------------------------------------------------
- name: Expo Prebuild
run: npx expo prebuild --platform android --clean
# -------------------------------------------------
# 3️⃣ Gradle cache (speeds up later runs)
# -------------------------------------------------
- name: Setup Gradle cache
uses: gradle/actions/setup-gradle@v3
with:
gradle-home-cache-cleanup: true
# -------------------------------------------------
# 4️⃣ Decode the Keystore (store it where every step can read it)
# -------------------------------------------------
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > "$KEYSTORE_PATH"
chmod 600 "$KEYSTORE_PATH"
echo "✅ Keystore written to $KEYSTORE_PATH"
# -------------------------------------------------
# 5️⃣ Make gradlew executable
# -------------------------------------------------
- name: Make gradlew executable
run: chmod +x android/gradlew
# -------------------------------------------------
# 6️⃣ Build the **unsigned** Release APK
# -------------------------------------------------
- name: Build Release APK (Unsigned)
run: |
cd android
./gradlew assembleRelease --no-daemon
# -------------------------------------------------
# 7️⃣ Sign the APK
# -------------------------------------------------
- name: Sign APK
id: sign_apk
run: |
set -euo pipefail
cd android
# locate the unsigned APK (covers both *-unsigned.apk and fallback *-release.apk)
APK_PATH=$(find ./app/build/outputs/apk/release \
\( -name "*-release-unsigned.apk" -o -name "*-release.apk" \) \
-print -quit)
if [[ -z "$APK_PATH" ]]; then
echo "❌ No APK found – aborting."
exit 1
fi
echo "📦 Found APK: $APK_PATH"
# Use ANDROID_SDK_ROOT fallback if ANDROID_HOME is not set
SDK_ROOT="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
if [[ -z "$SDK_ROOT" ]]; then
echo "❌ ANDROID_HOME and ANDROID_SDK_ROOT are not set – aborting."
exit 1
fi
# Pick the newest build-tools folder (no hard‑coded version)
BUILD_TOOLS_DIR=$(ls -1 "$SDK_ROOT/build-tools" | sort -V | tail -n1)
ZIPALIGN="$SDK_ROOT/build-tools/$BUILD_TOOLS_DIR/zipalign"
APKSIGNER="$SDK_ROOT/build-tools/$BUILD_TOOLS_DIR/apksigner"
# Align the APK (required before signing)
"$ZIPALIGN" -v -p 4 "$APK_PATH" release-aligned.apk
echo "✅ Aligned → release-aligned.apk"
# Sign the aligned APK
"$APKSIGNER" sign \
--ks "$KEYSTORE_PATH" \
--ks-key-alias "${{ secrets.KEY_ALIAS }}" \
--ks-pass "pass:${{ secrets.KEYSTORE_PASSWORD }}" \
--key-pass "pass:${{ secrets.KEY_PASSWORD }}" \
--out release-signed.apk \
release-aligned.apk
echo "✅ Signed APK → release-signed.apk"
# We keep the signed APK inside the android/ directory (repo path: android/release-signed.apk)
echo "signed_apk_path=android/release-signed.apk" >> $GITHUB_OUTPUT
# -------------------------------------------------
# 8️⃣ Build the Release AAB (Android App Bundle)
# -------------------------------------------------
- name: Build Release AAB
run: |
cd android
./gradlew bundleRelease --no-daemon
# -------------------------------------------------
# 9️⃣ Sign the AAB (jarsigner)
# -------------------------------------------------
- name: Sign AAB
run: |
cd android
AAB_PATH=$(find ./app/build/outputs/bundle/release -name "*-release.aab" -print -quit)
if [[ -z "$AAB_PATH" ]]; then
echo "❌ No AAB found – aborting."
exit 1
fi
echo "📦 Found AAB: $AAB_PATH"
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
-keystore "$KEYSTORE_PATH" \
-storepass "${{ secrets.KEYSTORE_PASSWORD }}" \
-keypass "${{ secrets.KEY_PASSWORD }}" \
"$AAB_PATH" "${{ secrets.KEY_ALIAS }}"
# Move signed AAB to android/release-signed.aab (kept inside android/ directory)
mv "$AAB_PATH" release-signed.aab
echo "✅ Signed AAB → android/release-signed.aab"
# -------------------------------------------------
# 🔍 Verify the signed APK (uses the same android/release-signed.apk path)
# -------------------------------------------------
- name: Verify APK signature
run: |
set -euo pipefail
# Use ANDROID_SDK_ROOT fallback if ANDROID_HOME is not set
SDK_ROOT="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
if [[ -z "$SDK_ROOT" ]]; then
echo "❌ ANDROID_HOME and ANDROID_SDK_ROOT are not set – aborting."
exit 1
fi
BUILD_TOOLS_DIR=$(ls -1 "$SDK_ROOT/build-tools" | sort -V | tail -n1)
APKSIGNER="$SDK_ROOT/build-tools/$BUILD_TOOLS_DIR/apksigner"
APK_FILE="android/release-signed.apk"
if [[ ! -f "$APK_FILE" ]]; then
echo "❌ $APK_FILE not found – aborting."
exit 1
fi
echo "📦 Verifying: $APK_FILE"
"$APKSIGNER" verify --print-certs "$APK_FILE"
# Optional: extract the SHA‑1 fingerprint for further checks
SHA1=$("$APKSIGNER" verify --print-certs "$APK_FILE" |
grep "SHA-1 digest:" | head -1 | awk '{print $3}')
echo "Detected SHA‑1: $SHA1"
# -------------------------------------------------
# 📦 Upload the signed artifacts
# -------------------------------------------------
- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: app-release-apk-${{ github.run_number }}
path: android/release-signed.apk
retention-days: 30
- name: Upload AAB artifact
uses: actions/upload-artifact@v4
with:
name: app-release-aab-${{ github.run_number }}
path: android/release-signed.aab
retention-days: 30
# -------------------------------------------------
# 🧹 Secure cleanup (always runs, even on failure)
# -------------------------------------------------
- name: Cleanup sensitive files
if: always()
run: |
rm -f "$KEYSTORE_PATH"
echo "✅ Keystore removed"