Publish signed Android APKs as gitea releases for Obtainium
This commit is contained in:
parent
05cb5c29f7
commit
0922a98eb6
3 changed files with 118 additions and 1 deletions
29
README.md
29
README.md
|
|
@ -8,6 +8,7 @@ A discord-like nostr client based on the idea of "relays as groups". Supports NI
|
|||
|
||||
- **Web** — [app.flotilla.social](https://app.flotilla.social), installable as a PWA
|
||||
- **Android** — [Google Play](https://play.google.com/store/apps/details?id=social.flotilla)
|
||||
- **Android APK** — [releases](https://gitea.coracle.social/coracle/flotilla/releases), see [Android releases](#android-releases)
|
||||
- **iOS** — [App Store](https://apps.apple.com/us/app/flotilla-chat/id6741344107)
|
||||
- **Your own server** — see [Deployment](#deployment)
|
||||
|
||||
|
|
@ -153,6 +154,34 @@ Use the AppImage or installed executable rather than the installer. This checks
|
|||
local assets, navigation, workers, and CSP using a disposable profile. Installation, reboot, and
|
||||
uninstall still require target-OS testing.
|
||||
|
||||
## Android releases
|
||||
|
||||
Signed APKs are attached to releases on the
|
||||
[releases page](https://gitea.coracle.social/coracle/flotilla/releases), so Android users can
|
||||
install and update outside an app store.
|
||||
|
||||
Bump the version, write its `CHANGELOG.md` section and push the matching tag, then:
|
||||
|
||||
```sh
|
||||
pnpm run release:android
|
||||
GITEA_TOKEN=... pnpm run publish:android
|
||||
```
|
||||
|
||||
`publish:android` creates the release for the tag, takes its notes from the changelog, and attaches
|
||||
the APK as `flotilla-<version>.apk`, replacing any existing asset of that name. It reads the
|
||||
repository and the APK path from `zapstore.yaml`, so a release and a zapstore publish ship the same
|
||||
file.
|
||||
|
||||
### Obtainium
|
||||
|
||||
[Obtainium](https://obtainium.imranr.dev/) installs and updates Android apps from their release
|
||||
pages. Gitea and Forgejo share a release API, so it works against this repository:
|
||||
|
||||
- App source URL: `https://gitea.coracle.social/coracle/flotilla`
|
||||
- Override source: `Forgejo (Codeberg)`
|
||||
|
||||
Obtainium reports the git tag as the version.
|
||||
|
||||
## Deployment
|
||||
|
||||
To run your own Flotilla, it's as simple as:
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@
|
|||
"build:server": "vite build --config vite.config.server.ts",
|
||||
"start": "node server.js",
|
||||
"release:android": "./scripts/build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
||||
"publish:android": "node scripts/publish-android-release.mjs",
|
||||
"bump": "node scripts/bump-version.mjs",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check src e2e packages playwright.config.ts capacitor.config.ts electron/*.ts electron/electron-builder.config.mjs scripts/dev-desktop.mjs scripts/package-desktop.mjs && eslint src e2e packages capacitor.config.ts electron/*.ts electron/electron-builder.config.mjs scripts/dev-desktop.mjs scripts/package-desktop.mjs",
|
||||
"lint": "prettier --check src e2e packages playwright.config.ts capacitor.config.ts electron/*.ts electron/electron-builder.config.mjs scripts/dev-desktop.mjs scripts/package-desktop.mjs scripts/publish-android-release.mjs && eslint src e2e packages capacitor.config.ts electron/*.ts electron/electron-builder.config.mjs scripts/dev-desktop.mjs scripts/package-desktop.mjs scripts/publish-android-release.mjs",
|
||||
"test": "playwright test",
|
||||
"test:desktop": "playwright test --config e2e/desktop/playwright.config.ts",
|
||||
"test:ui": "playwright test --ui",
|
||||
|
|
|
|||
87
scripts/publish-android-release.mjs
Normal file
87
scripts/publish-android-release.mjs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env node
|
||||
import {readFile} from "node:fs/promises"
|
||||
|
||||
const read = path => readFile(new URL(path, import.meta.url), "utf-8")
|
||||
|
||||
const token = process.env.GITEA_TOKEN
|
||||
const {name, version} = JSON.parse(await read("../package.json"))
|
||||
const changelog = await read("../CHANGELOG.md")
|
||||
const zapstore = await read("../zapstore.yaml")
|
||||
|
||||
const zapstoreField = key => {
|
||||
const match = zapstore.match(new RegExp(`^${key}:\\s*(\\S+)\\s*$`, "m"))
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`zapstore.yaml is missing ${key}`)
|
||||
}
|
||||
|
||||
return match[1]
|
||||
}
|
||||
|
||||
const repository = new URL(zapstoreField("repository"))
|
||||
const base = `${repository.origin}/api/v1/repos${repository.pathname}`
|
||||
|
||||
const api = async (method, path, {body, allow404} = {}) => {
|
||||
const multipart = body instanceof FormData
|
||||
const response = await fetch(base + path, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
...(body && !multipart ? {"Content-Type": "application/json"} : {}),
|
||||
},
|
||||
body: multipart ? body : body && JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (response.status === 404 && allow404) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${method} ${path} responded ${response.status}: ${await response.text()}`)
|
||||
}
|
||||
|
||||
return response.status === 204 ? undefined : response.json()
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw new Error("Set GITEA_TOKEN to a token with write access to the repository")
|
||||
}
|
||||
|
||||
const lines = changelog.split("\n")
|
||||
const heading = lines.indexOf(`# ${version}`)
|
||||
|
||||
if (heading < 0) {
|
||||
throw new Error(`CHANGELOG.md has no "# ${version}" section`)
|
||||
}
|
||||
|
||||
const remainder = lines.slice(heading + 1)
|
||||
const nextHeading = remainder.findIndex(line => line.startsWith("# "))
|
||||
const notes = (nextHeading < 0 ? remainder : remainder.slice(0, nextHeading)).join("\n").trim()
|
||||
const apk = await readFile(new URL(`../${zapstoreField("release_source")}`, import.meta.url))
|
||||
|
||||
if (!(await api("GET", `/tags/${version}`, {allow404: true}))) {
|
||||
throw new Error(`${repository} has no ${version} tag; push it before publishing`)
|
||||
}
|
||||
|
||||
const release =
|
||||
(await api("GET", `/releases/tags/${version}`, {allow404: true})) ??
|
||||
(await api("POST", "/releases", {body: {tag_name: version, name: version, body: notes}}))
|
||||
|
||||
const filename = `${name}-${version}.apk`
|
||||
const existing = release.assets?.find(asset => asset.name === filename)
|
||||
|
||||
if (existing) {
|
||||
await api("DELETE", `/releases/${release.id}/assets/${existing.id}`)
|
||||
}
|
||||
|
||||
const form = new FormData()
|
||||
|
||||
form.append("attachment", new Blob([apk]), filename)
|
||||
|
||||
const asset = await api(
|
||||
"POST",
|
||||
`/releases/${release.id}/assets?name=${encodeURIComponent(filename)}`,
|
||||
{body: form},
|
||||
)
|
||||
|
||||
console.log(asset.browser_download_url)
|
||||
Loading…
Reference in a new issue