kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
fix(release): Simplify Play Store deployment to upload-only (#3027)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>pull/3032/head
rodzic
15cdb04dba
commit
46282c3aec
|
@ -47,9 +47,13 @@ jobs:
|
||||||
id: get_version_name
|
id: get_version_name
|
||||||
run: echo "APP_VERSION_NAME=$(echo ${GITHUB_REF_NAME#v} | sed 's/-.*//')" >> $GITHUB_OUTPUT
|
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
|
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:
|
build-fdroid:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -245,50 +249,31 @@ jobs:
|
||||||
./build-artifacts/google/apk/app-google-release.apk
|
./build-artifacts/google/apk/app-google-release.apk
|
||||||
./build-artifacts/fdroid/app-fdroid-release.apk
|
./build-artifacts/fdroid/app-fdroid-release.apk
|
||||||
|
|
||||||
- name: Determine Play Store Action
|
- name: Determine Play Store Track
|
||||||
id: play_action
|
id: play_track
|
||||||
run: |
|
run: |
|
||||||
TAG_NAME="${{ github.ref_name }}"
|
TAG_NAME="${{ github.ref_name }}"
|
||||||
if [[ "$TAG_NAME" == *"-internal"* ]]; then
|
if [[ "$TAG_NAME" == *"-internal"* ]]; then
|
||||||
echo "track=internal" >> $GITHUB_OUTPUT
|
echo "track=internal" >> $GITHUB_OUTPUT
|
||||||
echo "action=upload" >> $GITHUB_OUTPUT
|
|
||||||
elif [[ "$TAG_NAME" == *"-closed"* ]]; then
|
elif [[ "$TAG_NAME" == *"-closed"* ]]; then
|
||||||
echo "track=NewAlpha" >> $GITHUB_OUTPUT
|
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
|
elif [[ "$TAG_NAME" == *"-open"* ]]; then
|
||||||
echo "track=beta" >> $GITHUB_OUTPUT
|
echo "track=beta" >> $GITHUB_OUTPUT
|
||||||
echo "from_track=NewAlpha" >> $GITHUB_OUTPUT
|
|
||||||
echo "action=promote" >> $GITHUB_OUTPUT
|
|
||||||
echo "user_fraction=1.0" >> $GITHUB_OUTPUT
|
|
||||||
else
|
else
|
||||||
echo "track=production" >> $GITHUB_OUTPUT
|
echo "track=production" >> $GITHUB_OUTPUT
|
||||||
echo "from_track=beta" >> $GITHUB_OUTPUT
|
|
||||||
echo "action=promote" >> $GITHUB_OUTPUT
|
|
||||||
echo "user_fraction=0.1" >> $GITHUB_OUTPUT
|
echo "user_fraction=0.1" >> $GITHUB_OUTPUT
|
||||||
|
echo "status=inProgress" >> $GITHUB_OUTPUT
|
||||||
fi
|
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
|
- 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
|
uses: r0adkll/upload-google-play@v1.1.3
|
||||||
with:
|
with:
|
||||||
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
|
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
|
||||||
packageName: com.geeksville.mesh
|
packageName: com.geeksville.mesh
|
||||||
releaseFiles: ./build-artifacts/google/bundle/app-google-release.aab
|
releaseFiles: ./build-artifacts/google/bundle/app-google-release.aab
|
||||||
track: ${{ steps.play_action.outputs.track }}
|
track: ${{ steps.play_track.outputs.track }}
|
||||||
status: ${{ steps.play_action.outputs.track == 'internal' && 'completed' || 'draft' }}
|
status: ${{ steps.play_track.outputs.status || (steps.play_track.outputs.track == 'internal' && 'completed' || 'draft') }}
|
||||||
|
userFraction: ${{ steps.play_track.outputs.userFraction }}
|
||||||
whatsNewDirectory: ./whatsnew/
|
whatsNewDirectory: ./whatsnew/
|
||||||
mappingFile: ./build-artifacts/google/mapping/mapping.txt
|
mappingFile: ./build-artifacts/google/mapping/mapping.txt
|
||||||
|
|
|
@ -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.
|
**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:
|
The workflow uses a simple and robust **"upload-only"** model. It automatically:
|
||||||
* Determines version information from the tag.
|
* Determines a `versionName` from the Git 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 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.
|
||||||
* Generates a changelog.
|
* Builds fresh F-Droid (APK) and Google (AAB, APK) artifacts for every run.
|
||||||
* Creates a **draft GitHub Release** and attaches the artifacts.
|
* Creates a **draft GitHub Release** and attaches the artifacts.
|
||||||
* Attests build provenance for 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`):**
|
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**.
|
||||||
* 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**.
|
|
||||||
|
|
||||||
## Prerequisites
|
## 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.
|
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.
|
2. **Automated Testing:** All automated tests must be passing.
|
||||||
3. **Versioning and Tagging Strategy:**
|
3. **Versioning and Tagging Strategy:**
|
||||||
* Tags **must** start with `v` and generally follow Semantic Versioning (e.g., `vX.X.X`).
|
* Tags **must** start with `v` and follow Semantic Versioning (e.g., `vX.X.X`).
|
||||||
* Use the correct suffixes for the desired release phase:
|
* Use the correct suffixes for the desired release track:
|
||||||
* **Internal/QA:** `vX.X.X-internal.Y`
|
* **Internal/QA:** `vX.X.X-internal.Y`
|
||||||
* **Closed Alpha:** `vX.X.X-closed.Y`
|
* **Closed Alpha:** `vX.X.X-closed.Y`
|
||||||
* **Open Alpha/Beta:** `vX.X.X-open.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
|
## Core Release Workflow: Triggering via Tag Push
|
||||||
|
|
||||||
The recommended release process follows the promotion chain.
|
1. **Create and push a tag for the desired release track.**
|
||||||
|
|
||||||
1. **Start with an Internal Release:** Create and push an `-internal` tag first.
|
|
||||||
```bash
|
```bash
|
||||||
# This build will be uploaded and rolled out on the 'internal' track
|
# This build will be uploaded and rolled out on the 'internal' track
|
||||||
git tag v1.2.3-internal.1
|
git tag v1.2.3-internal.1
|
||||||
git push origin 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
|
```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 tag v1.2.3-closed.1
|
||||||
git push origin 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)
|
## 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
|
### Phase 2: Closed Alpha Release
|
||||||
* **Tag format:** `vX.X.X-closed.Y`
|
* **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:**
|
* **Manual Steps:**
|
||||||
1. **GitHub:** Find and publish the **draft release**.
|
1. **GitHub:** Find and publish the **draft release**.
|
||||||
2. **Google Play Console:**
|
2. **Google Play Console:** Manually review the draft release and submit it for your closed alpha testers.
|
||||||
* **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.
|
|
||||||
|
|
||||||
### Phase 3: Open Alpha / Beta Release
|
### Phase 3: Open Alpha / Beta Release
|
||||||
* **Tag format:** `vX.X.X-open.Y`
|
* **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:**
|
* **Manual Steps:**
|
||||||
1. **GitHub:** Find and publish the **draft pre-release**.
|
1. **GitHub:** Find and publish the **draft pre-release**.
|
||||||
2. **Google Play Console:**
|
2. **Google Play Console:** Manually review the draft, add release notes, and submit it.
|
||||||
* **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.
|
|
||||||
|
|
||||||
### Phase 4: Production Release
|
### Phase 4: Production Release
|
||||||
* **Tag format:** `vX.X.X`
|
* **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:**
|
* **Manual Steps:**
|
||||||
1. **GitHub:** Find the **draft release**. **Crucially, uncheck "This is a pre-release"** before publishing.
|
1. **GitHub:** Find the **draft release**. **Crucially, uncheck "This is a pre-release"** before publishing.
|
||||||
2. **Google Play Console:**
|
2. **Google Play Console:** Manually review the release, add release notes, and **start the staged rollout**.
|
||||||
* **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.
|
|
||||||
|
|
|
@ -42,6 +42,8 @@ if (keystorePropertiesFile.exists()) {
|
||||||
FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) }
|
FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val gitVersionProvider = providers.of(GitVersionValueSource::class.java) {}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.geeksville.mesh"
|
namespace = "com.geeksville.mesh"
|
||||||
|
|
||||||
|
@ -58,10 +60,8 @@ android {
|
||||||
applicationId = Configs.APPLICATION_ID
|
applicationId = Configs.APPLICATION_ID
|
||||||
minSdk = Configs.MIN_SDK
|
minSdk = Configs.MIN_SDK
|
||||||
targetSdk = Configs.TARGET_SDK
|
targetSdk = Configs.TARGET_SDK
|
||||||
// Prioritize ENV, then fallback for versionCode
|
// Prioritize ENV, then fallback to git commit count for versionCode
|
||||||
versionCode =
|
versionCode = (System.getenv("VERSION_CODE") ?: gitVersionProvider.get()).toInt()
|
||||||
System.getenv("VERSION_CODE")?.toIntOrNull()
|
|
||||||
?: (System.currentTimeMillis() / 1000).toInt() // Meshtastic Development Build
|
|
||||||
versionName = System.getenv("VERSION_NAME") ?: Configs.VERSION_NAME_BASE
|
versionName = System.getenv("VERSION_NAME") ?: Configs.VERSION_NAME_BASE
|
||||||
testInstrumentationRunner = "com.geeksville.mesh.TestRunner"
|
testInstrumentationRunner = "com.geeksville.mesh.TestRunner"
|
||||||
buildConfigField("String", "MIN_FW_VERSION", "\"${Configs.MIN_FW_VERSION}\"")
|
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 {
|
dependencies {
|
||||||
implementation(project(":network"))
|
implementation(project(":network"))
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Ładowanie…
Reference in New Issue