test_release_script_generates_checksummed_artifacts hard-coded `dirty == 1`, which only held while the worktree happened to be dirty. The script derives `dirty` from actual git state and RELEASE_ALLOW_DIRTY is a permission flag, not a forced value — so the test failed the moment the tree was clean (all work committed). Compute the expected dirty flag the same way the script does so the test is deterministic on both clean and dirty trees. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
161 lines
6.2 KiB
Python
161 lines
6.2 KiB
Python
# 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)
|
|
|
|
|
|
def _git_worktree_dirty() -> int:
|
|
"""Mirror the dirty computation in release-artifacts.sh so the assertion
|
|
holds whether the tree is clean (work committed) or dirty (mid-change)."""
|
|
def q(*args: str) -> subprocess.CompletedProcess:
|
|
return subprocess.run(["git", *args], cwd=ROOT,
|
|
capture_output=True, text=True)
|
|
if q("diff", "--quiet").returncode != 0:
|
|
return 1
|
|
if q("diff", "--cached", "--quiet").returncode != 0:
|
|
return 1
|
|
if q("ls-files", "--others", "--exclude-standard").stdout.strip():
|
|
return 1
|
|
return 0
|
|
|
|
|
|
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"], _git_worktree_dirty())
|
|
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()
|