Release Android #9
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: Release Android | |
| # Required secrets: | |
| # ANDROID_SIGNING_KEY_BASE64 base64 of upload-keystore.jks | |
| # ANDROID_KEY_STORE_PASSWORD | |
| # ANDROID_KEY_ALIAS | |
| # ANDROID_KEY_PASSWORD | |
| # PLAY_SERVICE_ACCOUNT_JSON Play service-account JSON (raw, not base64). Optional; if absent, Play upload is skipped. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version_name: | |
| description: "Version name (e.g. 1.0.0)" | |
| required: true | |
| type: string | |
| version_code: | |
| description: "Version code (integer, defaults to a Unix timestamp)" | |
| required: false | |
| type: string | |
| jobs: | |
| release-android: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| permissions: | |
| contents: write | |
| env: | |
| PACKAGE_NAME: com.muxy.app | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Validate inputs | |
| id: validate | |
| run: | | |
| VERSION_NAME="${{ inputs.version_name }}" | |
| if ! [[ "$VERSION_NAME" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "Error: version_name must be X.Y.Z" | |
| exit 1 | |
| fi | |
| if [[ -n "${{ inputs.version_code }}" ]]; then | |
| VERSION_CODE="${{ inputs.version_code }}" | |
| else | |
| VERSION_CODE=$(date +%s) | |
| fi | |
| if ! [[ "$VERSION_CODE" =~ ^[0-9]+$ ]]; then | |
| echo "Error: version_code must be numeric" | |
| exit 1 | |
| fi | |
| MAJOR="${VERSION_NAME%%.*}" | |
| if [[ "$MAJOR" -ge 1 ]]; then | |
| TRACK="production" | |
| else | |
| TRACK="alpha" | |
| fi | |
| echo "Resolved track: $TRACK (versionName=$VERSION_NAME)" | |
| echo "version_name=$VERSION_NAME" >> "$GITHUB_OUTPUT" | |
| echo "version_code=$VERSION_CODE" >> "$GITHUB_OUTPUT" | |
| echo "track=$TRACK" >> "$GITHUB_OUTPUT" | |
| - name: Set up Node | |
| 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: Set up Gradle | |
| uses: gradle/actions/setup-gradle@v4 | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Set version in app.json | |
| env: | |
| VERSION_NAME: ${{ steps.validate.outputs.version_name }} | |
| VERSION_CODE: ${{ steps.validate.outputs.version_code }} | |
| run: | | |
| node -e ' | |
| const fs = require("fs"); | |
| const path = "app.json"; | |
| const cfg = JSON.parse(fs.readFileSync(path, "utf8")); | |
| cfg.expo.version = process.env.VERSION_NAME; | |
| cfg.expo.android = cfg.expo.android || {}; | |
| cfg.expo.android.versionCode = parseInt(process.env.VERSION_CODE, 10); | |
| fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n"); | |
| ' | |
| - name: Expo prebuild (Android) | |
| run: npx expo prebuild --platform android --no-install --clean --non-interactive | |
| - name: Make gradlew executable | |
| working-directory: android | |
| run: chmod +x ./gradlew | |
| - name: Decode signing key | |
| env: | |
| SIGNING_KEY_BASE64: ${{ secrets.ANDROID_SIGNING_KEY_BASE64 }} | |
| run: | | |
| if [[ -z "$SIGNING_KEY_BASE64" ]]; then | |
| echo "Error: ANDROID_SIGNING_KEY_BASE64 secret is required" | |
| exit 1 | |
| fi | |
| echo "$SIGNING_KEY_BASE64" | base64 --decode > android/app/upload-keystore.jks | |
| - name: Patch android/app/build.gradle with release signing config | |
| run: node scripts/lib/patch_signing.js android/app/build.gradle | |
| - name: Build signed Release AAB | |
| working-directory: android | |
| env: | |
| ANDROID_KEY_STORE_PASSWORD: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} | |
| ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} | |
| ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} | |
| run: ./gradlew bundleRelease -PreactNativeArchitectures=arm64-v8a,armeabi-v7a | |
| - name: Upload AAB artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-release-${{ steps.validate.outputs.version_name }}-${{ steps.validate.outputs.version_code }} | |
| path: android/app/build/outputs/bundle/release/*.aab | |
| if-no-files-found: error | |
| - name: Check Play upload secret | |
| id: play_secret | |
| env: | |
| PLAY_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} | |
| run: | | |
| if [[ -n "$PLAY_JSON" ]]; then | |
| echo "present=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "present=false" >> "$GITHUB_OUTPUT" | |
| echo "PLAY_SERVICE_ACCOUNT_JSON not set; skipping Play upload." | |
| fi | |
| - name: Upload to Play Store | |
| if: steps.play_secret.outputs.present == 'true' | |
| uses: r0adkll/upload-google-play@v1 | |
| with: | |
| serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} | |
| packageName: com.muxy.app | |
| releaseFiles: android/app/build/outputs/bundle/release/*.aab | |
| track: ${{ steps.validate.outputs.track }} | |
| status: draft | |
| changesNotSentForReview: ${{ steps.validate.outputs.track == 'production' }} | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PREV_TAG=$(gh release list \ | |
| --repo "${{ github.repository }}" \ | |
| --limit 100 \ | |
| --json tagName \ | |
| -q '[.[] | select(.tagName | startswith("android-v"))][0].tagName') | |
| if [[ -n "$PREV_TAG" ]]; then | |
| RANGE="$PREV_TAG..HEAD" | |
| else | |
| RANGE="HEAD" | |
| fi | |
| git log --no-merges -i --grep='^android:' --format='%s' "$RANGE" \ | |
| | sed -E 's/^[Aa]ndroid:[[:space:]]*/- /' \ | |
| > notes.md | |
| if [[ ! -s notes.md ]]; then | |
| echo "- No Android-facing changes since ${PREV_TAG:-initial release}" > notes.md | |
| fi | |
| gh release create "android-v${{ steps.validate.outputs.version_name }}" \ | |
| --repo "${{ github.repository }}" \ | |
| --title "Android v${{ steps.validate.outputs.version_name }}" \ | |
| --notes-file notes.md \ | |
| --latest=false \ | |
| --target "${{ github.sha }}" |