Use Forgejo releases for Wolfskin updates and match installed versions consistently #20
7 changed files with 314 additions and 70 deletions
|
|
@ -576,7 +576,7 @@ sealed interface AppPreference<Pref, T> {
|
||||||
val UpdateUrl =
|
val UpdateUrl =
|
||||||
AppStringPreference<AppPreferences>(
|
AppStringPreference<AppPreferences>(
|
||||||
title = R.string.update_url,
|
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 },
|
getter = { it.updateUrl },
|
||||||
setter = { prefs, value ->
|
setter = { prefs, value ->
|
||||||
prefs.update { updateUrl = value }
|
prefs.update { updateUrl = value }
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class UpdateChecker
|
||||||
@param:StandardOkHttpClient private val okHttpClient: OkHttpClient,
|
@param:StandardOkHttpClient private val okHttpClient: OkHttpClient,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val ASSET_NAME = "Wholphin"
|
const val ASSET_NAME = "Wolfskin"
|
||||||
const val APK_NAME = "$ASSET_NAME.apk"
|
const val APK_NAME = "$ASSET_NAME.apk"
|
||||||
|
|
||||||
private const val APK_MIME_TYPE = "application/vnd.android.package-archive"
|
private const val APK_MIME_TYPE = "application/vnd.android.package-archive"
|
||||||
|
|
@ -65,6 +65,53 @@ class UpdateChecker
|
||||||
private val NOTE_REGEX = Regex("<!-- app-note:(.+) -->")
|
private val NOTE_REGEX = Regex("<!-- app-note:(.+) -->")
|
||||||
|
|
||||||
val ACTIVE = BuildConfig.UPDATING_ENABLED
|
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 ->
|
||||||
|
val candidate = Version.tryFromString(release.jsonObject["name"]?.jsonPrimitive?.contentOrNull)
|
||||||
|
candidate != null && !candidate.isGreaterThan(version) && !version.isGreaterThan(candidate)
|
||||||
|
}?.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<String> = 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 +174,29 @@ class UpdateChecker
|
||||||
return Version.fromString(pkgInfo.versionName!!)
|
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? {
|
suspend fun getRelease(version: Version): Release? {
|
||||||
val url =
|
val request =
|
||||||
"https://api.github.com/repos/damontecres/Wholphin/releases/tags/v${version.major}.${version.minor}.${version.patch}"
|
Request
|
||||||
|
.Builder()
|
||||||
|
.url(RELEASES_URL)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
return withContext(WholphinDispatchers.IO) {
|
return withContext(WholphinDispatchers.IO) {
|
||||||
val request =
|
okHttpClient.newCall(request).execute().use {
|
||||||
Request
|
if (it.isSuccessful) {
|
||||||
.Builder()
|
val releases = Json.parseToJsonElement(it.body.string()).jsonArray
|
||||||
.url(url)
|
findReleaseByVersion(releases, version)?.let { json -> parseRelease(json, BuildConfig.DEBUG) }
|
||||||
.get()
|
} else {
|
||||||
.build()
|
Timber.w("Release lookup failed ${it.code}: ${it.message}")
|
||||||
getRelease(request)
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,36 +217,11 @@ class UpdateChecker
|
||||||
private fun getRelease(request: Request): Release? {
|
private fun getRelease(request: Request): Release? {
|
||||||
return okHttpClient.newCall(request).execute().use {
|
return okHttpClient.newCall(request).execute().use {
|
||||||
if (it.isSuccessful) {
|
if (it.isSuccessful) {
|
||||||
val result = Json.parseToJsonElement(it.body.string())
|
parseRelease(Json.parseToJsonElement(it.body.string()).jsonObject, BuildConfig.DEBUG)
|
||||||
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")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Update check failed ${it.code}: ${it.message}")
|
Timber.w("Update check failed ${it.code}: ${it.message}")
|
||||||
|
null
|
||||||
}
|
}
|
||||||
return@use null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
package com.github.damontecres.wholphin.test
|
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.services.getDownloadUrl
|
||||||
|
import com.github.damontecres.wholphin.util.Version
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -19,71 +24,151 @@ class TestUpdateChecker {
|
||||||
lateinit var releaseV108Json: JsonObject
|
lateinit var releaseV108Json: JsonObject
|
||||||
val assetsV108Json: JsonArray by lazy { releaseV108Json["assets"]!!.jsonArray }
|
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 val suffixedAssets: JsonArray by lazy { read("forgejo_release_suffixed.json")["assets"]!!.jsonArray }
|
||||||
|
|
||||||
|
private fun readElement(filename: String): JsonElement {
|
||||||
val resource = javaClass.classLoader?.getResource(filename)
|
val resource = javaClass.classLoader?.getResource(filename)
|
||||||
Assert.assertNotNull(resource)
|
Assert.assertNotNull(resource)
|
||||||
val fileContents = Paths.get(resource!!.toURI()).readText()
|
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
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
releaseJson = read("release_develop.json")
|
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
|
@Test
|
||||||
fun `Release chooses release`() {
|
fun `Release does not choose upstream Wholphin assets`() {
|
||||||
val url = getDownloadUrl(assetsJson, false, listOf())
|
val url = getDownloadUrl(assetsJson, false, listOf())
|
||||||
Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release.apk", url)
|
Assert.assertNull(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Choose abi`() {
|
fun `Choose abi does not choose upstream Wholphin assets`() {
|
||||||
val url = getDownloadUrl(assetsJson, false, listOf("arm64-v8a"))
|
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
|
@Test
|
||||||
fun `Choose unknown abi`() {
|
fun `Choose unknown abi does not choose upstream Wholphin assets`() {
|
||||||
val url = getDownloadUrl(assetsJson, false, listOf("unknown"))
|
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
|
@Test
|
||||||
fun `Debug chooses debug`() {
|
fun `Debug does not choose upstream Wholphin assets`() {
|
||||||
val url = getDownloadUrl(assetsJson, true, listOf())
|
val url = getDownloadUrl(assetsJson, true, listOf())
|
||||||
Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug.apk", url)
|
Assert.assertNull(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Choose debug abi`() {
|
fun `Choose debug abi does not choose upstream Wholphin assets`() {
|
||||||
val url = getDownloadUrl(assetsJson, true, listOf("arm64-v8a"))
|
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
|
@Test
|
||||||
fun `Test regular release`() {
|
fun `Test regular release does not choose upstream Wholphin assets`() {
|
||||||
releaseV108Json = read("release_v108.json")
|
releaseV108Json = read("release_v108.json")
|
||||||
getDownloadUrl(assetsV108Json, false, listOf("arm64-v8a")).let { url ->
|
Assert.assertNull(getDownloadUrl(assetsV108Json, false, listOf("arm64-v8a")))
|
||||||
Assert.assertEquals(
|
Assert.assertNull(getDownloadUrl(assetsV108Json, false, listOf("armeabi-v7a")))
|
||||||
"https://github.com/damontecres/Wholphin/releases/download/v1.0.8/Wholphin-arm64-v8a.apk",
|
Assert.assertNull(getDownloadUrl(assetsV108Json, false, emptyList()))
|
||||||
url,
|
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 `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<String>() to "Wolfskin-debug.apk",
|
||||||
|
)) {
|
||||||
|
Assert.assertEquals(forgejoDownload(name), getDownloadUrl(suffixedAssets, true, abis))
|
||||||
}
|
}
|
||||||
getDownloadUrl(assetsV108Json, false, listOf("armeabi-v7a")).let { url ->
|
}
|
||||||
Assert.assertEquals(
|
|
||||||
"https://github.com/damontecres/Wholphin/releases/download/v1.0.8/Wholphin-armeabi-v7a.apk",
|
@Test
|
||||||
url,
|
fun `Choose release suffixed ABI before universal and unsuffixed assets`() {
|
||||||
)
|
val assets = JsonArray(forgejoAssets + suffixedAssets)
|
||||||
}
|
for ((abis, name) in listOf(
|
||||||
getDownloadUrl(assetsV108Json, false, emptyList()).let { url ->
|
listOf("arm64-v8a") to "Wolfskin-release-arm64-v8a.apk",
|
||||||
Assert.assertEquals(
|
listOf("armeabi-v7a") to "Wolfskin-release.apk",
|
||||||
"https://github.com/damontecres/Wholphin/releases/download/v1.0.8/Wholphin.apk",
|
emptyList<String>() to "Wolfskin-release.apk",
|
||||||
url,
|
)) {
|
||||||
)
|
Assert.assertEquals(forgejoDownload(name), getDownloadUrl(assets, false, abis))
|
||||||
}
|
|
||||||
getDownloadUrl(assetsV108Json, true, listOf("arm64-v8a")).let { url ->
|
|
||||||
Assert.assertNull(url)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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")))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
44
app/src/test/resources/forgejo_release_latest.json
Normal file
44
app/src/test/resources/forgejo_release_latest.json
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"id": 42,
|
||||||
|
"name": "v1.0.9-2-g3094fc8c",
|
||||||
|
"tag_name": "pwolfskin-v1.0.9-2-g3094fc8c",
|
||||||
|
"body": "Wolfskin build\n<!-- app-note:Subtitle preference fix -->\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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
24
app/src/test/resources/forgejo_release_suffixed.json
Normal file
24
app/src/test/resources/forgejo_release_suffixed.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
52
app/src/test/resources/forgejo_releases.json
Normal file
52
app/src/test/resources/forgejo_releases.json
Normal file
|
|
@ -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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -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
|
place local extension AARs in `app/libs`: upstream prefers them over the pinned
|
||||||
GitHub Packages dependencies.
|
GitHub Packages dependencies.
|
||||||
|
|
||||||
|
Upload release assets renamed to `Wolfskin.apk` / `Wolfskin-<abi>.apk` for the
|
||||||
|
in-app updater. Gradle outputs
|
||||||
|
`Wholphin-default-release-<versionName>-<versionCode>[-<abi>].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
|
Upstream enables JVM unit tests only for debug variants; there is no
|
||||||
`testDefaultReleaseUnitTest` task. `make test` uses the closest default-flavor
|
`testDefaultReleaseUnitTest` task. `make test` uses the closest default-flavor
|
||||||
task without changing upstream's test configuration.
|
task without changing upstream's test configuration.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue