fix(release): Simplify Play Store deployment to upload-only (#3027)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>

(cherry picked from commit 46282c3aec)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2025-09-08 20:52:34 -05:00
parent 68da6d4e70
commit bf11b7b342
4 changed files with 118 additions and 143 deletions

View file

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

View file

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

View file

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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, GitVersionValueSource.Params> {
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()
}
}
}