Document packaging and release artifacts

This commit is contained in:
Luna 2026-06-17 00:22:43 -07:00
parent aee984d862
commit e0e5cd62d9
14 changed files with 470 additions and 13 deletions

View file

@ -85,6 +85,7 @@ Start with:
- `docs/ROADMAP.md` for sequenced work.
- `docs/COMMAND_REFERENCE.md` for CLI contracts.
- `docs/OPERATIONS.md` and `docs/RUNBOOKS.md` for operator workflows.
- `docs/PACKAGING.md` for package layout and distro install paths.
- `docs/THREAT_MODEL.md` for security boundaries and non-goals.
## Engineering Rules
@ -180,5 +181,8 @@ When changing behavior, update the relevant set:
- `docs/THREAT_MODEL.md` for trust-boundary/security-model changes.
- `docs/ROADMAP.md` for planned or completed roadmap movement.
- `docs/SCHEMAS.md` for stable JSON contracts.
- `docs/PACKAGING.md` for package/install behavior.
- `packaging/release-artifacts.sh` for release tarballs, checksums, and optional
GPG signatures.
- `docs/VERSION.json` and `docs/INDEX.md` for documentation versioning.
- `config/enodia-sentinel.toml` for config knobs.

View file

@ -4,8 +4,10 @@ LIBDIR := $(PREFIX)/lib/enodia-sentinel
SYSTEMDDIR := /etc/systemd/system
CONFDIR := /etc
LOGDIR := /var/log/enodia-sentinel
DOCDIR := $(PREFIX)/share/doc/enodia-sentinel
TMPFILESDIR := /etc/tmpfiles.d
.PHONY: install uninstall enable disable status logs check baseline drill test clean
.PHONY: install uninstall enable disable status logs check baseline drill test release-artifacts clean
# Installs the stdlib-only Python package as a plain directory + launcher
# wrapper (no pip, no virtualenv, no site-packages). Zero runtime deps.
@ -17,13 +19,27 @@ install:
install -Dm644 systemd/enodia-sentinel.service $(DESTDIR)$(SYSTEMDDIR)/enodia-sentinel.service
install -Dm644 systemd/enodia-sentinel-web.service $(DESTDIR)$(SYSTEMDDIR)/enodia-sentinel-web.service
install -Dm644 packaging/enodia-sentinel-fim.hook $(DESTDIR)/etc/pacman.d/hooks/enodia-sentinel-fim.hook
install -Dm644 packaging/enodia-sentinel.tmpfiles $(DESTDIR)$(TMPFILESDIR)/enodia-sentinel.conf
@if [ ! -e "$(DESTDIR)$(CONFDIR)/enodia-sentinel.toml" ]; then \
install -Dm644 config/enodia-sentinel.toml $(DESTDIR)$(CONFDIR)/enodia-sentinel.toml; \
echo "Installed default config at $(CONFDIR)/enodia-sentinel.toml"; \
echo "Installed default config at $(DESTDIR)$(CONFDIR)/enodia-sentinel.toml"; \
else \
install -Dm644 config/enodia-sentinel.toml $(DESTDIR)$(CONFDIR)/enodia-sentinel.toml.new; \
echo "Existing config preserved; new template at $(CONFDIR)/enodia-sentinel.toml.new"; \
echo "Existing config preserved; new template at $(DESTDIR)$(CONFDIR)/enodia-sentinel.toml.new"; \
fi
install -Dm644 README.md $(DESTDIR)$(DOCDIR)/README.md
install -Dm644 docs/INDEX.md $(DESTDIR)$(DOCDIR)/INDEX.md
install -Dm644 docs/COMMAND_REFERENCE.md $(DESTDIR)$(DOCDIR)/COMMAND_REFERENCE.md
install -Dm644 docs/SCHEMAS.md $(DESTDIR)$(DOCDIR)/SCHEMAS.md
install -Dm644 docs/PACKAGING.md $(DESTDIR)$(DOCDIR)/PACKAGING.md
install -Dm644 docs/OPERATIONS.md $(DESTDIR)$(DOCDIR)/OPERATIONS.md
install -Dm644 docs/RUNBOOKS.md $(DESTDIR)$(DOCDIR)/RUNBOOKS.md
install -Dm644 docs/RULES.md $(DESTDIR)$(DOCDIR)/RULES.md
install -Dm644 docs/SPECIFICATION.md $(DESTDIR)$(DOCDIR)/SPECIFICATION.md
install -Dm644 docs/ROADMAP.md $(DESTDIR)$(DOCDIR)/ROADMAP.md
install -Dm644 docs/THREAT_MODEL.md $(DESTDIR)$(DOCDIR)/THREAT_MODEL.md
install -Dm644 docs/VERSION.json $(DESTDIR)$(DOCDIR)/VERSION.json
install -Dm644 systemd/enodia-sentinel-ebpf.conf $(DESTDIR)$(DOCDIR)/examples/enodia-sentinel-ebpf.conf
install -dm750 $(DESTDIR)$(LOGDIR)
@echo "Installed. Run 'sudo make enable' to start the service."
@ -34,7 +50,9 @@ uninstall:
rm -f $(DESTDIR)$(SYSTEMDDIR)/enodia-sentinel.service
rm -f $(DESTDIR)$(SYSTEMDDIR)/enodia-sentinel-web.service
rm -f $(DESTDIR)/etc/pacman.d/hooks/enodia-sentinel-fim.hook
rm -f $(DESTDIR)$(TMPFILESDIR)/enodia-sentinel.conf
rm -f $(DESTDIR)$(CONFDIR)/enodia-sentinel.toml.new
rm -rf $(DESTDIR)$(DOCDIR)
@echo "Uninstalled. Config and logs preserved."
enable:
@ -66,5 +84,8 @@ web:
drill:
./src/sentinel-redteam
release-artifacts:
packaging/release-artifacts.sh
clean:
find . -type d -name __pycache__ -prune -exec rm -rf {} +

View file

@ -43,6 +43,8 @@ Project docs:
response, fleet, and assurance layers.
- [Operations guide](docs/OPERATIONS.md) — install checks, health checks, alert
workflow, baseline hygiene, and evidence export.
- [Packaging guide](docs/PACKAGING.md) — Arch package layout plus Debian/RPM
source-install paths.
- [JSON schemas](docs/SCHEMAS.md) — stable v1 contracts for alerts, incidents,
status, response plans, and response audit records.
- [IR runbooks](docs/RUNBOOKS.md) — confirm/preserve/contain/recover playbooks

View file

@ -15,6 +15,7 @@ same patch as any command, schema, or behavior documentation changes.
| [COMMAND_REFERENCE.md](COMMAND_REFERENCE.md) | Manpage-style CLI and service command contract. |
| [SCHEMAS.md](SCHEMAS.md) | Stable JSON schema IDs and required v1 fields. |
| [OPERATIONS.md](OPERATIONS.md) | Install checks, health checks, alert workflow, baseline hygiene, and evidence export. |
| [PACKAGING.md](PACKAGING.md) | Arch package layout and Debian/RPM source-install paths. |
| [RUNBOOKS.md](RUNBOOKS.md) | Incident response playbooks for common Sentinel findings. |
| [RULES.md](RULES.md) | Generated built-in event-rule reference by SID. |

153
docs/PACKAGING.md Normal file
View file

@ -0,0 +1,153 @@
# Enodia Sentinel Packaging
Applies to package version: `0.7.0`
Docs version: `0.7`
Schema generation: `v1`
Sentinel is a stdlib-only Python package installed as a plain source directory
plus launcher wrapper. The service and package are designed around Linux hosts
with systemd.
## Arch Linux
Arch is the primary packaged target.
Build from the repository root:
```bash
packaging/build-package.sh
```
Install the generated package:
```bash
sudo pacman -U enodia-sentinel-*.pkg.tar.zst
sudo systemctl daemon-reload
sudo systemctl enable --now enodia-sentinel.service
```
The Arch package installs:
| Path | Purpose |
|---|---|
| `/usr/bin/enodia-sentinel` | Launcher wrapper for the Python package. |
| `/usr/bin/sentinel-redteam` | Safe local red-team harness. |
| `/usr/lib/enodia-sentinel/enodia_sentinel/` | Stdlib-only Python package. |
| `/etc/enodia-sentinel.toml` | Operator configuration, preserved as a pacman backup file. |
| `/usr/lib/systemd/system/enodia-sentinel.service` | Hardened daemon unit. |
| `/usr/lib/systemd/system/enodia-sentinel-web.service` | Read-only dashboard unit. |
| `/usr/share/libalpm/hooks/enodia-sentinel-fim.hook` | Post-transaction FIM baseline refresh hook. |
| `/usr/lib/tmpfiles.d/enodia-sentinel.conf` | Runtime log directory definition. |
| `/usr/share/doc/enodia-sentinel/` | Versioned operator docs and examples. |
| `/var/log/enodia-sentinel/` | Alert snapshots, events, incidents, response plans, and audit logs. |
The pacman hook calls `/usr/bin/enodia-sentinel fim-update` after package
transactions so legitimate package changes refresh the FIM baseline. It does
not silence unexplained non-package drift.
Optional Arch packages:
- `python-bpfcc` for eBPF exec/syscall telemetry.
- `bpftrace` for short-lived exec capture inside forensic snapshots.
- `libnotify` for local desktop notifications.
- `libarchive` for signed-cache-package `.MTREE` verification helpers.
The default service is hardened for polling mode. To enable eBPF, copy the
packaged drop-in example and reload systemd:
```bash
sudo install -Dm644 /usr/share/doc/enodia-sentinel/examples/enodia-sentinel-ebpf.conf \
/etc/systemd/system/enodia-sentinel.service.d/ebpf.conf
sudo systemctl daemon-reload
sudo systemctl restart enodia-sentinel.service
```
## Debian and Ubuntu
There is no native `.deb` package yet. Use the source install path:
```bash
sudo apt install python3 iproute2 procps
sudo make install PREFIX=/usr/local
sudo systemctl daemon-reload
sudo systemctl enable --now enodia-sentinel.service
```
Notes:
- The installed launcher does not require pip or a virtualenv.
- `fim-check --packages` uses the available package-manager backend; Debian
package verification support should be validated for the target host before
relying on it operationally.
- The source install path includes the pacman hook file for parity with Arch
source installs. On non-Arch systems it is inert unless pacman/libalpm is
installed; Debian installs should manage FIM baseline refresh through
operator procedure or a future dpkg trigger package.
## RPM-family Linux
There is no native RPM package yet. Use the same source install path:
```bash
sudo dnf install python3 iproute procps-ng
sudo make install PREFIX=/usr/local
sudo systemctl daemon-reload
sudo systemctl enable --now enodia-sentinel.service
```
Notes:
- Native RPM packaging should eventually install systemd units under the distro
unit path, preserve `/etc/enodia-sentinel.toml`, and create a package-manager
trigger equivalent to the Arch FIM refresh hook.
- Validate package verification behavior on the target distro before enabling
signed-package mismatch alerting.
## Package Hardening Checklist
- Package version matches `pyproject.toml` and `enodia_sentinel.__version__`.
- Config is preserved across upgrades.
- Log directory is created with mode `0750`.
- Service units remain hardened and read-mostly by default.
- eBPF privilege relaxation is shipped only as an opt-in drop-in example.
- Package-manager hooks use absolute paths and do not depend on ambient `PATH`.
- Versioned operator docs are installed with the package.
## Release Artifacts
Release artifacts are built with:
```bash
make release-artifacts
```
The release target writes to `dist/` by default:
| Artifact | Purpose |
|---|---|
| `enodia-sentinel-<version>.tar.gz` | Versioned source release. |
| `SHA256SUMS` | SHA-256 checksums for the source tarball and release manifest. |
| `RELEASE_MANIFEST.json` | Project/version/git metadata for the generated release. |
| `SHA256SUMS.asc` | Optional detached GPG signature over `SHA256SUMS`. |
The script refuses a dirty git worktree by default so a release maps to a known
commit. For local packaging smoke tests only, set `RELEASE_ALLOW_DIRTY=1`.
To sign checksum artifacts:
```bash
RELEASE_SIGN=1 make release-artifacts
```
To select a specific GPG key:
```bash
RELEASE_SIGN=1 RELEASE_SIGNING_KEY=<key-id-or-fingerprint> make release-artifacts
```
Verification flow for operators:
```bash
gpg --verify SHA256SUMS.asc SHA256SUMS
sha256sum -c SHA256SUMS
```

View file

@ -93,8 +93,8 @@ Purpose: define a stable single-host product.
audits.
- ✅ Add compatibility tests for schema evolution.
- ✅ Add documentation versioning and manpage-style command reference.
- Harden packaging for Arch first, then document Debian/RPM install paths.
- Add signed release artifacts and checksums.
- Harden packaging for Arch first, then document Debian/RPM install paths.
- Add signed release artifacts and checksums.
- Improve dashboard from alert browser to local console:
incidents, posture, integrity state, response plans, and watchdog status.
- ✅ Add a local dashboard Settings menu with persistent themes for dark

View file

@ -4,5 +4,7 @@
"docs_version": "0.7",
"schema_generation": "v1",
"command_reference": "docs/COMMAND_REFERENCE.md",
"schema_reference": "docs/SCHEMAS.md"
"schema_reference": "docs/SCHEMAS.md",
"packaging_reference": "docs/PACKAGING.md",
"release_artifact_target": "make release-artifacts"
}

View file

@ -1,6 +1,6 @@
# Maintainer: Enodia
pkgname=enodia-sentinel
pkgver=0.6.0
pkgver=0.7.0
pkgrel=1
pkgdesc="Linux IDS/IPS/EDR agent with integrity, anti-rootkit, forensic evidence, and explicit prevention workflows"
arch=('any')
@ -9,7 +9,8 @@ license=('GPL-3.0-or-later')
depends=('python>=3.11' 'iproute2' 'procps-ng')
optdepends=('python-bpfcc: eBPF event-driven execve/syscall monitors (catches short-lived processes and memory telemetry)'
'bpftrace: execve tracing of short-lived processes in snapshots'
'libnotify: desktop notifications on alert')
'libnotify: desktop notifications on alert'
'libarchive: signed-package verification against cached package .MTREE manifests')
backup=('etc/enodia-sentinel.toml')
source=()
sha256sums=()
@ -24,7 +25,21 @@ package() {
install -Dm644 systemd/enodia-sentinel.service "$pkgdir/usr/lib/systemd/system/enodia-sentinel.service"
install -Dm644 systemd/enodia-sentinel-web.service "$pkgdir/usr/lib/systemd/system/enodia-sentinel-web.service"
install -Dm644 packaging/enodia-sentinel-fim.hook "$pkgdir/usr/share/libalpm/hooks/enodia-sentinel-fim.hook"
install -Dm644 packaging/enodia-sentinel.tmpfiles "$pkgdir/usr/lib/tmpfiles.d/enodia-sentinel.conf"
install -Dm644 config/enodia-sentinel.toml "$pkgdir/etc/enodia-sentinel.toml"
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
install -Dm644 docs/INDEX.md "$pkgdir/usr/share/doc/$pkgname/INDEX.md"
install -Dm644 docs/COMMAND_REFERENCE.md "$pkgdir/usr/share/doc/$pkgname/COMMAND_REFERENCE.md"
install -Dm644 docs/SCHEMAS.md "$pkgdir/usr/share/doc/$pkgname/SCHEMAS.md"
install -Dm644 docs/PACKAGING.md "$pkgdir/usr/share/doc/$pkgname/PACKAGING.md"
install -Dm644 docs/OPERATIONS.md "$pkgdir/usr/share/doc/$pkgname/OPERATIONS.md"
install -Dm644 docs/RUNBOOKS.md "$pkgdir/usr/share/doc/$pkgname/RUNBOOKS.md"
install -Dm644 docs/RULES.md "$pkgdir/usr/share/doc/$pkgname/RULES.md"
install -Dm644 docs/SPECIFICATION.md "$pkgdir/usr/share/doc/$pkgname/SPECIFICATION.md"
install -Dm644 docs/ROADMAP.md "$pkgdir/usr/share/doc/$pkgname/ROADMAP.md"
install -Dm644 docs/THREAT_MODEL.md "$pkgdir/usr/share/doc/$pkgname/THREAT_MODEL.md"
install -Dm644 docs/VERSION.json "$pkgdir/usr/share/doc/$pkgname/VERSION.json"
install -Dm644 systemd/enodia-sentinel-ebpf.conf "$pkgdir/usr/share/doc/$pkgname/examples/enodia-sentinel-ebpf.conf"
install -dm750 "$pkgdir/var/log/enodia-sentinel"
}

View file

@ -1,9 +1,14 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-3.0-or-later
# Build the Arch package from the repo root.
set -e
set -eu
cd "$(dirname "$0")"
command -v makepkg >/dev/null 2>&1 || {
echo "error: makepkg not found; install pacman/base-devel tooling first" >&2
exit 127
}
echo "Building enodia-sentinel package..."
makepkg -f --noconfirm
umask 022
makepkg -f --cleanbuild --noconfirm
mv -f *.pkg.tar.zst ../ 2>/dev/null || true
echo "Done. Install with: sudo pacman -U enodia-sentinel-*.pkg.tar.zst"

View file

@ -1,6 +1,6 @@
# Refresh the Enodia Sentinel file-integrity baseline after every package
# transaction, so legitimate package changes never trigger a FIM alert.
# Install as: /etc/pacman.d/hooks/enodia-sentinel-fim.hook
# Packaged as: /usr/share/libalpm/hooks/enodia-sentinel-fim.hook
[Trigger]
Operation = Install
Operation = Upgrade
@ -14,5 +14,5 @@ Target = opt/*
[Action]
Description = Refreshing Enodia Sentinel file-integrity baseline...
When = PostTransaction
Exec = /usr/bin/env enodia-sentinel fim-update
Exec = /usr/bin/enodia-sentinel fim-update
Depends = python

View file

@ -0,0 +1,3 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# Runtime log/state directory for Enodia Sentinel.
d /var/log/enodia-sentinel 0750 root root -

103
packaging/release-artifacts.sh Executable file
View file

@ -0,0 +1,103 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-3.0-or-later
# Build release artifacts: source tarball, checksum file, manifest, optional
# detached signature over SHA256SUMS.
set -euo pipefail
cd "$(dirname "$0")/.."
version="$(python3 - <<'PY'
import tomllib
from pathlib import Path
print(tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"])
PY
)"
project="enodia-sentinel"
prefix="${project}-${version}"
distdir="${DISTDIR:-dist}"
archive="${prefix}.tar.gz"
manifest="RELEASE_MANIFEST.json"
checksums="SHA256SUMS"
signature="SHA256SUMS.asc"
allow_dirty="${RELEASE_ALLOW_DIRTY:-0}"
sign="${RELEASE_SIGN:-0}"
signing_key="${RELEASE_SIGNING_KEY:-}"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
dirty=0
git diff --quiet || dirty=1
git diff --cached --quiet || dirty=1
if [ -n "$(git ls-files --others --exclude-standard)" ]; then
dirty=1
fi
git_commit="$(git rev-parse --verify HEAD)"
if [ "$dirty" -ne 0 ] && [ "$allow_dirty" != "1" ]; then
echo "error: worktree is dirty; commit changes or set RELEASE_ALLOW_DIRTY=1" >&2
exit 2
fi
else
dirty=1
git_commit=""
fi
mkdir -p "$distdir"
tmp="$(mktemp -d "${TMPDIR:-/tmp}/enodia-release.XXXXXX")"
cleanup() {
rm -rf "$tmp"
}
trap cleanup EXIT
mkdir -p "$tmp/$prefix"
tar \
--exclude='./.git' \
--exclude='./dist' \
--exclude='./__pycache__' \
--exclude='*/__pycache__' \
--exclude='./.pytest_cache' \
--exclude='./.mypy_cache' \
--exclude='./.ruff_cache' \
--exclude='*.pyc' \
--exclude='*.pkg.tar.zst' \
-cf - . | tar -xf - -C "$tmp/$prefix"
tar --sort=name --mtime='@0' --owner=0 --group=0 --numeric-owner \
-czf "$distdir/$archive" -C "$tmp" "$prefix"
generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
cat > "$distdir/$manifest" <<JSON
{
"project": "$project",
"version": "$version",
"generated_at": "$generated_at",
"git_commit": "$git_commit",
"dirty": $dirty,
"artifacts": [
"$archive",
"$checksums"
]
}
JSON
(
cd "$distdir"
sha256sum "$archive" "$manifest" > "$checksums"
)
if [ "$sign" = "1" ]; then
command -v gpg >/dev/null 2>&1 || {
echo "error: RELEASE_SIGN=1 requires gpg" >&2
exit 127
}
gpg_args=(--batch --yes --armor --detach-sign)
if [ -n "$signing_key" ]; then
gpg_args+=(--local-user "$signing_key")
fi
gpg "${gpg_args[@]}" -o "$distdir/$signature" "$distdir/$checksums"
echo "Signed $distdir/$signature"
else
echo "Signing skipped; set RELEASE_SIGN=1 to create $signature"
fi
echo "Wrote $distdir/$archive"
echo "Wrote $distdir/$checksums"
echo "Wrote $distdir/$manifest"

View file

@ -26,10 +26,12 @@ class TestDocsVersioning(unittest.TestCase):
self.assertEqual(docs_version["schema_generation"], "v1")
self.assertEqual(docs_version["command_reference"], "docs/COMMAND_REFERENCE.md")
self.assertEqual(docs_version["schema_reference"], "docs/SCHEMAS.md")
self.assertEqual(docs_version["packaging_reference"], "docs/PACKAGING.md")
self.assertEqual(docs_version["release_artifact_target"], "make release-artifacts")
def test_versioned_docs_headers_are_present(self):
version = json.loads((DOCS / "VERSION.json").read_text())
for name in ("INDEX.md", "COMMAND_REFERENCE.md", "SCHEMAS.md"):
for name in ("INDEX.md", "COMMAND_REFERENCE.md", "SCHEMAS.md", "PACKAGING.md"):
with self.subTest(name=name):
text = (DOCS / name).read_text()
self.assertIn(

146
tests/test_packaging.py Normal file
View file

@ -0,0 +1,146 @@
# SPDX-License-Identifier: GPL-3.0-or-later
"""Static checks for package metadata and install hardening."""
import hashlib
import json
import os
import re
import subprocess
import tempfile
import tomllib
import unittest
from pathlib import Path
from enodia_sentinel import __version__
ROOT = Path(__file__).resolve().parents[1]
def _read(path: str) -> str:
return (ROOT / path).read_text()
class TestArchPackaging(unittest.TestCase):
def test_pkgbuild_version_matches_project(self):
pkgbuild = _read("packaging/PKGBUILD")
pyproject = tomllib.loads(_read("pyproject.toml"))
docs_version = json.loads(_read("docs/VERSION.json"))
pkgver = re.search(r"^pkgver=([^\n]+)$", pkgbuild, re.M)
self.assertIsNotNone(pkgver)
self.assertEqual(pkgver.group(1), pyproject["project"]["version"])
self.assertEqual(pkgver.group(1), __version__)
self.assertEqual(docs_version["package_version"], pkgver.group(1))
def test_pkgbuild_installs_hardened_runtime_files(self):
pkgbuild = _read("packaging/PKGBUILD")
for expected in (
"/usr/bin/enodia-sentinel",
"/usr/lib/systemd/system/enodia-sentinel.service",
"/usr/lib/systemd/system/enodia-sentinel-web.service",
"/usr/share/libalpm/hooks/enodia-sentinel-fim.hook",
"/usr/lib/tmpfiles.d/enodia-sentinel.conf",
"/usr/share/doc/$pkgname/PACKAGING.md",
"/usr/share/doc/$pkgname/VERSION.json",
"/usr/share/doc/$pkgname/examples/enodia-sentinel-ebpf.conf",
"/var/log/enodia-sentinel",
):
self.assertIn(expected, pkgbuild)
self.assertIn("backup=('etc/enodia-sentinel.toml')", pkgbuild)
self.assertIn("install -dm750", pkgbuild)
def test_alpm_hook_uses_absolute_sentinel_path(self):
hook = _read("packaging/enodia-sentinel-fim.hook")
self.assertIn("Exec = /usr/bin/enodia-sentinel fim-update", hook)
self.assertNotIn("/usr/bin/env enodia-sentinel", hook)
self.assertIn("When = PostTransaction", hook)
def test_tmpfiles_declares_private_log_dir(self):
tmpfiles = _read("packaging/enodia-sentinel.tmpfiles")
self.assertIn("d /var/log/enodia-sentinel 0750 root root -", tmpfiles)
def test_build_script_uses_clean_package_build(self):
script = _read("packaging/build-package.sh")
self.assertIn("set -eu", script)
self.assertIn("command -v makepkg", script)
self.assertIn("--cleanbuild", script)
class TestReleaseArtifacts(unittest.TestCase):
def test_release_script_generates_checksummed_artifacts(self):
with tempfile.TemporaryDirectory() as d:
env = os.environ.copy()
env.update({
"DISTDIR": d,
"RELEASE_ALLOW_DIRTY": "1",
"RELEASE_SIGN": "0",
})
result = subprocess.run(
[str(ROOT / "packaging" / "release-artifacts.sh")],
cwd=ROOT,
env=env,
capture_output=True,
text=True,
timeout=30,
check=True,
)
self.assertIn("Signing skipped", result.stdout)
version = tomllib.loads(_read("pyproject.toml"))["project"]["version"]
archive = Path(d) / f"enodia-sentinel-{version}.tar.gz"
manifest = Path(d) / "RELEASE_MANIFEST.json"
checksums = Path(d) / "SHA256SUMS"
self.assertTrue(archive.is_file())
self.assertTrue(manifest.is_file())
self.assertTrue(checksums.is_file())
self.assertFalse((Path(d) / "SHA256SUMS.asc").exists())
data = json.loads(manifest.read_text())
self.assertEqual(data["project"], "enodia-sentinel")
self.assertEqual(data["version"], version)
self.assertEqual(data["dirty"], 1)
self.assertIn(archive.name, data["artifacts"])
self.assertIn("SHA256SUMS", data["artifacts"])
expected = {
archive.name: hashlib.sha256(archive.read_bytes()).hexdigest(),
manifest.name: hashlib.sha256(manifest.read_bytes()).hexdigest(),
}
actual = {}
for line in checksums.read_text().splitlines():
digest, name = line.split(maxsplit=1)
actual[name] = digest
self.assertEqual(actual, expected)
def test_release_script_supports_gpg_signing(self):
script = _read("packaging/release-artifacts.sh")
self.assertIn("RELEASE_SIGN", script)
self.assertIn("RELEASE_SIGNING_KEY", script)
self.assertIn("--armor --detach-sign", script)
self.assertIn("SHA256SUMS.asc", script)
class TestSourceInstallPackaging(unittest.TestCase):
def test_makefile_installs_docs_and_tmpfiles(self):
makefile = _read("Makefile")
self.assertIn("TMPFILESDIR := /etc/tmpfiles.d", makefile)
self.assertIn("packaging/enodia-sentinel.tmpfiles", makefile)
self.assertIn("docs/PACKAGING.md", makefile)
self.assertIn("docs/VERSION.json", makefile)
self.assertIn("systemd/enodia-sentinel-ebpf.conf", makefile)
def test_packaging_guide_documents_distro_paths(self):
guide = _read("docs/PACKAGING.md")
for phrase in (
"Arch is the primary packaged target",
"sudo pacman -U enodia-sentinel-*.pkg.tar.zst",
"There is no native `.deb` package yet",
"There is no native RPM package yet",
"Package-manager hooks use absolute paths",
):
self.assertIn(phrase, guide)
if __name__ == "__main__":
unittest.main()