ci(release): P1 hardening — versioning, F-Droid changelogs, R8 mapping, docs
All checks were successful
CI / ci (push) Successful in 8m11s
All checks were successful
CI / ci (push) Successful in 8m11s
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>
This commit is contained in:
101
docs/RELEASING.md
Normal file
101
docs/RELEASING.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# 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 leading `v` (`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
|
||||
|
||||
1. Move the `## [Unreleased]` section of `CHANGELOG.md` under 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.
|
||||
2. Optionally bump the committed `versionCode`/`versionName` in
|
||||
`app/build.gradle.kts` to match the new version (keeps local builds tidy).
|
||||
3. Commit, then tag and push:
|
||||
```bash
|
||||
git tag vX.Y.Z
|
||||
git push origin vX.Y.Z
|
||||
```
|
||||
4. 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 R8 `mapping.txt` to the
|
||||
Gitea release (best-effort).
|
||||
- **gitea-release** — creates/updates the Gitea release carrying the tag's
|
||||
CHANGELOG section as notes. Gated on `ci` only (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 the `FDROID_*` 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, verify `keystore.p12` and
|
||||
`config.yml` return 404 while `repo/index-v2.json` returns 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`.
|
||||
Reference in New Issue
Block a user