diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 2bccd5c..c7a4bf9 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -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}" \ diff --git a/README.md b/README.md index 555ad6e..ffac762 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/install/signing-key-ceremony.md b/docs/install/signing-key-ceremony.md index 679714f..59c5dd0 100644 --- a/docs/install/signing-key-ceremony.md +++ b/docs/install/signing-key-ceremony.md @@ -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. diff --git a/packaging/nfpm.yaml b/packaging/nfpm.yaml index 756e371..8f9ce5e 100644 --- a/packaging/nfpm.yaml +++ b/packaging/nfpm.yaml @@ -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 diff --git a/packaging/postinst.sh b/packaging/postinst.sh index 1dd1989..0c0f3f5 100755 --- a/packaging/postinst.sh +++ b/packaging/postinst.sh @@ -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 diff --git a/packaging/rpm/post.sh b/packaging/rpm/post.sh index b6092dd..7ee8bbb 100755 --- a/packaging/rpm/post.sh +++ b/packaging/rpm/post.sh @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..c1661ee --- /dev/null +++ b/tests/conftest.py @@ -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() diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 74b26fb..3d00bfd 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -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: diff --git a/tests/test_release.py b/tests/test_release.py index 87b12eb..3d08a5d 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -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]: diff --git a/tests/test_signing.py b/tests/test_signing.py index f7fce95..407bbfa 100644 --- a/tests/test_signing.py +++ b/tests/test_signing.py @@ -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: