diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7c4e91866..21e7bdc03 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -55,7 +55,7 @@ jobs:
# This will overflow Integer.MAX_VALUE in the year 6052, hopefully we'll have moved on by then.
run: echo "versionCode=$(( $(date +%s) / 60 ))" >> $GITHUB_OUTPUT
- build-fdroid:
+ release-google:
runs-on: ubuntu-latest
needs: prepare-build-info
steps:
@@ -79,54 +79,7 @@ jobs:
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
- - name: Load Fdroid secrets
- run: |
- echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
- echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
- env:
- KEYSTORE: ${{ secrets.KEYSTORE }}
- KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
- KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
-
- - name: Build F-Droid Release APK
- run: |
- ./gradlew :app:assembleFdroidRelease --parallel --continue --scan
- env:
- VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
- VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
-
- - name: Upload F-Droid APK artifact
- uses: actions/upload-artifact@v4
- with:
- name: fdroid-apk
- path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
- retention-days: 1
-
- build-google:
- runs-on: ubuntu-latest
- needs: prepare-build-info
- steps:
- - name: Checkout code
- uses: actions/checkout@v5
- with:
- fetch-depth: 0
- submodules: 'recursive'
-
- - name: Set up JDK 21
- uses: actions/setup-java@v5
- with:
- java-version: '21'
- distribution: 'jetbrains'
-
- - name: Setup Gradle
- uses: gradle/actions/setup-gradle@v4
- with:
- cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- build-scan-publish: true
- build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
- build-scan-terms-of-use-agree: 'yes'
-
- - name: Load Google secrets
+ - name: Load secrets
env:
GSERVICES: ${{ secrets.GSERVICES }}
KEYSTORE: ${{ secrets.KEYSTORE }}
@@ -135,6 +88,7 @@ jobs:
DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
+ GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
run: |
rm -f ./app/google-services.json # Ensure clean state
echo $GSERVICES > ./app/google-services.json
@@ -143,13 +97,33 @@ jobs:
echo "datadogApplicationId=$DATADOG_APPLICATION_ID" >> ./secrets.properties
echo "datadogClientToken=$DATADOG_CLIENT_TOKEN" >> ./secrets.properties
echo "MAPS_API_KEY=$GOOGLE_MAPS_API_KEY" >> ./secrets.properties
+ echo "$GOOGLE_PLAY_JSON_KEY" > ./fastlane/play-store-credentials.json
- - name: Build Google Release Artifacts (AAB and APK)
+ - name: Setup Fastlane
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '3.2'
+ bundler-cache: true
+
+ - name: Determine Fastlane Lane
+ id: fastlane_lane
run: |
- ./gradlew :app:bundleGoogleRelease :app:assembleGoogleRelease --parallel --continue --scan
+ TAG_NAME="${{ github.ref_name }}"
+ if [[ "$TAG_NAME" == *"-internal"* ]]; then
+ echo "lane=internal" >> $GITHUB_OUTPUT
+ elif [[ "$TAG_NAME" == *"-closed"* ]]; then
+ echo "lane=closed" >> $GITHUB_OUTPUT
+ elif [[ "$TAG_NAME" == *"-open"* ]]; then
+ echo "lane=open" >> $GITHUB_OUTPUT
+ else
+ echo "lane=production" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Build and Deploy Google Play Tracks with Fastlane
env:
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
+ run: bundle exec fastlane ${{ steps.fastlane_lane.outputs.lane }}
- name: Upload Google AAB artifact
uses: actions/upload-artifact@v4
@@ -165,21 +139,9 @@ jobs:
path: app/build/outputs/apk/google/release/app-google-release.apk
retention-days: 1
- - name: Upload Mapping File
- uses: actions/upload-artifact@v4
- with:
- name: mapping
- path: app/build/outputs/mapping/googleRelease/mapping.txt
- retention-days: 1
-
- publish-release:
+ release-fdroid:
runs-on: ubuntu-latest
- needs: [prepare-build-info, build-fdroid, build-google]
- outputs:
- RELEASE_UPLOAD_URL: ${{ steps.create_gh_release.outputs.upload_url }}
- CHANGELOG: ${{ steps.generate_changelog.outputs.changelog }}
- APP_VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
- APP_VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
+ needs: prepare-build-info
steps:
- name: Checkout code
uses: actions/checkout@v5
@@ -187,34 +149,69 @@ jobs:
fetch-depth: 0
submodules: 'recursive'
- - name: Download F-Droid APK
- uses: actions/download-artifact@v5
+ - name: Set up JDK 21
+ uses: actions/setup-java@v5
+ with:
+ java-version: '21'
+ distribution: 'jetbrains'
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v4
+ with:
+ cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
+ build-scan-publish: true
+ build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
+ build-scan-terms-of-use-agree: 'yes'
+
+ - name: Load secrets
+ env:
+ KEYSTORE: ${{ secrets.KEYSTORE }}
+ KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
+ KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
+ run: |
+ echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
+ echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
+
+ - name: Setup Fastlane
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '3.2'
+ bundler-cache: true
+
+ - name: Build F-Droid with Fastlane
+ env:
+ VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
+ VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
+ run: bundle exec fastlane fdroid_build
+
+ - name: Upload F-Droid APK artifact
+ uses: actions/upload-artifact@v4
with:
name: fdroid-apk
- path: ./build-artifacts/fdroid
+ path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
+ retention-days: 1
+ finalize-release:
+ runs-on: ubuntu-latest
+ needs: [release-google, release-fdroid]
+ steps:
- name: Download Google AAB
- uses: actions/download-artifact@v5
+ uses: actions/download-artifact@v4
with:
name: google-aab
- path: ./build-artifacts/google/bundle
+ path: ./google/bundle
- name: Download Google APK
- uses: actions/download-artifact@v5
+ uses: actions/download-artifact@v4
with:
name: google-apk
- path: ./build-artifacts/google/apk
+ path: ./google/apk
- - name: Download Mapping File
- uses: actions/download-artifact@v5
+ - name: Download F-Droid APK
+ uses: actions/download-artifact@v4
with:
- name: mapping
- path: ./build-artifacts/google/mapping
-
- - name: Create version_info.txt
- run: |
- echo "versionNameBase=${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}" > ./version_info.txt
- echo "versionCode=${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}" >> ./version_info.txt
+ name: fdroid-apk
+ path: ./fdroid
- name: Create GitHub Release
id: create_gh_release
@@ -224,56 +221,10 @@ jobs:
name: Release ${{ github.ref_name }}
generate_release_notes: true
files: |
- ./build-artifacts/google/bundle/app-google-release.aab
- ./build-artifacts/google/apk/app-google-release.apk
- ./build-artifacts/fdroid/app-fdroid-release.apk
- ./version_info.txt
+ ./google/bundle/app-google-release.aab
+ ./google/apk/app-google-release.apk
+ ./fdroid/app-fdroid-release.apk
draft: true
prerelease: ${{ contains(github.ref_name, '-internal') || contains(github.ref_name, '-closed') || contains(github.ref_name, '-open') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Create Play Store whatsnew File
- run: |
- mkdir -p whatsnew
- echo "For detailed release notes, please visit: ${{ steps.create_gh_release.outputs.url }}" > whatsnew/whatsnew-en-US
- shell: bash
-
- # Attest the build artifacts for supply chain security.
- # See: https://github.com/meshtastic/Meshtastic-Android/attestations
- - name: Attest Build Provenance
- uses: actions/attest-build-provenance@v3
- with:
- subject-path: |
- ./build-artifacts/google/bundle/app-google-release.aab
- ./build-artifacts/google/apk/app-google-release.apk
- ./build-artifacts/fdroid/app-fdroid-release.apk
-
- - name: Determine Play Store Track
- id: play_track
- run: |
- TAG_NAME="${{ github.ref_name }}"
- if [[ "$TAG_NAME" == *"-internal"* ]]; then
- echo "track=internal" >> $GITHUB_OUTPUT
- elif [[ "$TAG_NAME" == *"-closed"* ]]; then
- echo "track=NewAlpha" >> $GITHUB_OUTPUT
- elif [[ "$TAG_NAME" == *"-open"* ]]; then
- echo "track=beta" >> $GITHUB_OUTPUT
- else
- echo "track=production" >> $GITHUB_OUTPUT
- echo "user_fraction=0.1" >> $GITHUB_OUTPUT
- echo "status=inProgress" >> $GITHUB_OUTPUT
- fi
-
- - name: Upload to Google Play
- if: success() && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
- uses: r0adkll/upload-google-play@v1.1.3
- with:
- serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
- packageName: com.geeksville.mesh
- releaseFiles: ./build-artifacts/google/bundle/app-google-release.aab
- track: ${{ steps.play_track.outputs.track }}
- status: ${{ steps.play_track.outputs.status || (steps.play_track.outputs.track == 'internal' && 'completed' || 'draft') }}
- userFraction: ${{ steps.play_track.outputs.userFraction }}
- whatsNewDirectory: ./whatsnew/
- mappingFile: ./build-artifacts/google/mapping/mapping.txt
diff --git a/.gitignore b/.gitignore
index a8fe89f77..ba935995c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,5 @@ keystore.properties
# Secrets
/secrets.properties
+/fastlane/play-store-credentials.json
+/fastlane/report.xml
diff --git a/.ruby-version b/.ruby-version
new file mode 100644
index 000000000..a3ec5a4bd
--- /dev/null
+++ b/.ruby-version
@@ -0,0 +1 @@
+3.2
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 000000000..7a118b49b
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,3 @@
+source "https://rubygems.org"
+
+gem "fastlane"
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 000000000..8a8905f02
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,229 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ CFPropertyList (3.0.7)
+ base64
+ nkf
+ rexml
+ addressable (2.8.7)
+ public_suffix (>= 2.0.2, < 7.0)
+ artifactory (3.0.17)
+ atomos (0.1.3)
+ aws-eventstream (1.4.0)
+ aws-partitions (1.1157.0)
+ aws-sdk-core (3.232.0)
+ aws-eventstream (~> 1, >= 1.3.0)
+ aws-partitions (~> 1, >= 1.992.0)
+ aws-sigv4 (~> 1.9)
+ base64
+ bigdecimal
+ jmespath (~> 1, >= 1.6.1)
+ logger
+ aws-sdk-kms (1.112.0)
+ aws-sdk-core (~> 3, >= 3.231.0)
+ aws-sigv4 (~> 1.5)
+ aws-sdk-s3 (1.199.0)
+ aws-sdk-core (~> 3, >= 3.231.0)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.5)
+ aws-sigv4 (1.12.1)
+ aws-eventstream (~> 1, >= 1.0.2)
+ babosa (1.0.4)
+ base64 (0.3.0)
+ bigdecimal (3.2.3)
+ claide (1.1.0)
+ colored (1.2)
+ colored2 (3.1.2)
+ commander (4.6.0)
+ highline (~> 2.0.0)
+ declarative (0.0.20)
+ digest-crc (0.7.0)
+ rake (>= 12.0.0, < 14.0.0)
+ domain_name (0.6.20240107)
+ dotenv (2.8.1)
+ emoji_regex (3.2.3)
+ excon (0.112.0)
+ faraday (1.10.4)
+ faraday-em_http (~> 1.0)
+ faraday-em_synchrony (~> 1.0)
+ faraday-excon (~> 1.1)
+ faraday-httpclient (~> 1.0)
+ faraday-multipart (~> 1.0)
+ faraday-net_http (~> 1.0)
+ faraday-net_http_persistent (~> 1.0)
+ faraday-patron (~> 1.0)
+ faraday-rack (~> 1.0)
+ faraday-retry (~> 1.0)
+ ruby2_keywords (>= 0.0.4)
+ faraday-cookie_jar (0.0.7)
+ faraday (>= 0.8.0)
+ http-cookie (~> 1.0.0)
+ faraday-em_http (1.0.0)
+ faraday-em_synchrony (1.0.1)
+ faraday-excon (1.1.0)
+ faraday-httpclient (1.0.1)
+ faraday-multipart (1.1.1)
+ multipart-post (~> 2.0)
+ faraday-net_http (1.0.2)
+ faraday-net_http_persistent (1.2.0)
+ faraday-patron (1.0.0)
+ faraday-rack (1.0.0)
+ faraday-retry (1.0.3)
+ faraday_middleware (1.2.1)
+ faraday (~> 1.0)
+ fastimage (2.4.0)
+ fastlane (2.228.0)
+ CFPropertyList (>= 2.3, < 4.0.0)
+ addressable (>= 2.8, < 3.0.0)
+ artifactory (~> 3.0)
+ aws-sdk-s3 (~> 1.0)
+ babosa (>= 1.0.3, < 2.0.0)
+ bundler (>= 1.12.0, < 3.0.0)
+ colored (~> 1.2)
+ commander (~> 4.6)
+ dotenv (>= 2.1.1, < 3.0.0)
+ emoji_regex (>= 0.1, < 4.0)
+ excon (>= 0.71.0, < 1.0.0)
+ faraday (~> 1.0)
+ faraday-cookie_jar (~> 0.0.6)
+ faraday_middleware (~> 1.0)
+ fastimage (>= 2.1.0, < 3.0.0)
+ fastlane-sirp (>= 1.0.0)
+ gh_inspector (>= 1.1.2, < 2.0.0)
+ google-apis-androidpublisher_v3 (~> 0.3)
+ google-apis-playcustomapp_v1 (~> 0.1)
+ google-cloud-env (>= 1.6.0, < 2.0.0)
+ google-cloud-storage (~> 1.31)
+ highline (~> 2.0)
+ http-cookie (~> 1.0.5)
+ json (< 3.0.0)
+ jwt (>= 2.1.0, < 3)
+ mini_magick (>= 4.9.4, < 5.0.0)
+ multipart-post (>= 2.0.0, < 3.0.0)
+ naturally (~> 2.2)
+ optparse (>= 0.1.1, < 1.0.0)
+ plist (>= 3.1.0, < 4.0.0)
+ rubyzip (>= 2.0.0, < 3.0.0)
+ security (= 0.1.5)
+ simctl (~> 1.6.3)
+ terminal-notifier (>= 2.0.0, < 3.0.0)
+ terminal-table (~> 3)
+ tty-screen (>= 0.6.3, < 1.0.0)
+ tty-spinner (>= 0.8.0, < 1.0.0)
+ word_wrap (~> 1.0.0)
+ xcodeproj (>= 1.13.0, < 2.0.0)
+ xcpretty (~> 0.4.1)
+ xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
+ fastlane-sirp (1.0.0)
+ sysrandom (~> 1.0)
+ gh_inspector (1.1.3)
+ google-apis-androidpublisher_v3 (0.54.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-core (0.11.3)
+ addressable (~> 2.5, >= 2.5.1)
+ googleauth (>= 0.16.2, < 2.a)
+ httpclient (>= 2.8.1, < 3.a)
+ mini_mime (~> 1.0)
+ representable (~> 3.0)
+ retriable (>= 2.0, < 4.a)
+ rexml
+ google-apis-iamcredentials_v1 (0.17.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-playcustomapp_v1 (0.13.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-storage_v1 (0.31.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-cloud-core (1.8.0)
+ google-cloud-env (>= 1.0, < 3.a)
+ google-cloud-errors (~> 1.0)
+ google-cloud-env (1.6.0)
+ faraday (>= 0.17.3, < 3.0)
+ google-cloud-errors (1.5.0)
+ google-cloud-storage (1.47.0)
+ addressable (~> 2.8)
+ digest-crc (~> 0.4)
+ google-apis-iamcredentials_v1 (~> 0.1)
+ google-apis-storage_v1 (~> 0.31.0)
+ google-cloud-core (~> 1.6)
+ googleauth (>= 0.16.2, < 2.a)
+ mini_mime (~> 1.0)
+ googleauth (1.8.1)
+ faraday (>= 0.17.3, < 3.a)
+ jwt (>= 1.4, < 3.0)
+ multi_json (~> 1.11)
+ os (>= 0.9, < 2.0)
+ signet (>= 0.16, < 2.a)
+ highline (2.0.3)
+ http-cookie (1.0.8)
+ domain_name (~> 0.5)
+ httpclient (2.9.0)
+ mutex_m
+ jmespath (1.6.2)
+ json (2.13.2)
+ jwt (2.10.2)
+ base64
+ logger (1.7.0)
+ mini_magick (4.13.2)
+ mini_mime (1.1.5)
+ multi_json (1.17.0)
+ multipart-post (2.4.1)
+ mutex_m (0.3.0)
+ nanaimo (0.4.0)
+ naturally (2.3.0)
+ nkf (0.2.0)
+ optparse (0.6.0)
+ os (1.1.4)
+ plist (3.7.2)
+ public_suffix (6.0.2)
+ rake (13.3.0)
+ representable (3.2.0)
+ declarative (< 0.1.0)
+ trailblazer-option (>= 0.1.1, < 0.2.0)
+ uber (< 0.2.0)
+ retriable (3.1.2)
+ rexml (3.4.3)
+ rouge (3.28.0)
+ ruby2_keywords (0.0.5)
+ rubyzip (2.4.1)
+ security (0.1.5)
+ signet (0.21.0)
+ addressable (~> 2.8)
+ faraday (>= 0.17.5, < 3.a)
+ jwt (>= 1.5, < 4.0)
+ multi_json (~> 1.10)
+ simctl (1.6.10)
+ CFPropertyList
+ naturally
+ sysrandom (1.0.5)
+ terminal-notifier (2.0.0)
+ terminal-table (3.0.2)
+ unicode-display_width (>= 1.1.1, < 3)
+ trailblazer-option (0.1.2)
+ tty-cursor (0.7.1)
+ tty-screen (0.8.2)
+ tty-spinner (0.9.3)
+ tty-cursor (~> 0.7)
+ uber (0.1.0)
+ unicode-display_width (2.6.0)
+ word_wrap (1.0.0)
+ xcodeproj (1.27.0)
+ CFPropertyList (>= 2.3.3, < 4.0)
+ atomos (~> 0.1.3)
+ claide (>= 1.0.2, < 2.0)
+ colored2 (~> 3.1)
+ nanaimo (~> 0.4.0)
+ rexml (>= 3.3.6, < 4.0)
+ xcpretty (0.4.1)
+ rouge (~> 3.28.0)
+ xcpretty-travis-formatter (1.0.1)
+ xcpretty (~> 0.2, >= 0.0.7)
+
+PLATFORMS
+ arm64-darwin-24
+ ruby
+
+DEPENDENCIES
+ fastlane
+
+BUNDLED WITH
+ 2.6.9
diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md
index 3661d1692..8eff7360b 100644
--- a/RELEASE_PROCESS.md
+++ b/RELEASE_PROCESS.md
@@ -1,92 +1,131 @@
# Meshtastic-Android Release Process
-This document outlines the steps for releasing a new version of the Meshtastic-Android application. Adhering to this process ensures consistency and helps manage the release lifecycle, leveraging automation via the `release.yml` GitHub Action.
+This document outlines the steps for releasing a new version of the Meshtastic-Android application. The process is heavily automated using GitHub Actions and Fastlane, triggered by pushing a Git tag from a `release/*` branch.
-**Note on Automation:** The `release.yml` GitHub Action is primarily triggered by **pushing a Git tag** matching the pattern `v*` (e.g., `v1.2.3`, `v1.2.3-open.1`). It can also be manually triggered via `workflow_dispatch` from the GitHub Actions UI.
+## High-Level Overview
-The workflow uses a simple and robust **"upload-only"** model. It automatically:
-* Determines a `versionName` from the Git tag.
-* Generates a unique, always-increasing `versionCode` based on the number of minutes since the Unix epoch. This prevents `versionCode` conflicts and will not overflow until the year 6052.
-* Builds fresh F-Droid (APK) and Google (AAB, APK) artifacts for every run.
-* Creates a **draft GitHub Release** and attaches the artifacts.
-* Attests build provenance for the artifacts.
-* **Uploads** the newly built AAB directly to the appropriate track in the Google Play Console based on the tag.
+The automation is designed to be safe, repeatable, and efficient. When a new tag matching the `v*` pattern is pushed from a release branch, the `release.yml` GitHub Action workflow will:
-There is no promotion of builds between tracks; every release is a new, independent upload. Finalizing and publishing the GitHub Release and the Google Play Store submission remain **manual steps**.
+1. **Determine Versioning:** A `versionName` is derived from the Git tag, and a unique, always-increasing `versionCode` is generated from the current timestamp.
+2. **Build in Parallel:** Two jobs run simultaneously to build the `google` and `fdroid` flavors of the app.
+3. **Deploy to Google Play:** The `google` build job uses Fastlane to automatically upload the Android App Bundle (AAB) to the correct track on the Google Play Console, based on the tag name.
+4. **Create a Draft GitHub Release:** After both builds are complete, a final job gathers the artifacts (AAB, Google APK, and F-Droid APK) and creates a single, consolidated **draft** release on GitHub.
-## Prerequisites
+Finalizing and publishing the release on both GitHub and the Google Play Console are the only **manual steps**.
-Before initiating the release process, ensure the following are completed:
+## Versioning and Tagging Strategy
-1. **Main Branch Stability:** The `main` branch (or your chosen release branch) must be stable, with all features and bug fixes intended for the release merged and thoroughly tested.
-2. **Automated Testing:** All automated tests must be passing.
-3. **Versioning and Tagging Strategy:**
- * Tags **must** start with `v` and follow Semantic Versioning (e.g., `vX.X.X`).
- * Use the correct suffixes for the desired release track:
- * **Internal/QA:** `vX.X.X-internal.Y`
- * **Closed Alpha:** `vX.X.X-closed.Y`
- * **Open Alpha/Beta:** `vX.X.X-open.Y`
- * **Production:** `vX.X.X` (no suffix)
- * **Recommendation:** Before tagging, update `VERSION_NAME_BASE` in `buildSrc/src/main/kotlin/Configs.kt` to match the `X.X.X` part of your tag. This ensures consistency for local development builds.
+The entire process is driven by your Git tagging strategy. Tags **must** start with `v` and should follow Semantic Versioning. Use the correct suffix for the desired release track:
-## Core Release Workflow: Triggering via Tag Push
+* **Internal Track:** `vX.X.X-internal.Y` (e.g., `v2.3.5-internal.1`)
+* **Closed Track:** `vX.X.X-closed.Y` (e.g., `v2.3.5-closed.1`)
+* **Open Track:** `vX.X.X-open.Y` (e.g., `v2.3.5-open.1`)
+* **Production Track:** `vX.X.X` (e.g., `v2.3.5`)
-1. **Create and push a tag for the desired release track.**
- ```bash
- # This build will be uploaded and rolled out on the 'internal' track
- git tag v1.2.3-internal.1
- git push origin v1.2.3-internal.1
- ```
-2. **Wait for the workflow to complete.**
-3. **Verify the build** in the Google Play Console and with testers.
-4. When ready to advance to the next track, create and push a new tag.
- ```bash
- # This will create and upload a NEW build to the 'NewAlpha' (closed alpha) track
- git tag v1.2.3-closed.1
- git push origin v1.2.3-closed.1
- ```
+The `.Y` suffix is for iterations. If you find a bug in `v2.3.5-closed.1`, you would fix it on the release branch and tag the new commit as `v2.3.5-closed.2`.
-## Iterating on a Bad Build
+## Core Release Workflow
-If you discover a critical bug in a build, the process is simple:
+The entire release process happens on a dedicated release branch, allowing `main` to remain open for new feature development.
-1. **Fix the Code:** Merge the necessary bug fixes into your main branch.
-2. **Create a New Iteration Tag:** Create a new tag for the same release phase, simply incrementing the final number.
- ```bash
- # If v1.2.3-internal.1 was bad, the new build is v1.2.3-internal.2
- git tag v1.2.3-internal.2
- git push origin v1.2.3-internal.2
- ```
-3. **A New Build is Uploaded:** The workflow will run, generate a new epoch-minute-based `versionCode`, and upload a fresh build to the `internal` track. There is no risk of a `versionCode` collision.
+### 1. Creating the Release Branch
+First, create a `release/X.X.X` branch from a stable `main`. This branch is now "feature frozen." Only critical bug fixes should be added.
-## Managing Different Release Phases (Manual Steps Post-Workflow)
+As a housekeeping step, it's recommended to update the `VERSION_NAME_BASE` in `buildSrc/src/main/kotlin/Configs.kt` on this new branch. While the final release version is set by the Git tag in CI, this ensures local development builds have a sensible version name.
-After the `release.yml` workflow completes, manual actions are needed on GitHub and in the Google Play Console.
+```bash
+git checkout main
+git pull origin main
+git checkout -b release/2.3.5
+# (Now, update the version in buildSrc, commit the change, and then push)
+git push origin release/2.3.5
+```
-### Phase 1: Internal / QA Release
-* **Tag format:** `vX.X.X-internal.Y`
-* **Automated Action:** The AAB is **uploaded** to the `internal` track and rolled out automatically.
-* **Manual Steps:**
- 1. **GitHub:** Find the **draft release**, verify artifacts, and publish it if desired.
- 2. **Google Play Console:** Verify the release has been successfully rolled out to internal testers.
+### 2. Testing and Iterating on a Track
+Start by deploying to the `internal` track to begin testing.
-### Phase 2: Closed Alpha Release
-* **Tag format:** `vX.X.X-closed.Y`
-* **Automated Action:** A new AAB is built and **uploaded** as a **draft** to the `NewAlpha` track.
-* **Manual Steps:**
- 1. **GitHub:** Find and publish the **draft release**.
- 2. **Google Play Console:** Manually review the draft release and submit it for your closed alpha testers.
+**A. Create and Push a Tag:**
+Tag a commit on the release branch to trigger the automation.
+```bash
+# Ensure you are on the release branch
+git checkout release/2.3.5
-### Phase 3: Open Alpha / Beta Release
-* **Tag format:** `vX.X.X-open.Y`
-* **Automated Action:** A new AAB is built and **uploaded** as a **draft** to the `beta` track.
-* **Manual Steps:**
- 1. **GitHub:** Find and publish the **draft pre-release**.
- 2. **Google Play Console:** Manually review the draft, add release notes, and submit it.
+# Tag for the "Internal" track
+git tag v2.3.5-internal.1
+git push origin v2.3.5-internal.1
+```
-### Phase 4: Production Release
-* **Tag format:** `vX.X.X`
-* **Automated Action:** A new AAB is built and **uploaded** to the `production` track. By default, it is configured for a 10% staged rollout.
-* **Manual Steps:**
- 1. **GitHub:** Find the **draft release**. **Crucially, uncheck "This is a pre-release"** before publishing.
- 2. **Google Play Console:** Manually review the release, add release notes, and **start the staged rollout**.
+**B. Monitor and Verify:**
+Monitor the workflow in GitHub Actions. Once complete, verify the build in the Google Play Console and with your internal testers.
+
+**C. Apply Fixes (If Necessary):**
+If a bug is found, commit the fix to the release branch. Remember to also cherry-pick or merge this fix back to `main`. Then, create an iterated tag.
+```bash
+# Assuming you've committed a fix
+git tag v2.3.5-internal.2
+git push origin v2.3.5-internal.2
+```
+This will upload a new, fixed build to the same `internal` track. Repeat this process until the build is stable.
+
+### 3. Promoting to the Next Track
+Once you are confident that a build is stable, you can "promote" it to a wider audience by tagging the **exact same commit** for the next track.
+
+```bash
+# The commit tagged as v2.3.5-internal.2 is stable and ready for the "Closed" track
+git tag v2.3.5-closed.1
+git push origin v2.3.5-closed.1
+```
+This triggers the workflow again, but this time it will send the build to the `NewAlpha` track for your closed testers. You can then continue the cycle of testing, fixing, and promoting all the way to production.
+
+### 4. Merging Back to `main`
+After the final production release is complete and verified, merge the release branch back into `main` to ensure any hotfixes are included. Then, delete the release branch.
+```bash
+git checkout main
+git pull origin main
+git merge release/2.3.5
+git push origin main
+git branch -d release/2.3.5
+git push origin --delete release/2.3.5
+```
+
+## Manual Finalization Steps
+
+### For Internal Releases
+
+* **Automated Action:** The AAB is uploaded to the `internal` track and is **rolled out to 100% of testers automatically**.
+* **Your Manual Step:**
+ 1. **Verify the build** in the Google Play Console and with your internal testers.
+
+### For Closed Releases
+
+* **Automated Action:** The AAB is uploaded to the `NewAlpha` track and is **rolled out to 100% of testers automatically**.
+* **Your Manual Step:**
+ 1. **Verify the build** in the Google Play Console and with your closed track testers.
+
+### For Open Releases
+
+* **Automated Action:** The AAB is uploaded to the `beta` track and begins a **staged rollout to 25% of your open track testers automatically**.
+* **Your Manual Steps:**
+ 1. **Verify the build** in the Google Play Console.
+ 2. **(Optional)** Go to the GitHub "Releases" page, find the **draft release**, and publish it so your open track testers can see the official release notes.
+
+### For Production Releases
+
+* **Automated Action:**
+ * The AAB is uploaded to the `production` track in a **draft** state. It is **not** rolled out to any users.
+ * A corresponding **draft** release is created on GitHub with all build artifacts.
+* **Your Manual Steps:**
+ 1. **Publish on GitHub First:** Go to the GitHub "Releases" page and find the draft. Review the release notes and artifacts, then **Publish release**. This makes the release notes publicly visible.
+ 2. **Promote on Google Play Second:** *After* publishing on GitHub, go to your Google Play Console. Find the draft release, review it, and then proceed to **start the rollout to production**.
+
+## Monitoring the Release
+
+After a release has been rolled out to users (especially for Open and Production), it is crucial to monitor its performance.
+
+* **Google Play Console:** Keep a close eye on the **Vitals** section for your app. Pay special attention to the crash rate and ANR (Application Not Responding) rate. A sudden spike in these numbers is a strong indicator of a problem.
+* **Datadog:** Check your Datadog dashboards for any unusual trends or new errors that may have been introduced with the release.
+* **Crashlytics:** Review crash reports in Firebase Crashlytics to identify any new issues that users may be experiencing.
+* **User Reviews:** Monitor user reviews on the Google Play Store for any negative feedback or reports of bugs.
+* **Community Feedback:** Monitor Discord, GitHub Issues, and community forums for feedback from users who have received the update.
+
+If you identify a critical issue, be prepared to halt the rollout in the Google Play Console and tag a new, fixed version.
diff --git a/app/src/main/play/listings/en-US/full-description.txt b/app/src/main/play/listings/en-US/full-description.txt
deleted file mode 100644
index 07c4e52a6..000000000
--- a/app/src/main/play/listings/en-US/full-description.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-This is a beta release of the meshtastic.org project. We'd love you to try it and tell us what you think. You'll need to buy an inexpensive ($30ish) radio from a variety of vendors to use this application, see our website for details. We don't make these devices.
-
-***Please*** if you encounter problems or have questions: post on our forum at https://github.com/orgs/meshtastic/discussions and we'll work together to fix them (we are volunteer hobbyists). We would really appreciate good Google reviews if you think this is a good project.
diff --git a/app/src/main/play/listings/en-US/graphics/icon/1.png b/app/src/main/play/listings/en-US/graphics/icon/1.png
deleted file mode 100644
index c4069fac7..000000000
Binary files a/app/src/main/play/listings/en-US/graphics/icon/1.png and /dev/null differ
diff --git a/app/src/main/play/listings/en-US/short-description.txt b/app/src/main/play/listings/en-US/short-description.txt
deleted file mode 100644
index 7d1be0bfd..000000000
--- a/app/src/main/play/listings/en-US/short-description.txt
+++ /dev/null
@@ -1 +0,0 @@
-An inexpensive open-source GPS mesh radio for hiking, skiing, flying, marching.
diff --git a/app/src/main/play/listings/en-US/title.txt b/app/src/main/play/listings/en-US/title.txt
deleted file mode 100644
index efef08b61..000000000
--- a/app/src/main/play/listings/en-US/title.txt
+++ /dev/null
@@ -1 +0,0 @@
-Meshtastic
diff --git a/app/src/main/play/release-notes/en-US/alpha.txt b/app/src/main/play/release-notes/en-US/alpha.txt
deleted file mode 100644
index f87731416..000000000
--- a/app/src/main/play/release-notes/en-US/alpha.txt
+++ /dev/null
@@ -1 +0,0 @@
-For more information visit meshtastic.org. This application is made by volunteers. We are friendly and actively respond to forum posts with any questions you have. Post at https://github.com/orgs/meshtastic/discussions and we'll help.
diff --git a/app/src/main/play/release-notes/en-US/beta.txt b/app/src/main/play/release-notes/en-US/beta.txt
deleted file mode 100644
index f87731416..000000000
--- a/app/src/main/play/release-notes/en-US/beta.txt
+++ /dev/null
@@ -1 +0,0 @@
-For more information visit meshtastic.org. This application is made by volunteers. We are friendly and actively respond to forum posts with any questions you have. Post at https://github.com/orgs/meshtastic/discussions and we'll help.
diff --git a/app/src/main/play/release-notes/en-US/internal.txt b/app/src/main/play/release-notes/en-US/internal.txt
deleted file mode 100644
index 322aae877..000000000
--- a/app/src/main/play/release-notes/en-US/internal.txt
+++ /dev/null
@@ -1 +0,0 @@
-An internal build
diff --git a/app/src/main/play/release-notes/en-US/production.txt b/app/src/main/play/release-notes/en-US/production.txt
deleted file mode 100644
index f87731416..000000000
--- a/app/src/main/play/release-notes/en-US/production.txt
+++ /dev/null
@@ -1 +0,0 @@
-For more information visit meshtastic.org. This application is made by volunteers. We are friendly and actively respond to forum posts with any questions you have. Post at https://github.com/orgs/meshtastic/discussions and we'll help.
diff --git a/fastlane/Appfile b/fastlane/Appfile
new file mode 100644
index 000000000..619a4638e
--- /dev/null
+++ b/fastlane/Appfile
@@ -0,0 +1,2 @@
+json_key_file("./fastlane/play-store-credentials.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
+package_name("com.geeksville.mesh") # e.g. com.krausefx.app
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
new file mode 100644
index 000000000..b68ab9dd3
--- /dev/null
+++ b/fastlane/Fastfile
@@ -0,0 +1,86 @@
+# This file contains the fastlane.tools configuration
+# You can find the documentation at https://docs.fastlane.tools
+#
+# For a list of all available actions, check out
+#
+# https://docs.fastlane.tools/actions
+#
+# For a list of all available plugins, check out
+#
+# https://docs.fastlane.tools/plugins/available-plugins
+#
+
+# Uncomment the line if you want fastlane to automatically update itself
+# update_fastlane
+
+default_platform(:android)
+
+platform :android do
+ desc "Runs all the tests"
+ lane :test do
+ gradle(task: "test")
+ end
+
+ desc "Deploy a new version to the internal track on Google Play"
+ lane :internal do
+ aab_path = build_google_release
+ upload_to_play_store(
+ track: 'internal',
+ aab: aab_path,
+ release_status: 'completed'
+ )
+ end
+
+ desc "Deploy a new version to the closed track on Google Play"
+ lane :closed do
+ aab_path = build_google_release
+ upload_to_play_store(
+ track: 'NewAlpha',
+ aab: aab_path,
+ release_status: 'completed'
+ )
+ end
+
+ desc "Deploy a new version to the open track on Google Play"
+ lane :open do
+ aab_path = build_google_release
+ upload_to_play_store(
+ track: 'beta',
+ aab: aab_path,
+ release_status: 'inProgress',
+ rollout: '0.25'
+ )
+ end
+
+ desc "Deploy a new version to the production track on Google Play"
+ lane :production do
+ aab_path = build_google_release
+ upload_to_play_store(
+ track: 'production',
+ aab: aab_path,
+ release_status: 'draft'
+ )
+ end
+
+ desc "Build the F-Droid release"
+ lane :fdroid_build do
+ gradle(
+ task: "clean assembleFdroidRelease",
+ properties: {
+ "android.injected.version.name" => ENV['VERSION_NAME'],
+ "android.injected.version.code" => ENV['VERSION_CODE']
+ }
+ )
+ end
+
+ private_lane :build_google_release do
+ gradle(
+ task: "clean bundleGoogleRelease assembleGoogleRelease",
+ print_command: false,
+ properties: {
+ "android.injected.version.name" => ENV['VERSION_NAME'],
+ "android.injected.version.code" => ENV['VERSION_CODE']
+ }
+ )
+ end
+end
diff --git a/fastlane/README.md b/fastlane/README.md
new file mode 100644
index 000000000..7ec1207f1
--- /dev/null
+++ b/fastlane/README.md
@@ -0,0 +1,48 @@
+fastlane documentation
+----
+
+# Installation
+
+Make sure you have the latest version of the Xcode command line tools installed:
+
+```sh
+xcode-select --install
+```
+
+For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
+
+# Available Actions
+
+## Android
+
+### android test
+
+```sh
+[bundle exec] fastlane android test
+```
+
+Runs all the tests
+
+### android beta
+
+```sh
+[bundle exec] fastlane android beta
+```
+
+Submit a new Beta Build to Crashlytics Beta
+
+### android deploy
+
+```sh
+[bundle exec] fastlane android deploy
+```
+
+Deploy a new version to the Google Play
+
+----
+
+This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
+
+More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
+
+The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
diff --git a/app/src/main/play/contact-email.txt b/fastlane/metadata/android/contact-email.txt
similarity index 100%
rename from app/src/main/play/contact-email.txt
rename to fastlane/metadata/android/contact-email.txt
diff --git a/app/src/main/play/contact-website.txt b/fastlane/metadata/android/contact-website.txt
similarity index 100%
rename from app/src/main/play/contact-website.txt
rename to fastlane/metadata/android/contact-website.txt
diff --git a/app/src/main/play/default-language.txt b/fastlane/metadata/android/default-language.txt
similarity index 100%
rename from app/src/main/play/default-language.txt
rename to fastlane/metadata/android/default-language.txt
diff --git a/fastlane/metadata/android/en-US/changelogs/default.txt b/fastlane/metadata/android/en-US/changelogs/default.txt
new file mode 100644
index 000000000..0553de284
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/default.txt
@@ -0,0 +1 @@
+For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
new file mode 100644
index 000000000..84a470095
--- /dev/null
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -0,0 +1,21 @@
+Meshtastic is a tool for using Android devices with open-source, off-grid mesh radios. This app is the main client for the Meshtastic project, allowing you to manage your mesh devices and communicate with other users.
+
+For more information about the Meshtastic project, please visit our website: [meshtastic.org](https://www.meshtastic.org). The firmware that runs on the radio devices is a separate open-source project, which you can find here: [https://github.com/meshtastic/Meshtastic-device](https://github.com/meshtastic/Meshtastic-device).
+
+**Community and Support**
+
+This project is currently in beta. We would love to hear from you! If you have questions, feedback, or encounter any problems, please join our friendly and active community:
+
+* **Discussion Forum:** [https://github.com/orgs/meshtastic/discussions](https://github.com/orgs/meshtastic/discussions)
+* **Discord:** [https://discord.gg/meshtastic](https://discord.gg/meshtastic)
+* **Report an Issue:** [https://github.com/meshtastic/Meshtastic-Android/issues](https://github.com/meshtastic/Meshtastic-Android/issues)
+
+**Documentation**
+
+To learn more about the features and capabilities of this app and Meshtastic, please view our official documentation:
+[**View Documentation**](https://meshtastic.org/docs/)
+
+**Translations**
+
+You can help translate the app into your native language using Crowdin:
+[https://crowdin.meshtastic.org/android](https://crowdin.meshtastic.org/android)
diff --git a/app/src/main/play/listings/en-US/graphics/feature-graphic/1.png b/fastlane/metadata/android/en-US/images/featureGraphic.jpeg
similarity index 100%
rename from app/src/main/play/listings/en-US/graphics/feature-graphic/1.png
rename to fastlane/metadata/android/en-US/images/featureGraphic.jpeg
diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png
new file mode 100644
index 000000000..e3e10fb55
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png
similarity index 100%
rename from app/src/main/play/listings/en-US/graphics/phone-screenshots/1.png
rename to fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png
similarity index 100%
rename from app/src/main/play/listings/en-US/graphics/phone-screenshots/2.png
rename to fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png
similarity index 100%
rename from app/src/main/play/listings/en-US/graphics/phone-screenshots/3.png
rename to fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png
similarity index 100%
rename from app/src/main/play/listings/en-US/graphics/phone-screenshots/4.png
rename to fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png
similarity index 100%
rename from app/src/main/play/listings/en-US/graphics/phone-screenshots/5.png
rename to fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png
diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt
new file mode 100644
index 000000000..076a74f8a
--- /dev/null
+++ b/fastlane/metadata/android/en-US/short_description.txt
@@ -0,0 +1 @@
+The official app for Meshtastic, an open-source, off-grid, mesh GPS radio.
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt
new file mode 100644
index 000000000..c3ff69353
--- /dev/null
+++ b/fastlane/metadata/android/en-US/title.txt
@@ -0,0 +1 @@
+Meshtastic
\ No newline at end of file
diff --git a/app/src/main/play/listings/en-US/video-url.txt b/fastlane/metadata/android/en-US/video.txt
similarity index 100%
rename from app/src/main/play/listings/en-US/video-url.txt
rename to fastlane/metadata/android/en-US/video.txt