From 80ff1784376d86db8bb6d2d1c5b8c70fb85241de Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 23:47:20 +0000 Subject: [PATCH] Pin OTA releases to main and stop dev builds outranking them Two ways the updater could have served or blocked the wrong build: - release.yml also triggers on workflow_dispatch, which can be aimed at any branch from the Actions UI. That would have built dev code and published it as a real Release, which every install would then offer to download. The job now refuses to run off anything but main. - versionCode came from GITHUB_RUN_NUMBER, but that counter is per workflow, so the dev pipeline had its own independent sequence. A debug APK installed from a CI artifact already carried a higher versionCode than the first several releases would, and the updater's check is latest <= installed, so those users would have been told they were up to date indefinitely. Only release.yml now sets PLG_RELEASE, and only that stamps a real version; everything else stays at 1.0.0-dev. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN --- .github/workflows/release.yml | 9 +++++++++ README.md | 11 ++++++++++- app/build.gradle.kts | 26 ++++++++++++++++++++------ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c2976f9..fed4e80 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,16 @@ concurrency: jobs: build: name: Build APK and publish release + # workflow_dispatch can be aimed at any branch from the Actions UI. Releases + # are what the in-app updater installs, so only main may produce one — + # anything else is a no-op rather than shipping unreviewed code to everyone. + if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + env: + # Tells the build this is a real release, so the APK is stamped with the + # run number that matches the tag below. Absent everywhere else, which + # keeps dev and local builds at 1.0.0-dev. + PLG_RELEASE: "1" steps: - name: Checkout uses: actions/checkout@v4 diff --git a/README.md b/README.md index a85fc6a..8f68e4f 100644 --- a/README.md +++ b/README.md @@ -72,11 +72,20 @@ Requires JDK 17 and the Android SDK (compileSdk 34). ## CI - `.github/workflows/ci.yml` — builds, tests and lints on `dev` and on pull - requests into `dev` or `main`. + requests into `dev` or `main`. Its APK artifact is a dev build, versioned + `1.0.0-dev`. - `.github/workflows/release.yml` — on push to `main`, builds the APK and publishes it as a GitHub Release tagged `v1.0.`. The in-app updater reads that tag, which is why the APK's `versionCode` is the same run number. +**OTA updates follow `main`, and only `main`.** The updater asks GitHub for the +latest published Release; the release workflow is the only thing that creates +one, and it refuses to run off any branch but `main` — so a manual dispatch +aimed at `dev` does nothing rather than shipping unreviewed code to every +install. Only that workflow sets `PLG_RELEASE`, which is what stamps a real +`versionCode`; dev builds stay at `1.0.0-dev` so a side-loaded CI artifact can +never outrank a genuine release and suppress future updates. + The debug keystore is committed on purpose: every build is signed with the same key, so an OTA update installs over the previous one instead of being rejected for a signature mismatch. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d97dd7c..de1c796 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,10 +4,20 @@ plugins { id("org.jetbrains.kotlin.plugin.serialization") } -// On CI, GITHUB_RUN_NUMBER is set automatically and matches the release tag -// (v1.0.), so the shipped APK knows its own version and the -// in-app updater can tell whether a newer release exists. Locally it's a dev build. -val ciRunNumber = (System.getenv("GITHUB_RUN_NUMBER") ?: "").toIntOrNull() ?: 0 +// Only the release workflow stamps a real version. It sets PLG_RELEASE, and its +// GITHUB_RUN_NUMBER is the same number as the release tag it publishes +// (v1.0.), so the shipped APK knows its own version and the in-app +// updater can compare against the latest published release. +// +// Everything else — a local build, or the dev CI pipeline — stays at 1.0.0-dev. +// GITHUB_RUN_NUMBER alone is NOT enough to key off: it counts per workflow, so +// the dev pipeline has its own independent sequence. A debug APK installed from +// a CI artifact would otherwise carry that counter as its versionCode, could +// outrank a genuine release, and would leave the updater reporting "you're on +// the latest version" indefinitely. +val isReleaseBuild = System.getenv("PLG_RELEASE") == "1" +val releaseRunNumber = (System.getenv("GITHUB_RUN_NUMBER") ?: "").toIntOrNull() ?: 0 +val stampReleaseVersion = isReleaseBuild && releaseRunNumber > 0 android { namespace = "org.prolibertate.games" @@ -17,8 +27,12 @@ android { applicationId = "org.prolibertate.games" minSdk = 24 targetSdk = 34 - versionCode = if (ciRunNumber > 0) ciRunNumber else 1 - versionName = if (ciRunNumber > 0) "1.0.$ciRunNumber" else "1.0.0-dev" + // Dev builds sit at 1, below every release except the very first — + // a v1.0.1 release ties with a dev build and so is not offered as an + // update. Harmless, and preferable to versionCode 0, which Android + // documents as out of range. + versionCode = if (stampReleaseVersion) releaseRunNumber else 1 + versionName = if (stampReleaseVersion) "1.0.$releaseRunNumber" else "1.0.0-dev" } signingConfigs {