From b005049733955878af0df1393cf929ba0615c8cb Mon Sep 17 00:00:00 2001 From: xavierk Date: Thu, 3 Sep 2026 01:44:37 +0530 Subject: [PATCH] docs: release & packaging spec + ADR 0007 amending 0004 (map #33, task #42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/spec/release-packaging.md: decision-complete spec — compat matrix, Gitea 1.27.1 registry channel, nfpm toolchain, signing/key policy, release mechanics, package layout/ownership, maintainer-script contracts, initial config, make-install migration runbook. - docs/adr/0007: package delivery amends ADR 0004 (delivery/ownership only; runtime semantics inherited verbatim). 0004 status updated. - docs/research/: toolchain, gitea-registry, obs findings merged from research branches (assets of map tickets #34/#35/#36). - CONTEXT.md: Release + Rollback glossary terms (ticket #43). --- CONTEXT.md | 8 + .../0004-install-upgrade-removal-lifecycle.md | 2 +- docs/adr/0007-package-delivery-amends-0004.md | 34 +++ docs/research/deb-rpm-toolchain.md | 239 ++++++++++++++++++ docs/research/gitea-package-registry.md | 116 +++++++++ docs/research/obs-route.md | 73 ++++++ docs/spec/release-packaging.md | 105 ++++++++ 7 files changed, 576 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0007-package-delivery-amends-0004.md create mode 100644 docs/research/deb-rpm-toolchain.md create mode 100644 docs/research/gitea-package-registry.md create mode 100644 docs/research/obs-route.md create mode 100644 docs/spec/release-packaging.md diff --git a/CONTEXT.md b/CONTEXT.md index 1e0e4fb..5558469 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -80,6 +80,14 @@ _Avoid_: Uptime, sample count One scheduled or on-demand execution of the collector that interrogates the drive and extends the observation history. _Avoid_: Poll, daemon tick +**Release**: +A published version of Fenris: a version tag, its packages in the channel, and its human-readable change notes, all together; a bare tag is not one. +_Avoid_: Tag, upload, build + +**Rollback**: +Returning to an earlier release by restoring an observation-store snapshot and then installing that release; installing an older package over a newer store is unsupported. +_Avoid_: Downgrade, version pinning (as a promise) + **Deliberate disable**: A monitoring pause made through Fenris's own control path, closing the monitoring period so the paused time is excluded from the usage habit. _Avoid_: Manual stop, service stop diff --git a/docs/adr/0004-install-upgrade-removal-lifecycle.md b/docs/adr/0004-install-upgrade-removal-lifecycle.md index a56d1b0..09ba46d 100644 --- a/docs/adr/0004-install-upgrade-removal-lifecycle.md +++ b/docs/adr/0004-install-upgrade-removal-lifecycle.md @@ -2,7 +2,7 @@ ## Status -Accepted — resolves [Define installation, upgrade, and removal behavior](https://git.bongbetic.com/xavierk/Fenris/issues/9) on the [Wayfinder map](https://git.bongbetic.com/xavierk/Fenris/issues/1). +Accepted — resolves [Define installation, upgrade, and removal behavior](https://git.bongbetic.com/xavierk/Fenris/issues/9) on the [Wayfinder map](https://git.bongbetic.com/xavierk/Fenris/issues/1). Amended by [ADR 0007](0007-package-delivery-amends-0004.md): package delivery replaces `make install` as primary; layout/ownership and maintainer-script mechanics per 0007. Runtime semantics (dormant install, polkit-only elevation, snapshot + forward-only migration, one-generation rollback) unchanged. ## Context diff --git a/docs/adr/0007-package-delivery-amends-0004.md b/docs/adr/0007-package-delivery-amends-0004.md new file mode 100644 index 0000000..8a1621c --- /dev/null +++ b/docs/adr/0007-package-delivery-amends-0004.md @@ -0,0 +1,34 @@ +# 7. Package delivery: native deb + rpm packages, amending the installation lifecycle + +## Status + +Accepted — resolves [Task: Compose release spec + ADR amending 0004](https://git.bongbetic.com/xavierk/Fenris/issues/42) on the [Wayfinder map](https://git.bongbetic.com/xavierk/Fenris/issues/33). This ADR **amends [ADR 0004](0004-install-upgrade-removal-lifecycle.md)** 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 venv at `/opt/fenris`, a hand-rolled placement manifest, units in `/etc/systemd/system`. The release plan ([map](https://git.bongbetic.com/xavierk/Fenris/issues/33), decisions [Lock channel + toolchain](https://git.bongbetic.com/xavierk/Fenris/issues/38), [Signing + key policy](https://git.bongbetic.com/xavierk/Fenris/issues/39), [Package ownership](https://git.bongbetic.com/xavierk/Fenris/issues/40), [Migration path](https://git.bongbetic.com/xavierk/Fenris/issues/41), [Release cadence](https://git.bongbetic.com/xavierk/Fenris/issues/43)) 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, and Fedora 40+ (x86_64), with dependencies vendored in a bundled venv because every target distro ships `python3-textual` below Fenris's floor. 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](../spec/release-packaging.md); 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+), built by nfpm from a single `packaging/nfpm.yaml` over a staged `--copies` venv at `/opt/fenris`, published to the Gitea Debian/RPM registry and installed with `apt`/`dnf`. `sudo make install` remains as the dev fallback for machines without packages; the two deliveries are mutually exclusive per machine. Version scheme `-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](0003-service-lifecycle-and-sanctioned-toggle.md) §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 ` 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 via inline `/opt/fenris/bin/python3 -c "…migrate_to_latest…"` (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](0003-service-lifecycle-and-sanctioned-toggle.md) §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](../spec/release-packaging.md) and the tickets it cites; this ADR deliberately stops at lifecycle semantics. diff --git a/docs/research/deb-rpm-toolchain.md b/docs/research/deb-rpm-toolchain.md new file mode 100644 index 0000000..ec8b142 --- /dev/null +++ b/docs/research/deb-rpm-toolchain.md @@ -0,0 +1,239 @@ +# Research: deb + rpm packaging toolchain for bundled-venv builds + +Issue: #34 (parent plan: #33) — branch `research/toolchain` +Date: 2026-09-03 · target: Fenris 0.3.0, x86_64, Debian 12 / Ubuntu 22.04+24.04 / Fedora 40+ + +## TL;DR + +**Recommended: nfpm** with a build script that stages a `--copies` venv at +`/opt/fenris`. One `nfpm.yaml` is the single source of truth for both formats; +`nfpm pkg -p deb && nfpm pkg -p rpm` (one invocation per format — `-p` takes a +single string, verified in `internal/cmd/package.go`). Actively maintained +(releases v2.47.0, 2026-06-20; repo pushed 2026-08-31). Runner-up: fpm (active, +v1.18.0 gem 2026-08-26), but its "config" is a long CLI invocation per format — +the single source of truth degrades into a shell script. dh-virtualenv is +deb-only and its last upstream release is 2020-10 (effectively dormant); +rpmbuild spec is rpm-only and cannot share file lists with a deb build without +external generation. + +## Constraint evidence: distro textual is unusable (mostly) + +| Distro | python3-textual | Source | +|---|---|---| +| Debian 12 (bookworm) | **0.1.13-1** | https://packages.debian.org/bookworm/python3-textual | +| Ubuntu 22.04 (jammy) | **0.1.13-1** | https://packages.ubuntu.com/jammy/python3-textual | +| Ubuntu 24.04 (noble) | **0.1.13-1** | https://packages.ubuntu.com/noble/python3-textual | +| Fedora 40 | 0.48.1 | https://src.fedoraproject.org/rpms/python-textual (f40 spec) | +| Fedora 41 / 42 / 43 | 0.69.0 / 1.0.0 / 4.0.0 | same spec, f41–f43 branches | + +Fenris declares `textual>=0.40.0` (pyproject) but pins `textual==8.2.8` +(requirements.txt). Debian 12 + Ubuntu 22.04/24.04 are ~1 major era behind even +the *floor*; Fedora 40 technically meets `>=0.40` but not the pin. Verdict +unchanged: **vendor deps inside the package for all targets**; per-format +`depends:` only on `python3 (>= 3.9)`, `smartmontools`, `systemd`. + +## Tool-by-tool + +### 1. nfpm (goreleaser) — RECOMMENDED + +- **Route:** Makefile target builds staging tree → one `nfpm.yaml` → + `nfpm package -p deb` + `nfpm package -p rpm`. (Goreleaser release pipeline + can wrap both later.) +- **Shared assets:** version, description, maintainer, `depends`, + `contents:` file list, `scripts:` all live once in `nfpm.yaml`; per-format + deltas via `overrides: { deb: ..., rpm: ... }` and `packager:`-scoped + content entries (https://nfpm.goreleaser.com/configuration/, source + `www/content/docs/configuration.md`). +- **Prerequisites:** single static Go binary (`go install + github.com/goreleaser/nfpm/v2/cmd/nfpm@latest`, Homebrew, or release + tarball — https://nfpm.goreleaser.com/install/). No toolchain per distro, + no root, no containers required (build same tree for both formats). +- **Venv → file list:** stage with `python3 -m venv --copies staging/opt/fenris + && staging/opt/fenris/bin/pip install dist/fenris-*.whl`; map in one entry: + `contents: [{ src: staging/opt/fenris/, dst: /opt/fenris, type: tree }]`. + Shebangs point at fixed absolute `/opt/fenris/bin/python` → no relocation + issues. `--copies` avoids symlink-to-/usr breakage. Config file → + `type: config|noreplace` (=%config(noreplace) on rpm, conffile semantics on + deb). `/var/lib/fenris` store → `type: ghost` (rpm: owned-but-not-packed; + deb: ignored → create in `postinstall` script instead). +- **Systemd/polkit/libexec:** plain `contents:` entries — + `/usr/lib/systemd/system/fenris-collect.{service,timer}` (or + `/etc/systemd/system` to match current Makefile), polkit action at + `/usr/share/polkit-1/actions/`, helpers under `/usr/libexec/fenris/`. + `scripts:` supports `postinstall` (deb maintainer script / rpm scriptlet) — + run `systemctl daemon-reload`, create `/var/lib/fenris` root:fenris 2750, + group creation. +- **Upgrade/removal:** deb — dpkg replaces all non-conffile files, conffile + prompts/preserves (`.dpkg-new`) per Debian Policy ch-files + (https://www.debian.org/doc/debian-policy/ch-files.html); removal keeps + conffiles + unowned store; purge cleans. rpm — `rpm -U` replaces, + `%config(noreplace)` keeps local edits as `.rpmnew`; only owned dirs are + removed on erase (nfpm `type: dir` exists precisely to claim ownership — + docs warn not to claim distro-owned dirs). +- **Maintenance:** very active. goreleaser/nfpm, 2.6k stars, last push + 2026-08-31, v2.47.0 released 2026-06-20 (GitHub API). + +### 2. fpm — viable, weaker single-source-of-truth + +- **Route:** staging tree (same as above) then + `fpm -s dir -t deb ... staging/=/ ; fpm -s dir -t rpm ...`. +- **Shared assets:** none declarative — everything is CLI flags + (`-n`, `-v`, `--config-files`, `--deb-systemd`, `--directories`, + `--after-install`, `--rpm-posttrans`, …). Flag list: + https://fpm.readthedocs.io/en/latest/cli-reference.html. The two + invocations *will* drift unless wrapped in a Makefile that shares variables; + the "single source" is then a shell script, not a checked declarative file. + (`--deb-systemd` exists; no rpm-native unit macro — you hand it the unit + file plus `--rpm-posttrans` for daemon-reload.) +- **Prerequisites:** Ruby + gem (`gem install fpm`) or distro package; + building rpm side needs `rpmbuild` present for some features. +- **Venv → file list:** `-s dir` maps a directory into the package verbatim — + same staging-tree trick as nfpm. `--config-files /etc/fenris` marks + conffiles (deb) / %config (rpm). +- **Upgrade/removal:** identical downstream semantics to nfpm (native dpkg/rpm + behavior); differences are only in how metadata/scripts land in the + package. +- **Maintenance:** active — releases v1.16.0 (2024-12), v1.17.0 (2025-10), + v1.18.0 (2026-08-26); gem 1.18.0 on rubygems; ~11.5k stars. But docs are + openly "work in progress" (https://fpm.readthedocs.io/en/latest/). + +### 3. dh-virtualenv (Spotify) — deb-only, dorms + +- **Route:** debhelper add-on: `debian/rules` with + `dh $@ --with python-virtualenv --buildsystem=python_distutils`; + produces a .deb containing venv at `/opt/venvs/` + (`DH_VIRTUALENV_INSTALL_ROOT` overridable, `--builtin-venv` for `python -m + venv`). Docs: repo `doc/usage.rst`, `doc/tutorial.rst` + (https://github.com/spotify/dh-virtualenv). +- **Shared assets:** none with rpm — it cannot emit .rpm at all. Would still + need a second toolchain for Fedora → fails the criterion outright. +- **Prerequisites:** `build-essential debhelper devscripts equivs` + + `dh-virtualenv` (tutorial.rst); Debian 12 still ships it as + `dh-virtualenv 1.2.2-1.3` (https://packages.debian.org/bookworm/dh-virtualenv). +- **Venv → file list:** automatic — it builds the venv during the debhelper + sequence and rewrites shebangs; the .deb owns the whole venv tree. Least + manual work of all four, for deb alone. +- **Upgrade/removal:** standard dpkg; whole venv tree is package-owned, so + `apt remove` deletes it cleanly; `--pypi-url`/requirements handled by tool. +- **Maintenance:** last upstream release **1.2.2, 2020-10-22** (GitHub tag); + repo last pushed 2024-04-27, RTD docs 404. Effectively dormant upstream — + fine via Debian's own packaging, but risky as strategic dependency. +- **Bonus fact:** PyPI `dh-virtualenv` project now returns 404 — install only + from Debian repo / git. + +### 4. rpmbuild spec + vendored venv — rpm-native, no deb + +- **Route:** hand-written `fenris.spec`: `%install` stage builds venv into + `%{buildroot}/opt/fenris`, `%files` lists it plus units/polkit/libexec, + `%ghost %attr(2750,root,fenris) /var/lib/fenris`, `%config(noreplace)` for + `/etc/fenris`, `systemd_post/preun` macros for the timer. Reference style: + https://docs.fedoraproject.org/en-US/packaging-guidelines/. +- **Shared assets:** the spec is a second, parallel description of the same + file list — nothing is shared with any deb build without generating one + side from the other (e.g. generate spec + debian/control from a manifest). + Worst single-source-of-truth score. +- **Prerequisites:** `rpm-build`, mock/koji for cleanroots; Fedora toolchain + knowledge; per-distro `Release:`/dist tag handling. +- **Venv → file list:** `%files` line `%{buildroot}/opt/fenris/...` — venv + becomes ordinary payload; shebangs already absolute. +- **Upgrade/removal:** canonical rpm semantics (same as above) plus real + systemd scriptlet macros — the *best-behaved* rpm integration of the four, + at the cost of hand-maintained spec. +- **Maintenance:** rpmbuild itself is maintained forever (part of RPM), but + *your* spec is 100% hand-maintained duplication. + +## Comparison matrix + +| Criterion | nfpm | fpm | dh-virtualenv | rpmbuild spec | +|---|---|---|---|---| +| deb + rpm from one config | ✅ one YAML (2 invocations) | ⚠️ flags per invocation | ❌ deb only | ❌ rpm only | +| File list shared across formats | ✅ `contents:` | ⚠️ per-invocation args | n/a | ❌ | +| Vendored venv supported | ✅ staging `type: tree` | ✅ `-s dir` | ✅✅ automatic (deb) | ✅ `%files` | +| conffile / %config(noreplace) | ✅ `type: config\|noreplace` | ✅ `--config-files` | ✅ (debhelper) | ✅ `%config(noreplace)` | +| ghost store dir | ✅ `type: ghost` | ⚠️ `--rpm-ghost`? (no deb equiv) | ❌ | ✅ `%ghost` | +| systemd scriptlets | ✅ `scripts:` + macros? (plain scripts) | ✅ `--deb-systemd`, `--rpm-posttrans` | ✅ (deb) | ✅✅ native macros | +| Prereqs on build host | Go binary (or brew/apt tarball) | Ruby gem | debhelper stack | rpm-build + mock | +| Maintenance (2026) | 🟢 active (v2.47.0) | 🟢 active (v1.18.0) | 🔴 dormant since 2020 (Debian carries it) | 🟢 tool yes / 🔴 your spec | +| Risk | young-ish config schema churn | docs thin | dead upstream | duplication forever | + +## Proposed pipeline (sketch) + +```make +# Makefile additions (build only — install target stays for source installs) +stage: dist/fenris-*.whl + rm -rf build/stage + python3 -m venv --copies build/stage/opt/fenris + build/stage/opt/fenris/bin/pip install --no-compile dist/fenris-*.whl + install -D -m 0755 scripts/fenris build/stage/usr/bin/fenris + install -D -m 0755 src/fenris/monitor.py build/stage/usr/libexec/fenris/fenris-monitor + install -D -m 0755 src/fenris/collect.py build/stage/usr/libexec/fenris/fenris-collect + install -D -m 0644 units/fenris-collect.timer build/stage/usr/lib/systemd/system/fenris-collect.timer + install -D -m 0644 units/fenris-collect.service build/stage/usr/lib/systemd/system/fenris-collect.service + install -D -m 0644 polkit/com.bongbetic.fenris.monitor.policy \ + build/stage/usr/share/polkit-1/actions/com.bongbetic.fenris.monitor.policy + +package-deb package-rpm: stage + nfpm pkg -f packaging/nfpm.yaml -p deb -t dist/ + nfpm pkg -f packaging/nfpm.yaml -p rpm -t dist/ +``` + +```yaml +# packaging/nfpm.yaml (excerpt) +name: fenris +arch: amd64 +platform: linux +version: ${VERSION} # env expansion, documented feature +maintainer: Fenris Maintainers +description: SMART drive observation daemon with persistent TUI +homepage: https://git.bongbetic.com/xavierk/Fenris +depends: [smartmontools] +contents: + - src: build/stage/ # everything above + dst: / + type: tree + - dst: /etc/fenris # config dir; ship fenris.conf as config|noreplace + type: dir + - src: packaging/fenris.conf + dst: /etc/fenris/fenris.conf + type: config|noreplace + - dst: /var/lib/fenris # rpm: %ghost ownership; deb: create in postinst + type: ghost +scripts: + postinstall: packaging/postinst.sh # groupadd fenris; install -d -o root -g fenris -m 2750 /var/lib/fenris; systemctl daemon-reload (units shipped dormant) + preremove: packaging/prerm.sh # stop timer if running +overrides: + deb: + depends: [python3 (>= 3.9), smartmontools] + rpm: + depends: [python3 >= 3.9, smartmontools] +``` + +## Recommendation + +Adopt **nfpm + staged `--copies` venv**: closest to single source of truth +(one YAML for both formats), smallest prerequisite surface (one static binary), +actively maintained, and every Fenris constraint (units, polkit, libexec, +`/etc/fenris` conffile, `/var/lib/fenris` ghost/store) has a first-class +mapping. Keep fpm as documented fallback (identical staging tree, works +anywhere Ruby exists). Do not build the release pipeline on dh-virtualenv +(dormant, deb-only) or on a hand-maintained spec file (duplication, deb side +unaddressed). + +## Sources + +- nfpm config reference: https://nfpm.goreleaser.com/configuration/ (source: + goreleaser/nfpm `www/content/docs/configuration.md`, accessed 2026-09-03) +- nfpm CLI (single `-p`): goreleaser/nfpm `internal/cmd/package.go` +- nfpm releases/status: GitHub API, repo pushed 2026-08-31, v2.47.0 2026-06-20 +- fpm README + CLI reference: https://github.com/jordansissel/fpm, + https://fpm.readthedocs.io/en/latest/cli-reference.html; releases v1.18.0 + (2026-08-26), gem 1.18.0 +- dh-virtualenv docs: `doc/usage.rst`, `doc/tutorial.rst` @ master; tag 1.2.2 + dated 2020-10-22 (GitHub commits API); PyPI project 404; + Debian 12 package 1.2.2-1.3 (packages.debian.org) +- Distro textual versions: packages.debian.org, packages.ubuntu.com, + src.fedoraproject.org `python-textual.spec` f40–f43 +- Upgrade semantics: Debian Policy ch-files + (https://www.debian.org/doc/debian-policy/ch-files.html); Fedora packaging + guidelines (https://docs.fedoraproject.org/en-US/packaging-guidelines/); + RPM directive behavior quoted in nfpm config docs (%ghost, %config(noreplace)) diff --git a/docs/research/gitea-package-registry.md b/docs/research/gitea-package-registry.md new file mode 100644 index 0000000..b333c3c --- /dev/null +++ b/docs/research/gitea-package-registry.md @@ -0,0 +1,116 @@ +# Research: Gitea 1.27 Debian + RPM package registry feasibility + +Issue: [Fenris deb + rpm release plan](https://git.bongbetic.com/xavierk/Fenris/issues/33) → +[Research: Gitea 1.27 Debian + RPM package registry feasibility](https://git.bongbetic.com/xavierk/Fenris/issues/35) +Verified 2026-09-03 against live instance `https://git.bongbetic.com` (reports `1.27.1` via `/api/v1/version`) +and primary sources: docs.gitea.com 1.27 Debian/RPM registry pages and Gitea `v1.27.1` source (go-gitea/gitea tag). + +**Verdict: feasible.** Every publish/consume path tested live with throwaway packages `fenris-regtest` (all deleted afterward; package list verified empty). + +## 1. Publish paths (verified live, HTTP 201) + +### Debian (`.deb`) + +```bash +curl --user xavierk:$TOKEN --upload-file fenris_0.3.0_amd64.deb \ + "https://git.bongbetic.com/api/packages/xavierk/debian/pool/{distribution}/{component}/upload" +``` + +- `distribution` and `component` are free-form path segments chosen at upload time (e.g. `bookworm/main`, `noble/main`). Gitea derives apt suites from what was uploaded — verified: same .deb published to `pool/bookworm/main` and `pool/noble/main` (both 201), both then served in `dists/bookworm/` and `dists/noble/` with correct `Suite:`/`Codename:` headers. +- Republish of identical name+version+distribution+component+architecture → **409 Conflict** (verified). Must delete first. + +### RPM (`.rpm`) + +```bash +# no group (flat repo) +curl --user xavierk:$TOKEN --upload-file fenris-0.3.0-1.el9.x86_64.rpm \ + "https://git.bongbetic.com/api/packages/xavierk/rpm/upload" +# with group (distro tag, nestable) +curl --user xavierk:$TOKEN --upload-file fenris-0.3.0-1.fc40.x86_64.rpm \ + "https://git.bongbetic.com/api/packages/xavierk/rpm/el9/upload" # e.g. el9, rocky/el9, fc40 +``` + +- Group = free-form nesting used to partition repos per distro/track. Verified: publish to root group and `el9` group (both 201), duplicate → 409. +- Owner can be the user (`xavierk`) or an org; packages under a public owner are readable anonymously (verified: metadata fetches without auth succeeded). + +## 2. Consumer setup (exact commands) + +### apt clients + +```bash +sudo mkdir -p /etc/apt/keyrings +sudo curl -o /etc/apt/keyrings/gitea-xavierk.asc \ + https://git.bongbetic.com/api/packages/xavierk/debian/repository.key +echo "deb [signed-by=/etc/apt/keyrings/gitea-xavierk.asc] https://git.bongbetic.com/api/packages/xavierk/debian bookworm main" \ + | sudo tee /etc/apt/sources.list.d/gitea.list # one line per distribution +sudo apt update +apt install fenris # or fenris=0.3.0 +# private owner variant: https://{user}:{token}@git.bongbetic.com/api/packages/... in the URL +``` + +### dnf clients + +```bash +sudo dnf config-manager --add-repo https://git.bongbetic.com/api/packages/xavierk/rpm/el9.repo +# private owner: add user:token into the baseurl inside /etc/yum.repos.d/gitea-xavierk-el9.repo afterwards +sudo dnf install fenris # or fenris-0.3.0 +``` + +The served `.repo` (verified live) sets `gpgcheck=1` and points `gpgkey` at `…/rpm/repository.key`, so `dnf` auto-imports on first use. + +## 3. Metadata signing: native, not passthrough + +Gitea **signs generated metadata itself** with per-instance auto-generated PGP keys. Client-side signing config is limited to trusting the served keys. + +- Debian: `dists/{suite}/InRelease` is clearsigned; `Release.gpg` detached sig also served. Key (RSA) fetched from `…/debian/repository.key`, uid literally `(Automatically generated Debian Registry Key; created …)`. +- RPM: `repodata/repomd.xml.asc` detached ASCII-armored signature, uid `(RPM Registry)`. Key from `…/rpm/repository.key`. +- Both verified with `gpg --verify` → **Good signature** (keys are self-generated; the "not certified" warning is expected and handled by the signed-by/keyring flow above). +- The apt `Release` also advertises `Acquire-By-Hash: yes` with MD5/SHA1/SHA256/SHA512 indexes of `Packages`/`.gz`/`.xz` (verified live). RPM repomd carries sha256 checksums for `primary/filelists/other.xml.gz`. + +There is **no bring-your-own-signing-key config** for these registries in 1.27 — trust anchor is the instance's auto keys. For Fenris this is acceptable; TOFU over TLS via the key URLs above. + +## 4. Multi-distro metadata + +- Debian: distributions/suites are implicit — whatever `{distribution}` path segments appear on upload become `dists/{distribution}/` trees with `Suite:`/`Codename:` set to the segment. No server-side list to maintain; adding a new distro = upload with new segment + one more `deb …` sources line. Components likewise (`main`, etc.). Architectures come from each `.deb`'s control stanza (index served as `dists/{dist}/{component}/binary-{arch}/Packages`). +- RPM: same via `{group}` path segments (`el9`, `rocky/el9`, …); each group gets its own `repodata/`. No `basearch` filtering — clients pick the group; Gitea publishes whatever RPM arch was uploaded. + +## 5. Version retention + +- Default: **all versions retained indefinitely**; nothing auto-deletes. Old versions stay installable (`apt install fenris=0.2.9`, `dnf install fenris-0.2.9`). +- Republishing an existing name+version (deb: same dist/component/arch; rpm: same file name in group) → 409; overwrite requires delete-then-upload. +- Optional cleanup rules exist (per owner + package type): `KeepCount`, `KeepPattern`, `RemoveDays`, `RemovePattern`, `MatchFullName` (source: `models/packages/package_cleanup_rule.go`, executed by scheduled `CleanupTask` in `services/packages/cleanup/cleanup.go`). In 1.27.1 they are configurable **only in the web UI** (owner → Packages → Cleanup Rules); no v1 REST route (verified by route table grep of `routers/api/v1/api.go` — probes of `/api/v1/packages/{owner}/cleanuprules…` return 404/409-style errors). +- Deletes: format-specific `DELETE …/debian/pool/{dist}/{component}/{name}/{version}/{arch}` and `DELETE …/rpm/{group}/package/{name}/{version}/{arch}` (both verified, 204). Deleting last file removes the version. Generic fallback: `DELETE /api/v1/packages/{owner}/{type}/{name}/{version}`. + +## 6. Release attachment: not supported + +Gitea 1.27.1 has **no package↔release linkage**. Release assets (`…/releases/{id}/assets`) are standalone file uploads; the package model has no release field and no route links them (verified against `v1.27.1` source: `routers/api/v1/repo/release_attachment.go`, `models/packages/`). Options for Fenris releases: + +1. Publish `.deb`/`.rpm` to the registry (real apt/dnf install UX) and reference the registry URLs in release notes. +2. Additionally upload tarballs/SHA256SUMS as plain release attachments. +3. Generic registry (`PUT /api/packages/{owner}/generic/{name}/{version}/{filename}`) if an untyped artifact store is needed. + +## 7. Caveats for the release plan + +- Owner choice matters: publish under an **org** (e.g. `fenris`) if multiple maintainers need write; `xavierk` user owner works today (token owner is admin). +- Metadata access follows owner visibility — public owner → anonymous consumers, no token in URLs (current state, verified). Keep owner public for frictionless installs, or embed `user:token` in sources/baseurl. +- apt distro naming should match OS release names (`bookworm`, `trixie`, `noble`) purely for client convention; server accepts anything. +- RPM groups should mirror `$distver` (e.g. `el9`, `fc40`) so `.repo` selection is obvious per target. + +## 8. Test log (live, 2026-09-03) + +| Step | Result | +|---|---| +| `PUT debian/pool/bookworm/main/upload` | 201 | +| `PUT debian/pool/noble/main/upload` (multi-dist) | 201 | +| `PUT debian` duplicate | 409 (expected) | +| `PUT rpm/upload` (no group) | 201 | +| `PUT rpm/el9/upload` (group) | 201 | +| `PUT rpm` duplicate | 409 (expected) | +| `GET debian/repository.key` / `rpm/repository.key` | PGP public keys (200) | +| `GET dists/bookworm/{Release,InRelease,Packages}` | correct; `gpg --verify` Good signature | +| `GET rpm{,/el9}/repodata/repomd.xml{,.asc}` | 200; Good signature | +| `GET rpm{,/el9}.repo` | generated repo files with `gpgcheck=1` | +| Cleanup-rules REST probes | 404 (not in v1 API — UI only) | +| `DELETE` all four test entries | 204 ×4; package list then empty | + +Sources: [docs.gitea.com 1.27 Debian registry](https://docs.gitea.com/1.27/usage/packages/debian), [docs.gitea.com 1.27 RPM registry](https://docs.gitea.com/1.27/usage/packages/rpm), Gitea source tag `v1.27.1` (`routers/api/v1/api.go`, `models/packages/package_cleanup_rule.go`, `services/packages/cleanup/cleanup.go`), live instance `git.bongbetic.com`. diff --git a/docs/research/obs-route.md b/docs/research/obs-route.md new file mode 100644 index 0000000..d45032d --- /dev/null +++ b/docs/research/obs-route.md @@ -0,0 +1,73 @@ +# Research: OBS as an alternative build + distribution route + +Resolves [Research: OBS as alternative build + distribution route](https://git.bongbetic.com/xavierk/Fenris/issues/36) on the [Wayfinder map](https://git.bongbetic.com/xavierk/Fenris/issues/33). + +- Date: 2026-09-03 +- Verdict: **Reject OBS now; ship via the self-hosted Gitea 1.27.1 registry** (deb + rpm), and revisit OBS only if publishing reach becomes a goal. + +## Question + +Evaluate openSUSE Open Build Service (OBS) as the build + distribution route — deb build quality, vendoring `textual>=0.40` via source services (offline sandbox), signing, publishing reach, account/maintenance cost, build latency — against the Gitea registry on our matrix: Debian 12, Ubuntu 22.04/24.04, Fedora 40+, x86_64. + +## Findings + +### 1. deb build support quality — real, with quirks + +- OBS builds deb via the classic recipe trio: `debian.control`, `debian.rules`, `PACKAGE.dsc` (OBS User Guide §2.3 "Debian: Dsc"). The build phase runs `dpkg-buildpackage` on Debian-based distributions (§25.1.3 "Package Build"); Debian build environments can alternatively use the `debootstrap` build engine (§"Configuration File Syntax", `BuildEngine`). +- Quirk: release numbers are **not** auto-incremented across rebuilds unless the dsc carries `DEBTRANSFORM-RELEASE` (§2.3) — a packaging decision we'd own either way. +- Upstream build deps are available: `dh-virtualenv` and `dh-python` exist in Debian 12 (packages.debian.org, checked 2026-09-03), so the ADR-0004 venv/lockfile design maps onto an OBS dsc without patching the build root. +- All five matrix targets exist as public OBS build roots: `Debian:12`, `Ubuntu:22.04`, `Ubuntu:24.04`, `Fedora:40`, `Fedora:41` — each project `_meta` answered HTTP 200 on build.opensuse.org (checked 2026-09-03). +- Live proof of deb publishing quality: `isv:ownCloud:desktop/Debian_10` on download.opensuse.org serves a proper Debian archive (`Release`, `Release.gpg`, `InRelease` all HTTP 200, checked 2026-09-03). + +### 2. Vendoring textual≥0.40 — the offline sandbox forces the same work we already planned + +- The build environment has **no network**: "services requiring external network access are likely to fail in [buildtime] mode, because such access is not available if the build workers are running in secure mode (as is always the case at https://build.opensuse.org)" (User Guide §7.2, "Modes of Source Services"); Dockerfile builds likewise run "in a safe build environment without network access" (§29.3). +- Vendoring must therefore happen **before** the build, via source services that run server-side on commit (`default`/`trylocal` modes, §7.2) or via files committed to the package. The standard services are per-file fetchers — `download_url` (§22.1.3), `download_files`, `obs_scm`/`tar`/`set_version` (§8 SCM integration) — there is no "pip resolve" service, so a pinned dependency tree like Fenris's means either N `download_url` entries mirroring the committed lockfile, or simply committing the vendored wheel/sdist tree. +- Conclusion: OBS does not remove the vendoring step; it reproduces ADR-0004's committed-lockfile design with extra XML. Since distro `python3-textual` is 0.1.13 on Debian 12, Ubuntu 22.04 and 24.04 (packages.debian.org / packages.ubuntu.com, checked 2026-09-03) — far below the `>=0.40` floor — vendoring is unavoidable on any route. + +### 3. Signing — OBS key, not ours; Gitea deb repo is our key + +- OBS signs published repositories with the **instance's** key: one signer per partition "calls an external tool to execute the signing" (User Guide §23 "OBS Architecture", Signer); consumers accept the OBS repo key ("When prompted, accept the GPG key of the download repository", §1.10). A build.opensuse.org user cannot upload a personal signing key. Trust therefore flows to openSUSE infra, and the signature says nothing about Fenris's maintainers. +- Gitea 1.27.1's Debian registry serves apt metadata signed with the Gitea instance's PGP key (`repository.key` endpoint, `signed-by` in sources.list — docs.gitea.com, "Debian Package Registry"), i.e. **our** host and **our** key. The RPM registry serves a `.repo` endpoint but documents no GPG signing of repodata; rpm-file signing stays our choice at build time. + +### 4. Publishing reach — OBS wins reach; reach is not our bottleneck + +- OBS publishes home-project results to `https://download.opensuse.org/repositories/home:USER/` (§1.10) and offers generated download pages on software.opensuse.org (§17.4). That is genuine CDN-class reach. +- Caveats from the same docs: branched projects are **not** published by default (§1.10), and the repo is a live view of the project state — no release artefact pinning; deleting the project or flag disables distribution. +- The Gitea route's reach is exactly `git.bongbetic.com` plus whatever the README says — adequate for a named four-distro matrix whose users follow our instructions, and it keeps the release artefact under versioned control on the same host as the source. + +### 5. Account and maintenance cost — strictly additive + +- Using build.opensuse.org requires an openSUSE account (single sign-on; the web UI's "Sign up!") and work happens in `home:USERNAME` plus permitted subprojects (§"Setting Up Your Home Project for the First Time"; §23 "OBS Concepts" on home projects). +- Day-to-day: `osc` + `_service` XML + dsc/spec recipes maintained in OBS's own package VCS, kept in sync with Fenris's git. The SCM bridge (`scmsync`) does support self-hosted Gitea ("We also support Self-Hosted instances from GitHub, GitLab and Gitea", §8.1.3; setup in §28.1.2 — build descriptions must live in the repo's top level), but it also disables OBS-side workflows (no `_link` merging, limited workflows, §28.1.1). +- No published quota/SLA for the public instance; capacity and availability are a shared commons. The Gitea route needs zero new accounts, zero new artefact formats beyond the two package recipes we must write anyway, and reuses the existing release host. + +### 6. Build latency — shared queue vs. deterministic local + +- OBS routes every commit through scheduler → dispatcher → shared workers; the dispatcher "tries to assign jobs fairly between the project repositories" using a per-repository load model (§23, Scheduler/Dispatcher). For our five tiny x86_64 jobs this is typically minutes, but there is no documented SLA and the queue is global — worst case is unbounded (estimate; the docs guarantee only fairness, not latency). +- The Gitea route builds wherever `make` runs and publishes with one authenticated `PUT` per artefact (docs.gitea.com: Debian `PUT .../pool/{distribution}/{component}/upload`; RPM `PUT .../rpm/{group}/upload`). Latency = build time, fully under our control. + +## Comparison on the 4-distro matrix + +| Axis | OBS (build.opensuse.org) | Gitea 1.27.1 registry | +|---|---|---| +| Debian 12 / Ubuntu 22.04/24.04 deb | dsc + dpkg-buildpackage; DEBTRANSFORM-RELEASE quirk | we build the same deb locally, upload via PUT | +| Fedora 40+ rpm | spec + rpmbuild in Fedora roots | same spec built locally, `.repo` grouping (`fedora/40`) | +| Vendoring textual≥0.40 | offline sandbox forces committed vendored tree (no pip service) | same committed vendored tree (ADR-0004 lockfile) | +| Signing | OBS instance key (not ours) | deb repo signed with our key; rpm repodata unsigned | +| Reach | download.opensuse.org CDN + software.o.o pages | our domain only | +| Accounts/infra | new openSUSE account, osc workflow, commons SLA-free | zero new infra | +| Latency | global shared queue, minutes typical, no SLA | deterministic (local build) | + +## Recommendation + +**Reject OBS as the build + distribution route for Fenris.** The offline sandbox forces the exact vendoring work the Gitea route already requires, so OBS adds cost (account, osc/source-service maintenance, external commons in the release path, queue latency) without removing any; its one real advantage — CDN and software.o.o reach — does not matter for a hobby project whose four target distros are served by one signed apt repo and one rpm repo on the existing Gitea host, under our own key. + +Revisit trigger: if Fenris later wants one-click installs via software.opensuse.org, architectures beyond x86_64, or many more distro targets — the deb publishing quality (verified live) and self-hosted-Gitea SCM bridge make OBS a viable amplifier then. + +## Sources + +- OBS User Guide (openbuildservice.org/help/manuals/obs-user-guide/, PDF): §2.3 Debian: Dsc; §7 Using Source Services (offline buildtime services, modes); §8.1.3 Supported SCMs; §17.4 download pages; §22.1.3 download_url; §23 OBS Architecture (Scheduler/Dispatcher/Signer); §25.1.3 Package Build; §28.1 SCM bridge; §29.3 Dockerfile builds (no network); §1.10 Installing Packages from OBS; "Configuration File Syntax" (BuildEngine, Repotype: debian). +- Live checks (2026-09-03): `Debian:12`/`Ubuntu:22.04`/`Ubuntu:24.04`/`Fedora:40`/`Fedora:41` project `_meta` on build.opensuse.org (all 200); `isv:ownCloud:desktop/Debian_10` `Release`/`Release.gpg`/`InRelease` on download.opensuse.org (all 200). +- packages.debian.org / packages.ubuntu.com (2026-09-03): `python3-textual` 0.1.13 on bookworm, jammy, noble; `dh-virtualenv`, `dh-python` present in bookworm. +- docs.gitea.com, "Debian Package Registry" and "RPM Package Registry" (1.27 line): apt sources with `signed-by` + `repository.key`, `PUT` upload endpoints, `.repo` groups. diff --git a/docs/spec/release-packaging.md b/docs/spec/release-packaging.md new file mode 100644 index 0000000..0ba3f3b --- /dev/null +++ b/docs/spec/release-packaging.md @@ -0,0 +1,105 @@ +# Fenris release and packaging specification + +**Status: decision-complete.** Assembled by [Task: Compose release spec + ADR amending 0004](https://git.bongbetic.com/xavierk/Fenris/issues/42) from the closed tickets of the Wayfinder map [Fenris deb + rpm release plan](https://git.bongbetic.com/xavierk/Fenris/issues/33). This document is normative for the follow-up **execution effort** that builds and publishes packages; no packages are built here. + +**Canonical roles.** [ADR 0007](../adr/0007-package-delivery-amends-0004.md) records the lifecycle rationale (amending [ADR 0004](../adr/0004-install-upgrade-removal-lifecycle.md)); 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](../adr/) and the [redesign specification](fenris-redesign.md) verbatim; nothing here overrides them. Terminology follows the glossary in [`CONTEXT.md`](../../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 | + +- Architecture: **x86_64 only** (arm64 only if real ARM hardware appears — map fog). +- Dependencies are vendored in a bundled venv for every target: Debian 12 and Ubuntu 22.04/24.04 ship `python3-textual` 0.1.13, far below the floor; Fedora 40+ ships ≥ 0.48 but below the pin ([toolchain research](../research/deb-rpm-toolchain.md)). No distro `python3-textual` dependency ever enters package metadata. +- Package metadata `depends:`/`Requires:` are exactly `python3 (>= 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 install` keeps the checkout's floor. +- The bundled venv is staged with `python3 -m venv --copies` at `/opt/fenris`: shebangs point at the fixed absolute `/opt/fenris/bin/python`, and the stdlib still comes from the host interpreter, which is why `python3 (>= 3.10)` is a hard dependency. + +## 2. Distribution channel + +- **Channel:** the self-hosted Gitea 1.27.1 package registry at `git.bongbetic.com`, owner public for anonymous consumers ([registry research](../research/gitea-package-registry.md); [OBS rejected](../research/obs-route.md)). +- **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 `fenris` group — `PUT /api/packages/{owner}/rpm/fenris/upload` — serving Fedora 40+ collectively. +- **Consumer setup (install docs, normative):** + - apt: keyring file from `…/debian/repository.key` via `signed-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 ` — the in-repo, Fenris-owned `.repo` with `gpgkey` pointing at the published packaging key and `repo_gpgcheck=0`. **Gitea's auto-generated `.repo` is never mentioned in docs**: it sets `gpgcheck=1` against the instance auto-key, which never signed our rpm payload — a trap that breaks installs. + +## 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 (venv `--copies` → `/opt/fenris` tree, plus wrapper, helpers, units, polkit policy, sysusers/tmpfiles fragments) referenced by `nfpm.yaml` as a `type: tree` content entry — no hand-maintained file lists. +- **Entry point:** `make package` → `dist/fenris__amd64.deb` + `dist/fenris--1.x86_64.rpm`. +- **Version scheme:** `-1` in 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](../research/deb-rpm-toolchain.md) sketches the pipeline. + +## 4. Signing and key policy + +- **RPM payload: signed.** rpmsign with the dedicated packaging key, wired through the nfpm config. 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 `, 2-year expiry, no master/subkey hierarchy (single maintainer, manual builds). Private key lives in the password manager only; each release does import → sign → delete — nothing permanent on any build host. +- **Public key publication:** in-repo `packaging/keys/fenris-packaging.asc` (raw URL doubles as the `.repo` gpgkey target), release notes, docs page. No keyservers — TOFU-over-TLS. +- **Rotation (outline):** new key published alongside old; rpm signed with the new key; `fenris.repo` gpgkey 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:** tag → `make release` (manual: `make package` + rpmsign + registry PUTs + attach `.deb`, `.rpm`, `SHA256SUMS` to the release entry). +- **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:** no runners are registered on the instance today ([Actions runner research](https://git.bongbetic.com/xavierk/Fenris/issues/37)), so the manual flow above is primary. A dormant `.gitea/workflows/release.yml` (`on: push: tags: ['v*']`, single job, host-mode runner) is committed alongside; if it fires, it replicates `make release`. Cheapest future upgrade: one `act_runner` static binary in host-label mode on the existing Gitea host. + +## 6. Package layout and ownership + +Per [ADR 0007](../adr/0007-package-delivery-amends-0004.md) §2 — the dpkg/rpm database is the manifest; no `manifest.txt` ships: + +| Artifact | Location | Ownership | +|---|---|---| +| Bundled venv | `/opt/fenris` | 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.txt` **or** `/etc/systemd/system/fenris-collect.timer` exists (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 inline `/opt/fenris/bin/python3 -c "…migrate_to_latest…"` → `daemon-reload` → restart `fenris-collect.timer` only if unit contents changed **and** it is active. `/var/lib/fenris` is never rebuilt; an in-flight oneshot finishes on its old interpreter. +- **prerm / %preun:** sanctioned disable (`fenris-monitor disable --now`, closing the monitoring 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`. +- **Removal mapping:** deb `remove` ≈ `make uninstall` (conffile + store survive); deb `purge` ≈ `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](../adr/0003-service-lifecycle-and-sanctioned-toggle.md) §3). The shipped default is placeholder-commented and carries no active selector — a fresh install reads as a `configuration error`-free dormant system until the first `fenris 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](https://git.bongbetic.com/xavierk/Fenris/issues/40): nothing in any delivery writes the selector — `make install` never wrote `fenris.conf` either; 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/fenris` shadows `/usr/bin/fenris` on PATH. +- **No-move continuity:** `/var/lib/fenris` untouched; existing `fenris` group → sysusers no-op; existing dir → tmpfiles no-op; hand-written `fenris.conf` survives (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 period `user_disabled`; after migration the user opts back in with `fenris monitor resume` — one ≤15-min sample gap, honest against the endurance timeline. +- **Mutual exclusion:** package and `make install` never 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](https://git.bongbetic.com/xavierk/Fenris/issues/38), [Signing + key policy](https://git.bongbetic.com/xavierk/Fenris/issues/39), [Package ownership + ADR 0004 amendment](https://git.bongbetic.com/xavierk/Fenris/issues/40), [Migration path from make-install systems to packages](https://git.bongbetic.com/xavierk/Fenris/issues/41), [Release cadence + stable/testing channel split](https://git.bongbetic.com/xavierk/Fenris/issues/43), [Actions runner availability](https://git.bongbetic.com/xavierk/Fenris/issues/37), and the research tickets [deb + rpm packaging toolchain](https://git.bongbetic.com/xavierk/Fenris/issues/34), [Gitea 1.27 package registry feasibility](https://git.bongbetic.com/xavierk/Fenris/issues/35), [OBS route](https://git.bongbetic.com/xavierk/Fenris/issues/36).*