Skip to content

Build Signed Android APK/AAB #32

Build Signed Android APK/AAB

Build Signed Android APK/AAB #32

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
output_format:
description: "Output Format"
required: true
default: "aab"
type: choice
options:
- apk
- aab
- both
should_sign:
description: "Sign the build?"
required: true
default: "true"
type: choice
options:
- "true"
- "false"
push:
tags:
- "v*.*.*"
jobs:
build-android:
runs-on: ubuntu-latest
steps:
- 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
- name: Install dependencies
run: npm install --legacy-peer-deps
- name: Expo Prebuild
run: npx expo prebuild --platform android --clean
- name: Setup Gradle cache
uses: gradle/actions/setup-gradle@v3
with:
gradle-home-cache-cleanup: true
- name: Decode Keystore
run: |
if [ -z "${{ secrets.KEYSTORE_BASE64 }}" ]; then
echo "❌ Missing secret: KEYSTORE_BASE64"
exit 1
fi
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > release.keystore
chmod 600 release.keystore
ls -lh release.keystore || true
echo "✅ Keystore decoded"
- name: Make gradlew executable
run: chmod +x android/gradlew
- name: Build Release APK (unsigned)
if: ${{ github.event.inputs.output_format == 'apk' || github.event.inputs.output_format == 'both' }}
run: |
set -e
cd android
./gradlew assembleRelease --no-daemon --stacktrace
- name: Build Release AAB
if: ${{ github.event.inputs.output_format == 'aab' || github.event.inputs.output_format == 'both' }}
run: |
set -e
cd android
./gradlew bundleRelease --no-daemon --stacktrace
- name: Inspect environment & SDK
run: |
echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT"
echo "Listing available build-tools:"
ls -1 "$ANDROID_SDK_ROOT/build-tools" || true
echo "apksigner version (if available):"
for p in "$ANDROID_SDK_ROOT"/build-tools/*/apksigner; do
if [ -x "$p" ]; then
echo "-> $p"
"$p" version || true
fi
done
echo "java version:"
java -version || true
- name: Inspect Keystore
if: ${{ github.event.inputs.should_sign == 'true' }}
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
run: |
set -e
if [ ! -f release.keystore ]; then
echo "❌ release.keystore not found"
exit 1
fi
echo "=== Keystore contents ==="
keytool -list -v -keystore release.keystore -storepass "${KEYSTORE_PASSWORD}" || true
- name: Sign APK
if: ${{ github.event.inputs.should_sign == 'true' && (github.event.inputs.output_format == 'apk' || github.event.inputs.output_format == 'both') }}
id: sign_apk
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
run: |
set -euo pipefail
cd android || exit 1
echo "Finding APK (searching typical output locations)..."
# find the most likely release APK (unsigned or signed)
APK_PATH=$(find app/build/outputs/apk -type f -iname "*release*.apk" | sort | tail -n1 || true)
if [ -z "$APK_PATH" ]; then
echo "❌ APK not found!"
ls -R app/build/outputs || true
exit 1
fi
echo "📦 Found APK: $APK_PATH"
# Locate build-tools
if [ -d "$ANDROID_SDK_ROOT/build-tools/34.0.0" ]; then
BUILD_TOOLS_PATH="$ANDROID_SDK_ROOT/build-tools/34.0.0"
else
BUILD_TOOLS_VERSION=$(ls -1 "$ANDROID_SDK_ROOT/build-tools" | sort -V | tail -1 || true)
if [ -z "$BUILD_TOOLS_VERSION" ]; then
echo "❌ No build-tools found under $ANDROID_SDK_ROOT/build-tools"
exit 1
fi
BUILD_TOOLS_PATH="$ANDROID_SDK_ROOT/build-tools/$BUILD_TOOLS_VERSION"
fi
echo "Build tools path: $BUILD_TOOLS_PATH"
if [ ! -x "$BUILD_TOOLS_PATH/zipalign" ] || [ ! -x "$BUILD_TOOLS_PATH/apksigner" ]; then
echo "❌ zipalign or apksigner not found in $BUILD_TOOLS_PATH"
ls -l "$BUILD_TOOLS_PATH" || true
exit 1
fi
echo "🔧 Aligning APK..."
# Always create a fresh aligned APK as input to apksigner
"$BUILD_TOOLS_PATH/zipalign" -v -p 4 "$APK_PATH" release-aligned.apk
echo "🔐 Signing APK..."
"$BUILD_TOOLS_PATH/apksigner" sign \
--ks ../release.keystore \
--ks-key-alias "${KEY_ALIAS}" \
--ks-pass "pass:${KEYSTORE_PASSWORD}" \
--key-pass "pass:${KEY_PASSWORD}" \
--out release-signed.apk \
release-aligned.apk
echo "✅ APK signed: release-signed.apk"
echo "signed_apk_path=release-signed.apk" >> $GITHUB_OUTPUT
- name: Verify APK signature
if: ${{ github.event.inputs.should_sign == 'true' && (github.event.inputs.output_format == 'apk' || github.event.inputs.output_format == 'both') }}
env:
BUILD_TOOLS_PATH: ${{ env.BUILD_TOOLS_PATH }}
run: |
set -euo pipefail
cd android || exit 1
# re-detect build-tools (for safety)
if [ -d "$ANDROID_SDK_ROOT/build-tools/34.0.0" ]; then
BUILD_TOOLS_PATH="$ANDROID_SDK_ROOT/build-tools/34.0.0"
else
BUILD_TOOLS_VERSION=$(ls -1 "$ANDROID_SDK_ROOT/build-tools" | sort -V | tail -1 || true)
BUILD_TOOLS_PATH="$ANDROID_SDK_ROOT/build-tools/$BUILD_TOOLS_VERSION"
fi
if [ ! -f release-signed.apk ]; then
echo "❌ release-signed.apk not found"
ls -lh . || true
exit 1
fi
echo "🔍 Verifying APK signature..."
# use exit code of apksigner verify to determine success
if "$BUILD_TOOLS_PATH/apksigner" verify --print-certs release-signed.apk >/dev/null 2>&1; then
echo "✅ APK is properly signed"
echo ""
echo "=== Certificate Information ==="
"$BUILD_TOOLS_PATH/apksigner" verify --print-certs release-signed.apk || true
else
echo "❌ APK signature verification failed!"
"$BUILD_TOOLS_PATH/apksigner" verify release-signed.apk || true
echo ""
echo "Listing release dir for debugging:"
ls -l
exit 1
fi
- name: Sign AAB
if: ${{ github.event.inputs.should_sign == 'true' && (github.event.inputs.output_format == 'aab' || github.event.inputs.output_format == 'both') }}
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
run: |
set -euo pipefail
cd android || exit 1
AAB_PATH=$(find app/build/outputs/bundle -type f -iname "*.aab" | sort | tail -n1 || true)
if [ -z "$AAB_PATH" ]; then
echo "❌ AAB not found!"
ls -R app/build/outputs || true
exit 1
fi
echo "📦 Found AAB: $AAB_PATH"
echo "🔐 Signing AAB with jarsigner..."
# jarsigner may be present on the runner; this will sign the AAB container.
# If you use Play App Signing by Google Play, consider uploading unsigned and let Play handle signing.
jarsigner -verbose \
-sigalg SHA256withRSA \
-digestalg SHA-256 \
-keystore ../release.keystore \
-storepass "${KEYSTORE_PASSWORD}" \
-keypass "${KEY_PASSWORD}" \
"${AAB_PATH}" \
"${KEY_ALIAS}"
cp "$AAB_PATH" ../release-signed.aab
echo "✅ AAB signed: release-signed.aab"
- name: Upload APK artifact
if: ${{ (github.event.inputs.output_format == 'apk' || github.event.inputs.output_format == 'both') && github.event.inputs.should_sign == 'true' }}
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
if: ${{ (github.event.inputs.output_format == 'aab' || github.event.inputs.output_format == 'both') && github.event.inputs.should_sign == 'true' }}
uses: actions/upload-artifact@v4
with:
name: app-release-aab-${{ github.run_number }}
path: release-signed.aab
retention-days: 30
- name: Cleanup sensitive files
if: always()
run: |
rm -f release.keystore || true
rm -f android/release-aligned.apk || true
rm -f android/release-signed.apk || true
rm -f release-signed.aab || true
echo "✅ Cleanup completed"