chore: Add Play release cycle #31

ref:
This commit is contained in:
Janez T
2026-04-26 08:24:04 +02:00
parent 4181b33da1
commit a472b0c05c
4 changed files with 255 additions and 31 deletions

View File

@@ -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 <<EOF > 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

4
android/Gemfile Normal file
View File

@@ -0,0 +1,4 @@
source "https://rubygems.org"
gem "fastlane"
gem "ostruct"

View File

@@ -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

128
docs/google-play-release.md Normal file
View File

@@ -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-<tag>-android.apk`
- Release AAB asset: `meshcore-sar-<tag>-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.