diff --git a/README.md b/README.md index b79af56..744df29 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,7 @@ Run a read-only health snapshot from the Debian server with: ./jeannie capacity ./jeannie recover-plan ./jeannie recover-power --dry-run +./jeannie impact --since HEAD~1 ./jeannie scorecard ./jeannie explain scorecard ./jeannie gitops-status @@ -330,6 +331,10 @@ edge verification. Debian runtime, Gitea, RPi DNS, Pimox workers, Kubernetes, GitOps/apps, and edge/public checks. Use `recover-power --dry-run` to preview the sequence. +`impact` explains what a change may touch before you apply it. Use +`./jeannie impact --since HEAD~1` for the last commit or pass paths +like `bootstrap/edge` and `apps/website`. + `scorecard` rolls up backup readiness, GitOps health, DNS/RPi health, cluster health, edge health, capacity pressure, security posture, public certificates, inventory, and docs freshness into a compact pass/warn/fail report. diff --git a/README.md.tmpl b/README.md.tmpl index 634627a..26f4cd0 100644 --- a/README.md.tmpl +++ b/README.md.tmpl @@ -274,6 +274,7 @@ Run a read-only health snapshot from the Debian server with: ./{{ main_script }} capacity ./{{ main_script }} recover-plan ./{{ main_script }} recover-power --dry-run +./{{ main_script }} impact --since HEAD~1 ./{{ main_script }} scorecard ./{{ main_script }} explain scorecard ./{{ main_script }} gitops-status @@ -330,6 +331,10 @@ edge verification. Debian runtime, Gitea, RPi DNS, Pimox workers, Kubernetes, GitOps/apps, and edge/public checks. Use `recover-power --dry-run` to preview the sequence. +`impact` explains what a change may touch before you apply it. Use +`./{{ main_script }} impact --since HEAD~1` for the last commit or pass paths +like `bootstrap/edge` and `apps/website`. + `scorecard` rolls up backup readiness, GitOps health, DNS/RPi health, cluster health, edge health, capacity pressure, security posture, public certificates, inventory, and docs freshness into a compact pass/warn/fail report. diff --git a/docs/jeannie.1.md b/docs/jeannie.1.md index 5be2163..ff3ad55 100644 --- a/docs/jeannie.1.md +++ b/docs/jeannie.1.md @@ -104,6 +104,10 @@ and DuckDNS-to-OCI edge IP drift from the canonical inventory. state, workload images, public URL status, and latest Gitea/OpenTofu backup pointers. +`impact [--since REF] [PATH...]` +: Explain which homelab areas are affected by changed files or explicit paths, +including risk, touched services, validation commands, and apply path. + `backup-status` : Report freshness for Gitea backups, OpenTofu state backups, restore drill reports, and repo-managed Pi-hole restore inputs. The main `status` cascade diff --git a/infra/jeannie-impact/rules.tsv b/infra/jeannie-impact/rules.tsv new file mode 100644 index 0000000..deded13 --- /dev/null +++ b/infra/jeannie-impact/rules.tsv @@ -0,0 +1,17 @@ +pattern area risk touches validate apply +bootstrap/edge/ Edge/high public routing high OCI edge nginx;public website route;Gitea public route;Tailscale backend path ./jeannie doctor-edge;./jeannie cert-check;curl -I https://lab2025.duckdns.org/ ./jeannie plan edge +bootstrap/cluster/ Kubernetes cluster high kubeadm control plane;worker joins;CNI;cluster state ./jeannie doctor-cluster;./jeannie status ./jeannie plan cluster +bootstrap/platform/ Platform services medium Argo CD;monitoring;registry;Traefik dependencies ./jeannie status;./jeannie gitops-status;./jeannie grafana-dashboards list ./jeannie plan platform +bootstrap/provisioning/ Pimox provisioning high Pimox template;worker VMs;preseed/PXE ./jeannie preflight;./jeannie doctor-cluster ./jeannie plan provisioning +apps/ Applications medium GitOps apps;website;security lab;dashboards ./jeannie gitops-status;./jeannie status ./jeannie apps +apps/website/ Public website medium website deployment;public route;image build;Redis/Ollama dependencies ./jeannie status;./jeannie cert-check ./jeannie apps +infra/rpi-services/ RPi DNS services medium Pi-hole;Unbound;Uptime Kuma;RPi Docker ./jeannie doctor-rpi;./jeannie status ./jeannie rpi-services +infra/gitea/ Gitea medium Gitea Docker stack;repo bootstrap;public /git route ./jeannie doctor-gitea;./jeannie doctor-edge ./jeannie deploy-gitea +infra/arr-stack/ ARR stack medium Debian Docker ARR services;volumes under /data/arr ./jeannie status manual docker compose in infra/arr-stack +infra/tailscale/ Tailscale policy medium Tailnet ACLs;routes;peer access ./jeannie tailnet-policy-check;./jeannie doctor-edge manual tailnet policy apply +infra/resource-budgets.yml Capacity policy low resource budget reports;capacity warnings ./jeannie resource-budget;./jeannie capacity none +homelab.yml Canonical inventory high all rendered docs;scripts;edge/Gitea/RPi/Pimox IPs ./jeannie inventory-check;./jeannie validate;./jeannie preflight none +jeannie Main operator script high all Jeannie commands and pipeline flow bash -n jeannie;shellcheck jeannie;./jeannie help none +scripts/ Jeannie helper scripts medium reporting;doctors;automation helpers bash -n scripts/*;shellcheck scripts/* none +README.md.tmpl Generated docs low README.md;operator documentation scripts/render-docs --check scripts/render-docs +docs/ Documentation low runbooks;manual pages scripts/render-docs --check none diff --git a/jeannie b/jeannie index 0adea3a..9a9024d 100755 --- a/jeannie +++ b/jeannie @@ -5871,6 +5871,10 @@ ask_homelab() { "${REPO_ROOT}/scripts/ask" "${@:2}" } +impact() { + "${REPO_ROOT}/scripts/impact" "${@:2}" +} + security_scan() { require_debian_server "security-scan" "${REPO_ROOT}/scripts/security-scan" all @@ -6125,6 +6129,7 @@ Operator View And Reports capacity Compact capacity and placement report. recover-plan Print disaster recovery order and prerequisites. recover-power [--dry-run] Run or preview post-outage recovery. + impact [--since REF] [PATH...] Explain affected lab areas before changes. map [--dot] Print the homelab dependency map. change-journal {list|path} Show risky-command journal entries. release-snapshot Write a pre-change release snapshot. @@ -6389,6 +6394,9 @@ case "${1:-}" in ask) ask_homelab "$@" ;; + impact) + impact "$@" + ;; security-scan) security_scan ;; diff --git a/scripts/impact b/scripts/impact new file mode 100755 index 0000000..cda23e4 --- /dev/null +++ b/scripts/impact @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +RULES_FILE="${HOMELAB_IMPACT_RULES:-${REPO_ROOT}/infra/jeannie-impact/rules.tsv}" + +usage() { + cat <<'EOF' +Usage: ./jeannie impact [--since REF] [PATH...] + +Explain which homelab areas are touched by changed files or explicit paths. +Default mode analyzes the current working tree diff. +EOF +} + +since="" +paths=() +while [ "$#" -gt 0 ]; do + case "$1" in + --since) + since="${2:-}" + shift 2 + ;; + -h|--help|help) + usage + exit 0 + ;; + *) + paths+=("$1") + shift + ;; + esac +done + +if [ ! -s "$RULES_FILE" ]; then + echo "Missing impact rules: $RULES_FILE" >&2 + exit 1 +fi + +if [ "${#paths[@]}" -eq 0 ]; then + if [ -n "$since" ]; then + mapfile -t paths < <(git -C "$REPO_ROOT" diff --name-only "$since"...HEAD) + else + mapfile -t paths < <(git -C "$REPO_ROOT" status --short | awk '{print $NF}') + fi +fi + +if [ "${#paths[@]}" -eq 0 ]; then + echo "No changed files or paths to analyze." + exit 0 +fi + +python3 - "$RULES_FILE" "${paths[@]}" <<'PY' +import csv +import sys + +rules_file = sys.argv[1] +paths = [path.strip() for path in sys.argv[2:] if path.strip()] + +with open(rules_file, encoding="utf-8", newline="") as handle: + rules = list(csv.DictReader(handle, delimiter="\t")) + +matches = [] +for path in paths: + normalized = path.removeprefix("./") + for rule in rules: + pattern = rule["pattern"] + if normalized == pattern.rstrip("/") or normalized.startswith(pattern): + matches.append((normalized, rule)) + +print("Jeannie Impact") +print("==============") +print("Files:") +for path in paths: + print(f" {path}") + +if not matches: + print("\nNo specific impact rule matched.") + print("Validate:") + print(" ./jeannie validate") + raise SystemExit(0) + +seen = set() +print("\nImpacted Areas") +for path, rule in matches: + key = (rule["area"], rule["risk"]) + if key in seen: + continue + seen.add(key) + print(f"\n- {rule['area']}") + print(f" risk: {rule['risk']}") + print(" touches:") + for item in rule["touches"].split(";"): + print(f" - {item}") + print(" validate:") + for command in rule["validate"].split(";"): + print(f" {command}") + if rule["apply"] != "none": + print(" apply path:") + for command in rule["apply"].split(";"): + print(f" {command}") + else: + print(" apply path: none") +PY