feat: the F-Droid repository moves to halkyon.app
All checks were successful
CI / check (push) Successful in 9m23s
CI / deploy (push) Successful in 15s

The repository is Halkyon's, for all of its apps, so its address is the
brand's: https://halkyon.app/fdroid/repo. learn.halkyon.app stays Halkyon
Learn's own address, for its website later. Moved before anyone but the
maintainer had added it; the address is permanent once people have.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-05 20:41:54 +02:00
commit d149d4a5e3
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
6 changed files with 11 additions and 10 deletions

View file

@ -120,7 +120,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
- **CI** (`.forgejo/workflows/ci.yaml`) runs `tool/check.sh` on every push, on the Forgejo runner on sahaquiel (`deploy/runner/`). Jobs run directly on the host as the unprivileged user `runner`, never in containers: Docker access would be root on the production server. The runner's systemd unit keeps jobs away from production (no `/home`, no Docker socket, Docker's container networks unreachable, memory and CPU capped); keep it that way when changing it. `runner` has user ID 61000 and must never share one with a container's processes (PostgreSQL and the server run as 999, not remapped): the same user ID can read their environment and files. The job's first step (*Sandbox*) fails if any of this leaks. Flutter is pinned in the workflow (`FLUTTER_VERSION`): move it together with the local one. The server tests there get throwaway passwords from the environment (`SERVERPOD_…`, new for every job), since `passwords.yaml` is never committed; without them they exit with code 1 and no message (Serverpod's own template does the same). CI also runs `serverpod generate` and fails if the committed generated code differs from its output. CI runs on pushes only: a pull request from a fork would run its code on the production server. - **CI** (`.forgejo/workflows/ci.yaml`) runs `tool/check.sh` on every push, on the Forgejo runner on sahaquiel (`deploy/runner/`). Jobs run directly on the host as the unprivileged user `runner`, never in containers: Docker access would be root on the production server. The runner's systemd unit keeps jobs away from production (no `/home`, no Docker socket, Docker's container networks unreachable, memory and CPU capped); keep it that way when changing it. `runner` has user ID 61000 and must never share one with a container's processes (PostgreSQL and the server run as 999, not remapped): the same user ID can read their environment and files. The job's first step (*Sandbox*) fails if any of this leaks. Flutter is pinned in the workflow (`FLUTTER_VERSION`): move it together with the local one. The server tests there get throwaway passwords from the environment (`SERVERPOD_…`, new for every job), since `passwords.yaml` is never committed; without them they exit with code 1 and no message (Serverpod's own template does the same). CI also runs `serverpod generate` and fails if the committed generated code differs from its output. CI runs on pushes only: a pull request from a fork would run its code on the production server.
- **A green `main` deploys learn.halkyon.eu** through `deploy/redeploy.sh`, the only command the runner's key may run as `deploy` (pinned in `authorized_keys`, from sahaquiel itself only). It deploys the commit CI checked while it is still the tip, as a fast-forward, only if every new commit is signed by a key in `deploy/allowed-signers` (the deployed copy decides), after dumping the database (the last 3 dumps stay in `~deploy/backups`: the way back from a deploy, not backups), applies the migrations in a run of their own before switching (`--role=maintenance --apply-migrations`: a failing migration stops the deploy while the old server keeps serving, so migrations must keep the old server working until the switch), and fails unless the new server answers. A failed deploy leaves the checkout at the deployed commit; run the workflow again to retry. By hand: `ssh sahaquiel-deploy halkyon-learn/deploy/redeploy.sh <commit>`. Never `docker compose down -v` there. - **A green `main` deploys learn.halkyon.eu** through `deploy/redeploy.sh`, the only command the runner's key may run as `deploy` (pinned in `authorized_keys`, from sahaquiel itself only). It deploys the commit CI checked while it is still the tip, as a fast-forward, only if every new commit is signed by a key in `deploy/allowed-signers` (the deployed copy decides), after dumping the database (the last 3 dumps stay in `~deploy/backups`: the way back from a deploy, not backups), applies the migrations in a run of their own before switching (`--role=maintenance --apply-migrations`: a failing migration stops the deploy while the old server keeps serving, so migrations must keep the old server working until the switch), and fails unless the new server answers. A failed deploy leaves the checkout at the deployed commit; run the workflow again to retry. By hand: `ssh sahaquiel-deploy halkyon-learn/deploy/redeploy.sh <commit>`. Never `docker compose down -v` there.
- **Backups** (`deploy/backups.md`): every hour, `deploy/backup.sh` (a systemd timer, as `deploy`) dumps the database, which holds people's files too, and restic stores it in a Hetzner Storage Box (24 hourly, 7 daily, 4 weekly, 3 monthly); the box's own snapshots protect it from a taken-over server. A restore takes the instance back up to an hour, and people who started since lose what their device held: their app no longer finds its session, starts a new anonymous account and deletes the old folder. So a change that makes devices lose their session, or keeps deleted people's data longer, changes what a restore costs; say so in `deploy/backups.md`. - **Backups** (`deploy/backups.md`): every hour, `deploy/backup.sh` (a systemd timer, as `deploy`) dumps the database, which holds people's files too, and restic stores it in a Hetzner Storage Box (24 hourly, 7 daily, 4 weekly, 3 monthly); the box's own snapshots protect it from a taken-over server. A restore takes the instance back up to an hour, and people who started since lose what their device held: their app no longer finds its session, starts a new anonymous account and deletes the old folder. So a change that makes devices lose their session, or keeps deleted people's data longer, changes what a restore costs; say so in `deploy/backups.md`.
- **Releases** are the project's, not an instance's: the release, the Snap Store listing and the website point to learn.halkyon.app; the builds connect to learn.halkyon.eu, the project's instance, until the app lets people choose one. They come from a developer's laptop, never from CI: `tool/release.sh vX.Y.Z` on `main` as pushed, with the tag signed by a key in `deploy/allowed-signers` and pushed, and the build number raised. It builds one universal APK from a checkout of the tag, signed with the app's key, and the snap (`snap/snapcraft.yaml`, the committed HEAD, built in LXD), both with `SERVER_URL=https://learn.halkyon.eu/`; signs `SHA256SUMS` with GPG; creates the release on code.halkyon.app through Forgejo's API; and uploads the snap to the Store's edge channel. The key's password and the Forgejo token are asked for at each run and never stored. The app's key is a PKCS #12 keystore outside the repository (also in the maintainer's password manager); there is no Google Play, so it is the app's identity: lost, installed apps can't be updated. Its certificate's SHA-256 is in `tool/release/android-certificate.sha256`, and the script releases no APK signed otherwise; other release builds fall back to the debug key (`android/app/build.gradle.kts`), so `flutter run --release` works anywhere. The APK also goes into the project's own F-Droid repository, https://learn.halkyon.app/fdroid/repo, through `tool/fdroid_repo.sh` (fdroidserver v2.4.5): its settings and the app's listing are in `tool/release/fdroid/` (the anti-features TetheredNet and NonFreeNet declared with their reasons and F-Droid's own icons); its index is signed with a key of its own (a PKCS #12 keystore beside the app's, password asked for, certificate pinned in `tool/release/fdroid-index-certificate.sha256`, since a new key makes the F-Droid app refuse the repository); and it is uploaded to ramiel as the user `fdroid`, whose key can only write into the folder Caddy serves (rrsync), the APKs first and the indexes last. - **Releases** are the project's, not an instance's: the release, the Snap Store listing and the website point to learn.halkyon.app; the builds connect to learn.halkyon.eu, the project's instance, until the app lets people choose one. They come from a developer's laptop, never from CI: `tool/release.sh vX.Y.Z` on `main` as pushed, with the tag signed by a key in `deploy/allowed-signers` and pushed, and the build number raised. It builds one universal APK from a checkout of the tag, signed with the app's key, and the snap (`snap/snapcraft.yaml`, the committed HEAD, built in LXD), both with `SERVER_URL=https://learn.halkyon.eu/`; signs `SHA256SUMS` with GPG; creates the release on code.halkyon.app through Forgejo's API; and uploads the snap to the Store's edge channel. The key's password and the Forgejo token are asked for at each run and never stored. The app's key is a PKCS #12 keystore outside the repository (also in the maintainer's password manager); there is no Google Play, so it is the app's identity: lost, installed apps can't be updated. Its certificate's SHA-256 is in `tool/release/android-certificate.sha256`, and the script releases no APK signed otherwise; other release builds fall back to the debug key (`android/app/build.gradle.kts`), so `flutter run --release` works anywhere. The APK also goes into the project's own F-Droid repository, https://halkyon.app/fdroid/repo, through `tool/fdroid_repo.sh` (fdroidserver v2.4.5): its settings and the app's listing are in `tool/release/fdroid/` (the anti-features TetheredNet and NonFreeNet declared with their reasons and F-Droid's own icons); its index is signed with a key of its own (a PKCS #12 keystore beside the app's, password asked for, certificate pinned in `tool/release/fdroid-index-certificate.sha256`, since a new key makes the F-Droid app refuse the repository); and it is uploaded to ramiel as the user `fdroid`, whose key can only write into the folder Caddy serves (rrsync), the APKs first and the indexes last.
## Rules the code relies on ## Rules the code relies on

View file

@ -111,7 +111,7 @@ the CI runner described in `deploy/runner/`, deploys only commits signed by a ke
Releases are published on [code.halkyon.app](https://code.halkyon.app/project-halkyon/halkyon_learn/releases), Releases are published on [code.halkyon.app](https://code.halkyon.app/project-halkyon/halkyon_learn/releases),
for Android also in the project's F-Droid repository for Android also in the project's F-Droid repository
(`https://learn.halkyon.app/fdroid/repo?fingerprint=E8082788756F6B7E682AA22FCEAD24B53E2A8B0912AD66D65E8C2D4F07BB94B0`, (`https://halkyon.app/fdroid/repo?fingerprint=E8082788756F6B7E682AA22FCEAD24B53E2A8B0912AD66D65E8C2D4F07BB94B0`,
added in the F-Droid app as a repository), and for Linux in the Snap Store added in the F-Droid app as a repository), and for Linux in the Snap Store
(`halkyon-learn`). Their builds connect to learn.halkyon.eu, the project's instance. (`halkyon-learn`). Their builds connect to learn.halkyon.eu, the project's instance.

View file

@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Updates the F-Droid repository at https://learn.halkyon.app/fdroid/repo and # Updates the F-Droid repository at https://halkyon.app/fdroid/repo and
# uploads it: adds an APK if one is given, signs the index with the repository's # uploads it: adds an APK if one is given, signs the index with the repository's
# key, and uploads everything to ramiel as the user fdroid, whose key can only # key, and uploads everything to ramiel as the user fdroid, whose key can only
# write into the folder Caddy serves (rrsync). tool/release.sh runs it with each # write into the folder Caddy serves (rrsync). tool/release.sh runs it with each
@ -26,7 +26,7 @@ keystore="${HALKYON_FDROID_KEYSTORE:-$HOME/.local/share/halkyon-learn/fdroid-ind
readonly key_alias='halkyon-fdroid' readonly key_alias='halkyon-fdroid'
readonly pinned_file='tool/release/fdroid-index-certificate.sha256' readonly pinned_file='tool/release/fdroid-index-certificate.sha256'
readonly upload_host='ramiel-fdroid' readonly upload_host='ramiel-fdroid'
readonly repo_url='https://learn.halkyon.app/fdroid/repo' readonly repo_url='https://halkyon.app/fdroid/repo'
fail() { fail() {
echo "✗ $*" >&2 echo "✗ $*" >&2

View file

@ -2,7 +2,7 @@
# Builds a release and publishes it: the Android APK (one for every phone, signed with # Builds a release and publishes it: the Android APK (one for every phone, signed with
# the app's key) and the Linux snap, both connecting to learn.halkyon.eu, the project's # the app's key) and the Linux snap, both connecting to learn.halkyon.eu, the project's
# instance; a list of their checksums signed with GPG; all of it as a release on # instance; a list of their checksums signed with GPG; all of it as a release on
# code.halkyon.app, the APK in the F-Droid repository at learn.halkyon.app # code.halkyon.app, the APK in the F-Droid repository at halkyon.app
# (tool/fdroid_repo.sh), and the snap in the Snap Store's edge channel. Run on a # (tool/fdroid_repo.sh), and the snap in the Snap Store's edge channel. Run on a
# developer's laptop, never in CI: the app's key never leaves it. # developer's laptop, never in CI: the app's key never leaves it.
# #
@ -198,7 +198,7 @@ the project's instance.
- **Android:** \`$apk\`, one APK for every phone, signed with the app's key; the \ - **Android:** \`$apk\`, one APK for every phone, signed with the app's key; the \
SHA-256 of its certificate is \`$certificate\`. SHA-256 of its certificate is \`$certificate\`.
- **F-Droid:** the same APK in the repository \ - **F-Droid:** the same APK in the repository \
https://learn.halkyon.app/fdroid/repo?fingerprint=$fdroid_fingerprint https://halkyon.app/fdroid/repo?fingerprint=$fdroid_fingerprint
- **Linux:** \`$snap\`, also in the Snap Store: \`snap install halkyon-learn --edge\`. - **Linux:** \`$snap\`, also in the Snap Store: \`snap install halkyon-learn --edge\`.
\`SHA256SUMS\` lists the files' checksums, signed with the GPG key \`$signer\` \ \`SHA256SUMS\` lists the files' checksums, signed with the GPG key \`$signer\` \

View file

@ -1,5 +1,5 @@
# The SHA-256 of the certificate that signs the indexes of the F-Droid repository at # The SHA-256 of the certificate that signs the indexes of the F-Droid repository at
# https://learn.halkyon.app/fdroid/repo (key alias halkyon-fdroid), as keytool prints it. # https://halkyon.app/fdroid/repo (key alias halkyon-fdroid), as keytool prints it.
# The F-Droid app knows the repository by it; tool/fdroid_repo.sh uploads no index # The F-Droid app knows the repository by it; tool/fdroid_repo.sh uploads no index
# signed otherwise. # signed otherwise.
E8:08:27:88:75:6F:6B:7E:68:2A:A2:2F:CE:AD:24:B5:3E:2A:8B:09:12:AD:66:D6:5E:8C:2D:4F:07:BB:94:B0 E8:08:27:88:75:6F:6B:7E:68:2A:A2:2F:CE:AD:24:B5:3E:2A:8B:09:12:AD:66:D6:5E:8C:2D:4F:07:BB:94:B0

View file

@ -1,9 +1,10 @@
# The F-Droid repository at https://learn.halkyon.app/fdroid/repo, kept by # The F-Droid repository at https://halkyon.app/fdroid/repo, for all of
# Halkyon's apps, kept by
# tool/fdroid_repo.sh: this folder is copied into the repository's working # tool/fdroid_repo.sh: this folder is copied into the repository's working
# folder (outside Git) before every update. Nothing secret is in here: the index # folder (outside Git) before every update. Nothing secret is in here: the index
# key's path and password come from the environment. # key's path and password come from the environment.
repo_url: https://learn.halkyon.app/fdroid/repo repo_url: https://halkyon.app/fdroid/repo
archive_url: https://learn.halkyon.app/fdroid/archive archive_url: https://halkyon.app/fdroid/archive
repo_name: Halkyon repo_name: Halkyon
repo_description: >- repo_description: >-
Halkyon's apps, signed by their maintainer: the same APKs as on Halkyon's apps, signed by their maintainer: the same APKs as on