update build.yml #27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: | ||
| KEYSTORE_PATH: ${{ github.workspace }}/release.keystore | ||
| # Ensure Gradle user home is explicit and consistent for caching | ||
| GRADLE_USER_HOME: ${{ runner.temp }}/gradle-user-home | ||
| 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 | ||
| # Ensure ANDROID_SDK_ROOT is set (use known runner path when available) | ||
| - name: Ensure ANDROID_SDK_ROOT | ||
| shell: bash | ||
| run: | | ||
| if [ -z "${ANDROID_SDK_ROOT:-}" ] && [ -d "/usr/local/lib/android/sdk" ]; then | ||
| echo "ANDROID_SDK_ROOT=/usr/local/lib/android/sdk" >> $GITHUB_ENV | ||
| fi | ||
| # print for debugging | ||
| echo "ANDROID_SDK_ROOT=${ANDROID_SDK_ROOT:-$(cat $GITHUB_ENV 2>/dev/null | sed -n 's/^ANDROID_SDK_ROOT=//p' || true)}" | ||
| - name: Install dependencies | ||
| run: npm ci --legacy-peer-deps | ||
| # Expo prebuild generates the android project (creates gradlew etc.) | ||
| - name: Expo Prebuild | ||
| run: npx expo prebuild --platform android --clean | ||
| # Make gradlew executable immediately after prebuild so subsequent steps can run it | ||
| - name: Make gradlew executable | ||
| run: chmod +x android/gradlew | ||
| # Setup Gradle cache (non-fatal on cache service issues) | ||
| - name: Setup Gradle cache | ||
| uses: gradle/actions/setup-gradle@v3 | ||
| continue-on-error: true | ||
| with: | ||
| gradle-home-cache-cleanup: true | ||
| # Ensure Gradle distribution is downloaded via the wrapper (resilient fallback) | ||
| - name: Ensure Gradle distribution (force wrapper download) | ||
| run: | | ||
| cd android | ||
| # Try running a cheap Gradle task to trigger a download if the distribution isn't present. | ||
| ./gradlew --no-daemon -v || (echo "Gradle wrapper failed to run; printing wrapper properties for debug" && cat gradle/wrapper/gradle-wrapper.properties && exit 1) | ||
| - name: Decode Keystore | ||
| run: | | ||
| echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > "$KEYSTORE_PATH" | ||
| chmod 600 "$KEYSTORE_PATH" | ||
| echo "✅ Keystore written to $KEYSTORE_PATH" | ||
| # Build APK (unsigned) | ||
| - name: Build Release APK (Unsigned) | ||
| run: | | ||
| cd android | ||
| ./gradlew assembleRelease --no-daemon | ||
| - name: Sign APK | ||
| id: sign_apk | ||
| run: | | ||
| set -euo pipefail | ||
| cd android | ||
| 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" | ||
| 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) | ||
| ZIPALIGN="$SDK_ROOT/build-tools/$BUILD_TOOLS_DIR/zipalign" | ||
| APKSIGNER="$SDK_ROOT/build-tools/$BUILD_TOOLS_DIR/apksigner" | ||
| "$ZIPALIGN" -v -p 4 "$APK_PATH" release-aligned.apk | ||
| echo "✅ Aligned → release-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" | ||
| echo "signed_apk_path=android/release-signed.apk" >> $GITHUB_OUTPUT | ||
| - name: Build Release AAB | ||
| run: | | ||
| cd android | ||
| ./gradlew bundleRelease --no-daemon | ||
| # Sign AAB: remove any existing META-INF signature files first to avoid multiple chains | ||
| - name: Sign AAB (remove existing signatures first) | ||
| run: | | ||
| set -euo pipefail | ||
| 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" | ||
| echo "🔍 Removing pre-existing signature files from META-INF (if any)..." | ||
| # remove common signature files from META-INF inside the zip (aab is a zip) | ||
| zip -d "$AAB_PATH" "META-INF/*.SF" "META-INF/*.RSA" "META-INF/*.DSA" "META-INF/*.EC" >/dev/null 2>&1 || true | ||
| echo "📄 META-INF after cleanup (first 200 lines):" | ||
| unzip -l "$AAB_PATH" "META-INF/*" | sed -n '1,200p' || true | ||
| echo "🔐 Signing AAB with jarsigner..." | ||
| jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \ | ||
| -keystore "$KEYSTORE_PATH" \ | ||
| -storepass "${{ secrets.KEYSTORE_PASSWORD }}" \ | ||
| -keypass "${{ secrets.KEY_PASSWORD }}" \ | ||
| "$AAB_PATH" "${{ secrets.KEY_ALIAS }}" | ||
| mv "$AAB_PATH" release-signed.aab | ||
| echo "✅ Signed AAB → android/release-signed.aab" | ||
| CERT_COUNT=$(unzip -Z1 release-signed.aab | grep -Eo 'META-INF/[^/]+\.(RSA|DSA|EC)' | sort -u | wc -l || true) | ||
| echo "Detected META-INF certificate files (count): $CERT_COUNT" | ||
| if [[ "$CERT_COUNT" -gt 1 ]]; then | ||
| echo "❌ There are still multiple certificate files in META-INF. Aborting to avoid Play Store rejection." | ||
| exit 1 | ||
| fi | ||
| echo "✅ AAB has a single certificate chain." | ||
| - name: Verify APK signature | ||
| run: | | ||
| set -euo pipefail | ||
| 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" | ||
| SHA1=$("$APKSIGNER" verify --print-certs "$APK_FILE" | | ||
| grep "SHA-1 digest:" | head -1 | awk '{print $3}') | ||
| echo "Detected SHA‑1: $SHA1" | ||
| - 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 | ||
| - name: Cleanup sensitive files | ||
| if: always() | ||
| run: | | ||
| rm -f "$KEYSTORE_PATH" | ||
| echo "✅ Keystore removed" | ||