The optional XBPS publisher signs repository metadata after package signing. Remove the runner key only after publication and release asset upload.
14 KiB
Fenris release and packaging specification
Status: decision-complete. Assembled by Task: Compose release spec + ADR amending 0004 from the closed tickets of the Wayfinder map Fenris deb + rpm release plan. This document is normative for the follow-up execution effort that builds and publishes packages; no packages are built here.
Canonical roles. ADR 0007 records the lifecycle rationale (amending ADR 0004); this document restates the operative contracts — compat matrix, channel, toolchain, signing, release mechanics, package layout, maintainer-script behavior, migration — so the executing effort never needs Wayfinder-ticket access. Runtime semantics come from ADRs 0001–0006 and the redesign specification verbatim; nothing here overrides them. Terminology follows the glossary in CONTEXT.md, including Release and Rollback.
Binding language. Must, exactly, and never are normative.
1. Compatibility matrix
| Target | Version | Format | Registry placement |
|---|---|---|---|
| Debian 12 (bookworm) | — | deb | debian/pool/bookworm/main |
| Ubuntu 22.04 (jammy) | — | deb | debian/pool/jammy/main |
| Ubuntu 24.04 (noble) | — | deb | debian/pool/noble/main |
| Fedora 40+ | every release | rpm | rpm/fenris group |
| openSUSE Tumbleweed | rolling | rpm | rpm/fenris group |
- Architecture: x86_64 only (arm64 only if real ARM hardware appears — map fog).
- Dependencies are vendored as locked, pure-Python runtime packages for every target: Debian 12 and Ubuntu 22.04/24.04 ship
python3-textual0.1.13, far below the floor; Fedora 40+ ships ≥ 0.48 but below the pin (toolchain research). No distropython3-textualdependency ever enters package metadata. - Package metadata
depends:/Requires:are exactlypython3 (>= 3.10),smartmontools,systemd— the Python floor is 3.10 (oldest supported distro interpreter, Ubuntu 22.04), bumping ADR 0004 §10's 3.9 gate for packages;make installkeeps the checkout's floor. - Runtime packages are staged with
python3 -m pip --target /opt/fenris/vendor; entry points run the target system'spython3with that directory on the import path. No package ships a copied Python interpreter, avoiding build-host ABI paths and rolling-distribution minor-version breakage.
2. Distribution channel
- Channel: the self-hosted Gitea 1.27.1 package registry at
git.bongbetic.com, owner public for anonymous consumers (registry research; OBS rejected). - Single channel. No stable/testing split — deferred until external users ask to track pre-release builds (map fog). Every published version is retained indefinitely (registry has no REST cleanup; republishing a filename is a 409).
- deb publication: one deb artifact PUT to each codename pool —
PUT /api/packages/{owner}/debian/pool/{bookworm|jammy|noble}/main/upload. - rpm publication: one rpm artifact PUT to the single
fenrisgroup —PUT /api/packages/{owner}/rpm/fenris/upload— serving Fedora 40+ collectively. - Consumer setup (install docs, normative):
- apt: keyring file from
…/debian/repository.keyviasigned-by, one sources line per distribution, instance Debian Registry Key fingerprint printed beside the curl one-liner (TOFU hardening). - dnf:
dnf config-manager --add-repo <raw-url of packaging/fenris.repo>— the in-repo, Fenris-owned.repowithgpgkeypointing at the published packaging key andrepo_gpgcheck=0. Gitea's auto-generated.repois never mentioned in docs: it setsgpgcheck=1against the instance auto-key, which never signed our rpm payload — a trap that breaks installs.
- apt: keyring file from
3. Build toolchain
- nfpm for both formats from a single
packaging/nfpm.yaml— one config,overrides:for per-format deltas, two invocations (nfpm pkg -p deb,nfpm pkg -p rpm). fpm is dropped entirely (CLI-flag config drifts); no hand rpm spec; dh-virtualenv is deb-only and dormant since 2020. - Single source of truth: version injected from
pyproject.toml; file lists generated by a staging script (locked runtime packages →/opt/fenris/vendor, plus wrapper, helpers, units, polkit policy, sysusers/tmpfiles fragments) referenced bynfpm.yamlas atype: treecontent entry — no hand-maintained file lists. - Entry point:
make package→dist/fenris_<v>_amd64.deb+dist/fenris-<v>-1.x86_64.rpm. - Version scheme:
<pyproject-version>-1in both formats; a rebuild of the same upstream version bumps the revision (-2,-3, …) — the same filename is never re-PUT (registry 409s duplicates). - Authoring
nfpm.yaml, the staging script, and the workflow file is execution — deliberately not part of the decision map. The toolchain research doc sketches the pipeline.
4. Signing and key policy
- RPM payload: signed. rpmsign with the dedicated packaging key, invoked by
make sign-rpmafter the package is built. This is required, not optional: it is the only working dnf-native verification path. - deb: unsigned. apt never verifies payload signatures; trust = instance-signed
InRelease(signed-by keyring) + TLS + Acquire-By-Hash. Manual-download integrity is covered by SHA256SUMS. - SHA256SUMS: clearsigned with the packaging key — the trust anchor for manually downloaded release assets, independent of TLS.
- Packaging key: single dedicated key, RSA 3072, UID
Fenris Packaging <packaging@bongbetic.com>, 2-year expiry, no master/subkey hierarchy. The private key is stored as the repository Actions secretGPG_PRIVATE_KEY. The release workflow imports it on the self-hosted runner, verifies it against the in-repo public key, signs the RPM and SHA256SUMS, then deletes the runner's keyring copy in analways()cleanup step. The full ceremony is documented indocs/install/signing-key-ceremony.md. - XBPS key: separate RSA 3072 key stored as the repository Actions secret
XBPS_SIGNING_KEY; its public key and fingerprint are published inFenris-xbps. The release workflow uses it for the XBPS package and, when publication is explicitly requested, the repository index. A finalalways()cleanup deletes its runner copy after publication and release asset upload. - Public key publication: in-repo
packaging/keys/fenris-packaging.asc(raw URL doubles as the.repogpgkey target), release notes, docs page. No keyservers — TOFU-over-TLS. - Rotation (outline): new key published alongside old; rpm signed with the new key;
fenris.repogpgkey lists both URLs (dnf accepts multiple); old key dropped after one release cycle. Procedure details stay in map fog.
5. Release mechanics
- A Release is: a version tag, its packages in the channel, a Gitea release entry with notes, and a clearsigned SHA256SUMS — all together. Bare tags are forbidden (tag without packages + release entry is not a Release).
- Cadence: on-demand. Tag when user-visible changes or fixes accumulate; no calendar, no empty releases, no frequency SLA, no RC ceremony — fixes ship as a revision bump of the current version.
- Versioning: plain semver. Major = breaking CLI/config/unit change; store schema changes ride the natural bump (the forward-only refusal handles old-reader/new-store).
- Promotion flow: bump
pyproject.tomland the matching datedCHANGELOG.mdsection, then push tagv<version>. The repository-scoped Gitea Actions workflow builds and validates the deb, rpm, checksums, and release entry. XBPS publication remains a manual dispatch option after host acceptance.make releaseis for local artifact preparation and does not replace the tag workflow as the supported publication path. The ceremony is documented indocs/install/signing-key-ceremony.md. - Rollback: installing an older package over a newer store is unsupported — the store's forward-only version refusal fails it by design. Documented rollback = restore the observation-store snapshot, then install the old Release. No automatic downgrade machinery exists or will be built.
- CI: a repository-scoped self-hosted runner is registered and online (checked 2026-09-29).
.gitea/workflows/release.ymlis the tag-triggered release path; maintainers must confirm runner availability and required Gitea secrets before tagging. The workflow publishes deb/rpm packages and release assets; Void publication is withheld unless the signed XBPS host-release step is explicitly requested.
6. Package layout and ownership
Per ADR 0007 §2 — the dpkg/rpm database is the manifest; no manifest.txt ships:
| Artifact | Location | Ownership |
|---|---|---|
| Bundled runtime packages | /opt/fenris/vendor |
package (tree) |
| Wrapper | /usr/bin/fenris |
package |
| Helpers | /usr/libexec/fenris/{fenris-monitor,fenris-collect} |
package — exactly these two, no new polkit-reachable binaries |
| Units | /usr/lib/systemd/system/fenris-collect.{timer,service} |
package (vendor placement; /etc/systemd/system is admin-only) |
| Polkit policy | /usr/share/polkit-1/actions/com.bongbetic.fenris.monitor.policy |
package |
| sysusers fragment | /usr/lib/sysusers.d/fenris.conf (g fenris -) |
package |
| tmpfiles fragment | /usr/lib/tmpfiles.d/fenris.conf (d /var/lib/fenris 2750 root fenris -) |
package |
| Configuration | /etc/fenris/fenris.conf |
package as conffile / %config(noreplace) — placeholder-commented default, no active selector |
| Observation store | /var/lib/fenris/observations.db (+ WAL, .bak) |
never owned, never ghosted — the package owns the directory only |
7. Maintainer-script contracts
- preinst / %pre: abort with a pointer to the migration runbook (§9) if
/var/lib/fenris/manifest.txtor/etc/systemd/system/fenris-collect.timerexists (dual marker covers pre-manifest make installs). No auto-clean — scripts never delete files outside the package DB. - postinst / %post (install):
systemd-sysusers,systemd-tmpfiles --create,systemctl daemon-reload. Nothing else — no enable, no preset, no start; no preset file ships. - postinst / %post (upgrade): snapshot
observations.db→.bak(one generation) → forward-only schema migration via targetpython3with/opt/fenris/vendoron its import path →daemon-reload→ restartfenris-collect.timeronly if unit contents changed and it is active./var/lib/fenrisis never rebuilt; an in-flight oneshot finishes on its old interpreter. - prerm / %preun: sanctioned disable (
fenris-monitor disable --now, closing the monitoring perioduser_disabled) on remove/erase only, never on upgrade — deb prerm upgrade case is a no-op; rpm%preungated on$1 -eq 0. - Removal mapping: deb
remove≈make uninstall(conffile + store survive); debpurge≈make purge(+.bak, group cleanup); rpm erase ≈make uninstall(unmodified config removed, modified survives as.rpmsave); rpm purge = documented manual command.
8. Initial configuration
- The device selector is entered by hand: root edits
/etc/fenris/fenris.conf(world-readable, exactly one key per ADR 0003 §3). The shipped default is placeholder-commented and carries no active selector — a fresh install reads as aconfiguration error-free dormant system until the firstfenris monitor resume+ edit, exactly the dormant-install contract. - No configuration verb is added to
fenris-monitor; the polkit surface stays at one binary. (This resolves the open item from Package ownership + ADR 0004 amendment: nothing in any delivery writes the selector —make installnever wrotefenris.confeither; hand-editing has been the model since ADR 0003.) - On upgrade, local edits survive; a changed package default lands as
.dpkg-new/.rpmnew.
9. Migration from make-install systems
- Runbook only — no migration script, no auto-clean. Population is author machines plus a few testers; store and config survive by path continuity.
- Remove-then-install, mandatory:
sudo make uninstall(preserves store +/etc/fenris) →apt install fenris/dnf install fenris. Over-install is forbidden: stale/etc/systemd/system/fenris-collect.*silently shadows vendor units (systemd precedence),/usr/local/bin/fenrisshadows/usr/bin/fenrison PATH. - No-move continuity:
/var/lib/fenrisuntouched; existingfenrisgroup → sysusers no-op; existing dir → tmpfiles no-op; hand-writtenfenris.confsurvives (dpkg ships the default as.dpkg-new; rpm as.rpmnew); store schema caught up by the upgrade-path migration. - Reset-to-dormant:
make uninstall's sanctioned disable closes the open perioduser_disabled; after migration the user opts back in withfenris monitor resume— one ≤15-min sample gap, honest against the endurance timeline. - Mutual exclusion: package and
make installnever on the same machine. The dev loop is checkout +make test; a dev-local install mode stays in map fog.
10. Out of scope
- Building and publishing packages (the follow-up execution effort).
- Snap/Flatpak/AppImage/Homebrew, PyPI as an install path.
- Stable/testing channel split, COPR/PPA fallback, arm64 — map fog until demand appears.
Assembled from Lock channel + toolchain, Signing + key policy, Package ownership + ADR 0004 amendment, Migration path from make-install systems to packages, Release cadence + stable/testing channel split, Actions runner availability, and the research tickets deb + rpm packaging toolchain, Gitea 1.27 package registry feasibility, OBS route.