From a472b0c05c928658b2a86bcde9ea4ede73d03cd0 Mon Sep 17 00:00:00 2001 From: Janez T Date: Sun, 26 Apr 2026 08:24:04 +0200 Subject: [PATCH] chore: Add Play release cycle #31 ref: --- .github/workflows/build-artifacts.yml | 72 ++++++++++++++- android/Gemfile | 4 + android/fastlane/Fastfile | 82 +++++++++++------ docs/google-play-release.md | 128 ++++++++++++++++++++++++++ 4 files changed, 255 insertions(+), 31 deletions(-) create mode 100644 android/Gemfile create mode 100644 docs/google-play-release.md diff --git a/.github/workflows/build-artifacts.yml b/.github/workflows/build-artifacts.yml index 2169fca..18c283f 100644 --- a/.github/workflows/build-artifacts.yml +++ b/.github/workflows/build-artifacts.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: "17" @@ -44,6 +44,14 @@ jobs: cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }} pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }} + - name: Setup Ruby + if: github.event_name == 'release' + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + working-directory: android + - name: Cache Android NDK uses: actions/cache@v4 with: @@ -55,22 +63,76 @@ jobs: - name: Install dependencies run: flutter pub get + - name: Validate Android release secrets + if: github.event_name == 'release' + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} + run: | + test -n "$ANDROID_KEYSTORE_BASE64" + test -n "$ANDROID_KEYSTORE_PASSWORD" + test -n "$ANDROID_KEY_ALIAS" + test -n "$ANDROID_KEY_PASSWORD" + test -n "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" + + - name: Create Android signing files + if: github.event_name == 'release' + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > android/release-keystore.jks + cat < android/key.properties + storePassword=$ANDROID_KEYSTORE_PASSWORD + keyPassword=$ANDROID_KEY_PASSWORD + keyAlias=$ANDROID_KEY_ALIAS + storeFile=../release-keystore.jks + EOF + - name: Build APK run: flutter build apk --release - - name: Prepare APK artifact + - name: Build Play release AAB + if: github.event_name == 'release' + run: flutter build appbundle --release + + - name: Prepare Android artifacts run: | RAW_TAG="${{ github.event.release.tag_name || github.ref_name }}" TAG="${RAW_TAG//\//-}" cp build/app/outputs/flutter-apk/app-release.apk "${APP_NAME}-${TAG}-android.apk" + if [ -f build/app/outputs/bundle/release/app-release.aab ]; then + cp build/app/outputs/bundle/release/app-release.aab "${APP_NAME}-${TAG}-play.aab" + fi - - name: Upload APK artifact + - name: Upload Android artifacts uses: actions/upload-artifact@v4 with: name: release-android - path: "${{ env.APP_NAME }}-*-android.apk" + path: | + ${{ env.APP_NAME }}-*-android.apk + ${{ env.APP_NAME }}-*-play.aab if-no-files-found: error + - name: Upload Play release to internal testing + if: github.event_name == 'release' + env: + GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} + SKIP_ANDROID_BUILD: "1" + run: cd android && bundle exec fastlane android internal + + - name: Upload Play release to production + if: github.event_name == 'release' && !github.event.release.prerelease + env: + GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} + SKIP_ANDROID_BUILD: "1" + run: cd android && bundle exec fastlane android production + build-linux: name: Build Linux App runs-on: ubuntu-latest @@ -348,6 +410,7 @@ jobs: body: | Release assets are published for all supported build targets: - Android (`.apk`) + - Google Play (`.aab`) - Linux (`.tar.gz`) - macOS (`.dmg`) - Windows (`.zip`) @@ -362,6 +425,7 @@ jobs: 4. Install the signed `.ipa` on your iPhone using Apple Configurator 2 or Xcode (Devices and Simulators). files: | dist/*.apk + dist/*.aab dist/*.tar.gz dist/*.zip dist/*.dmg diff --git a/android/Gemfile b/android/Gemfile new file mode 100644 index 0000000..859faf2 --- /dev/null +++ b/android/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem "fastlane" +gem "ostruct" diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index cd0233a..f18cbc5 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -16,44 +16,72 @@ default_platform(:android) platform :android do + def project_root + File.expand_path("../..", __dir__) + end + + def play_service_account_json + ENV.fetch("GOOGLE_PLAY_SERVICE_ACCOUNT_JSON") + end + + def play_build_command + "cd #{project_root} && flutter build appbundle --release" + end + + def play_aab_path + File.join(project_root, "build/app/outputs/bundle/release/app-release.aab") + end + + def play_package_name + "com.meshcore.sar.meshcore_sar_app" + end + + def direct_apk_build_command + "cd #{project_root} && flutter build apk --release" + end + + def direct_apk_path + File.join(project_root, "build/app/outputs/flutter-apk/app-release.apk") + end + + def skip_android_build? + ENV["SKIP_ANDROID_BUILD"] == "1" + end + desc "Runs all the tests" lane :test do gradle(task: "test") end desc "Build release APK" - lane :beta do - # Get the project root directory (two levels up from android/fastlane/) - project_root = File.expand_path("../..", __dir__) + lane :direct_apk do + sh(direct_apk_build_command) - # Build APK with Flutter - sh("cd #{project_root} && flutter build apk --release") - - apk_path = File.join(project_root, "build/app/outputs/flutter-apk/app-release.apk") - - # Uncomment to distribute via Firebase App Distribution: - # firebase_app_distribution( - # app: "YOUR_FIREBASE_APP_ID", - # apk_path: apk_path, - # groups: "testers" - # ) - - UI.success("APK built at: #{apk_path}") + UI.success("APK built successfully at: #{direct_apk_path}") end - desc "Deploy a new version to the Google Play" - lane :deploy do - # Get the project root directory (two levels up from android/fastlane/) - project_root = File.expand_path("../..", __dir__) - - # Build AAB with Flutter (required for Play Store since 2021) - sh("cd #{project_root} && flutter build appbundle --release") - - aab_path = File.join(project_root, "build/app/outputs/bundle/release/app-release.aab") + desc "Build Play release AAB and upload to internal testing" + lane :internal do + sh(play_build_command) unless skip_android_build? upload_to_play_store( - aab: aab_path, - track: "internal" + aab: play_aab_path, + package_name: play_package_name, + track: "internal", + release_status: "draft", + json_key_data: play_service_account_json + ) + end + + desc "Build Play release AAB and upload to production" + lane :production do + sh(play_build_command) unless skip_android_build? + + upload_to_play_store( + aab: play_aab_path, + package_name: play_package_name, + track: "production", + json_key_data: play_service_account_json ) end end diff --git a/docs/google-play-release.md b/docs/google-play-release.md new file mode 100644 index 0000000..03e73b2 --- /dev/null +++ b/docs/google-play-release.md @@ -0,0 +1,128 @@ +# Google Play Release Runbook + +## Purpose + +This runbook prepares and ships the Android release build for `com.meshcore.sar.meshcore_sar_app`. + +## Distribution Modes + +- GitHub release: signed APK for direct install or manual distribution. +- Google Play: signed AAB uploaded through Fastlane to Play internal testing and, for non-prerelease GitHub releases, production. + +## Required Secrets and Local Files + +GitHub Actions secrets for `dz0ny/meshcore-sar`: + +- `ANDROID_KEYSTORE_BASE64` +- `ANDROID_KEYSTORE_PASSWORD` +- `ANDROID_KEY_ALIAS` +- `ANDROID_KEY_PASSWORD` +- `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` + +Local files used to seed the secrets: + +- `android/key.properties` +- release keystore referenced by `android/key.properties` +- `/Users/dz0ny/android-keystores/fastlane-480919-0cb30c62db50.json` + +The Play Console service account must have release access for `com.meshcore.sar.meshcore_sar_app`. + +## CI Release Flow + +The release flow runs from `.github/workflows/build-artifacts.yml` when a GitHub release is published. + +Android release steps: + +1. Validate all Android signing and Google Play secrets. +2. Recreate `android/release-keystore.jks` and `android/key.properties` from GitHub secrets. +3. Build the signed APK with `flutter build apk --release`. +4. Build the signed Play AAB with `flutter build appbundle --release`. +5. Upload the APK and AAB as GitHub release assets. +6. Upload the AAB to Play internal testing with `bundle exec fastlane android internal`. +7. Upload the AAB to Play production when the GitHub release is not marked as prerelease. + +The same workflow also builds Linux, macOS, Windows, iOS unsigned, and web artifacts. + +## Versioning Rules + +- Version source of truth: `pubspec.yaml`. +- Android `versionCode` is the number after `+`. +- `versionCode` must always increase for Play uploads. +- `make bump`, `make build`, and `make bundle` increment the version. +- Use `make build-no-bump` or `make bundle-no-bump` only when intentionally rebuilding the same version locally. + +## Manual Build Commands + +Run commands from the repo root. + +```bash +flutter build apk --release +flutter build appbundle --release +``` + +Repo shortcuts: + +```bash +make build +make bundle +make build-no-bump +make bundle-no-bump +``` + +## Fastlane Lanes + +Run commands from `android/`. + +```bash +bundle exec fastlane android direct_apk +bundle exec fastlane android internal +bundle exec fastlane android production +``` + +Set `SKIP_ANDROID_BUILD=1` when an AAB has already been built and Fastlane should only upload it. + +```bash +SKIP_ANDROID_BUILD=1 GOOGLE_PLAY_SERVICE_ACCOUNT_JSON="$(cat /Users/dz0ny/android-keystores/fastlane-480919-0cb30c62db50.json)" bundle exec fastlane android internal +``` + +## Expected Artifacts + +- APK: `build/app/outputs/flutter-apk/app-release.apk` +- AAB: `build/app/outputs/bundle/release/app-release.aab` +- Release APK asset: `meshcore-sar--android.apk` +- Release AAB asset: `meshcore-sar--play.aab` + +## Internal Testing Release Checklist + +1. Confirm `pubspec.yaml` version is correct and `versionCode` increased. +2. Confirm `android/key.properties` points to the release keystore for local builds. +3. Confirm all five GitHub Actions secrets exist. +4. Publish a prerelease in GitHub to build assets and upload Play internal testing without production promotion. +5. Install from the internal testing track on a real Android device. +6. Smoke test startup, permissions, map, messaging, telemetry, and offline map behavior. + +## Production Release Checklist + +1. Complete internal testing validation first. +2. Confirm Play Console forms are current: + - App content + - Data safety + - App access + - Ads declaration + - Content rating +3. Confirm store assets are current: + - app icon + - feature graphic + - phone screenshots + - tablet screenshots if used + - support URL + - privacy policy URL +4. Publish a non-prerelease GitHub release. +5. Confirm the GitHub release has the APK and AAB assets. +6. Confirm Play Console has the new internal and production release entries. + +## Rollback Rules + +- Never reuse or lower an Android `versionCode`. +- If a Play release is bad, halt rollout in Play Console and ship a new higher-version fix. +- Keep GitHub direct APK and Play AAB artifacts separate; do not upload APKs to Play.