Release: dev to main #7
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Assembles the Cloudflare Pages site for plg.wallacenet.ca into ./pages-out.
|
||||||
|
#
|
||||||
|
# Usage: stage-pages-site.sh <prod|dev> <tag> <apk-path>
|
||||||
|
#
|
||||||
|
# A Pages deploy replaces the whole site, and both channels live in one site,
|
||||||
|
# so this stages the channel being published and then re-fetches the *other*
|
||||||
|
# channel's current files from the live site so the deploy does not erase them.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
channel="$1"; tag="$2"; apk="$3"
|
||||||
|
base="${PAGES_BASE_URL:-https://plg.wallacenet.ca}"
|
||||||
|
out="pages-out"
|
||||||
|
limit=$((25 * 1024 * 1024)) # Cloudflare Pages: 25 MiB per file
|
||||||
|
|
||||||
|
case "$channel" in prod) other=dev ;; dev) other=prod ;; *) echo "bad channel: $channel" >&2; exit 2 ;; esac
|
||||||
|
|
||||||
|
rm -rf "$out"
|
||||||
|
mkdir -p "$out/$channel" "$out/$other"
|
||||||
|
cp site/index.html site/_headers "$out/"
|
||||||
|
|
||||||
|
name="$(basename "$apk")"
|
||||||
|
cp "$apk" "$out/$channel/$name"
|
||||||
|
cp "$apk" "$out/$channel/ProLibertateGames.apk"
|
||||||
|
jq -n --arg tag "$tag" --arg name "$name" --arg ch "$channel" --arg url "$base/$channel/$name" \
|
||||||
|
'{tag: $tag, apkName: $name, apkUrl: $url, channel: $ch}' > "$out/$channel/latest.json"
|
||||||
|
|
||||||
|
# Carry the other channel over. 404 means it was never published; any other
|
||||||
|
# failure must stop the deploy rather than silently dropping that channel.
|
||||||
|
code="$(curl -sS -o "$out/$other/latest.json" -w '%{http_code}' "$base/$other/latest.json" || true)"
|
||||||
|
: "${code:=000}"
|
||||||
|
if [ "$code" = 404 ]; then
|
||||||
|
echo "No $other channel published yet; skipping."
|
||||||
|
rm -rf "$out/$other"
|
||||||
|
elif [ "$code" = 200 ]; then
|
||||||
|
other_name="$(jq -er .apkName "$out/$other/latest.json")"
|
||||||
|
curl -fsS -o "$out/$other/$other_name" "$base/$other/$other_name"
|
||||||
|
cp "$out/$other/$other_name" "$out/$other/ProLibertateGames.apk"
|
||||||
|
else
|
||||||
|
echo "Fetching $other/latest.json returned HTTP $code; refusing to deploy." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
find "$out" -name '*.apk' -size +"${limit}c" -print | grep . >&2 && {
|
||||||
|
echo "APK exceeds Cloudflare Pages' 25 MiB per-file limit." >&2; exit 1; } || true
|
||||||
|
find "$out" -type f | sort
|
||||||
@@ -30,6 +30,8 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build and check
|
name: Build and check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.stage.outputs.version }}
|
||||||
env:
|
env:
|
||||||
# Stamp the APK as a dev-channel build exactly when it is a build of the
|
# Stamp the APK as a dev-channel build exactly when it is a build of the
|
||||||
# dev branch, which is exactly when it can be published below. Pull
|
# dev branch, which is exactly when it can be published below. Pull
|
||||||
@@ -108,6 +110,15 @@ jobs:
|
|||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
echo "apk=$dest" >> "$GITHUB_OUTPUT"
|
echo "apk=$dest" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Upload staged dev APK for the Pages deploy
|
||||||
|
if: github.ref == 'refs/heads/dev'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: plg-dev-staged
|
||||||
|
path: ${{ steps.stage.outputs.apk }}
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
- name: Publish dev channel prerelease
|
- name: Publish dev channel prerelease
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
env:
|
env:
|
||||||
@@ -125,3 +136,42 @@ jobs:
|
|||||||
Preview build: newer than production, and less tested. Switch to the
|
Preview build: newer than production, and less tested. Switch to the
|
||||||
Dev update channel in Settings to be offered these. Marked as a
|
Dev update channel in Settings to be offered these. Marked as a
|
||||||
prerelease so production-channel installs never see it."
|
prerelease so production-channel installs never see it."
|
||||||
|
|
||||||
|
# Publishes the build to https://plg.wallacenet.ca (Cloudflare Pages). Runs
|
||||||
|
# alongside the GitHub release above for one deployment so already-installed
|
||||||
|
# apps, which still read GitHub, can fetch the updater that reads Cloudflare.
|
||||||
|
#
|
||||||
|
# Both workflows deploy into one Pages site, and a deploy replaces the whole
|
||||||
|
# site, so they share a concurrency group and never run at the same time.
|
||||||
|
deploy-pages:
|
||||||
|
name: Deploy to Cloudflare Pages
|
||||||
|
needs: build
|
||||||
|
if: github.ref == 'refs/heads/dev'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: plg-pages-deploy
|
||||||
|
cancel-in-progress: false
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download staged APK
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: plg-dev-staged
|
||||||
|
path: staged
|
||||||
|
|
||||||
|
- name: Assemble site
|
||||||
|
run: .github/scripts/stage-pages-site.sh dev "${{ needs.build.outputs.version }}" "staged/ProLibertateGames-${{ needs.build.outputs.version }}.apk"
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Creates the project on first run; fails harmlessly if it exists.
|
||||||
|
npx --yes wrangler@3 pages project create plg --production-branch main || true
|
||||||
|
# --branch main makes this the production deployment, which is what
|
||||||
|
# plg.wallacenet.ca serves, whichever branch the workflow ran on.
|
||||||
|
npx --yes wrangler@3 pages deploy pages-out --project-name plg --branch main --commit-dirty=true
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ jobs:
|
|||||||
# anything else is a no-op rather than shipping unreviewed code to everyone.
|
# anything else is a no-op rather than shipping unreviewed code to everyone.
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.stage.outputs.version }}
|
||||||
env:
|
env:
|
||||||
# Stamps the APK as a production-channel build, versioned with the run
|
# Stamps the APK as a production-channel build, versioned with the run
|
||||||
# number that matches the tag published below.
|
# number that matches the tag published below.
|
||||||
@@ -69,7 +71,7 @@ jobs:
|
|||||||
- name: Upload APK as workflow artifact
|
- name: Upload APK as workflow artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ProLibertateGames-apk
|
name: plg-prod-staged
|
||||||
path: ${{ steps.stage.outputs.apk }}
|
path: ${{ steps.stage.outputs.apk }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
@@ -91,3 +93,42 @@ jobs:
|
|||||||
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:
|
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"
|
https://github.com/${{ github.repository }}/releases/latest/download/ProLibertateGames.apk"
|
||||||
|
|
||||||
|
# Publishes the build to https://plg.wallacenet.ca (Cloudflare Pages). Runs
|
||||||
|
# alongside the GitHub release above for one deployment so already-installed
|
||||||
|
# apps, which still read GitHub, can fetch the updater that reads Cloudflare.
|
||||||
|
#
|
||||||
|
# Both workflows deploy into one Pages site, and a deploy replaces the whole
|
||||||
|
# site, so they share a concurrency group and never run at the same time.
|
||||||
|
deploy-pages:
|
||||||
|
name: Deploy to Cloudflare Pages
|
||||||
|
needs: build
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: plg-pages-deploy
|
||||||
|
cancel-in-progress: false
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download staged APK
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: plg-prod-staged
|
||||||
|
path: staged
|
||||||
|
|
||||||
|
- name: Assemble site
|
||||||
|
run: .github/scripts/stage-pages-site.sh prod "${{ needs.build.outputs.version }}" "staged/ProLibertateGames-${{ needs.build.outputs.version }}.apk"
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Creates the project on first run; fails harmlessly if it exists.
|
||||||
|
npx --yes wrangler@3 pages project create plg --production-branch main || true
|
||||||
|
# --branch main makes this the production deployment, which is what
|
||||||
|
# plg.wallacenet.ca serves, whichever branch the workflow ran on.
|
||||||
|
npx --yes wrangler@3 pages deploy pages-out --project-name plg --branch main --commit-dirty=true
|
||||||
|
|||||||
@@ -198,9 +198,31 @@ production package instead.
|
|||||||
publishes it as a GitHub Release tagged `v<series>.<run-number>`. The in-app updater
|
publishes it as a GitHub Release tagged `v<series>.<run-number>`. The in-app updater
|
||||||
reads that tag, which is why the APK's `versionCode` is the same run number.
|
reads that tag, which is why the APK's `versionCode` is the same run number.
|
||||||
|
|
||||||
|
### Hosting: plg.wallacenet.ca
|
||||||
|
|
||||||
|
Both channels are also deployed to one Cloudflare Pages project (`plg`), served at
|
||||||
|
`https://plg.wallacenet.ca`:
|
||||||
|
|
||||||
|
```
|
||||||
|
/ landing page (reads both latest.json files)
|
||||||
|
/prod/latest.json {tag, apkName, apkUrl, channel} — what the updater reads
|
||||||
|
/prod/ProLibertateGames.apk, /prod/ProLibertateGames-v<series>.<n>.apk
|
||||||
|
/dev/... same, for the dev channel
|
||||||
|
```
|
||||||
|
|
||||||
|
Each workflow's `deploy-pages` job runs `.github/scripts/stage-pages-site.sh`,
|
||||||
|
which stages its own channel and re-fetches the other channel from the live site,
|
||||||
|
because a Pages deploy replaces everything. The two share a concurrency group so
|
||||||
|
they cannot race. Needs repo secrets `CLOUDFLARE_API_TOKEN` (Account → Cloudflare
|
||||||
|
Pages → Edit) and `CLOUDFLARE_ACCOUNT_ID`. Pages caps files at 25 MiB.
|
||||||
|
|
||||||
|
The in-app updater reads these `latest.json` files. GitHub Releases are still
|
||||||
|
published for one transition deployment so installs that predate this change can
|
||||||
|
fetch it; after that, remove the GitHub release steps from both workflows.
|
||||||
|
|
||||||
### Installing without the app
|
### Installing without the app
|
||||||
|
|
||||||
The newest production build is always at this link, which never changes:
|
The newest production build is always at **https://plg.wallacenet.ca/prod/ProLibertateGames.apk** (dev: `/dev/ProLibertateGames.apk`). The GitHub link below remains during the transition:
|
||||||
|
|
||||||
**https://github.com/adrianoftyriel/ProLibertateGames/releases/latest/download/ProLibertateGames.apk**
|
**https://github.com/adrianoftyriel/ProLibertateGames/releases/latest/download/ProLibertateGames.apk**
|
||||||
|
|
||||||
@@ -367,7 +389,7 @@ game/ rules engines and AI — pure Kotlin, no Android, unit-tested
|
|||||||
net/ wire protocol, LAN transport, lobby, match driver
|
net/ wire protocol, LAN transport, lobby, match driver
|
||||||
score/ scorekeeper sheet — pure Kotlin, unit-tested — and its store
|
score/ scorekeeper sheet — pure Kotlin, unit-tested — and its store
|
||||||
settings/ DataStore-backed preferences
|
settings/ DataStore-backed preferences
|
||||||
update/ GitHub Releases OTA updater, and the channel rule — pure, tested
|
update/ Cloudflare Pages OTA updater, and the channel rule — pure, tested
|
||||||
ui/ Compose screens
|
ui/ Compose screens
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<!-- OTA updates from GitHub Releases, plus LAN play over TCP sockets. -->
|
<!-- OTA updates from plg.wallacenet.ca, plus LAN play over TCP sockets. -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<!-- Network Service Discovery needs multicast to see hosts on the LAN. -->
|
<!-- Network Service Discovery needs multicast to see hosts on the LAN. -->
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ enum class UpdateChannel(val label: String, val blurb: String) {
|
|||||||
/** Marks a tag or a version name as belonging to the dev channel. */
|
/** Marks a tag or a version name as belonging to the dev channel. */
|
||||||
const val DEV_SUFFIX = "-dev"
|
const val DEV_SUFFIX = "-dev"
|
||||||
|
|
||||||
/** One published build, as read off a GitHub release. */
|
/** One published build, as read off a channel's `latest.json`. */
|
||||||
data class Release(
|
data class Release(
|
||||||
val tag: String,
|
val tag: String,
|
||||||
val versionCode: Int,
|
val versionCode: Int,
|
||||||
@@ -70,7 +70,7 @@ fun versionCodeOfTag(tag: String): Int? =
|
|||||||
* nothing at all.
|
* nothing at all.
|
||||||
*
|
*
|
||||||
* Only an actual update is worth a word. Opening the app is not asking a
|
* Only an actual update is worth a word. Opening the app is not asking a
|
||||||
* question, so a check that finds nothing — or that cannot reach GitHub at all
|
* question, so a check that finds nothing — or that cannot reach the update server at all
|
||||||
* — stays quiet; a phone on a train would otherwise be told off for it every
|
* — stays quiet; a phone on a train would otherwise be told off for it every
|
||||||
* single launch. The button in Settings *is* asking a question, and reports
|
* single launch. The button in Settings *is* asking a question, and reports
|
||||||
* every answer including those two.
|
* every answer including those two.
|
||||||
|
|||||||
@@ -8,19 +8,17 @@ import android.provider.Settings
|
|||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Over-the-air updates from the repository's GitHub Releases.
|
* Over-the-air updates from plg.wallacenet.ca (Cloudflare Pages).
|
||||||
*
|
*
|
||||||
* Release tags are v<series>.<run-number> and the APK's versionCode is that same run
|
* Release tags are v<series>.<run-number> and the APK's versionCode is that same run
|
||||||
* number, so comparing the two is enough to tell whether a newer build exists.
|
* number, so comparing the two is enough to tell whether a newer build exists.
|
||||||
* The repository's releases must be publicly downloadable — no token is
|
* Everything there is public — no token is embedded in the app.
|
||||||
* embedded in the app, and GitHub blocks anonymous access to private assets.
|
|
||||||
*
|
*
|
||||||
* There is nothing to choose here. The channel comes from the running APK, so
|
* There is nothing to choose here. The channel comes from the running APK, so
|
||||||
* a dev install looks only at dev builds and a production install only at
|
* a dev install looks only at dev builds and a production install only at
|
||||||
@@ -48,65 +46,37 @@ class Updater(private val activity: Activity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Production reads GitHub's "latest release", which by definition skips
|
* Each channel publishes a `latest.json` next to its APK on Cloudflare
|
||||||
* prereleases. Dev walks the release list for the newest prerelease, which
|
* Pages: `/prod/latest.json` and `/dev/latest.json`. The two live in one
|
||||||
* is what the CI pipeline publishes from the dev branch.
|
* site but separate paths, so a build can only ever look down its own line.
|
||||||
*/
|
*/
|
||||||
private fun fetchLatestRelease(channel: UpdateChannel): Release? = when (channel) {
|
private fun fetchLatestRelease(channel: UpdateChannel): Release? {
|
||||||
UpdateChannel.PRODUCTION ->
|
val path = if (channel == UpdateChannel.DEV) "dev" else "prod"
|
||||||
getJsonObject("https://api.github.com/repos/$OWNER/$REPO/releases/latest")
|
return getJsonObject("$BASE_URL/$path/latest.json")?.let { toRelease(it) }
|
||||||
?.let { toRelease(it) }
|
|
||||||
|
|
||||||
UpdateChannel.DEV ->
|
|
||||||
getJsonArray("https://api.github.com/repos/$OWNER/$REPO/releases?per_page=30")
|
|
||||||
?.let { releases ->
|
|
||||||
// GitHub returns newest first.
|
|
||||||
(0 until releases.length())
|
|
||||||
.map { releases.getJSONObject(it) }
|
|
||||||
.firstOrNull { it.optBoolean("prerelease") && !it.optBoolean("draft") }
|
|
||||||
?.let { toRelease(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toRelease(json: JSONObject): Release? {
|
private fun toRelease(json: JSONObject): Release? {
|
||||||
val tag = json.optString("tag_name").ifBlank { return null }
|
val tag = json.optString("tag").ifBlank { return null }
|
||||||
val versionCode = versionCodeOfTag(tag) ?: return null
|
val versionCode = versionCodeOfTag(tag) ?: return null
|
||||||
val channel = channelOfTag(tag, json.optBoolean("prerelease"))
|
val apkUrl = json.optString("apkUrl").ifBlank { return null }
|
||||||
|
|
||||||
val assets = json.optJSONArray("assets") ?: 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(
|
return Release(
|
||||||
tag = tag,
|
tag = tag,
|
||||||
versionCode = versionCode,
|
versionCode = versionCode,
|
||||||
apkUrl = chosen.getString("browser_download_url"),
|
apkUrl = apkUrl,
|
||||||
apkName = chosen.optString("name"),
|
apkName = json.optString("apkName"),
|
||||||
channel = channel,
|
channel = channelOfTag(tag, prerelease = false),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openConnection(url: String): HttpURLConnection =
|
private fun getJsonObject(url: String): JSONObject? {
|
||||||
(URL(url).openConnection() as HttpURLConnection).apply {
|
val connection = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||||
requestMethod = "GET"
|
requestMethod = "GET"
|
||||||
setRequestProperty("Accept", "application/vnd.github+json")
|
|
||||||
setRequestProperty("User-Agent", "ProLibertateGames-Updater")
|
setRequestProperty("User-Agent", "ProLibertateGames-Updater")
|
||||||
connectTimeout = 15_000
|
connectTimeout = 15_000
|
||||||
readTimeout = 15_000
|
readTimeout = 15_000
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getJsonObject(url: String): JSONObject? {
|
|
||||||
val connection = openConnection(url)
|
|
||||||
try {
|
try {
|
||||||
|
// 404 means the channel has nothing published yet.
|
||||||
if (connection.responseCode != 200) return null
|
if (connection.responseCode != 200) return null
|
||||||
return JSONObject(connection.inputStream.bufferedReader().use { it.readText() })
|
return JSONObject(connection.inputStream.bufferedReader().use { it.readText() })
|
||||||
} finally {
|
} finally {
|
||||||
@@ -114,16 +84,6 @@ class Updater(private val activity: Activity) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getJsonArray(url: String): JSONArray? {
|
|
||||||
val connection = openConnection(url)
|
|
||||||
try {
|
|
||||||
if (connection.responseCode != 200) return null
|
|
||||||
return JSONArray(connection.inputStream.bufferedReader().use { it.readText() })
|
|
||||||
} finally {
|
|
||||||
connection.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Read back from the installed APK's own version name. */
|
/** Read back from the installed APK's own version name. */
|
||||||
fun installedChannel(): UpdateChannel = channelOfVersionName(installedVersionName())
|
fun installedChannel(): UpdateChannel = channelOfVersionName(installedVersionName())
|
||||||
|
|
||||||
@@ -175,7 +135,6 @@ class Updater(private val activity: Activity) {
|
|||||||
activity.packageManager.getPackageInfo(activity.packageName, 0).versionName ?: "unknown"
|
activity.packageManager.getPackageInfo(activity.packageName, 0).versionName ?: "unknown"
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val OWNER = "adrianoftyriel"
|
const val BASE_URL = "https://plg.wallacenet.ca"
|
||||||
const val REPO = "ProLibertateGames"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/*/latest.json
|
||||||
|
Cache-Control: no-cache
|
||||||
|
Access-Control-Allow-Origin: *
|
||||||
|
|
||||||
|
/*.apk
|
||||||
|
Content-Type: application/vnd.android.package-archive
|
||||||
|
Cache-Control: public, max-age=300
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Pro Libertate Games</title>
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
|
||||||
|
body { max-width: 32rem; margin: 3rem auto; padding: 0 1rem; line-height: 1.5; }
|
||||||
|
.card { border: 1px solid #8884; border-radius: .5rem; padding: 1rem; margin: 1rem 0; }
|
||||||
|
a.dl { display: inline-block; padding: .5rem 1rem; border-radius: .4rem; background: #2563eb; color: #fff; text-decoration: none; }
|
||||||
|
small { opacity: .7; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Pro Libertate Games</h1>
|
||||||
|
<p>Android builds. Allow <em>Install unknown apps</em> for your browser to sideload.</p>
|
||||||
|
<div class="card" id="production"><h2>Production</h2><p>Loading…</p></div>
|
||||||
|
<div class="card" id="dev"><h2>Dev</h2><p>Loading…</p></div>
|
||||||
|
<script>
|
||||||
|
for (const [channel, path] of [["production", "prod"], ["dev", "dev"]]) {
|
||||||
|
const box = document.getElementById(channel);
|
||||||
|
const title = box.querySelector("h2").outerHTML;
|
||||||
|
fetch(`/${path}/latest.json`, { cache: "no-cache" })
|
||||||
|
.then(r => r.ok ? r.json() : Promise.reject())
|
||||||
|
.then(j => {
|
||||||
|
box.innerHTML = `${title}<p><a class="dl" href="${j.apkUrl}">Download ${j.tag}</a></p>` +
|
||||||
|
`<small>Always newest: <a href="/${path}/ProLibertateGames.apk">/${path}/ProLibertateGames.apk</a></small>`;
|
||||||
|
})
|
||||||
|
.catch(() => { box.innerHTML = `${title}<p>No build published yet.</p>`; });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user