diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3fafbbbd5..7c4e91866 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -47,9 +47,13 @@ jobs:
id: get_version_name
run: echo "APP_VERSION_NAME=$(echo ${GITHUB_REF_NAME#v} | sed 's/-.*//')" >> $GITHUB_OUTPUT
- - name: Calculate Version Code
+ - name: Calculate Version Code from Epoch
id: calculate_version_code
- uses: ./.github/actions/calculate-version-code
+ # We use epoch minutes to ensure a unique, always-incrementing version code.
+ # This is compatible with our release strategy of tagging the same commit for different
+ # channels (internal, closed, open, prod), as each build needs a unique code.
+ # 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:
runs-on: ubuntu-latest
@@ -245,50 +249,31 @@ jobs:
./build-artifacts/google/apk/app-google-release.apk
./build-artifacts/fdroid/app-fdroid-release.apk
- - name: Determine Play Store Action
- id: play_action
+ - name: Determine Play Store Track
+ id: play_track
run: |
TAG_NAME="${{ github.ref_name }}"
if [[ "$TAG_NAME" == *"-internal"* ]]; then
echo "track=internal" >> $GITHUB_OUTPUT
- echo "action=upload" >> $GITHUB_OUTPUT
elif [[ "$TAG_NAME" == *"-closed"* ]]; then
echo "track=NewAlpha" >> $GITHUB_OUTPUT
- echo "from_track=internal" >> $GITHUB_OUTPUT
- echo "action=promote" >> $GITHUB_OUTPUT
- echo "user_fraction=1.0" >> $GITHUB_OUTPUT
elif [[ "$TAG_NAME" == *"-open"* ]]; then
echo "track=beta" >> $GITHUB_OUTPUT
- echo "from_track=NewAlpha" >> $GITHUB_OUTPUT
- echo "action=promote" >> $GITHUB_OUTPUT
- echo "user_fraction=1.0" >> $GITHUB_OUTPUT
else
echo "track=production" >> $GITHUB_OUTPUT
- echo "from_track=beta" >> $GITHUB_OUTPUT
- echo "action=promote" >> $GITHUB_OUTPUT
echo "user_fraction=0.1" >> $GITHUB_OUTPUT
+ echo "status=inProgress" >> $GITHUB_OUTPUT
fi
- - name: Attempt to Promote on Google Play
- id: promote
- if: success() && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && steps.play_action.outputs.action == 'promote'
- uses: kevin-david/promote-play-release@v1.2.0
- continue-on-error: true
- with:
- service-account-json-raw: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
- package-name: com.geeksville.mesh
- to-track: ${{ steps.play_action.outputs.track }}
- from-track: ${{ steps.play_action.outputs.from_track }}
- user-fraction: ${{ steps.play_action.outputs.user_fraction }}
-
- name: Upload to Google Play
- if: success() && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && (steps.play_action.outputs.action == 'upload' || steps.promote.outcome == 'failure')
+ 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_action.outputs.track }}
- status: ${{ steps.play_action.outputs.track == 'internal' && 'completed' || 'draft' }}
+ 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/RELEASE_PROCESS.md b/RELEASE_PROCESS.md
index 1ff94c29a..3661d1692 100644
--- a/RELEASE_PROCESS.md
+++ b/RELEASE_PROCESS.md
@@ -1,144 +1,92 @@
# Meshtastic-Android Release Process
-zzThis 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. Adhering to this process ensures consistency and helps manage the release lifecycle, leveraging automation via the `release.yml` GitHub Action.
-**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 (select the desired branch/tag/commit).
+**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.
-The workflow automatically:
-* Determines version information from the tag.
-* Builds F-Droid (APK) and Google (AAB, APK) artifacts. If artifacts for the same commit SHA have been built before, it will use the cached artifacts instead of rebuilding.
-* Generates a changelog.
-* Creates a **draft GitHub Release**. The release is marked as a "pre-release" if the tag name contains `-internal`, `-closed`, or `-open`.
-* Attaches build artifacts, `version_info.txt`, and `changelog.txt` to the draft GitHub Release.
+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 AAB to the Google Play Console as a **draft** to a track determined by the tag name:
- * `internal` (for tags with `-internal`)
- * `NewAlpha` (for tags with `-closed`)
- * `beta` (for tags with `-open`)
- * `production` (for tags without these suffixes)
+* **Uploads** the newly built AAB directly to the appropriate track in the Google Play Console based on the tag.
-Finalizing and publishing the GitHub Release and the Google Play Store submission are **manual steps**.
+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**.
## Prerequisites
Before initiating the release process, ensure the following are completed:
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 (unit, integration, UI) must be passing on the release candidate code, and CI checks on pull requests must be green.
+2. **Automated Testing:** All automated tests must be passing.
3. **Versioning and Tagging Strategy:**
- * The primary source for the release version name in the CI workflow is the Git tag (e.g., `v1.2.3` results in version name `1.2.3`).
- * Tags **must** start with `v` and generally follow Semantic Versioning (e.g., `vX.X.X`).
- * For pre-releases, use suffixes that the workflow recognizes to set the GitHub pre-release flag and Play Store track:
- * **Internal/QA:** `vX.X.X-internal.Y` (e.g., `v1.2.3-internal.1`)
- * **Closed Alpha:** `vX.X.X-closed.Y` (e.g., `v1.2.3-closed.1`)
- * **Open Alpha/Beta:** `vX.X.X-open.Y` (e.g., `v1.2.3-open.1`)
- * **Production releases** use no suffix (e.g., `vX.X.X`). The `Y` in suffixes is an increment for iterations of the same pre-release type.
- * **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 if the app uses this value internally. The CI workflow derives `APP_VERSION_NAME` directly from the tag and passes it to Gradle.
-4. **Final Checks:** Perform thorough manual testing of critical user flows and new features on various devices and Android versions.
+ * 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.
## Core Release Workflow: Triggering via Tag Push
-The primary way to initiate a release is by creating and pushing a tag:
-
-1. **Ensure Local Branch is Synced:**
+1. **Create and push a tag for the desired release track.**
```bash
- # Example: if releasing from main
- git checkout main
- git pull origin main
+ # 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. **Create and Push Tag:**
- Tag the commit you intend to release (e.g., the head of `main` or a release branch).
+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
- # Example for an open beta release
- git tag v1.2.3-open.1
- git push origin v1.2.3-open.1
- ```
- Or, for a production release:
- ```bash
- git tag v1.2.3
- git push origin v1.2.3
+ # 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
```
-Pushing the tag automatically triggers the `release.yml` GitHub Action, which performs the automated steps listed in the "Note on Automation" section at the beginning of this document.
+## Iterating on a Bad Build
+
+If you discover a critical bug in a build, the process is simple:
+
+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.
## Managing Different Release Phases (Manual Steps Post-Workflow)
After the `release.yml` workflow completes, manual actions are needed on GitHub and in the Google Play Console.
### Phase 1: Internal / QA Release
-* **Tag format:** `vX.X.X-internal.Y` (e.g., `v1.2.3-internal.1`)
-* **Branching (Optional):**
- * Consider creating a `release/x.x.x` branch from `main`.
- * Update `Configs.kt` on this branch.
- * Create a draft PR from `release/x.x.x` to `main`. Tag a commit on this branch.
-* **Manual Steps Post-Workflow:**
- 1. **GitHub:**
- * Navigate to the "Releases" page of the repository.
- * Find the **draft release** (e.g., "Release v1.2.3-internal.1"). It will be marked as "pre-release".
- * Verify the attached artifacts.
- * You can choose to publish this pre-release if you want it formally listed, or simply use the artifacts from the draft for internal distribution.
- 2. **Google Play Console:**
- * The AAB will be uploaded as a **draft** to the **`qa` track**.
- * Review the draft release in the Play Console and promote/submit it as needed for your internal testers.
+* **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.
### Phase 2: Closed Alpha Release
-* **Tag format:** `vX.X.X-closed.Y` (e.g., `v1.2.3-closed.1`)
-* **Manual Steps Post-Workflow:**
- 1. **GitHub:**
- * Find the **draft release**. It will be marked as "pre-release".
- * Verify artifacts. Consider publishing it as a pre-release for wider internal visibility if appropriate.
- 2. **Google Play Console:**
- * The AAB will be a **draft** on the **`newalpha` track**.
- * Review and submit it for your closed alpha testers.
+* **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.
### Phase 3: Open Alpha / Beta Release
-* **Tag format:** `vX.X.X-open.Y` (e.g., `v1.2.3-open.1`)
-* **Manual Steps Post-Workflow:**
- 1. **GitHub:**
- * Find the **draft release**. It will be marked as "pre-release".
- * Edit the release: Review the title (e.g., "Release v1.2.3-open.1") and the auto-generated changelog.
- * Ensure "This is a pre-release" is checked.
- * **Publish the release** on GitHub. This makes it visible to the public.
- 2. **Google Play Console:**
- * The AAB will be a **draft** on the **`beta` track**.
- * Review, add release notes (can copy from GitHub changelog), and submit it for your open testers.
+* **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.
### Phase 4: Production Release
-* **Tag format:** `vX.X.X` (e.g., `v1.2.3`)
-* **Branching:**
- * Ensure all changes for the release are merged into `main`.
- * Tag the final merge commit on `main`.
-* **Manual Steps Post-Workflow:**
- 1. **GitHub:**
- * Find the **draft release** (e.g., "Release v1.2.3").
- * Edit the release: Review title and changelog.
- * **Crucially, uncheck "This is a pre-release"**.
- * **Publish the release** on GitHub.
- 2. **Google Play Console:**
- * The AAB will be a **draft** on the **`production` track**.
- * Review, add release notes.
- * **Start a staged rollout** or release to 100% of users.
-
-## Iterating on Pre-Releases
-
-If bugs are found in an internal, closed, or open alpha/beta:
-1. Commit fixes to your development branch (e.g., `release/x.x.x` or `main`).
-2. Create a new, incremented tag (e.g., if `v1.2.3-open.1` had bugs, use `v1.2.3-open.2`).
-3. Push the new tag.
-4. Follow the manual post-workflow steps for that release phase again.
-
-## Post-Release Activities
-
-1. **Monitoring:** Closely monitor app performance, crash reports, and user feedback.
-2. **Communication:** Announce the new release to the user community as appropriate.
-3. **Hotfixes (for Production Releases):**
- * If a critical bug is found in a production release:
- 1. Create a hotfix branch (e.g., `hotfix/x.x.y`) from `main` (or directly from the production tag `vX.X.X`).
- 2. Implement and test the fix.
- 3. Update `VERSION_NAME_BASE` in `buildSrc/src/main/kotlin/Configs.kt` for the patch version (e.g., `1.2.4`).
- 4. Merge the hotfix branch into `main`.
- 5. Tag the merge commit on `main` with the new patch version (e.g., `v1.2.4`).
- 6. Push the new tag (e.g., `git push origin v1.2.4`). This triggers the `release.yml` workflow.
- 7. Follow the **Manual Steps Post-Workflow** for a **Production Release** (uncheck "pre-release" on GitHub, manage production track draft in Play Console).
-
----
+* **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**.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e8faff6df..2cd4d0443 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -41,6 +41,8 @@ if (keystorePropertiesFile.exists()) {
FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) }
}
+val gitVersionProvider = providers.of(GitVersionValueSource::class.java) {}
+
android {
namespace = "com.geeksville.mesh"
@@ -57,10 +59,8 @@ android {
applicationId = Configs.APPLICATION_ID
minSdk = Configs.MIN_SDK
targetSdk = Configs.TARGET_SDK
- // Prioritize ENV, then fallback for versionCode
- versionCode =
- System.getenv("VERSION_CODE")?.toIntOrNull()
- ?: (System.currentTimeMillis() / 1000).toInt() // Meshtastic Development Build
+ // Prioritize ENV, then fallback to git commit count for versionCode
+ versionCode = (System.getenv("VERSION_CODE") ?: gitVersionProvider.get()).toInt()
versionName = System.getenv("VERSION_NAME") ?: Configs.VERSION_NAME_BASE
testInstrumentationRunner = "com.geeksville.mesh.TestRunner"
buildConfigField("String", "MIN_FW_VERSION", "\"${Configs.MIN_FW_VERSION}\"")
@@ -220,6 +220,8 @@ androidComponents {
}
}
+project.afterEvaluate { logger.lifecycle("Version code is set to: ${android.defaultConfig.versionCode}") }
+
dependencies {
implementation(project(":network"))
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
diff --git a/buildSrc/src/main/kotlin/GitVersionValueSource.kt b/buildSrc/src/main/kotlin/GitVersionValueSource.kt
new file mode 100644
index 000000000..e5ba969d7
--- /dev/null
+++ b/buildSrc/src/main/kotlin/GitVersionValueSource.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import org.gradle.api.provider.ValueSource
+import org.gradle.api.provider.ValueSourceParameters
+import org.gradle.process.ExecOperations
+import javax.inject.Inject
+
+abstract class GitVersionValueSource : ValueSource {
+ interface Params : ValueSourceParameters
+ @get:Inject
+ abstract val execOperations: ExecOperations
+
+ override fun obtain(): String {
+ val output = java.io.ByteArrayOutputStream()
+ return try {
+ execOperations.exec {
+ commandLine("git", "rev-list", "--count", "HEAD")
+ standardOutput = output
+ }
+ output.toString().trim()
+ } catch (e: Exception) {
+ (System.currentTimeMillis() / 1000).toString()
+ }
+ }
+}
\ No newline at end of file