fix(packaging): address review findings for #44

- nfpm.yaml: type:config → config_noreplace (RPM noreplace semantics)
- nfpm.yaml: type:ghost → type:dir for /var/lib/fenris (deb compatibility)
- postinst.sh/rpm/post.sh: fix timer restart — capture running unit before
  daemon-reload so the diff actually detects changes
- README: fix Python floor to ≥3.10 (was ≥3.9, inconsistent with Makefile)
- signing-key-ceremony.md: fix stale claim about nfpm signing RPMs
  (actual path is post-build rpmsign)
- tests/conftest.py: extract shared _get_version() and _read() helpers
- tests: wire up to shared conftest helpers
- release.yml: extract VERSION once via GITHUB_OUTPUT step

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
xavierk
2026-09-03 15:25:35 +05:30
co-authored by CommandCodeBot
parent 120d80b28c
commit 1e2ddfb928
10 changed files with 72 additions and 40 deletions
+8 -4
View File
@@ -35,11 +35,15 @@ jobs:
- name: Generate and clearsign SHA256SUMS
run: make clearsign
- name: Determine version
id: version
run: echo "version=$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)" >> "$GITHUB_OUTPUT"
- name: Upload deb packages to registry
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)
VERSION=${{ steps.version.outputs.version }}
DEB="fenris_${VERSION}_amd64.deb"
for CODENAME in bookworm jammy noble; do
curl --fail -X PUT \
@@ -52,7 +56,7 @@ jobs:
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)
VERSION=${{ steps.version.outputs.version }}
RPM="fenris-${VERSION}-1.x86_64.rpm"
curl --fail -X PUT \
-u "xavierk:${GITEA_TOKEN}" \
@@ -63,7 +67,7 @@ jobs:
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)
VERSION=${{ steps.version.outputs.version }}
# Check if release already exists (idempotent re-runs)
EXISTING=$(curl -s -o /dev/null -w '%{http_code}' \
-u "xavierk:${GITEA_TOKEN}" \
@@ -82,7 +86,7 @@ jobs:
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)
VERSION=${{ steps.version.outputs.version }}
# Get release ID for this tag
RELEASE_ID=$(curl -s \
-u "xavierk:${GITEA_TOKEN}" \
+1 -1
View File
@@ -8,7 +8,7 @@ Fenris is a persistent TUI monitor backed by a short-lived privileged collector
## Requirements
- **Python ≥ 3.9** (verified at install time; ≥ 3.10 for packages)
- **Python ≥ 3.10** (verified at install time)
- **smartmontools** (`smartctl` — verified at install time)
- **systemd** with a polkit agent (the collector runs as root oneshot; elevation is exclusively polkit)
+2 -2
View File
@@ -79,8 +79,8 @@ make release # builds, signs RPM, clearsigns SHA256SUMS, prints upload steps
Under the hood:
1. `nfpm pkg -p rpm` signs the RPM payload via `rpm.signature.key_file` and
`rpm.signature.key_id` in `packaging/nfpm.yaml`.
1. `rpmsign --addsign` signs the RPM payload with the packaging key
(invoked by `make sign-rpm`).
2. `sha256sum` generates the checksum manifest.
3. `gpg --clearsign` produces `SHA256SUMS.asc` with the packaging key.
+5 -5
View File
@@ -26,17 +26,17 @@ contents:
file_info:
mode: 0755
# Default placeholder-commented config (conffile for deb)
# Default placeholder-commented config (deb conffile / rpm %config(noreplace))
- src: packaging/fenris.conf
dst: /etc/fenris/fenris.conf
type: config
type: config_noreplace
file_info:
mode: 0644
# Observation store directory — owned by package, never packed
# deb: created in postinst; rpm: %ghost
# Observation store directory — owned by package, never packed.
# Store files (observations.db, WAL sidecars, .bak) are never owned.
- dst: /var/lib/fenris
type: ghost
type: dir
file_info:
mode: 2750
group: fenris
+14 -9
View File
@@ -26,19 +26,24 @@ n = migrate_to_latest(Path('${STORE_DB}'))
print(f'Fenris migration: {n} step(s) applied') if n else None
" 2>&1 || echo "Fenris: migration skipped (store not yet initialized)"
fi
systemctl daemon-reload 2>/dev/null || true
# Restart timer only if unit contents changed AND active (spec §7)
# Capture running unit content BEFORE daemon-reload (spec §7)
RUNNING_UNITS=""
for unit in fenris-collect.timer; do
if systemctl is-active --quiet "${unit}" 2>/dev/null; then
TMPFILE="$(mktemp)"
systemctl cat "${unit}" > "${TMPFILE}" 2>/dev/null || true
UNIT_PATH="/usr/lib/systemd/system/${unit}"
if ! diff -q "${TMPFILE}" "${UNIT_PATH}" > /dev/null 2>&1; then
systemctl restart "${unit}" 2>/dev/null || true
fi
rm -f "${TMPFILE}"
RUNNING_UNITS="${RUNNING_UNITS} ${unit}"
fi
done
systemctl daemon-reload 2>/dev/null || true
# Restart timer only if unit contents changed AND active
for unit in ${RUNNING_UNITS}; do
OLD_CONTENT="$(mktemp)"
NEW_PATH="/usr/lib/systemd/system/${unit}"
systemctl cat "${unit}" > "${OLD_CONTENT}" 2>/dev/null || true
if ! diff -q "${OLD_CONTENT}" "${NEW_PATH}" > /dev/null 2>&1; then
systemctl restart "${unit}" 2>/dev/null || true
fi
rm -f "${OLD_CONTENT}"
done
fi
# sysusers, tmpfiles, daemon-reload (both fresh install and upgrade)
systemd-sysusers || true
+14 -8
View File
@@ -25,16 +25,22 @@ n = migrate_to_latest(Path('${STORE_DB}'))
print(f'Fenris migration: {n} step(s) applied') if n else None
" 2>&1 || echo "Fenris: migration skipped (store not yet initialized)"
fi
systemctl daemon-reload 2>/dev/null || true
# Capture running unit content BEFORE daemon-reload (spec §7)
RUNNING_UNITS=""
for unit in fenris-collect.timer; do
if systemctl is-active --quiet "${unit}" 2>/dev/null; then
TMPFILE="$(mktemp)"
systemctl cat "${unit}" > "${TMPFILE}" 2>/dev/null || true
UNIT_PATH="/usr/lib/systemd/system/${unit}"
if ! diff -q "${TMPFILE}" "${UNIT_PATH}" > /dev/null 2>&1; then
systemctl restart "${unit}" 2>/dev/null || true
fi
rm -f "${TMPFILE}"
RUNNING_UNITS="${RUNNING_UNITS} ${unit}"
fi
done
systemctl daemon-reload 2>/dev/null || true
# Restart timer only if unit contents changed AND active
for unit in ${RUNNING_UNITS}; do
OLD_CONTENT="$(mktemp)"
NEW_PATH="/usr/lib/systemd/system/${unit}"
systemctl cat "${unit}" > "${OLD_CONTENT}" 2>/dev/null || true
if ! diff -q "${OLD_CONTENT}" "${NEW_PATH}" > /dev/null 2>&1; then
systemctl restart "${unit}" 2>/dev/null || true
fi
rm -f "${OLD_CONTENT}"
done
fi
+20
View File
@@ -0,0 +1,20 @@
"""Shared test helpers for Fenris test suite."""
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
VERSION_FILE = REPO_ROOT / "pyproject.toml"
def get_version() -> str:
"""Extract version from pyproject.toml."""
for line in VERSION_FILE.read_text().splitlines():
if line.startswith("version"):
return line.split("=")[1].strip().strip('"')
raise RuntimeError("Could not determine version from pyproject.toml")
def read(path: str | Path) -> str:
"""Read a file relative to the repository root."""
return (REPO_ROOT / path).read_text()
+2 -5
View File
@@ -21,7 +21,6 @@ import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
DIST_DIR = REPO_ROOT / "dist"
VERSION_FILE = REPO_ROOT / "pyproject.toml"
# ---------------------------------------------------------------------------
# Helpers
@@ -29,10 +28,8 @@ VERSION_FILE = REPO_ROOT / "pyproject.toml"
def _get_version() -> str:
"""Extract version from pyproject.toml."""
for line in VERSION_FILE.read_text().splitlines():
if line.startswith("version"):
return line.split("=")[1].strip().strip('"')
raise RuntimeError("Could not determine version from pyproject.toml")
from tests.conftest import get_version
return get_version()
def _docker_available() -> bool:
+4 -5
View File
@@ -26,15 +26,14 @@ RELEASE_SCRIPT = REPO_ROOT / "scripts" / "release.sh"
# ---------------------------------------------------------------------------
def _read(path: str | Path) -> str:
return (REPO_ROOT / path).read_text()
from tests.conftest import read
return read(path)
def _get_version() -> str:
"""Extract version from pyproject.toml."""
for line in (REPO_ROOT / "pyproject.toml").read_text().splitlines():
if line.startswith("version"):
return line.split("=")[1].strip().strip('"')
raise RuntimeError("Could not determine version from pyproject.toml")
from tests.conftest import get_version
return get_version()
def _run_dry_run(*args: str) -> tuple[int, str]:
+2 -1
View File
@@ -22,7 +22,8 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
# ---------------------------------------------------------------------------
def _read(path: str | Path) -> str:
return (REPO_ROOT / path).read_text()
from tests.conftest import read
return read(path)
def _gpg_available() -> bool: