From 331a620f83e2da34dd499afe1f8b64f12e0fff77 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 15:33:44 +0000 Subject: [PATCH] Publish a fixed-name APK so the latest build has a permanent link GitHub's /releases/latest/download/ redirect needs a file name that stays put, and the release asset is named after its version. Each production release now carries the same build twice: the versioned copy, and a plain ProLibertateGames.apk, which makes this link permanent: .../releases/latest/download/ProLibertateGames.apk Only the production channel gets one. /releases/latest skips prereleases, which is exactly what stops a production install being handed a dev build, so there is no equivalent dev link to offer. The updater now picks its asset by name rather than taking the first .apk it sees. Both copies are byte-identical, so nothing would have broken either way, but relying on upload order for which file gets installed is not something to leave lying around. --- .github/workflows/release.yml | 12 ++++++- README.md | 16 +++++++++ .../org/prolibertate/games/update/Updater.kt | 33 +++++++++++-------- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f00587b..978bf67 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,6 +53,11 @@ jobs: src="app/build/outputs/apk/debug/app-debug.apk" dest="ProLibertateGames-${version}.apk" cp "$src" "$dest" + # The same build again under a name that never changes. GitHub's + # /releases/latest/download/ redirect needs a constant file + # name, so this second copy is what makes a permanent download link + # possible — the versioned copy stays as the traceable one. + cp "$src" "ProLibertateGames.apk" echo "version=$version" >> "$GITHUB_OUTPUT" echo "apk=$dest" >> "$GITHUB_OUTPUT" @@ -70,9 +75,14 @@ jobs: set -euo pipefail gh release create "${{ steps.stage.outputs.version }}" \ "${{ steps.stage.outputs.apk }}" \ + "ProLibertateGames.apk" \ --repo "${{ github.repository }}" \ --target "${{ github.sha }}" \ --title "ProLibertateGames ${{ steps.stage.outputs.version }}" \ --notes "Automated release built from \`${{ github.sha }}\` on push to \`main\`. - Download **${{ steps.stage.outputs.apk }}** below and install it on an Android device (you may need to allow *Install unknown apps*). This is a debug-signed build." + Download **${{ steps.stage.outputs.apk }}** below and install it on an Android device (you may need to allow *Install unknown apps*). This is a debug-signed build. + + Both files below are the same build. \`ProLibertateGames.apk\` is a copy under a fixed name, so this link always fetches the newest release and can be bookmarked or shared: + + https://github.com/${{ github.repository }}/releases/latest/download/ProLibertateGames.apk" diff --git a/README.md b/README.md index db4866a..537870c 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,22 @@ Requires JDK 17 and the Android SDK (compileSdk 34). 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. +### Installing without the app + +The newest production build is always at this link, which never changes: + +**https://github.com/adrianoftyriel/ProLibertateGames/releases/latest/download/ProLibertateGames.apk** + +Every release carries the same APK twice — once named for its version, and once +as plain `ProLibertateGames.apk`, because GitHub's `/releases/latest/download/` +redirect needs a file name that stays put. The in-app updater deliberately picks +the versioned copy, so an installed build can always be traced back to its tag. + +There is no equivalent link for the dev channel: `/releases/latest` skips +prereleases by design, and that is exactly what keeps a production install from +being handed a dev build. Dev builds are reached through the in-app updater with +the channel set to dev, or from the releases page. + ### Update channels Both pipelines publish, to two separate channels: diff --git a/app/src/main/java/org/prolibertate/games/update/Updater.kt b/app/src/main/java/org/prolibertate/games/update/Updater.kt index 3f353b9..fbfa3f4 100644 --- a/app/src/main/java/org/prolibertate/games/update/Updater.kt +++ b/app/src/main/java/org/prolibertate/games/update/Updater.kt @@ -120,20 +120,25 @@ class Updater(private val activity: Activity) { } val assets = json.optJSONArray("assets") ?: return null - for (i in 0 until assets.length()) { - val asset = assets.getJSONObject(i) - val name = asset.optString("name") - if (name.endsWith(".apk")) { - return Release( - tag = tag, - versionCode = versionCode, - apkUrl = asset.getString("browser_download_url"), - apkName = name, - channel = channel, - ) - } - } - return null + val apks = (0 until assets.length()) + .map { assets.getJSONObject(it) } + .filter { it.optString("name").endsWith(".apk") } + + // A production release carries the same build twice: once named for its + // version, and once under a fixed name so a permanent download link + // exists. Pick the versioned one explicitly rather than whichever was + // uploaded first, so what gets installed is traceable to its tag. + val chosen = apks.firstOrNull { it.optString("name").contains(tag) } + ?: apks.firstOrNull() + ?: return null + + return Release( + tag = tag, + versionCode = versionCode, + apkUrl = chosen.getString("browser_download_url"), + apkName = chosen.optString("name"), + channel = channel, + ) } private fun openConnection(url: String): HttpURLConnection =