Publish a fixed-name APK so the latest build has a permanent link

GitHub's /releases/latest/download/<name> 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.
This commit is contained in:
Claude
2026-08-06 15:33:44 +00:00
parent d6e237ce4a
commit 331a620f83
3 changed files with 46 additions and 15 deletions
+11 -1
View File
@@ -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/<name> 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"
+16
View File
@@ -111,6 +111,22 @@ Requires JDK 17 and the Android SDK (compileSdk 34).
publishes it as a GitHub Release tagged `v1.0.<run-number>`. 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:
@@ -120,21 +120,26 @@ 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")) {
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 = asset.getString("browser_download_url"),
apkName = name,
apkUrl = chosen.getString("browser_download_url"),
apkName = chosen.optString("name"),
channel = channel,
)
}
}
return null
}
private fun openConnection(url: String): HttpURLConnection =
(URL(url).openConnection() as HttpURLConnection).apply {