(semver drives Android versionCode/versionName and package.json). The build-image/ Dockerfile pins the same toolchain for a portable, containerized Gitea Actions runner (build-image.sh builds and pushes it) so releases don't depend on any one machine's local setup. Pushing vX.Y.Z now builds and publishes a Gitea Release automatically.
15 KiB
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)
- Enroll in the Google Play Developer program (one-time account fee).
- Create the app in Play Console, set its package name
(
de.ladkau.schwertundmagieonpebblecompanionapp) — this is permanent. - 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).
- Store listing: title, short/full description, icon (512×512 PNG), feature graphic (1024×500), phone screenshots (min 2, current device's actual aspect ratio).
- 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.
- 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/.sofiles 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:
subdirpoints to the Gradle project root (wheregradlewlives).gradle: [release]tells F-Droid to run./gradlew assembleRelease.ndkmust match thendkVersioninapp/build.gradle.kts(30.0.14904198). F-Droid's build server installs the requested NDK version automatically.prebuildruns the VICE and nibtools cross-compilation before Gradle invokes CMake. TheNDKenvironment variable is set by F-Droid's build server, matching whatbuild_vice.shandbuild_nibtools.shexpect.- F-Droid signs with its own key, so
keystore.propertiesis not needed on the build server. The signing config inbuild.gradle.ktsdegrades 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 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.
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)
- Enable Actions for the repo: repo Settings → Actions → enable, if not already on by default for this Gitea instance.
- Generate a runner registration token: Site Admin → Actions → Runners (or the repo/org-scoped equivalent) → "Create new runner".
- On any machine with Docker that can reach both your Gitea instance and
your container registry:
The image after
# 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 self-hosted:docker://node:20-bookworm ./act_runner daemondocker://in--labelsis only a fallback for jobs that don't specify their owncontainer:— irrelevant here since.gitea/workflows/release.ymlalways pins its own image, but the runner still needs a Docker-executor label registered to use that executor at all. The label name itself (self-hosted) must matchruns-on:in.gitea/workflows/release.yml— edit both together if you rename it. - The runner's Docker daemon needs pull access to the registry — run
docker login <registry>once on that machine with the same credentials asregistry.env.
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) |
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):
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
./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.propertiesexists locally and is not tracked by git./dist.sh(or a tag push, §4) succeeds and producesdist/schwert-und-magie-<version>.{aab,apk,pbw}- AAB installs/runs on a real device (test via
bundletoolor 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 buildpasses locally (§3) .pbwsideloads and runs correctly against the signed companion app build