OTA: name release assets after their tag, download from one host
Build and Release APK / Build APK and publish release (push) Canceled after 0s

Two things the first live release check on git.wallacenet.ca showed:

- The publish script uploaded the APK under whatever the build called it
  (app-debug.apk), which is the name the updater only finds by its fallback
  rule. The script now names the asset after its tag, so the primary rule —
  "the asset whose name contains the tag" — is the one that fires.
- The instance answers for two host names and reports asset URLs on the one
  it treats as canonical, so the app would have fetched its release list from
  git.wallacenet.ca and its APK from elsewhere. onReportedHost() now keeps the
  path the server chose and puts the download on the host the app already
  trusts, tested in UpdatePolicyTest.

Co-Authored-By: Paperclip <[email protected]>
This commit is contained in:
adrianoftyriel
2026-10-13 22:13:09 +00:00
co-authored by Paperclip
parent fad4778ad8
commit a827a38ae6
4 changed files with 57 additions and 5 deletions
@@ -1,5 +1,7 @@
package org.prolibertate.games.update
import java.net.URI
/**
* Which line of builds an APK belongs to.
*
@@ -65,6 +67,28 @@ fun channelOfTag(tag: String, prerelease: Boolean): UpdateChannel =
fun versionCodeOfTag(tag: String): Int? =
tag.substringAfterLast('.').removeSuffix(DEV_SUFFIX).toIntOrNull()
/**
* Rewrites a download URL onto [host], keeping its scheme, path and query.
*
* A release server can answer for more than one host name, and it hands out
* asset URLs on whichever one it treats as canonical — which need not be the
* one the app was told to talk to. Fetching the release list from one host and
* the APK from another is a single DNS change away from a channel that half
* works, so the app stays on the host it decided to trust and takes only the
* path from what the server reported.
*
* Anything that cannot be parsed is returned unchanged: a broken URL fails
* loudly at download time, while a swallowed one would fail as a silent
* "something went wrong".
*/
fun onReportedHost(url: String, host: String): String = try {
val reported = URI(url)
val scheme = reported.scheme ?: "https"
URI(scheme, null, host, -1, reported.path, reported.query, null).toString()
} catch (e: Exception) {
url
}
/**
* The build a check on launch should interrupt somebody for, or null to say
* nothing at all.
@@ -105,8 +105,8 @@ class Updater(private val activity: Activity) {
.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
// A production release may carry 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) }
@@ -116,7 +116,7 @@ class Updater(private val activity: Activity) {
return Release(
tag = tag,
versionCode = versionCode,
apkUrl = chosen.getString("browser_download_url"),
apkUrl = onReportedHost(chosen.getString("browser_download_url"), SERVER_HOST),
apkName = chosen.optString("name"),
channel = channel,
)
@@ -204,6 +204,7 @@ class Updater(private val activity: Activity) {
private companion object {
/** The Gitea instance that publishes this app's releases. */
const val SERVER = "https://git.wallacenet.ca"
const val SERVER_HOST = "git.wallacenet.ca"
const val OWNER = "devadmin"
const val REPO = "PilgrimsOfKatan"
@@ -53,6 +53,28 @@ class UpdatePolicyTest {
assertNull(versionCodeOfTag("nightly"))
}
// -- Where the download happens ----------------------------------------
@Test
fun `a release asset is fetched from this app's host, keeping its path`() {
// The instance reports its canonical host; the app was told to trust
// another. Same bytes, one host, whatever the server names itself.
assertEquals(
"https://git.wallacenet.ca/devadmin/PilgrimsOfKatan/releases/download/" +
"v1.4.1001-dev/PilgrimsOfKatan-v1.4.1001-dev.apk",
onReportedHost(
"https://git.saskvalleytech.com/devadmin/PilgrimsOfKatan/releases/download/" +
"v1.4.1001-dev/PilgrimsOfKatan-v1.4.1001-dev.apk",
"git.wallacenet.ca",
),
)
// A URL that already says the right host is left exactly as it is.
val already = "https://git.wallacenet.ca/devadmin/PilgrimsOfKatan/releases/download/v1.4.1/a.apk"
assertEquals(already, onReportedHost(already, "git.wallacenet.ca"))
// Anything unparseable survives untouched rather than being dropped.
assertEquals("not a url", onReportedHost("not a url", "git.wallacenet.ca"))
}
// -- The wall -----------------------------------------------------------
@Test