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 =