From 0f7a23faa8c52d53759170e2aed1f7e9adb4da5e Mon Sep 17 00:00:00 2001 From: justinzeus Date: Thu, 22 Oct 2026 14:47:50 +0200 Subject: [PATCH 1/2] Point updater at Forgejo releases and Wolfskin assets --- .../wholphin/preferences/AppPreference.kt | 2 +- .../wholphin/services/UpdateChecker.kt | 107 ++++++++++++------ .../wholphin/test/TestUpdateChecker.kt | 105 +++++++++++------ .../resources/forgejo_release_latest.json | 44 +++++++ app/src/test/resources/forgejo_releases.json | 52 +++++++++ 5 files changed, 238 insertions(+), 72 deletions(-) create mode 100644 app/src/test/resources/forgejo_release_latest.json create mode 100644 app/src/test/resources/forgejo_releases.json diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index 4213fe94..09de8f2e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -576,7 +576,7 @@ sealed interface AppPreference { val UpdateUrl = AppStringPreference( title = R.string.update_url, - defaultValue = "https://api.github.com/repos/damontecres/Wholphin/releases/latest", + defaultValue = "https://git.justinvisser.org/api/v1/repos/justinzeus/wolfskin/releases/latest", getter = { it.updateUrl }, setter = { prefs, value -> prefs.update { updateUrl = value } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt index a00de8e4..3b53138d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt @@ -57,7 +57,7 @@ class UpdateChecker @param:StandardOkHttpClient private val okHttpClient: OkHttpClient, ) { companion object { - const val ASSET_NAME = "Wholphin" + const val ASSET_NAME = "Wolfskin" const val APK_NAME = "$ASSET_NAME.apk" private const val APK_MIME_TYPE = "application/vnd.android.package-archive" @@ -65,6 +65,52 @@ class UpdateChecker private val NOTE_REGEX = Regex("") val ACTIVE = BuildConfig.UPDATING_ENABLED + + private const val RELEASES_URL = "https://git.justinvisser.org/api/v1/repos/justinzeus/wolfskin/releases?limit=50" + + /** + * Find the release in a Forgejo release list whose name parses to the given version + */ + fun findReleaseByVersion( + releases: JsonArray, + version: Version, + ): JsonObject? = + releases + .firstOrNull { release -> + Version.tryFromString(release.jsonObject["name"]?.jsonPrimitive?.contentOrNull) == version + }?.jsonObject + + /** + * Parse a single release object (Forgejo or GitHub shape) into a [Release], or null if its name is not a version + */ + fun parseRelease( + json: JsonObject, + debug: Boolean, + supportedABIs: List = Build.SUPPORTED_ABIS.toList(), + ): Release? { + val name = json["name"]?.jsonPrimitive?.contentOrNull + val version = Version.tryFromString(name) + if (version == null) { + Timber.w("Update version parsing failed. name=$name") + return null + } + val publishedAt = json["published_at"]?.jsonPrimitive?.contentOrNull + val body = json["body"]?.jsonPrimitive?.contentOrNull + val downloadUrl = + json["assets"]?.jsonArray?.let { assets -> getDownloadUrl(assets, debug, supportedABIs) } + Timber.v("version=$version, downloadUrl=$downloadUrl") + val notes = + if (body.isNotNullOrBlank()) { + NOTE_REGEX + .findAll(body) + .map { m -> + m.groupValues[1] + }.toList() + } else { + emptyList() + } + return Release(version, downloadUrl, publishedAt, body, notes) + } } /** @@ -127,17 +173,29 @@ class UpdateChecker return Version.fromString(pkgInfo.versionName!!) } + /** + * Get the release whose name parses to the given version. + * + * Wolfskin releases are named by `git describe` (e.g. `v1.0.9-2-g3094fc8c`) rather than tagged `vX.Y.Z`, + * so this searches the release list instead of fetching a tag. + */ suspend fun getRelease(version: Version): Release? { - val url = - "https://api.github.com/repos/damontecres/Wholphin/releases/tags/v${version.major}.${version.minor}.${version.patch}" + val request = + Request + .Builder() + .url(RELEASES_URL) + .get() + .build() return withContext(WholphinDispatchers.IO) { - val request = - Request - .Builder() - .url(url) - .get() - .build() - getRelease(request) + okHttpClient.newCall(request).execute().use { + if (it.isSuccessful) { + val releases = Json.parseToJsonElement(it.body.string()).jsonArray + findReleaseByVersion(releases, version)?.let { json -> parseRelease(json, BuildConfig.DEBUG) } + } else { + Timber.w("Release lookup failed ${it.code}: ${it.message}") + null + } + } } } @@ -158,36 +216,11 @@ class UpdateChecker private fun getRelease(request: Request): Release? { return okHttpClient.newCall(request).execute().use { if (it.isSuccessful) { - val result = Json.parseToJsonElement(it.body.string()) - val name = result.jsonObject["name"]?.jsonPrimitive?.contentOrNull - val version = Version.tryFromString(name) - val publishedAt = - result.jsonObject["published_at"]?.jsonPrimitive?.contentOrNull - val body = result.jsonObject["body"]?.jsonPrimitive?.contentOrNull - val downloadUrl = - result.jsonObject["assets"] - ?.jsonArray - ?.let { assets -> getDownloadUrl(assets, BuildConfig.DEBUG) } - Timber.v("version=$version, downloadUrl=$downloadUrl") - if (version != null) { - val notes = - if (body.isNotNullOrBlank()) { - NOTE_REGEX - .findAll(body) - .map { m -> - m.groupValues[1] - }.toList() - } else { - emptyList() - } - return@use Release(version, downloadUrl, publishedAt, body, notes) - } else { - Timber.w("Update version parsing failed. name=$name") - } + parseRelease(Json.parseToJsonElement(it.body.string()).jsonObject, BuildConfig.DEBUG) } else { Timber.w("Update check failed ${it.code}: ${it.message}") + null } - return@use null } } diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt index 4cae61ba..f0f51931 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt @@ -1,11 +1,15 @@ package com.github.damontecres.wholphin.test +import com.github.damontecres.wholphin.services.UpdateChecker import com.github.damontecres.wholphin.services.getDownloadUrl +import com.github.damontecres.wholphin.util.Version import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import org.junit.Assert import org.junit.Before import org.junit.Test @@ -19,71 +23,104 @@ class TestUpdateChecker { lateinit var releaseV108Json: JsonObject val assetsV108Json: JsonArray by lazy { releaseV108Json["assets"]!!.jsonArray } - private fun read(filename: String): JsonObject { + private val forgejoLatest: JsonObject by lazy { read("forgejo_release_latest.json") } + private val forgejoAssets: JsonArray by lazy { forgejoLatest["assets"]!!.jsonArray } + private val forgejoReleases: JsonArray by lazy { readElement("forgejo_releases.json").jsonArray } + + private fun readElement(filename: String): JsonElement { val resource = javaClass.classLoader?.getResource(filename) Assert.assertNotNull(resource) val fileContents = Paths.get(resource!!.toURI()).readText() - return Json.parseToJsonElement(fileContents).jsonObject + return Json.parseToJsonElement(fileContents) } + private fun read(filename: String): JsonObject = readElement(filename).jsonObject + + private fun forgejoDownload(name: String) = + "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/$name" + @Before fun setup() { releaseJson = read("release_develop.json") } + // The GitHub fixtures are upstream Wholphin releases: their assets are named Wholphin*.apk, which + // the Wolfskin updater must not select. + @Test - fun `Release chooses release`() { + fun `Release does not choose upstream Wholphin assets`() { val url = getDownloadUrl(assetsJson, false, listOf()) - Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release.apk", url) + Assert.assertNull(url) } @Test - fun `Choose abi`() { + fun `Choose abi does not choose upstream Wholphin assets`() { val url = getDownloadUrl(assetsJson, false, listOf("arm64-v8a")) - Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-arm64-v8a.apk", url) + Assert.assertNull(url) } @Test - fun `Choose unknown abi`() { + fun `Choose unknown abi does not choose upstream Wholphin assets`() { val url = getDownloadUrl(assetsJson, false, listOf("unknown")) - Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release.apk", url) + Assert.assertNull(url) } @Test - fun `Debug chooses debug`() { + fun `Debug does not choose upstream Wholphin assets`() { val url = getDownloadUrl(assetsJson, true, listOf()) - Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug.apk", url) + Assert.assertNull(url) } @Test - fun `Choose debug abi`() { + fun `Choose debug abi does not choose upstream Wholphin assets`() { val url = getDownloadUrl(assetsJson, true, listOf("arm64-v8a")) - Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-arm64-v8a.apk", url) + Assert.assertNull(url) } @Test - fun `Test regular release`() { + fun `Test regular release does not choose upstream Wholphin assets`() { releaseV108Json = read("release_v108.json") - getDownloadUrl(assetsV108Json, false, listOf("arm64-v8a")).let { url -> - Assert.assertEquals( - "https://github.com/damontecres/Wholphin/releases/download/v1.0.8/Wholphin-arm64-v8a.apk", - url, - ) - } - getDownloadUrl(assetsV108Json, false, listOf("armeabi-v7a")).let { url -> - Assert.assertEquals( - "https://github.com/damontecres/Wholphin/releases/download/v1.0.8/Wholphin-armeabi-v7a.apk", - url, - ) - } - getDownloadUrl(assetsV108Json, false, emptyList()).let { url -> - Assert.assertEquals( - "https://github.com/damontecres/Wholphin/releases/download/v1.0.8/Wholphin.apk", - url, - ) - } - getDownloadUrl(assetsV108Json, true, listOf("arm64-v8a")).let { url -> - Assert.assertNull(url) - } + Assert.assertNull(getDownloadUrl(assetsV108Json, false, listOf("arm64-v8a"))) + Assert.assertNull(getDownloadUrl(assetsV108Json, false, listOf("armeabi-v7a"))) + Assert.assertNull(getDownloadUrl(assetsV108Json, false, emptyList())) + Assert.assertNull(getDownloadUrl(assetsV108Json, true, listOf("arm64-v8a"))) + } + + @Test + fun `Parse Forgejo latest release`() { + val release = UpdateChecker.parseRelease(forgejoLatest, debug = false, supportedABIs = listOf("arm64-v8a")) + checkNotNull(release) + Assert.assertEquals(Version.fromString("v1.0.9-2-g3094fc8c"), release.version) + Assert.assertEquals(forgejoDownload("Wolfskin-arm64-v8a.apk"), release.downloadUrl) + Assert.assertEquals("2026-10-20T09:05:00Z", release.publishedAt) + Assert.assertEquals(listOf("Subtitle preference fix"), release.notes) + } + + @Test + fun `Choose Wolfskin asset for abi`() { + Assert.assertEquals(forgejoDownload("Wolfskin-arm64-v8a.apk"), getDownloadUrl(forgejoAssets, false, listOf("arm64-v8a"))) + Assert.assertEquals(forgejoDownload("Wolfskin-armeabi-v7a.apk"), getDownloadUrl(forgejoAssets, false, listOf("armeabi-v7a"))) + } + + @Test + fun `Choose universal Wolfskin asset for unknown abi`() { + Assert.assertEquals(forgejoDownload("Wolfskin.apk"), getDownloadUrl(forgejoAssets, false, listOf("unknown"))) + Assert.assertEquals(forgejoDownload("Wolfskin.apk"), getDownloadUrl(forgejoAssets, false, emptyList())) + } + + @Test + fun `Debug asset is not offered when release has none`() { + Assert.assertNull(getDownloadUrl(forgejoAssets, true, listOf("arm64-v8a"))) + } + + @Test + fun `Find installed version in release list`() { + val release = UpdateChecker.findReleaseByVersion(forgejoReleases, Version.fromString("1.0.9-2-g3094fc8c")) + Assert.assertEquals("v1.0.9-2-g3094fc8c", release?.get("name")?.jsonPrimitive?.content) + } + + @Test + fun `Find no release for version missing from list`() { + Assert.assertNull(UpdateChecker.findReleaseByVersion(forgejoReleases, Version.fromString("1.0.9-3-gffffff"))) } } diff --git a/app/src/test/resources/forgejo_release_latest.json b/app/src/test/resources/forgejo_release_latest.json new file mode 100644 index 00000000..f99734b9 --- /dev/null +++ b/app/src/test/resources/forgejo_release_latest.json @@ -0,0 +1,44 @@ +{ + "id": 42, + "name": "v1.0.9-2-g3094fc8c", + "tag_name": "pwolfskin-v1.0.9-2-g3094fc8c", + "body": "Wolfskin build\n\n**Full Changelog**: https://git.justinvisser.org/justinzeus/wolfskin/compare/v1.0.8...v1.0.9", + "draft": false, + "prerelease": false, + "created_at": "2026-10-20T09:00:00Z", + "published_at": "2026-10-20T09:05:00Z", + "target_commitish": "3094fc8c", + "html_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/tag/pwolfskin-v1.0.9-2-g3094fc8c", + "assets": [ + { + "id": 101, + "name": "Wolfskin.apk", + "size": 12345678, + "download_count": 3, + "created_at": "2026-10-20T09:05:00Z", + "uuid": "11111111-1111-1111-1111-111111111111", + "type": "attachment", + "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin.apk" + }, + { + "id": 102, + "name": "Wolfskin-arm64-v8a.apk", + "size": 7654321, + "download_count": 1, + "created_at": "2026-10-20T09:05:00Z", + "uuid": "22222222-2222-2222-2222-222222222222", + "type": "attachment", + "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin-arm64-v8a.apk" + }, + { + "id": 103, + "name": "Wolfskin-armeabi-v7a.apk", + "size": 6543210, + "download_count": 0, + "created_at": "2026-10-20T09:05:00Z", + "uuid": "33333333-3333-3333-3333-333333333333", + "type": "attachment", + "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin-armeabi-v7a.apk" + } + ] +} diff --git a/app/src/test/resources/forgejo_releases.json b/app/src/test/resources/forgejo_releases.json new file mode 100644 index 00000000..ab84e411 --- /dev/null +++ b/app/src/test/resources/forgejo_releases.json @@ -0,0 +1,52 @@ +[ + { + "id": 44, + "name": "v1.0.10-0-gfeedbee", + "tag_name": "pwolfskin-v1.0.10-0-gfeedbee", + "body": "", + "draft": false, + "prerelease": false, + "created_at": "2026-10-22T08:00:00Z", + "published_at": "2026-10-22T08:05:00Z", + "target_commitish": "feedbee", + "html_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/tag/pwolfskin-v1.0.10-0-gfeedbee", + "assets": [ + { + "id": 104, + "name": "Wolfskin.apk", + "size": 12345679, + "download_count": 0, + "created_at": "2026-10-22T08:05:00Z", + "uuid": "44444444-4444-4444-4444-444444444444", + "type": "attachment", + "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.10-0-gfeedbee/Wolfskin.apk" + } + ] + }, + { + "id": 42, + "name": "v1.0.9-2-g3094fc8c", + "tag_name": "pwolfskin-v1.0.9-2-g3094fc8c", + "body": "Wolfskin build", + "draft": false, + "prerelease": false, + "created_at": "2026-10-20T09:00:00Z", + "published_at": "2026-10-20T09:05:00Z", + "target_commitish": "3094fc8c", + "html_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/tag/pwolfskin-v1.0.9-2-g3094fc8c", + "assets": [] + }, + { + "id": 40, + "name": "nightly", + "tag_name": "nightly", + "body": "", + "draft": false, + "prerelease": true, + "created_at": "2026-10-10T09:00:00Z", + "published_at": "2026-10-10T09:05:00Z", + "target_commitish": "main", + "html_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/tag/nightly", + "assets": [] + } +] -- 2.49.1 From 1715041df3d3375df49cd89105afd622eb267f99 Mon Sep 17 00:00:00 2001 From: justinzeus Date: Thu, 22 Oct 2026 16:06:09 +0200 Subject: [PATCH 2/2] Match updater versions by ordering and cover Wolfskin asset selection --- .../wholphin/services/UpdateChecker.kt | 3 +- .../wholphin/test/TestUpdateChecker.kt | 48 +++++++++++++++++++ .../resources/forgejo_release_suffixed.json | 24 ++++++++++ docker/README.md | 5 ++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 app/src/test/resources/forgejo_release_suffixed.json diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt index 3b53138d..706285f2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt @@ -77,7 +77,8 @@ class UpdateChecker ): JsonObject? = releases .firstOrNull { release -> - Version.tryFromString(release.jsonObject["name"]?.jsonPrimitive?.contentOrNull) == version + val candidate = Version.tryFromString(release.jsonObject["name"]?.jsonPrimitive?.contentOrNull) + candidate != null && !candidate.isGreaterThan(version) && !version.isGreaterThan(candidate) }?.jsonObject /** diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt index f0f51931..2e84e2ca 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt @@ -7,6 +7,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive @@ -26,6 +27,7 @@ class TestUpdateChecker { private val forgejoLatest: JsonObject by lazy { read("forgejo_release_latest.json") } private val forgejoAssets: JsonArray by lazy { forgejoLatest["assets"]!!.jsonArray } private val forgejoReleases: JsonArray by lazy { readElement("forgejo_releases.json").jsonArray } + private val suffixedAssets: JsonArray by lazy { read("forgejo_release_suffixed.json")["assets"]!!.jsonArray } private fun readElement(filename: String): JsonElement { val resource = javaClass.classLoader?.getResource(filename) @@ -113,12 +115,58 @@ class TestUpdateChecker { Assert.assertNull(getDownloadUrl(forgejoAssets, true, listOf("arm64-v8a"))) } + @Test + fun `Choose debug ABI before universal and fall back for missing ABI`() { + for ((abis, name) in listOf( + listOf("arm64-v8a") to "Wolfskin-debug-arm64-v8a.apk", + listOf("unknown") to "Wolfskin-debug.apk", + emptyList() to "Wolfskin-debug.apk", + )) { + Assert.assertEquals(forgejoDownload(name), getDownloadUrl(suffixedAssets, true, abis)) + } + } + + @Test + fun `Choose release suffixed ABI before universal and unsuffixed assets`() { + val assets = JsonArray(forgejoAssets + suffixedAssets) + for ((abis, name) in listOf( + listOf("arm64-v8a") to "Wolfskin-release-arm64-v8a.apk", + listOf("armeabi-v7a") to "Wolfskin-release.apk", + emptyList() to "Wolfskin-release.apk", + )) { + Assert.assertEquals(forgejoDownload(name), getDownloadUrl(assets, false, abis)) + } + } + + @Test + fun `Release and debug assets do not cross build types`() { + val debugAssets = JsonArray(suffixedAssets.filter { it.jsonObject["name"]!!.jsonPrimitive.content.contains("-debug") }) + val releaseAssets = JsonArray(suffixedAssets.filter { it.jsonObject["name"]!!.jsonPrimitive.content.contains("-release") }) + Assert.assertNull(getDownloadUrl(debugAssets, false, listOf("arm64-v8a"))) + Assert.assertNull(getDownloadUrl(releaseAssets, true, listOf("arm64-v8a"))) + } + @Test fun `Find installed version in release list`() { val release = UpdateChecker.findReleaseByVersion(forgejoReleases, Version.fromString("1.0.9-2-g3094fc8c")) Assert.assertEquals("v1.0.9-2-g3094fc8c", release?.get("name")?.jsonPrimitive?.content) } + @Test + fun `Find release with different hash abbreviation length`() { + val release = UpdateChecker.findReleaseByVersion(forgejoReleases, Version.fromString("1.0.9-2-g3094fc8")) + Assert.assertEquals("v1.0.9-2-g3094fc8c", release?.get("name")?.jsonPrimitive?.content) + } + + @Test + fun `Find release with absent or zero commit count`() { + val release = UpdateChecker.findReleaseByVersion(forgejoReleases, Version.fromString("1.0.10")) + Assert.assertEquals("v1.0.10-0-gfeedbee", release?.get("name")?.jsonPrimitive?.content) + val plainRelease = JsonObject(forgejoReleases.first().jsonObject + ("name" to JsonPrimitive("v1.0.10"))) + val plainReleases = JsonArray(listOf(plainRelease)) + Assert.assertEquals(plainReleases.first(), UpdateChecker.findReleaseByVersion(plainReleases, Version.fromString("1.0.10-0-gfeedbee"))) + } + @Test fun `Find no release for version missing from list`() { Assert.assertNull(UpdateChecker.findReleaseByVersion(forgejoReleases, Version.fromString("1.0.9-3-gffffff"))) diff --git a/app/src/test/resources/forgejo_release_suffixed.json b/app/src/test/resources/forgejo_release_suffixed.json new file mode 100644 index 00000000..c2155081 --- /dev/null +++ b/app/src/test/resources/forgejo_release_suffixed.json @@ -0,0 +1,24 @@ +{ + "name": "v1.0.9-2-g3094fc8c", + "tag_name": "pwolfskin-v1.0.9-2-g3094fc8c", + "published_at": "2026-10-20T09:05:00Z", + "body": "Wolfskin release and debug builds", + "assets": [ + { + "name": "Wolfskin-debug.apk", + "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin-debug.apk" + }, + { + "name": "Wolfskin-release.apk", + "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin-release.apk" + }, + { + "name": "Wolfskin-debug-arm64-v8a.apk", + "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin-debug-arm64-v8a.apk" + }, + { + "name": "Wolfskin-release-arm64-v8a.apk", + "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin-release-arm64-v8a.apk" + } + ] +} diff --git a/docker/README.md b/docker/README.md index 353a716f..f1a1b7d0 100644 --- a/docker/README.md +++ b/docker/README.md @@ -17,6 +17,11 @@ use the catalog's exact Wholphin extension version (currently `0.2.4`). Do not place local extension AARs in `app/libs`: upstream prefers them over the pinned GitHub Packages dependencies. +Upload release assets renamed to `Wolfskin.apk` / `Wolfskin-.apk` for the +in-app updater. Gradle outputs +`Wholphin-default-release--[-].apk`; strip the +flavor, version, and code as well as replacing the prefix. + Upstream enables JVM unit tests only for debug variants; there is no `testDefaultReleaseUnitTest` task. `make test` uses the closest default-flavor task without changing upstream's test configuration. -- 2.49.1