From 46282c3aec61a924af99a849e3d26cf95fd9caef Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:52:34 -0500 Subject: [PATCH] fix(release): Simplify Play Store deployment to upload-only (#3027) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .github/workflows/release.yml | 41 ++++------ RELEASE_PROCESS.md | 74 ++++++++----------- app/build.gradle.kts | 10 ++- .../src/main/kotlin/GitVersionValueSource.kt | 40 ++++++++++ 4 files changed, 90 insertions(+), 75 deletions(-) create mode 100644 buildSrc/src/main/kotlin/GitVersionValueSource.kt 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 8df90f7f8..3661d1692 100644 --- a/RELEASE_PROCESS.md +++ b/RELEASE_PROCESS.md @@ -4,24 +4,15 @@ This document outlines the steps for releasing a new version of the Meshtastic-A **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. +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. -* Deploys to the Google Play Console using a smart **"promote-or-upload"** strategy based on the Git tag: +* **Uploads** the newly built AAB directly to the appropriate track in the Google Play Console based on the tag. - * **Internal Release (`vX.X.X-internal.Y`):** - * Always **uploads** the AAB to the `internal` track. The release is automatically finalized and rolled out to internal testers. - - * **Promotions (`-closed`, `-open`, production):** - * The workflow first attempts to **promote** an existing build from the previous track. The promotion path is: `internal` -> `NewAlpha` (closed) -> `beta` (open) -> `production`. - * **Fallback Safety Net:** If a promotion fails (e.g., the corresponding build doesn't exist on the source track), the workflow **will not fail**. Instead, it automatically falls back to **uploading** the newly built AAB directly to the target track as a **draft**. - -This fallback mechanism makes the process resilient but adds a crucial manual checkpoint: **any release created via fallback upload will be a draft and requires you to manually review and roll it out in the Google Play Console.** - -Finalizing and publishing the GitHub Release and the Google Play Store submission remain **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 @@ -30,8 +21,8 @@ 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 must be passing. 3. **Versioning and Tagging Strategy:** - * Tags **must** start with `v` and generally follow Semantic Versioning (e.g., `vX.X.X`). - * Use the correct suffixes for the desired release phase: + * 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` @@ -40,22 +31,33 @@ Before initiating the release process, ensure the following are completed: ## Core Release Workflow: Triggering via Tag Push -The recommended release process follows the promotion chain. - -1. **Start with an Internal Release:** Create and push an `-internal` tag first. +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. **Promote to the Next Phase:** Once the internal build is verified, create and push a tag for the next phase. +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 promote the v1.2.3 build from 'internal' to 'NewAlpha' + # 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 each tag automatically triggers the `release.yml` GitHub Action. +## 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) @@ -70,35 +72,21 @@ After the `release.yml` workflow completes, manual actions are needed on GitHub ### Phase 2: Closed Alpha Release * **Tag format:** `vX.X.X-closed.Y` -* **Automated Action:** The workflow attempts to **promote** the build from `internal` to the `NewAlpha` track. +* **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:** - * **If promotion succeeded:** The release will be live on the `NewAlpha` track. Verify its status. - * **If promotion failed (fallback):** The AAB will be a **draft** on the `NewAlpha` track. You must manually review and submit it for your closed alpha testers. + 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` -* **Automated Action:** The workflow attempts to **promote** the build from `alpha` to the `beta` track. +* **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:** - * **If promotion succeeded:** The release will be live on the `beta` track. Verify its status. - * **If promotion failed (fallback):** The AAB will be a **draft** on the `beta` track. You must manually review, add release notes, and submit it. + 2. **Google Play Console:** Manually review the draft, add release notes, and submit it. ### Phase 4: Production Release * **Tag format:** `vX.X.X` -* **Automated Action:** The workflow attempts to **promote** the build from `beta` to the `production` track. +* **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:** - * **If promotion succeeded:** The release will be live on the `production` track. - * **If promotion failed (fallback):** The AAB will be a **draft** on the `production` track. You must manually review, add release notes, and **start a staged rollout**. - -## Iterating on Pre-Releases - -If bugs are found in a release: -1. Commit fixes to your development branch. -2. Create a new, incremented tag for the **same release phase** (e.g., if `v1.2.3-open.1` had bugs, create `v1.2.3-open.2`). -3. Push the new tag. This will trigger a new upload to the `internal` track (if it's an internal tag) or a new promotion/fallback for other tracks. -4. Follow the manual post-workflow steps for that release phase again. + 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 e1ecee811..bc802c420 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -42,6 +42,8 @@ if (keystorePropertiesFile.exists()) { FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) } } +val gitVersionProvider = providers.of(GitVersionValueSource::class.java) {} + android { namespace = "com.geeksville.mesh" @@ -58,10 +60,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}\"") @@ -221,6 +221,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