Build Signed Android APK/AAB #33
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 | |
| 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: | |
| # ------------------------------------------------ | |
| # 0️⃣ Checkout + basic setup | |
| # ------------------------------------------------ | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: npm | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: 17 | |
| - uses: android-actions/setup-android@v3 | |
| # ------------------------------------------------ | |
| # 1️⃣ Install npm deps (Expo pre‑build) | |
| # ------------------------------------------------ | |
| - run: npm ci --legacy-peer-deps | |
| - run: npx expo prebuild --platform android --clean | |
| # ------------------------------------------------ | |
| # 2️⃣ Cache Gradle (speed‑up) | |
| # ------------------------------------------------ | |
| - uses: gradle/actions/setup-gradle@v3 | |
| with: | |
| gradle-home-cache-cleanup: true | |
| # ------------------------------------------------ | |
| # 3️⃣ Decode the keystore (used by Gradle for signing) | |
| # ------------------------------------------------ | |
| - name: Decode keystore | |
| run: | | |
| echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > release.keystore | |
| chmod 600 release.keystore | |
| env: | |
| KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} | |
| # ------------------------------------------------ | |
| # 4️⃣ Make gradlew executable | |
| # ------------------------------------------------ | |
| - run: chmod +x android/gradlew | |
| # ------------------------------------------------ | |
| # 5️⃣ Build the artefacts (release or debug) | |
| # ------------------------------------------------ | |
| - name: Build Release APK (unsigned) | |
| if: ${{ github.event.inputs.output_format == 'apk' || github.event.inputs.output_format == 'both' }} | |
| run: | | |
| cd android | |
| ./gradlew assembleRelease --no-daemon --stacktrace | |
| - name: Build Release AAB (unsigned) | |
| if: ${{ github.event.inputs.output_format == 'aab' || github.event.inputs.output_format == 'both' }} | |
| run: | | |
| cd android | |
| ./gradlew bundleRelease --no-daemon --stacktrace | |
| # ------------------------------------------------ | |
| # 6️⃣ (Optional) Sign the APK only – AAB stays unsigned | |
| # ------------------------------------------------ | |
| - 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 | |
| # locate the unsigned apk | |
| APK_PATH=$(find app/build/outputs/apk -type f -name "*release*.apk" | sort | tail -n1) | |
| echo "🔎 Unsigned APK: $APK_PATH" | |
| # locate the newest build‑tools folder (contains zipalign & apksigner) | |
| BT=$(ls -1 "$ANDROID_SDK_ROOT/build-tools" | sort -V | tail -1) | |
| echo "Using build‑tools $BT" | |
| ZIPALIGN="$ANDROID_SDK_ROOT/build-tools/$BT/zipalign" | |
| APKSIGNER="$ANDROID_SDK_ROOT/build-tools/$BT/apksigner" | |
| # Align | |
| "$ZIPALIGN" -v -p 4 "$APK_PATH" release-aligned.apk | |
| # Sign | |
| "$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 "✅ Signed APK ready: release-signed.apk" | |
| echo "signed_apk_path=release-signed.apk" >> $GITHUB_OUTPUT | |
| # ------------------------------------------------ | |
| # 7️⃣ Upload artefacts | |
| # ------------------------------------------------ | |
| - name: Upload signed APK | |
| if: ${{ github.event.inputs.should_sign == 'true' && (github.event.inputs.output_format == 'apk' || github.event.inputs.output_format == 'both') }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: app-release-apk-${{ github.run_number }} | |
| path: android/release-signed.apk | |
| retention-days: 30 | |
| - name: Upload AAB (unsigned – Play will sign it) | |
| if: ${{ github.event.inputs.output_format == 'aab' || github.event.inputs.output_format == 'both' }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: app-release-aab-${{ github.run_number }} | |
| path: android/app/build/outputs/bundle/release/app-release.aab | |
| retention-days: 30 | |
| # ------------------------------------------------ | |
| # 8️⃣ Cleanup secret files | |
| # ------------------------------------------------ | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| rm -f release.keystore | |
| rm -f android/release-aligned.apk | |
| rm -f android/release-signed.apk | |
| echo "🧹 Cleanup done" |