mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 08:20:36 +00:00
72
.github/workflows/build-artifacts.yml
vendored
72
.github/workflows/build-artifacts.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
@@ -44,6 +44,14 @@ jobs:
|
|||||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
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') }}
|
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
|
- name: Cache Android NDK
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -55,22 +63,76 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
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
|
- name: Build APK
|
||||||
run: flutter build apk --release
|
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: |
|
run: |
|
||||||
RAW_TAG="${{ github.event.release.tag_name || github.ref_name }}"
|
RAW_TAG="${{ github.event.release.tag_name || github.ref_name }}"
|
||||||
TAG="${RAW_TAG//\//-}"
|
TAG="${RAW_TAG//\//-}"
|
||||||
cp build/app/outputs/flutter-apk/app-release.apk "${APP_NAME}-${TAG}-android.apk"
|
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
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-android
|
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
|
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:
|
build-linux:
|
||||||
name: Build Linux App
|
name: Build Linux App
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -348,6 +410,7 @@ jobs:
|
|||||||
body: |
|
body: |
|
||||||
Release assets are published for all supported build targets:
|
Release assets are published for all supported build targets:
|
||||||
- Android (`.apk`)
|
- Android (`.apk`)
|
||||||
|
- Google Play (`.aab`)
|
||||||
- Linux (`.tar.gz`)
|
- Linux (`.tar.gz`)
|
||||||
- macOS (`.dmg`)
|
- macOS (`.dmg`)
|
||||||
- Windows (`.zip`)
|
- Windows (`.zip`)
|
||||||
@@ -362,6 +425,7 @@ jobs:
|
|||||||
4. Install the signed `.ipa` on your iPhone using Apple Configurator 2 or Xcode (Devices and Simulators).
|
4. Install the signed `.ipa` on your iPhone using Apple Configurator 2 or Xcode (Devices and Simulators).
|
||||||
files: |
|
files: |
|
||||||
dist/*.apk
|
dist/*.apk
|
||||||
|
dist/*.aab
|
||||||
dist/*.tar.gz
|
dist/*.tar.gz
|
||||||
dist/*.zip
|
dist/*.zip
|
||||||
dist/*.dmg
|
dist/*.dmg
|
||||||
|
|||||||
4
android/Gemfile
Normal file
4
android/Gemfile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
gem "fastlane"
|
||||||
|
gem "ostruct"
|
||||||
@@ -16,44 +16,72 @@
|
|||||||
default_platform(:android)
|
default_platform(:android)
|
||||||
|
|
||||||
platform :android do
|
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"
|
desc "Runs all the tests"
|
||||||
lane :test do
|
lane :test do
|
||||||
gradle(task: "test")
|
gradle(task: "test")
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Build release APK"
|
desc "Build release APK"
|
||||||
lane :beta do
|
lane :direct_apk do
|
||||||
# Get the project root directory (two levels up from android/fastlane/)
|
sh(direct_apk_build_command)
|
||||||
project_root = File.expand_path("../..", __dir__)
|
|
||||||
|
|
||||||
# Build APK with Flutter
|
UI.success("APK built successfully at: #{direct_apk_path}")
|
||||||
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}")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Deploy a new version to the Google Play"
|
desc "Build Play release AAB and upload to internal testing"
|
||||||
lane :deploy do
|
lane :internal do
|
||||||
# Get the project root directory (two levels up from android/fastlane/)
|
sh(play_build_command) unless skip_android_build?
|
||||||
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")
|
|
||||||
|
|
||||||
upload_to_play_store(
|
upload_to_play_store(
|
||||||
aab: aab_path,
|
aab: play_aab_path,
|
||||||
track: "internal"
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
128
docs/google-play-release.md
Normal file
128
docs/google-play-release.md
Normal 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.
|
||||||
Reference in New Issue
Block a user