Bootstrap Pilgrims of Katan: rebrand, Gitea CI and OTA
Build and Release APK / Build APK and publish release (push) Canceled after 0s

Import of ProLibertateGames (upstream main, GPL-3.0) rebranded for
Wallacenet and repointed at git.wallacenet.ca.

- Identity: applicationId ca.wallacenet.pilgrimsofkatan (+ .dev for the dev
  channel), launcher names "Pilgrims of Katan" / "Pilgrims of Katan DEV",
  neutral hexagon branding replacing the Wallace tartan. Uncial Antiqua keeps
  its OFL licence, and the GPL-3.0 LICENSE plus upstream attribution stay.
- OTA: Updater.kt now reads this repo's own Gitea releases
  (/api/v1/repos/devadmin/PilgrimsOfKatan/releases...) and BuildConfig.
  OTA_ENABLED is on; Gitea answers the same JSON shape the code already parsed.
- Build numbers: versionCode = CI run number + plgVersionRunOffset (1000),
  with the tag and versionName derived from the same figure. The offset is
  permanent, because Gitea Actions restarts run numbers at 1 and a reset would
  otherwise make Android reject the next build as a downgrade.
- CI: ci.yml publishes a dev prerelease from dev, release.yml a production
  release from main, both through scripts/publish-release.sh (Gitea REST API,
  runnable by hand until an Actions runner is registered).
- Docs: docs/CI.md records the channel, numbering and runner decisions;
  docs/README.md seeds the per-game RULES-* conventions; CLAUDE.md carries the
  agent ground rules.

Verified: ./gradlew assembleDebug and testDebugUnitTest green on JDK 17 /
compileSdk 34, both variants stamped, installing side by side under the new
identity.

Co-Authored-By: Paperclip <[email protected]>
This commit is contained in:
adrianoftyriel
2026-10-13 22:03:27 +00:00
co-authored by Paperclip
parent d27c2db894
commit fad4778ad8
32 changed files with 760 additions and 773 deletions
+136
View File
@@ -0,0 +1,136 @@
# CI, releases, and OTA — decisions and how to build
Status as of the Pilgrims of Katan bootstrap (WALA-118).
## Decisions
1. **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 `upstream` remote 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.
2. **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 through `scripts/publish-release.sh` — Gitea's REST API, the
same routes the app's updater reads — rather than the `gh` CLI, which a runner
image need not carry.
| Workflow | Trigger | Publishes |
| --- | --- | --- |
| `ci.yml` | push to `dev`, PRs into `dev`/`main`, manual | dev prerelease, from `dev` only |
| `release.yml` | push to `main`, manual | production release, from `main` only |
Pull 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.
3. **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-android` and
`actions/upload-artifact`. Until then the manual build below and the manual
publish script are the working paths.
4. **The local build is the guaranteed path.** Workflows need a runner that may
not exist yet; `./gradlew assembleDebug` + `./gradlew testDebugUnitTest`
work 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` (currently `1.4`) is the human-facing series, set by hand
in `gradle.properties`.
- `plgVersionRunOffset` (currently `1000`) is a **permanent floor**, also set by
hand in `gradle.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)
```sh
# 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)
```sh
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`, and attaches the
APK through `POST …/releases/{id}/assets` — which is the upload route Gitea's
Swagger declares for this instance (1.27.3). 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/latest` is 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 `versionCode` among 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 carrying `browser_download_url`. Gitea says
`limit` where GitHub says `per_page`.
- Each asset must be named so it contains its tag —
`PilgrimsOfKatan-v1.4.1001.apk` for tag `v1.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.
+45
View File
@@ -0,0 +1,45 @@
# docs/
Conventions for this repo's documentation. Seeded at bootstrap (WALA-118).
## Per-game rules: `RULES-<game>.md`
Every game in `GameCatalog` gets one file describing how *this app* implements
it — not a generic rule dump. The rules code
(`app/src/main/java/org/prolibertate/games/game/<game>/`) is the source of
truth; the doc exists so a human or an agent can check intent against
behaviour without reading Kotlin first.
Naming: the catalogue id, lowercase, e.g. `crazy8s` → `RULES-crazy8s.md`.
| Game (catalogue id) | Rules doc |
| --- | --- |
| Backgammon | [RULES-backgammon.md](RULES-backgammon.md) |
| Checkers | [RULES-checkers.md](RULES-checkers.md) |
| Chess | [RULES-chess.md](RULES-chess.md) |
| Crazy 8s | [RULES-crazy8s.md](RULES-crazy8s.md) |
| Cribbage | [RULES-cribbage.md](RULES-cribbage.md) |
| Euchre | [RULES-euchre.md](RULES-euchre.md) |
| FreeCell | — (TODO) |
| Golf | [RULES-golf.md](RULES-golf.md) |
| Hearts | — (TODO) |
| Kaiser | [RULES-kaiser.md](RULES-kaiser.md) |
| Klondike | — (TODO) |
| Mastermind | [RULES-mastermind.md](RULES-mastermind.md) |
| Morris | [RULES-morris.md](RULES-morris.md) |
| Peg solitaire | — (TODO) |
| Pirates | [RULES-pirates.md](RULES-pirates.md) |
| President | [RULES-president.md](RULES-president.md) |
| Pyramid | — (TODO) |
| Sequence | [RULES-sequence.md](RULES-sequence.md) |
| Spider | — (TODO) |
| Ta Yü | [RULES-tayu.md](RULES-tayu.md) |
| Wizard | [RULES-wizard.md](RULES-wizard.md) |
| Yahtzee | — (TODO) |
## Other docs
- [CI.md](CI.md) — build, CI, releases, OTA decisions.
- `licenses/` — third-party licences that must travel with the app
(Uncial Antiqua under the SIL OFL; the app itself is GPL-3.0, see `/LICENSE`).
- Repo-level agent notes live in [`CLAUDE.md`](../CLAUDE.md) at the root.