P1.3 Versioning: the git tag is already the de-facto single source of truth (every published versionCode uses MAJOR*10000+MINOR*100+PATCH; committed 13 was a stale outlier). Align the committed default to 20000 and document the scheme in a comment + docs/RELEASING.md. P1.4 F-Droid changelogs: a tag-only step extracts the tag's CHANGELOG section into metadata/.../en-US/changelogs/<versionCode>.txt so clients show a per-version "What's New". Also upload metadata/ (non-secret, never web-served) alongside repo/ so changelog history survives across releases. P1.5 R8 mapping: attach mapping-<version>.txt.gz to the Gitea release (best-effort, continue-on-error) so user crash stacktraces stay deobfuscatable. The gitea-release notes step is now an upsert (PATCH if the release already exists) so it composes with the mapping step creating the release first. P1.6 docs/RELEASING.md: release ritual, versioning scheme, secrets inventory, key custody/recovery, manual re-sign path, F-Droid repo details. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.9 KiB
Releasing Calendula
Calendula is distributed through a self-hosted F-Droid repository. Every
release is built, signed, and published automatically by
.gitea/workflows/release.yaml when a version tag is pushed.
Versioning — the git tag is the single source of truth
A release is defined by its tag, vMAJOR.MINOR.PATCH (e.g. v2.1.0). At
release time the workflow derives both Gradle fields from the tag:
versionName= the tag without the leadingv(2.1.0)versionCode=MAJOR*10000 + MINOR*100 + PATCH(2.1.0→20100)
So MINOR and PATCH each have room for 0–99. The values committed in
app/build.gradle.kts are only the dev/local default — CI overwrites them
from the tag. Keep the committed versionCode/versionName matching the
latest released tag so local builds are sanely versioned; the published
value always comes from the tag.
Published version codes so far: v0.1.0→100 … v1.0.0→10000 … v2.0.0→20000.
Cutting a release
- Move the
## [Unreleased]section ofCHANGELOG.mdunder a new## [X.Y.Z] — <date>heading (Keep a Changelog format). The text between that heading and the next## [becomes both the Gitea release notes and the F-Droid per-version changelog. - Optionally bump the committed
versionCode/versionNameinapp/build.gradle.ktsto match the new version (keeps local builds tidy). - Commit, then tag and push:
git tag vX.Y.Z git push origin vX.Y.Z - The push triggers the release workflow. Hold UI releases for on-device review and explicit go-ahead before tagging.
What the pipeline does
release.yaml has three jobs:
- ci — unit tests + a debug assemble (sanity).
- build-and-deploy — derives the version, builds & signs the release APK
with the app key, copies it into the F-Droid repo, generates the per-version
changelog, re-signs the F-Droid index with the repo key, uploads
repo/+metadata/to the box, and attaches the R8mapping.txtto the Gitea release (best-effort). - gitea-release — creates/updates the Gitea release carrying the tag's
CHANGELOG section as notes. Gated on
cionly (not the deploy) so notes publish even if the F-Droid upload hiccups.
Manual re-sign / recovery
A manual workflow_dispatch of the release workflow from a branch (not a
tag) runs a re-sign-only path: it skips the APK build and just re-signs
the existing F-Droid index with the configured repo key and re-uploads. Use
this for key rotation or repo recovery without publishing a new app version.
Secrets (Gitea → repo Settings → Actions → Secrets)
| Secret | Purpose |
|---|---|
KEYSTORE_BASE64, KEY_PASSWORD, KEY_ALIAS |
App signing key — signs the APK. Losing it means existing installs can't be updated. |
FDROID_KEYSTORE_BASE64 |
F-Droid repo signing key (keystore.p12, base64). Signs the repo index. |
FDROID_CONFIG_BASE64 |
F-Droid config.yml (base64) — repo metadata + keystore passwords. |
HETZNER_HOST, HETZNER_USER, HETZNER_PASS |
Upload target for the F-Droid repo. |
GITHUB_TOKEN |
Provided by Gitea Actions; used to create the release + attach assets. |
The two keys are independent: the app key signs APKs; the repo key
signs the index (its fingerprint is what users pin). Neither key nor the
F-Droid config.yml is ever uploaded to the server — they live only in CI
secrets and are reconstructed in-runner. If FDROID_KEYSTORE_BASE64 /
FDROID_CONFIG_BASE64 are unset the workflow fails loudly rather than
minting a new repo key (which would break every user's pinned fingerprint).
Key custody & recovery
- Offline backups of both keys (and passwords) live in a password manager. These are the only safe copies — losing them is unrecoverable.
- App key lost → no existing install can be updated again; you'd have to ship a new app under a new applicationId.
- Repo key lost or compromised → rotate it, publish the new fingerprint in
the README, and have users remove + re-add the repo. To rotate: generate a
new
keystore.p12+config.yml, set them as theFDROID_*secrets, update the README fingerprint, and run the manual re-sign dispatch above.
F-Droid repo
- URL:
https://apps.dev.jeanlucmakiola.de/dev/fdroid/repo - Fingerprint (current):
C2C0640402BF458FC0ED957AF0B37AA4C14022E72F89CE90B5965B458CF73425 - Served from the Hetzner storage box. nginx serves only
…/fdroid/repo/— the working dir (key, config, metadata) sits above it and must never be web-reachable. After any webserver change, verifykeystore.p12andconfig.ymlreturn 404 whilerepo/index-v2.jsonreturns 200.
Crash deobfuscation
Each release attaches mapping-<version>.txt.gz (the R8 mapping) to its Gitea
release. To deobfuscate a user stacktrace, download the mapping for that
version and run it through retrace.