A live publish on git.wallacenet.ca kept coming back invisible to the updater: the create accepted "draft": false, and the release was still stored as a draft once the APK was attached — so the anonymous list the app reads came back empty even though every publish step reported success. The script now attaches first, then reads the release back and PATCHes the state if it did not stick, failing loudly if the correction is refused. A draft is not a channel a preview build can silently become. Also lists all 22 catalogue games in the README's table; seven were missing, which read as "the app has 15 games". Verified against the live instance: anonymous list shows v1.4.1001-dev as a published prerelease with a tag-named asset, the production /releases/latest correctly returns 404 while only a prerelease exists, and an anonymous download through the URL the app builds matches the built APK byte for byte. Co-Authored-By: Paperclip <[email protected]>
6.8 KiB
CI, releases, and OTA — decisions and how to build
Status as of the Pilgrims of Katan bootstrap (WALA-118).
Decisions
-
The release channel is this instance's own releases. The app ships from https://git.wallacenet.ca/devadmin/PilgrimsOfKatan, and the in-app updater reads that repo's releases over Gitea's REST API. GitHub is not a channel for this project; the only GitHub URLs left are the
upstreamremote and the attribution link in the README.The repo is public on purpose: the updater downloads anonymously, with no token embedded in the APK. A private repo would make every check fail (the API answers 404 to anonymous callers), not leak anything — but it would also stop updates dead.
-
CI lives in Actions-compatible workflows in
.github/workflows/. Gitea Actions runs workflows from.github/workflows/(or.gitea/workflows/), so these files serve both a GitHub-style runner and the Gitea instance. Publishing goes throughscripts/publish-release.sh— Gitea's REST API, the same routes the app's updater reads — rather than theghCLI, which a runner image need not carry.Workflow Trigger Publishes ci.ymlpush to dev, PRs intodev/main, manualdev prerelease, from devonlyrelease.ymlpush to main, manualproduction release, from mainonlyPull requests build and check but never publish. Prereleases are marked so the "latest release" endpoint — which skips them — can never hand a dev build to a production install.
-
A runner has to be registered before any of that executes. As of the bootstrap the instance has no Actions runner, so the workflows are correct but idle: a Gitea runner (Docker or host) must be registered against the instance and able to resolve
actions/checkout,actions/setup-java,android-actions/setup-androidandactions/upload-artifact. Until then the manual build below and the manual publish script are the working paths. -
The local build is the guaranteed path. Workflows need a runner that may not exist yet;
./gradlew assembleDebug+./gradlew testDebugUnitTestwork on any machine with JDK 17 and the Android SDK (compileSdk 34), and CI's green means exactly these two commands plus lint.
Build numbers, tags, and why updates never downgrade
An APK is <series>.<build number>, the release tag is v<series>.<build number>, and the build number is what Android sees as versionCode:
build number = CI run number + plgVersionRunOffset (gradle.properties)
plgVersionSeries(currently1.4) is the human-facing series, set by hand ingradle.properties.plgVersionRunOffset(currently1000) is a permanent floor, also set by hand ingradle.properties, set once at bootstrap and never changed.
Why the offset exists: Gitea Actions restarts run numbers at 1 for each
workflow, so without it the first release of a fresh instance would be build
1 — the number an unstamped local build already carries — and any reset of
the counter would hand Android a lower versionCode, which it refuses as a
downgrade. With the offset, every stamped build starts at 1001 and only ever
climbs, because the run number is monotonic and the offset never moves. The
gradle build and both workflows read the same two properties, so the tag and
the APK inside it cannot name different versions.
The series is a name for people: bumping it changes nothing about ordering,
because versionCode never includes the series.
Unstamped builds (local, or a PR build) are versionCode 1, below every
release — the honest answer for a build nobody stamped.
Manual build (works today, no runner needed)
# JDK 17, Android SDK with platform 34 + build-tools 34
export JAVA_HOME=... # a JDK 17 install
export ANDROID_HOME=... # SDK root (or set sdk.dir in local.properties)
./gradlew assembleDebug # dev-channel app (default: ca.wallacenet.pilgrimsofkatan.dev)
PLG_CHANNEL=production ./gradlew assembleDebug # production app (ca.wallacenet.pilgrimsofkatan)
./gradlew testDebugUnitTest # JVM unit tests
./gradlew lintDebug # Android lint (CI runs this too)
The two variants install side by side: different applicationId, different
launcher name (Pilgrims of Katan DEV vs Pilgrims of Katan), different brand
colour.
Manual release (works today, no runner needed)
GITEA_TOKEN=… ./scripts/publish-release.sh v1.4.1001 app/build/outputs/apk/debug/app-debug.apk release "$(git rev-parse HEAD)"
The script takes <tag> <apk> <prerelease|release> [target-sha], creates the
release through POST /api/v1/repos/{owner}/{repo}/releases, attaches the APK
through POST …/releases/{id}/assets — which is the upload route Gitea's
Swagger declares for this instance (1.27.3) — and then reads the release back
and corrects its state if it did not stick. That last step is not decoration:
this instance has been observed to accept draft: false at create time and
store the release as a draft after the attachment lands, and a draft is
invisible to the updater, so the script verifies rather than assumes. If the
tag already exists it publishes into the existing release instead of failing,
so a retry is safe.
What the in-app updater reads
app/src/main/java/org/prolibertate/games/update/Updater.kt:
| Channel | Endpoint |
|---|---|
| production | GET /api/v1/repos/devadmin/PilgrimsOfKatan/releases/latest |
| dev | GET /api/v1/repos/devadmin/PilgrimsOfKatan/releases?limit=30 |
/releases/latestis documented by Gitea as the most recent non-prerelease, non-draft release — the same rule GitHub implements, so the production channel keeps skipping dev builds.- The dev path takes the highest
versionCodeamong the prereleases rather than whatever came first on the page, so it does not depend on server ordering. - The JSON shape is the one the code already parsed from GitHub:
tag_name,prerelease,draft, and assets carryingbrowser_download_url. Gitea sayslimitwhere GitHub saysper_page. - Each asset must be named so it contains its tag —
PilgrimsOfKatan-v1.4.1001.apkfor tagv1.4.1001— because the updater picks the versioned APK out of the release's assets by that rule.
BuildConfig.OTA_ENABLED is a kill switch, not a channel decision: it is
true, and setting it false makes every check report that and fetch nothing.
UpdatePolicy.kt — channel rules, tag parsing, verdicts — is pure and stays
under test either way.
Keystore
app/debug.keystore is committed deliberately and must never be rotated:
every build — local, CI, release — signs with it, so an OTA update installs
over the previous build instead of being rejected for a signature mismatch.