# 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=<keystore password>
keyAlias=sum-release
keyPassword=<key password>
```

### 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/<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:

```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 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:

```bash
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:
   ```bash
   # 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

```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), either run `dist.sh` directly with the host toolchain:

```bash
VERSION=1.2.3 ./dist.sh
```

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

```bash
./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

```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-<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
