Files
schwert_und_magie_on_pebble/docs/publish.md
T
ml 8881241fd8 Split the monolithic build-image.sh into three scripts: build-image.sh now only builds locally, run-image.sh
runs dist.sh inside that local image via a bind mount, andcupload-image.sh pushes the already-built image.
The Dockerfile also warms the Gradle dependency cache in a throwaway build stage, and pre-installs
the pinned CMake version, so `run-image.sh` doesn't re-download the same Maven dependencies on every run.

dist.sh now strips the -pre+meta suffix before writing package.json's version, since Pebble's build
tooling parses it strictly as X.Y.Z integers and was rejecting suffixed versions.
2026-07-04 05:36:14 +02:00

16 KiB
Raw Blame History

Publishing guide

Steps to generate signing credentials and publish both apps. See docs/architecture.md for what each app does and CLAUDE.md for build commands.

1. .gitignore

Keystore files and credential properties must never be committed. Already added to .gitignore:

*.jks
*.keystore
keystore.properties

If you generate a keystore with a different name/extension, add that specific path too — don't rely on a broad glob you might forget to check.

2. Android companion app → Google Play

2.1 Generate an upload keystore

keytool -genkeypair -v \
  -keystore SchwertUndMagieOnPebbleCompanionApp/release.keystore \
  -alias sum-release \
  -keyalg RSA -keysize 2048 -validity 10000

keytool will prompt for a keystore password, a key password (can be the same), and your name/org details for the certificate (these become public metadata in the signed APK, not secret). Store the keystore file and both passwords in a password manager — losing this keystore means you can never publish an update to the same Play Store listing again under the same app; Google cannot recover or reset it for you.

2.2 Store credentials in a gitignored properties file

Create SchwertUndMagieOnPebbleCompanionApp/keystore.properties (already gitignored, never commit it):

storeFile=release.keystore
storePassword=<keystore password>
keyAlias=sum-release
keyPassword=<key password>

2.3 Wire the signing config

Add to SchwertUndMagieOnPebbleCompanionApp/app/build.gradle.kts, near the top:

import java.util.Properties

val keystoreProps = Properties().apply {
    rootProject.file("keystore.properties").takeIf { it.exists() }
        ?.reader()?.use { load(it) }
}

Inside the android { } block:

signingConfigs {
    create("release") {
        keystoreProps["storeFile"]?.let { storeFile = file(it as String) }
        storePassword = keystoreProps["storePassword"] as String?
        keyAlias       = keystoreProps["keyAlias"] as String?
        keyPassword    = keystoreProps["keyPassword"] as String?
    }
}

buildTypes {
    release {
        signingConfig = signingConfigs.getByName("release")
        // ...existing isMinifyEnabled / proguardFiles
    }
}

This degrades gracefully (null signing config values) on any machine without keystore.properties — CI or a contributor's checkout — rather than failing the whole Gradle configuration.

2.4 Build the release bundle

./dist.sh   # builds both apps, copies signed artifacts into dist/

This runs ./gradlew bundleRelease assembleRelease in SchwertUndMagieOnPebbleCompanionApp/ and pebble build in SchwertUndMagieOnPebbleWatchApp/, then copies the outputs to dist/schwert-und-magie.{aab,apk,pbw}.

Before building, the script hard-stops (nonzero exit, nothing written to dist/) if the pebble CLI, gradlew, the Android SDK, the NDK version pinned in app/build.gradle.kts, or the VICE/nibtools source tarballs are missing — these are required for the build to succeed at all. It warns but still builds if keystore.properties (§2.1-2.3) is missing or points at a nonexistent keystore file, and checks the resulting APK's signature with apksigner afterward, warning if it's unsigned or debug-signed. In practice an incomplete signing config also makes Gradle's own signReleaseBundle task fail outright, so dist/ won't be overwritten with an unusable artifact either way — but don't rely on that as the primary check; heed the warning.

Play Store requires the AAB (schwert-und-magie.aab), not the APK — Play re-packages per-device APKs from it (including ABI splits, so arm64-v8a/ x86_64 native VICE libraries each ship only to matching devices). The APK (schwert-und-magie.apk) is for direct sideload distribution (§2.6 below) and F-Droid-style testing, not Play Store upload.

2.5 Play Console setup (one-time, per app listing)

  1. Enroll in the Google Play Developer program (one-time account fee).
  2. Create the app in Play Console, set its package name (de.ladkau.schwertundmagieonpebblecompanionapp) — this is permanent.
  3. App content: privacy policy URL, content rating questionnaire, data safety form (declare what data the app collects — this app's only network activity is the loopback HTTP server talking to Core for Pebble, so "no data collected/shared" likely applies, but fill out the form yourself).
  4. Store listing: title, short/full description, icon (512×512 PNG), feature graphic (1024×500), phone screenshots (min 2, current device's actual aspect ratio).
  5. Enroll in Play App Signing when prompted on first upload — Google re-signs your AAB with its own key for distribution; your upload keystore (§2.1) only needs to be kept for future uploads to this listing, not for the keys end users' devices actually trust.
  6. Upload the AAB to an internal testing track first, verify the install works on a real device, then promote to closed/open testing or production.

2.6 Versioning for future releases

Comes from the git tag automatically — see §4. Pushing vX.Y.Z (or running VERSION=X.Y.Z ./dist.sh locally) patches versionCode (derived as X*10000 + Y*100 + Z, which strictly increases as long as X.Y.Z itself does) and versionName in app/build.gradle.kts at build time, then reverts them — no manual edit needed.

3. Android companion app → F-Droid

F-Droid is a community-run FOSS app catalogue that builds apps from source on its own infrastructure and signs them with its own key. The companion app is a good fit: it is open source, ships no proprietary binaries in the repo, and the copyrighted ROM and disk files are handled entirely at runtime by the user.

3.1 Inclusion policy check

F-Droid's key requirement is that everything needed to build the app is in the repo and is free/open-source. Verify:

  • VICE 3.8 source is GPLv2 (tarball in res/). ✓
  • nibtools source is GPLv2 (tarball in res/). ✓
  • No prebuilt binaries committed (.a/.so files are build outputs in .gitignore). ✓
  • No non-free SDKs or closed-source dependencies in build.gradle.kts. ✓
  • C64 ROMs and game disks are not bundled — user-supplied at runtime. ✓

3.2 Prepare the fdroiddata metadata file

F-Droid apps are registered by adding a YAML file to the fdroiddata repository. Fork it, then create metadata/de.ladkau.schwertundmagieonpebblecompanionapp.yml:

Categories:
  - Games
License: GPL-2.0-or-later
AuthorName: Matthias Ladkau
SourceCode: https://github.com/<your-repo>
IssueTracker: https://github.com/<your-repo>/issues

AutoName: Schwert und Magie on Pebble

RepoType: git
Repo: https://github.com/<your-repo>

Builds:
  - versionName: '1.0'
    versionCode: 1
    commit: <git-tag-or-sha>
    subdir: SchwertUndMagieOnPebbleCompanionApp
    gradle:
      - release
    ndk: 30.0.14904198
    prebuild:
      - bash app/src/main/jni/build_vice.sh
      - bash app/src/main/jni/build_nibtools.sh

AutoUpdateMode: None
UpdateCheckMode: None

Key points:

  • subdir points to the Gradle project root (where gradlew lives).
  • gradle: [release] tells F-Droid to run ./gradlew assembleRelease.
  • ndk must match the ndkVersion in app/build.gradle.kts (30.0.14904198). F-Droid's build server installs the requested NDK version automatically.
  • prebuild runs the VICE and nibtools cross-compilation before Gradle invokes CMake. The NDK environment variable is set by F-Droid's build server, matching what build_vice.sh and build_nibtools.sh expect.
  • F-Droid signs with its own key, so keystore.properties is not needed on the build server. The signing config in build.gradle.kts degrades gracefully when the file is absent.

3.3 Test the build locally with fdroid-server

Before submitting, reproduce the F-Droid build environment locally:

# Install fdroid-server (Debian/Ubuntu)
sudo apt install fdroidserver

# In the fdroiddata checkout:
fdroid build de.ladkau.schwertundmagieonpebblecompanionapp --latest

This runs the build inside a Docker container that mirrors the F-Droid build server. A successful local build means the merge request is very likely to pass.

3.4 Submit the merge request

Push your metadata/ addition to your fdroiddata fork and open a merge request against gitlab.com/fdroid/fdroiddata. The F-Droid team reviews the metadata and, if the build passes on their infrastructure, merges it and includes the app in the next index update (published roughly weekly).

3.5 Versioning for F-Droid updates

Each new release requires a new Builds entry in the metadata file with an incremented versionCode and the corresponding git commit or tag. versionCode must match what that tag produces — X*10000 + Y*100 + Z for tag vX.Y.Z (§2.6, §4).

4. Automated releases via Gitea Actions

Pushing a tag vX.Y.Z builds both apps and publishes a Gitea Release with schwert-und-magie-X.Y.Z.{aab,apk,pbw} attached — no local ./dist.sh run needed. The tag is the only source of truth for the version; nothing needs to be bumped in source files beforehand (dist.sh patches versionCode/ versionName/package.json at build time and reverts them — see §4.4 for running the same thing locally).

The whole toolchain (Android SDK/NDK, Pebble SDK) lives in build-image/Dockerfile, built locally and pushed to a container registry by three separate scripts (below). This keeps the setup portable: the runner just needs Docker and pulls that image, so it isn't tied to any one machine's local toolchain install and can be moved or re-registered elsewhere without touching this repo's build scripts.

4.1 Build environment image

build-image/Dockerfile bakes in the Android SDK/NDK and Pebble SDK versions pinned in app/build.gradle.kts, matching what's validated for local builds. It does not contain the release keystore — that's injected at job runtime from Actions secrets (§4.3), never baked into the image.

Three scripts, kept separate so a Dockerfile change can be built and tested locally before anything is pushed to the registry:

cp registry.env.example registry.env   # fill in your registry credentials
./build-image.sh                       # builds :latest and the pinned VERSION tag, locally only
./run-image.sh                         # runs dist.sh inside that local image — sanity-check before publishing
./upload-image.sh                      # pushes the already-built :latest and VERSION tag to cr.ladkau.de

Rebuild and push whenever build-image/Dockerfile changes (e.g. a Pebble SDK or Android NDK version bump) — bump build-image/VERSION first so the tag is meaningful. The workflow (§4.2) pulls :latest by default; if you need a release to be reproducible against an exact toolchain image, pin the image: line in .gitea/workflows/release.yml to the versioned tag instead.

4.2 Runner setup (one-time)

  1. Enable Actions for the repo: repo Settings → Actions → enable, if not already on by default for this Gitea instance.

  2. Generate a runner registration token: Site Admin → Actions → Runners (or the repo/org-scoped equivalent) → "Create new runner".

  3. On any machine with Docker that can reach both your Gitea instance and your container registry:

    # https://gitea.com/gitea/act_runner — grab the latest release binary
    ./act_runner register --no-interactive \
      --instance <your gitea URL> --token <token> \
      --name <runner-name> --labels ubuntu-latest:docker://node:20-bookworm
    ./act_runner daemon
    

    The image after docker:// in --labels is only a fallback for jobs that don't specify their own container: — irrelevant here since .gitea/workflows/release.yml always pins its own image, but the runner still needs a Docker-executor label registered to use that executor at all. The label name itself (ubuntu-latest above) must match runs-on: in .gitea/workflows/release.yml — edit both together if you rename it, or reuse a label an existing runner already advertises (check Site Admin → Actions → Runners) to skip registering a new one entirely.

    No manual docker login needed on the runner host — the workflow's container: block authenticates the image pull itself via the REGISTRY_USER/REGISTRY_PASSWORD secrets (§4.3).

4.3 Repo secrets

Settings → Actions → Secrets, add:

Secret Value
RELEASE_KEYSTORE_B64 base64 -w0 SchwertUndMagieOnPebbleCompanionApp/release.keystore
RELEASE_KEYSTORE_PROPERTIES the full contents of SchwertUndMagieOnPebbleCompanionApp/keystore.properties (§2.2)
REGISTRY_USER same as REGISTRY_USER in registry.env
REGISTRY_PASSWORD same as REGISTRY_PASSWORD in registry.env

secrets.GITEA_TOKEN (used to create the release and upload assets) is Gitea's own auto-generated per-job token — nothing to create or add yourself. .gitea/workflows/release.yml requests contents: write explicitly so release creation works regardless of this instance's default Actions permission mode.

4.4 Cutting a release

git tag v1.2.3
git push origin v1.2.3

Watch the run under the repo's Actions tab. On success, the release appears under the repo's Releases page with the three versioned artifacts attached.

To build the same versioned artifacts locally without pushing a tag (e.g. to test before releasing), either run dist.sh directly with the host toolchain:

VERSION=1.2.3 ./dist.sh

or run it inside the build-image container (same environment the runner uses — see §4.1):

./run-image.sh 1.2.3

5. Pebble watch app → Rebble app store / direct distribution

The official Pebble app store shut down years ago; the community-run Rebble store is the closest equivalent today, alongside the 2025 Core Devices relaunch of Pebble hardware. Check Rebble's current developer portal directly for their exact submission flow and requirements — that's outside this repo and changes independently of it.

5.1 Build the artifact

./dist.sh   # also builds the Android side; see §2.4

or, to build just the watch app:

cd SchwertUndMagieOnPebbleWatchApp
pebble build   # produces build/SchwertUndMagieOnPebbleWatchApp.pbw

dist.sh copies the result to dist/schwert-und-magie.pbw. The .pbw is the complete distributable — it bundles all target platforms (aplite/basalt/chalk/diorite/emery/flint/gabbro) declared in package.json. There is no signing step analogous to Android; Pebble apps are not cryptographically signed by the developer.

5.2 Direct distribution (no store)

Anyone with the .pbw file and Core for Pebble installed can sideload it — this is the lowest-friction path and doesn't depend on any third party's store being operational. This watch app is tightly coupled to the companion app's HTTP bridge (see docs/architecture.md §1), so it's not really meaningful as a standalone listing anyway — distribute both together.

5.3 Store submission (if Rebble's process applies)

Expect to need: an app icon resource (not yet configured in package.json — there's no icon/menu-icon entry currently, only the in-app IMAGE_SPLASH bitmap), a short description, and screenshots per platform. Generate screenshots the same way used during development:

pebble install --emulator emery --vnc
pebble screenshot --vnc --no-open docs/screenshots/emery.png

(repeat per target platform you want a store screenshot for).

5.4 Versioning

Comes from the git tag automatically — see §4. dist.sh patches SchwertUndMagieOnPebbleWatchApp/package.json's version field at build time and reverts it afterward; no manual edit needed.

6. Pre-publish checklist

  • Release keystore generated, passwords saved in a password manager, both gitignored (§1, §2.1)
  • keystore.properties exists locally and is not tracked by git
  • ./dist.sh (or a tag push, §4) succeeds and produces dist/schwert-und-magie-<version>.{aab,apk,pbw}
  • AAB installs/runs on a real device (test via bundletool or Play internal testing)
  • Play Console store listing content complete (icon, screenshots, privacy policy, content rating, data safety form)
  • F-Droid metadata YAML created and fdroid build passes locally (§3)
  • .pbw sideloads and runs correctly against the signed companion app build