Files
Fenris/Makefile
T
xavierkandCommandCodeBot 120d80b28c release: one-command build, sign, publish, and attach — plus dormant workflow (#52)
Implements the full release flow: a single script builds both deb and rpm
packages, signs the RPM payload, generates and clearsigns SHA256SUMS, uploads
to the Gitea package registry (deb to bookworm/jammy/noble pools, rpm to the
fenris group), creates a Gitea release entry with notes, and attaches all
artifacts.

Key changes:
- scripts/release.sh: new release script with --dry-run and --publish modes
- tests/test_release.py: 32 structural tests (dry-run output, filenames,
  revision bumping, bare tag prevention, CI workflow, Makefile targets)
- Makefile: added release-run and release-dry-run targets
- .gitea/workflows/release.yml: extended dormant workflow with signing,
  upload, release creation, and artifact attachment (idempotent re-runs)
- docs/install/signing-key-ceremony.md: added one-time live probe section
  documenting throwaway package publish, apt/dnf verification, and cleanup

Acceptance criteria met:
- One release command performs build, sign, publish, and attach
- Dry-run mode prints every command; tests assert output without network
- Revision bumping on 409 (same-version rebuilds increment release number)
- Dormant CI workflow replicates the flow (queues harmlessly without runner)
- Live probe documented with throwaway package end-to-end
- No bare tags: release API creates tag atomically with release entry

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-09-03 14:44:49 +05:30

334 lines
16 KiB
Makefile

# Fenris Makefile
# Spec: §10.1-10.6
SHELL := /bin/bash
PYTHON := python3
VENV_DIR := /opt/fenris
BIN_DIR := /usr/local/bin
LIBEXEC_DIR := /usr/libexec/fenris
UNIT_DIR := /etc/systemd/system
POLKIT_DIR := /usr/share/polkit-1/actions
CONF_DIR := /etc/fenris
DATA_DIR := /var/lib/fenris
# Placement manifest
MANIFEST := $(DATA_DIR)/manifest.txt
# Legacy history path (IN-4)
LEGACY_HISTORY := ./data/history.jsonl
.PHONY: help install upgrade uninstall purge update-deps test lint check-python check-smartctl import-legacy stage package-deb package-rpm package generate-test-key sign-rpm checksums clearsign release release-run release-dry-run clean
help:
@echo "Fenris NVMe endurance monitor"
@echo ""
@echo "Targets:"
@echo " install - Install Fenris (builds wheel, installs to /opt/fenris)"
@echo " upgrade - Upgrade Fenris (reinstall wheel, sync units)"
@echo " uninstall - Uninstall Fenris (preserves config and store)"
@echo " purge - Remove everything including config and store"
@echo " test - Run tests"
@echo " lint - Run linter"
@echo " update-deps - Update dependency pins"
@echo " stage - Stage packaging tree for nfpm"
@echo " package - Build deb + rpm packages"
@echo " package-deb - Build deb package only"
@echo " package-rpm - Build rpm package only"
@echo " generate-test-key - Create throwaway GPG key for CI/testing"
@echo " sign-rpm - Sign RPM payload with packaging key"
@echo " checksums - Generate SHA256SUMS manifest"
@echo " clearsign - Clearsign SHA256SUMS with packaging key"
@echo " release - Full release (build, sign, checksum, print upload steps)"
@echo " release-run - Execute the full release flow via scripts/release.sh"
@echo " release-dry-run - Dry-run of the release flow (prints commands only)"
@echo " clean - Remove build artifacts"
# ─── Pre-install gates ──────────────────────────────────────────────────────
check-python:
@echo "=== Verifying Python ≥ 3.10 ==="
@$(PYTHON) -c "import sys; v=sys.version_info; exit(0 if (v>=(3,10)) else 1)" || { echo "Error: Python 3.10+ required (found $$($(PYTHON) --version 2>&1))"; exit 1; }
check-smartctl:
@echo "=== Verifying smartctl ==="
@smartctl --version 2>/dev/null | head -1 || { echo "Error: smartctl not found (install smartmontools)"; exit 1; }
# ─── Build ──────────────────────────────────────────────────────────────────
dist/fenris-*.whl: pyproject.toml src/fenris/*.py
@mkdir -p dist
$(PYTHON) -m build --wheel -o dist
# ─── Install ────────────────────────────────────────────────────────────────
install: check-python check-smartctl dist/fenris-*.whl
@echo "=== Creating directories ==="
@sudo mkdir -p $(LIBEXEC_DIR)
@sudo mkdir -p $(CONF_DIR)
@sudo mkdir -p $(POLKIT_DIR)
@echo "=== Creating data directory (root-written, group-read) ==="
@sudo groupadd -f fenris
@sudo install -d -o root -g fenris -m 2750 $(DATA_DIR)
@echo "=== Installing venv with pinned dependencies ==="
@sudo rm -rf $(VENV_DIR)
@sudo $(PYTHON) -m venv $(VENV_DIR)
@sudo $(VENV_DIR)/bin/pip install --upgrade pip --quiet
@sudo $(VENV_DIR)/bin/pip install dist/fenris-*.whl --quiet
@echo "=== Installing wrapper ==="
@sudo install -m 0755 scripts/fenris $(BIN_DIR)/fenris
@echo "=== Installing helpers ==="
@sudo install -m 0755 src/fenris/monitor.py $(LIBEXEC_DIR)/fenris-monitor
@sudo install -m 0755 src/fenris/collect.py $(LIBEXEC_DIR)/fenris-collect
@echo "=== Installing systemd units (dormant — not enabled/started) ==="
@sudo install -m 0644 units/fenris-collect.timer $(UNIT_DIR)/
@sudo install -m 0644 units/fenris-collect.service $(UNIT_DIR)/
@sudo systemctl daemon-reload
@echo "=== Installing polkit policy ==="
@sudo install -m 0644 polkit/com.bongbetic.fenris.monitor.policy $(POLKIT_DIR)/
@echo "=== Recording manifest (IN-2, IN-10) ==="
@echo "# Fenris placement manifest — do not edit" | sudo tee $(MANIFEST) > /dev/null
@echo "# Generated by: sudo make install" | sudo tee -a $(MANIFEST) > /dev/null
@echo "# Timestamp: $$(date -u +%%Y-%%m-%%dT%%H:%%M:%%SZ)" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(BIN_DIR)/fenris" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(LIBEXEC_DIR)/fenris-monitor" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(LIBEXEC_DIR)/fenris-collect" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(UNIT_DIR)/fenris-collect.timer" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(UNIT_DIR)/fenris-collect.service" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(POLKIT_DIR)/com.bongbetic.fenris.monitor.policy" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(VENV_DIR)" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(DATA_DIR)" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(CONF_DIR)" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(MANIFEST)" | sudo tee -a $(MANIFEST) > /dev/null
@echo "=== Install complete ==="
@echo "Units installed but NOT enabled or started (dormant — IN-3)."
@echo "To start monitoring: fenris monitor resume"
@$(MAKE) --no-print-directory import-legacy
# ─── Legacy import (IN-4, ST-7) ────────────────────────────────────────────
import-legacy:
@if [ -f "$(LEGACY_HISTORY)" ]; then \
echo "=== Detected legacy history: $(LEGACY_HISTORY) ==="; \
echo "Running idempotent import..."; \
$(VENV_DIR)/bin/python3 -c "import sys; sys.path.insert(0, 'src'); from fenris.legacy import import_legacy_history; from fenris.store import init_store; from pathlib import Path; conn = init_store(Path('$(DATA_DIR)/observations.db')); r = import_legacy_history(conn, Path('$(LEGACY_HISTORY)')); conn.close(); print(f' Samples imported: {r.get(\"samples_imported\", 0)}'); print(f' Hours imported: {r.get(\"hours_imported\", 0)}'); print(f' Malformed lines: {r.get(\"malformed_lines\", 0)}') if not r.get('skipped') else print(' Skipped: already imported')" || echo " Warning: import failed (non-fatal)"; \
else \
echo "=== No legacy history found at $(LEGACY_HISTORY) ==="; \
fi
# ─── Upgrade ────────────────────────────────────────────────────────────────
upgrade: dist/fenris-*.whl
@echo "=== Upgrading Fenris ==="
@echo "=== Snapshotting database (IN-6) ==="
@sudo cp $(DATA_DIR)/observations.db $(DATA_DIR)/observations.db.bak 2>/dev/null || true
@echo "=== Installing new wheel with pinned dependencies ==="
@sudo $(VENV_DIR)/bin/pip install dist/fenris-*.whl --quiet
@echo "=== Syncing units against manifest ==="
@sudo install -m 0644 units/fenris-collect.timer $(UNIT_DIR)/
@sudo install -m 0644 units/fenris-collect.service $(UNIT_DIR)/
@sudo install -m 0644 polkit/com.bongbetic.fenris.monitor.policy $(POLKIT_DIR)/
@sudo install -m 0755 scripts/fenris $(BIN_DIR)/fenris
@sudo install -m 0755 src/fenris/monitor.py $(LIBEXEC_DIR)/fenris-monitor
@sudo install -m 0755 src/fenris/collect.py $(LIBEXEC_DIR)/fenris-collect
@sudo systemctl daemon-reload
@echo "=== Updating manifest ==="
@echo "# Fenris placement manifest — do not edit" | sudo tee $(MANIFEST) > /dev/null
@echo "# Generated by: sudo make upgrade" | sudo tee -a $(MANIFEST) > /dev/null
@echo "# Timestamp: $$(date -u +%%Y-%%m-%%dT%%H:%%M:%%SZ)" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(BIN_DIR)/fenris" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(LIBEXEC_DIR)/fenris-monitor" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(LIBEXEC_DIR)/fenris-collect" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(UNIT_DIR)/fenris-collect.timer" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(UNIT_DIR)/fenris-collect.service" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(POLKIT_DIR)/com.bongbetic.fenris.monitor.policy" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(VENV_DIR)" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(DATA_DIR)" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(CONF_DIR)" | sudo tee -a $(MANIFEST) > /dev/null
@echo "$(MANIFEST)" | sudo tee -a $(MANIFEST) > /dev/null
@echo "=== Restarting timer only if contents changed and active (IN-5) ==="
@for unit in fenris-collect.timer fenris-collect.service; do \
TMPFILE=$$(mktemp); \
sudo systemctl cat $$unit > $$TMPFILE 2>/dev/null || true; \
if ! diff -q $$TMPFILE $(UNIT_DIR)/$$unit > /dev/null 2>&1; then \
if systemctl is-active --quiet $$unit; then \
echo " $$unit changed and active — restarting"; \
sudo systemctl restart $$unit; \
fi; \
fi; \
rm -f $$TMPFILE; \
done
@echo "=== Applying forward-only schema migrations (IN-5, IN-6) ==="
@sudo $(VENV_DIR)/bin/python3 -c "from fenris.store import migrate_to_latest; from pathlib import Path; n = migrate_to_latest(Path('$(DATA_DIR)/observations.db')); print(f' Migration steps applied: {n}') if n else print(' Schema already current')"
@echo "=== Upgrade complete ==="
# ─── Uninstall (IN-7) ──────────────────────────────────────────────────────
uninstall:
@echo "=== Uninstalling Fenris ==="
@if [ -x $(LIBEXEC_DIR)/fenris-monitor ]; then \
echo "=== Performing sanctioned disable (§10.4) ==="; \
sudo $(LIBEXEC_DIR)/fenris-monitor disable --now || true; \
fi
@echo "=== Stopping units ==="
@sudo systemctl stop fenris-collect.timer 2>/dev/null || true
@sudo systemctl disable fenris-collect.timer 2>/dev/null || true
@sudo systemctl daemon-reload
@echo "=== Removing installed files (preserving config and store) ==="
@-rm -f $(BIN_DIR)/fenris
@-rm -f $(LIBEXEC_DIR)/fenris-monitor
@-rm -f $(LIBEXEC_DIR)/fenris-collect
@-rmdir $(LIBEXEC_DIR) 2>/dev/null || true
@-rm -f $(UNIT_DIR)/fenris-collect.timer
@-rm -f $(UNIT_DIR)/fenris-collect.service
@-rm -f $(POLKIT_DIR)/com.bongbetic.fenris.monitor.policy
@sudo rm -rf $(VENV_DIR)
@-sudo rm -f $(MANIFEST)
@echo "=== Uninstall complete ==="
@echo "Config preserved at $(CONF_DIR)"
@echo "Store preserved at $(DATA_DIR)"
# ─── Purge ──────────────────────────────────────────────────────────────────
purge: uninstall
@echo "=== Purging Fenris ==="
@-sudo rm -rf $(CONF_DIR)
@-sudo rm -rf $(DATA_DIR)
@echo "=== Purge complete ==="
# ─── Test ───────────────────────────────────────────────────────────────────
test:
$(PYTHON) -m pytest tests/ -v
# ─── Lint ───────────────────────────────────────────────────────────────────
lint:
$(PYTHON) -m ruff check src/ tests/
# ─── Dependencies (IN-8) ───────────────────────────────────────────────────
update-deps:
$(PYTHON) -m pip compile pyproject.toml -o requirements.txt
# ─── Packaging (spec §3, §4, §5) ────────────────────────────────────────────
# Version is sourced from pyproject.toml for both formats
FENRIS_VERSION := $(shell sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)
# GPG signing — packaging key UID (spec §4)
PACKAGING_KEY ?= packaging@bongbetic.com
stage: dist/fenris-*.whl
@echo "=== Staging packaging tree (v$(FENRIS_VERSION)) ==="
bash packaging/stage.sh "$(FENRIS_VERSION)"
package-deb: stage
@echo "=== Building deb package ==="
VERSION="$(FENRIS_VERSION)" nfpm pkg -f packaging/nfpm.yaml -p deb -t dist/
@echo "=== deb package built: dist/fenris_$(FENRIS_VERSION)_amd64.deb ==="
package-rpm: stage
@echo "=== Building rpm package ==="
VERSION="$(FENRIS_VERSION)" nfpm pkg -f packaging/nfpm.yaml -p rpm -t dist/
@echo "=== rpm package built: dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm ==="
package: package-deb package-rpm
@echo "=== Both packages built in dist/ ==="
# ─── GPG key management ─────────────────────────────────────────────────────
generate-test-key:
@echo "=== Generating throwaway test GPG key ==="
@echo "This key is for CI/testing only — never use for real releases."
printf '%%no-protection\nKey-Type: RSA\nKey-Length: 3072\nName-Real: Fenris Packaging (TESTING ONLY)\nName-Email: packaging-test@bongbetic.com\nExpire-Date: 0\n%%commit\n' | \
gpg --batch --gen-key
@echo "=== Test key created. Fingerprint: ==="
@gpg --fingerprint packaging-test@bongbetic.com
# ─── Signing ────────────────────────────────────────────────────────────────
sign-rpm: package-rpm
@echo "=== Signing RPM payload ==="
@rpm --import packaging/keys/fenris-packaging.asc 2>/dev/null || true
rpmsign --addsign --define "_gpg_name $(PACKAGING_KEY)" \
dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm
@echo "=== RPM signed ==="
@rpm -Kv dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm
checksums: package
@echo "=== Generating SHA256SUMS ==="
cd dist && sha256sum fenris_$(FENRIS_VERSION)_amd64.deb \
fenris-$(FENRIS_VERSION)-1.x86_64.rpm > SHA256SUMS
@echo "=== SHA256SUMS written ==="
@cat dist/SHA256SUMS
clearsign: checksums
@echo "=== Clearsigning SHA256SUMS ==="
gpg --batch --yes --clearsign --local-user $(PACKAGING_KEY) \
dist/SHA256SUMS
@echo "=== SHA256SUMS.asc written ==="
# ─── Release (spec §5) ──────────────────────────────────────────────────────
release: package sign-rpm clearsign
@echo ""
@echo "=== Release v$(FENRIS_VERSION) ==="
@echo ""
@echo "Artifacts:"
@ls -la dist/fenris_$(FENRIS_VERSION)_amd64.deb \
dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm \
dist/SHA256SUMS.asc 2>/dev/null
@echo ""
@echo "Verify signing (manual):"
@echo " rpm -Kv dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm"
@echo " gpg --verify dist/SHA256SUMS.asc dist/SHA256SUMS"
@echo ""
@echo "Upload to registry:"
@echo " curl -X PUT -u user:token -T dist/fenris_$(FENRIS_VERSION)_amd64.deb \\"
@echo " 'https://git.bongbetic.com/api/packages/xavierk/debian/pool/bookworm/main/upload'"
@echo " curl -X PUT -u user:token -T dist/fenris_$(FENRIS_VERSION)_amd64.deb \\"
@echo " 'https://git.bongbetic.com/api/packages/xavierk/debian/pool/jammy/main/upload'"
@echo " curl -X PUT -u user:token -T dist/fenris_$(FENRIS_VERSION)_amd64.deb \\"
@echo " 'https://git.bongbetic.com/api/packages/xavierk/debian/pool/noble/main/upload'"
@echo " curl -X PUT -u user:token -T dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm \\"
@echo " 'https://git.bongbetic.com/api/packages/xavierk/rpm/fenris/upload'"
@echo ""
@echo "Create Gitea release with notes and attach:"
@echo " dist/fenris_$(FENRIS_VERSION)_amd64.deb"
@echo " dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm"
@echo " dist/SHA256SUMS.asc"
@echo ""
@echo "Key ceremony: delete the private key after upload."
@echo " See docs/install/signing-key-ceremony.md"
# ─── Automated release flow (issue #52) ──────────────────────────────────────
release-run:
bash scripts/release.sh --publish
release-dry-run:
bash scripts/release.sh --dry-run
clean:
@echo "=== Cleaning build artifacts ==="
rm -rf build/stage dist/fenris-*.deb dist/fenris-*.rpm dist/SHA256SUMS*