# 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 ```bash 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): ```properties storeFile=release.keystore storePassword= keyAlias=sum-release keyPassword= ``` ### 2.3 Wire the signing config Add to `SchwertUndMagieOnPebbleCompanionApp/app/build.gradle.kts`, near the top: ```kotlin import java.util.Properties val keystoreProps = Properties().apply { rootProject.file("keystore.properties").takeIf { it.exists() } ?.reader()?.use { load(it) } } ``` Inside the `android { }` block: ```kotlin 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 ```bash ./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](https://gitlab.com/fdroid/fdroiddata) repository. Fork it, then create `metadata/de.ladkau.schwertundmagieonpebblecompanionapp.yml`: ```yaml Categories: - Games License: GPL-2.0-or-later AuthorName: Matthias Ladkau SourceCode: https://github.com/ IssueTracker: https://github.com//issues AutoName: Schwert und Magie on Pebble RepoType: git Repo: https://github.com/ Builds: - versionName: '1.0' versionCode: 1 commit: 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: ```bash # 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 and pushed to a container registry by `build-image.sh`. 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. ```bash cp registry.env.example registry.env # fill in your registry credentials ./build-image.sh # builds + pushes :latest and the pinned VERSION tag ``` 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: ```bash # https://gitea.com/gitea/act_runner — grab the latest release binary ./act_runner register --no-interactive \ --instance --token \ --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 ```bash 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): ```bash VERSION=1.2.3 ./dist.sh ``` ## 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 ```bash ./dist.sh # also builds the Android side; see §2.4 ``` or, to build just the watch app: ```bash 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: ```bash 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-.{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