Files
Fenris/docs/adr/0007-package-delivery-amends-0004.md

7.4 KiB

7. Package delivery: native deb + rpm packages, amending the installation lifecycle

Status

Accepted — resolves Task: Compose release spec + ADR amending 0004 on the Wayfinder map. This ADR amends ADR 0004 on delivery and file ownership only; every runtime semantic of 0004 — dormant install, polkit-only elevation, observation-store snapshot + forward-only migration, one-generation rollback — is inherited verbatim, restated below where the package delivery changes who performs it.

Context

ADR 0004 fixed delivery as sudo make install from a source checkout: wheel into a Fenris-owned runtime directory at /opt/fenris, a hand-rolled placement manifest, units in /etc/systemd/system. The release plan (map, decisions Lock channel + toolchain, Signing + key policy, Package ownership, Migration path, Release cadence) now ships Fenris as native deb + rpm packages built by nfpm and published to the self-hosted Gitea 1.27.1 package registry, for Debian 12, Ubuntu 22.04/24.04, Fedora 40+, and openSUSE Tumbleweed (x86_64), with locked pure-Python dependencies vendored at /opt/fenris/vendor. Packages become the primary delivery; ADR 0004's delivery model demotes to a dev fallback.

The implementation-ready operative contracts live in the release and packaging specification; this ADR records the decisions and their rationale.

Decision

Amendments to ADR 0004, section by section:

  1. Delivery (amended). Packages are primary: one deb per codename pool (bookworm, jammy, noble) and one rpm (group fenris, Fedora 40+ and openSUSE Tumbleweed), built by nfpm from a single packaging/nfpm.yaml over locked pure-Python runtime packages staged at /opt/fenris/vendor, published to the Gitea Debian/RPM registry and installed with apt, dnf, or zypper. sudo make install remains as the dev fallback for machines without packages; the two deliveries are mutually exclusive per machine. Version scheme <pyproject-version>-1, revision bump on rebuild.
  2. Layout and manifest (amended). The hand-rolled manifest model is retired: the dpkg/rpm database is the manifest, and nothing like manifest.txt ships. Package-owned layout: units in /usr/lib/systemd/system (vendor placement; /etc/systemd/system is admin-only for drop-ins and enable state); helpers stay in /usr/libexec/fenris (exactly fenris-monitor and fenris-collect — no new polkit-reachable binaries); polkit policy in /usr/share/polkit-1/actions/; wrapper at /usr/bin/fenris (FHS; /usr/local/bin remains make install's). The fenris group is declared in /usr/lib/sysusers.d/fenris.conf (g fenris -) and /var/lib/fenris in /usr/lib/tmpfiles.d/fenris.conf (d /var/lib/fenris 2750 root fenris -), both invoked from the maintainer scripts. The package owns the /var/lib/fenris directory only; observations.db, WAL sidecars, and .bak are never owned and never ghosted — ghost-erase would delete the store, violating 0004 §8.
  3. Privilege (unchanged). Root acts through maintainer scripts at install/upgrade/removal time; at runtime, elevation is exclusively polkit, exactly as 0004 §3 and ADR 0003 §5 fix it.
  4. Dormant install (restated for packages). A fresh package install is fully dormant: postinst/%post performs systemctl daemon-reload (plus systemd-sysusers and systemd-tmpfiles --create) and nothing else — never enable, never preset, never start; no preset file ships. The sanctioned toggle (fenris monitor resume) remains the only opt-in.
  5. Legacy import (narrowed). Auto-detection of ./data/history.jsonl is scoped to make install only — a package install has no checkout to inspect. fenris import <path> remains available as the only import path from packages.
  6. Upgrade (inherited, maintainer-script mechanics). Upgrades arrive as packages from the single registry channel. postinst/%post on upgrade: snapshot observations.db → one-generation .bak, run forward-only schema migrations through the target python3 with /opt/fenris/vendor on its import path (no new binaries), daemon-reload, and restart fenris-collect.timer only if unit contents changed and it is active. /var/lib/fenris is never rebuilt; a running oneshot finishes on its old interpreter.
  7. Rollback (unchanged, plus one hard edge). One-generation .bak semantics are unchanged. Package downgrade is additionally unsupported: forward-only store-version refusal means installing an older package over a newer store fails by design; documented rollback = restore the snapshot, then install the old release.
  8. Removal (mapped). deb remove ≈ make uninstall (conffile and store survive); deb purge ≈ make purge (plus .bak and group cleanup); rpm erase ≈ make uninstall (unmodified config removed, modified survives as .rpmsave; purge is a documented manual command). prerm/%preun performs the sanctioned disable — fenris-monitor disable --now, closing the period user_disabled — on remove/erase only, never on upgrade (deb prerm upgrade case is a no-op; rpm %preun gated on $1 -eq 0).
  9. Conffile semantics (new). /etc/fenris/fenris.conf ships as a placeholder-commented default with no active device selector — deb conffile, rpm %config(noreplace). The device selector is entered by hand (root edits the file), as in both prior deliveries; no configuration verb is added to fenris-monitor, and ADR 0003 §3's read-and-validate-at-collection-time semantics are untouched. On upgrade, local edits survive as-is; a changed package default lands beside them as .dpkg-new/.rpmnew.
  10. Migration from make-install systems (new). Remove-then-install via runbook only — no migration script, no auto-clean. preinst/%pre aborts with a pointer to the runbook if make-install remnants are detected (/var/lib/fenris/manifest.txt or /etc/systemd/system/fenris-collect.timer). Store and config survive by path continuity; the migration resets the system to dormant and the user opts back in with fenris monitor resume.

Consequences

  • Package installs, upgrades, and removals carry dpkg/rpm-native semantics; nothing in Fenris's own tooling duplicates them.
  • The manifest was 0004's answer to "what did the installer place"; the package database answers it better, and uninstall-keeps-store now holds by package ownership rather than by manifest discipline.
  • make install and packages are mutually exclusive per machine; over-install is blocked, not repaired (stale /etc units would silently shadow vendor units).
  • Hand-edited configuration remains the model: the device selector is a root-edited file in every delivery, keeping the polkit surface at exactly one binary.
  • Release mechanics — channel, signing, cadence, rollback documentation — are fixed in the release and packaging specification and the tickets it cites; this ADR deliberately stops at lifecycle semantics.