Files
ProLibertateGames/docs/CI.md
adrianoftyrielandPaperclip 4d7cd6182f
Build and Release APK / Build APK and publish release (push) Failing after 7s
Rename the installed Android package to ca.wallacenet.prolibertategames
Follows the Gitea repo rename: the applicationId (and its .dev variant)
now matches the repo, the launcher name and the docs, instead of carrying
the abandoned Pilgrims of Katan identity.

The JVM namespace stays org.prolibertate.games, so no source file moves;
only the installed identity changes.

Co-Authored-By: Paperclip <[email protected]>
2026-10-14 14:47:13 +00:00

6.8 KiB

CI, releases, and OTA — decisions and how to build

Status as of the ProLibertateGames bootstrap (WALA-118).

Decisions

  1. The release channel is this instance's own releases. The app ships from https://git.wallacenet.ca/devadmin/ProLibertateGames, 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)

# 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.prolibertategames.dev)
PLG_CHANNEL=production ./gradlew assembleDebug   # production app (ca.wallacenet.prolibertategames)
./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 (Pro Libertate Games DEV vs Pro Libertate Games), different tartan sett.

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/ProLibertateGames/releases/latest
dev GET /api/v1/repos/devadmin/ProLibertateGames/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 — ProLibertateGames-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.