Compare commits
6 Commits
86e061dd72
...
fb57233ce8
| Author | SHA1 | Date |
|---|---|---|
|
|
fb57233ce8 | |
|
|
1954a2e975 | |
|
|
eabc5b049b | |
|
|
31723307d5 | |
|
|
791c9d5822 | |
|
|
d0004d99f8 |
24
README.md
24
README.md
|
|
@ -272,8 +272,12 @@ Run a read-only health snapshot from the Debian server with:
|
||||||
./jeannie help
|
./jeannie help
|
||||||
./jeannie status
|
./jeannie status
|
||||||
./jeannie capacity
|
./jeannie capacity
|
||||||
|
./jeannie capacity-advisor
|
||||||
|
./jeannie capacity-limits
|
||||||
./jeannie recover-plan
|
./jeannie recover-plan
|
||||||
./jeannie recover-power --dry-run
|
./jeannie recover-power --dry-run
|
||||||
|
./jeannie impact --since HEAD~1
|
||||||
|
./jeannie review-last-change
|
||||||
./jeannie scorecard
|
./jeannie scorecard
|
||||||
./jeannie explain scorecard
|
./jeannie explain scorecard
|
||||||
./jeannie gitops-status
|
./jeannie gitops-status
|
||||||
|
|
@ -322,6 +326,15 @@ Kubernetes, Pimox, RPi, and placement signals without dumping full details. Use
|
||||||
`./jeannie capacity --details`, `--json`, `--only problems`, or
|
`./jeannie capacity --details`, `--json`, `--only problems`, or
|
||||||
`--verbose` for the older full capacity dump.
|
`--verbose` for the older full capacity dump.
|
||||||
|
|
||||||
|
`capacity-advisor` interprets capacity, resource-budget, and control-plane
|
||||||
|
placement signals to recommend whether to add Pimox VMs, taint Debian, or fix
|
||||||
|
resource definitions first.
|
||||||
|
|
||||||
|
`capacity-limits` prints suggested Kubernetes `resources.requests` and
|
||||||
|
`resources.limits` YAML snippets for containers missing them. It uses
|
||||||
|
`infra/resource-recommendations.yml` as a conservative starting profile and does
|
||||||
|
not edit manifests.
|
||||||
|
|
||||||
`recover-plan` prints the disaster recovery order and lightweight prerequisite
|
`recover-plan` prints the disaster recovery order and lightweight prerequisite
|
||||||
checks, from Debian and Gitea through DNS, Pimox, Kubernetes, GitOps apps, and
|
checks, from Debian and Gitea through DNS, Pimox, Kubernetes, GitOps apps, and
|
||||||
edge verification.
|
edge verification.
|
||||||
|
|
@ -330,6 +343,13 @@ edge verification.
|
||||||
Debian runtime, Gitea, RPi DNS, Pimox workers, Kubernetes, GitOps/apps, and
|
Debian runtime, Gitea, RPi DNS, Pimox workers, Kubernetes, GitOps/apps, and
|
||||||
edge/public checks. Use `recover-power --dry-run` to preview the sequence.
|
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`.
|
||||||
|
|
||||||
|
`review-last-change` summarizes the last commit, runs impact analysis for the
|
||||||
|
touched files, and prints validation plus rollback hints.
|
||||||
|
|
||||||
`scorecard` rolls up backup readiness, GitOps health, DNS/RPi health, cluster
|
`scorecard` rolls up backup readiness, GitOps health, DNS/RPi health, cluster
|
||||||
health, edge health, capacity pressure, security posture, public certificates,
|
health, edge health, capacity pressure, security posture, public certificates,
|
||||||
inventory, and docs freshness into a compact pass/warn/fail report.
|
inventory, and docs freshness into a compact pass/warn/fail report.
|
||||||
|
|
@ -1089,12 +1109,16 @@ Build the local homelab knowledge index separately from the main deployment:
|
||||||
```bash
|
```bash
|
||||||
./jeannie ai-index
|
./jeannie ai-index
|
||||||
./jeannie ai-check
|
./jeannie ai-check
|
||||||
|
./jeannie ask "how do I check edge to Gitea?"
|
||||||
```
|
```
|
||||||
|
|
||||||
The index is built from the non-secret source list in
|
The index is built from the non-secret source list in
|
||||||
`infra/ai/knowledge-sources.txt` and defaults to `/data/homelab-ai/index`.
|
`infra/ai/knowledge-sources.txt` and defaults to `/data/homelab-ai/index`.
|
||||||
Doctor commands use it as extra context when it exists, but infrastructure
|
Doctor commands use it as extra context when it exists, but infrastructure
|
||||||
deployment does not depend on it.
|
deployment does not depend on it.
|
||||||
|
Use `ask` as the runbook and command finder. It returns the closest indexed
|
||||||
|
docs/scripts by default; set `LAB_AI_ASK_LLM=true` to ask Ollama after
|
||||||
|
retrieval.
|
||||||
|
|
||||||
The CV page has two client-side presentation modes:
|
The CV page has two client-side presentation modes:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -272,8 +272,12 @@ Run a read-only health snapshot from the Debian server with:
|
||||||
./{{ main_script }} help
|
./{{ main_script }} help
|
||||||
./{{ main_script }} status
|
./{{ main_script }} status
|
||||||
./{{ main_script }} capacity
|
./{{ main_script }} capacity
|
||||||
|
./{{ main_script }} capacity-advisor
|
||||||
|
./{{ main_script }} capacity-limits
|
||||||
./{{ main_script }} recover-plan
|
./{{ main_script }} recover-plan
|
||||||
./{{ main_script }} recover-power --dry-run
|
./{{ main_script }} recover-power --dry-run
|
||||||
|
./{{ main_script }} impact --since HEAD~1
|
||||||
|
./{{ main_script }} review-last-change
|
||||||
./{{ main_script }} scorecard
|
./{{ main_script }} scorecard
|
||||||
./{{ main_script }} explain scorecard
|
./{{ main_script }} explain scorecard
|
||||||
./{{ main_script }} gitops-status
|
./{{ main_script }} gitops-status
|
||||||
|
|
@ -322,6 +326,15 @@ Kubernetes, Pimox, RPi, and placement signals without dumping full details. Use
|
||||||
`./{{ main_script }} capacity --details`, `--json`, `--only problems`, or
|
`./{{ main_script }} capacity --details`, `--json`, `--only problems`, or
|
||||||
`--verbose` for the older full capacity dump.
|
`--verbose` for the older full capacity dump.
|
||||||
|
|
||||||
|
`capacity-advisor` interprets capacity, resource-budget, and control-plane
|
||||||
|
placement signals to recommend whether to add Pimox VMs, taint Debian, or fix
|
||||||
|
resource definitions first.
|
||||||
|
|
||||||
|
`capacity-limits` prints suggested Kubernetes `resources.requests` and
|
||||||
|
`resources.limits` YAML snippets for containers missing them. It uses
|
||||||
|
`infra/resource-recommendations.yml` as a conservative starting profile and does
|
||||||
|
not edit manifests.
|
||||||
|
|
||||||
`recover-plan` prints the disaster recovery order and lightweight prerequisite
|
`recover-plan` prints the disaster recovery order and lightweight prerequisite
|
||||||
checks, from Debian and Gitea through DNS, Pimox, Kubernetes, GitOps apps, and
|
checks, from Debian and Gitea through DNS, Pimox, Kubernetes, GitOps apps, and
|
||||||
edge verification.
|
edge verification.
|
||||||
|
|
@ -330,6 +343,13 @@ edge verification.
|
||||||
Debian runtime, Gitea, RPi DNS, Pimox workers, Kubernetes, GitOps/apps, and
|
Debian runtime, Gitea, RPi DNS, Pimox workers, Kubernetes, GitOps/apps, and
|
||||||
edge/public checks. Use `recover-power --dry-run` to preview the sequence.
|
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`.
|
||||||
|
|
||||||
|
`review-last-change` summarizes the last commit, runs impact analysis for the
|
||||||
|
touched files, and prints validation plus rollback hints.
|
||||||
|
|
||||||
`scorecard` rolls up backup readiness, GitOps health, DNS/RPi health, cluster
|
`scorecard` rolls up backup readiness, GitOps health, DNS/RPi health, cluster
|
||||||
health, edge health, capacity pressure, security posture, public certificates,
|
health, edge health, capacity pressure, security posture, public certificates,
|
||||||
inventory, and docs freshness into a compact pass/warn/fail report.
|
inventory, and docs freshness into a compact pass/warn/fail report.
|
||||||
|
|
@ -1089,12 +1109,16 @@ Build the local homelab knowledge index separately from the main deployment:
|
||||||
```bash
|
```bash
|
||||||
./{{ main_script }} ai-index
|
./{{ main_script }} ai-index
|
||||||
./{{ main_script }} ai-check
|
./{{ main_script }} ai-check
|
||||||
|
./{{ main_script }} ask "how do I check edge to Gitea?"
|
||||||
```
|
```
|
||||||
|
|
||||||
The index is built from the non-secret source list in
|
The index is built from the non-secret source list in
|
||||||
`infra/ai/knowledge-sources.txt` and defaults to `/data/homelab-ai/index`.
|
`infra/ai/knowledge-sources.txt` and defaults to `/data/homelab-ai/index`.
|
||||||
Doctor commands use it as extra context when it exists, but infrastructure
|
Doctor commands use it as extra context when it exists, but infrastructure
|
||||||
deployment does not depend on it.
|
deployment does not depend on it.
|
||||||
|
Use `ask` as the runbook and command finder. It returns the closest indexed
|
||||||
|
docs/scripts by default; set `LAB_AI_ASK_LLM=true` to ask Ollama after
|
||||||
|
retrieval.
|
||||||
|
|
||||||
The CV page has two client-side presentation modes:
|
The CV page has two client-side presentation modes:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,16 @@ deployment readiness, pod restarts, node pressure, disk pressure, Traefik
|
||||||
Pimox, RPi, and placement signals. Use `capacity --details`, `--json`, `--only
|
Pimox, RPi, and placement signals. Use `capacity --details`, `--json`, `--only
|
||||||
problems`, or `--verbose` for the older full dump.
|
problems`, or `--verbose` for the older full dump.
|
||||||
|
|
||||||
|
`capacity-advisor`
|
||||||
|
: Interpret capacity, resource-budget, and control-plane placement signals to
|
||||||
|
recommend whether to add Pimox VMs, taint Debian, or fix resource definitions
|
||||||
|
first.
|
||||||
|
|
||||||
|
`capacity-limits [namespace]`
|
||||||
|
: Recommend Kubernetes `resources.requests` and `resources.limits` YAML snippets
|
||||||
|
for containers missing them. Uses `infra/resource-recommendations.yml` and does
|
||||||
|
not edit manifests.
|
||||||
|
|
||||||
`recover-plan`
|
`recover-plan`
|
||||||
: Print the ordered disaster recovery checklist and lightweight prerequisite
|
: Print the ordered disaster recovery checklist and lightweight prerequisite
|
||||||
checks for Debian, Gitea, RPi DNS, Pimox, Kubernetes, GitOps apps, and edge
|
checks for Debian, Gitea, RPi DNS, Pimox, Kubernetes, GitOps apps, and edge
|
||||||
|
|
@ -104,6 +114,14 @@ and DuckDNS-to-OCI edge IP drift from the canonical inventory.
|
||||||
state, workload images, public URL status, and latest Gitea/OpenTofu backup
|
state, workload images, public URL status, and latest Gitea/OpenTofu backup
|
||||||
pointers.
|
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.
|
||||||
|
|
||||||
|
`review-last-change [REF]`
|
||||||
|
: Summarize a commit, list touched files, run impact analysis, and print generic
|
||||||
|
validation plus rollback hints. Defaults to `HEAD`.
|
||||||
|
|
||||||
`backup-status`
|
`backup-status`
|
||||||
: Report freshness for Gitea backups, OpenTofu state backups, restore drill
|
: Report freshness for Gitea backups, OpenTofu state backups, restore drill
|
||||||
reports, and repo-managed Pi-hole restore inputs. The main `status` cascade
|
reports, and repo-managed Pi-hole restore inputs. The main `status` cascade
|
||||||
|
|
@ -275,6 +293,10 @@ structure.
|
||||||
: Check local AI helper prerequisites, knowledge index, and Ollama availability
|
: Check local AI helper prerequisites, knowledge index, and Ollama availability
|
||||||
when the backstage helper is enabled.
|
when the backstage helper is enabled.
|
||||||
|
|
||||||
|
`ask QUESTION...`
|
||||||
|
: Search the local homelab knowledge index for relevant docs, runbooks, scripts,
|
||||||
|
and commands. Set `LAB_AI_ASK_LLM=true` to ask Ollama after retrieval.
|
||||||
|
|
||||||
### Defensive Security
|
### Defensive Security
|
||||||
|
|
||||||
`security-scan`
|
`security-scan`
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ pattern meaning diagnose_commands jeannie_fix_commands other_fix_commands risk a
|
||||||
unsupported template variable Generated docs found a template placeholder that render-docs does not support. ./jeannie validate none Edit README.md.tmpl or scripts/render-docs low no Usually a docs/template mismatch rather than an infrastructure issue.
|
unsupported template variable Generated docs found a template placeholder that render-docs does not support. ./jeannie validate none Edit README.md.tmpl or scripts/render-docs low no Usually a docs/template mismatch rather than an infrastructure issue.
|
||||||
generated README.md is stale README.md does not match README.md.tmpl rendering. ./jeannie validate none scripts/render-docs low candidate A future docs-render Jeannie command could safely wrap this.
|
generated README.md is stale README.md does not match README.md.tmpl rendering. ./jeannie validate none scripts/render-docs low candidate A future docs-render Jeannie command could safely wrap this.
|
||||||
missing_resource_policy Workloads are missing resource requests or limits, so capacity planning is unreliable. ./jeannie resource-budget;./jeannie capacity none Edit app manifests or Helm values medium no Add requests first, then limits for workloads you own.
|
missing_resource_policy Workloads are missing resource requests or limits, so capacity planning is unreliable. ./jeannie resource-budget;./jeannie capacity none Edit app manifests or Helm values medium no Add requests first, then limits for workloads you own.
|
||||||
|
pods missing requests/limits Some Kubernetes pods are missing resource requests or limits, so the scheduler and capacity reports cannot fully trust workload sizing. ./jeannie resource-budget --details;./jeannie capacity none Add requests/limits in the app manifests, Helm values, or platform chart values medium no Yes, this is fixed from code/config by adding resources to workloads you own; avoid auto-generating limits blindly.
|
||||||
missing requests One or more containers do not declare CPU or memory requests. ./jeannie resource-budget --details;./jeannie capacity none Edit app manifests or Helm values medium no Start with application namespaces before system namespaces.
|
missing requests One or more containers do not declare CPU or memory requests. ./jeannie resource-budget --details;./jeannie capacity none Edit app manifests or Helm values medium no Start with application namespaces before system namespaces.
|
||||||
missing limits One or more containers do not declare CPU or memory limits. ./jeannie resource-budget --details;./jeannie capacity none Edit app manifests or Helm values medium no Limits are useful but can cause throttling; set them deliberately.
|
missing limits One or more containers do not declare CPU or memory limits. ./jeannie resource-budget --details;./jeannie capacity none Edit app manifests or Helm values medium no Limits are useful but can cause throttling; set them deliberately.
|
||||||
resource policy Capacity or resource policy needs attention. ./jeannie resource-budget;./jeannie capacity none Edit infra/resource-budgets.yml or app manifests medium no Do not hide pressure by only changing the budget file.
|
resource policy Capacity or resource policy needs attention. ./jeannie resource-budget;./jeannie capacity none Edit infra/resource-budgets.yml or app manifests medium no Do not hide pressure by only changing the budget file.
|
||||||
|
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
defaults:
|
||||||
|
request_cpu_m: 50
|
||||||
|
request_memory_mib: 128
|
||||||
|
limit_cpu_multiplier: 4
|
||||||
|
limit_memory_multiplier: 2
|
||||||
|
|
||||||
|
namespaces:
|
||||||
|
website-production:
|
||||||
|
request_cpu_m: 75
|
||||||
|
request_memory_mib: 128
|
||||||
|
limit_cpu_multiplier: 4
|
||||||
|
limit_memory_multiplier: 2
|
||||||
|
demos-static:
|
||||||
|
request_cpu_m: 25
|
||||||
|
request_memory_mib: 64
|
||||||
|
limit_cpu_multiplier: 4
|
||||||
|
limit_memory_multiplier: 2
|
||||||
|
heimdall:
|
||||||
|
request_cpu_m: 50
|
||||||
|
request_memory_mib: 128
|
||||||
|
limit_cpu_multiplier: 4
|
||||||
|
limit_memory_multiplier: 2
|
||||||
|
arr-stack:
|
||||||
|
request_cpu_m: 150
|
||||||
|
request_memory_mib: 512
|
||||||
|
limit_cpu_multiplier: 3
|
||||||
|
limit_memory_multiplier: 2
|
||||||
|
argocd:
|
||||||
|
request_cpu_m: 100
|
||||||
|
request_memory_mib: 256
|
||||||
|
limit_cpu_multiplier: 3
|
||||||
|
limit_memory_multiplier: 2
|
||||||
43
jeannie
43
jeannie
|
|
@ -5866,6 +5866,19 @@ PY
|
||||||
echo "AI check passed."
|
echo "AI check passed."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ask_homelab() {
|
||||||
|
require_debian_server "ask"
|
||||||
|
"${REPO_ROOT}/scripts/ask" "${@:2}"
|
||||||
|
}
|
||||||
|
|
||||||
|
impact() {
|
||||||
|
"${REPO_ROOT}/scripts/impact" "${@:2}"
|
||||||
|
}
|
||||||
|
|
||||||
|
review_last_change() {
|
||||||
|
"${REPO_ROOT}/scripts/review-last-change" "${@:2}"
|
||||||
|
}
|
||||||
|
|
||||||
security_scan() {
|
security_scan() {
|
||||||
require_debian_server "security-scan"
|
require_debian_server "security-scan"
|
||||||
"${REPO_ROOT}/scripts/security-scan" all
|
"${REPO_ROOT}/scripts/security-scan" all
|
||||||
|
|
@ -6002,6 +6015,16 @@ resource_budget() {
|
||||||
"${REPO_ROOT}/scripts/resource-budget" "${@:2}"
|
"${REPO_ROOT}/scripts/resource-budget" "${@:2}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
capacity_advisor() {
|
||||||
|
require_debian_server "capacity-advisor"
|
||||||
|
"${REPO_ROOT}/scripts/capacity-advisor" "${@:2}"
|
||||||
|
}
|
||||||
|
|
||||||
|
capacity_limits() {
|
||||||
|
require_debian_server "capacity-limits"
|
||||||
|
"${REPO_ROOT}/scripts/capacity-limits" "${@:2}"
|
||||||
|
}
|
||||||
|
|
||||||
control_plane() {
|
control_plane() {
|
||||||
require_debian_server "control-plane"
|
require_debian_server "control-plane"
|
||||||
"${REPO_ROOT}/scripts/control-plane" "${@:2}"
|
"${REPO_ROOT}/scripts/control-plane" "${@:2}"
|
||||||
|
|
@ -6118,8 +6141,12 @@ Operator View And Reports
|
||||||
status Cascading health check from host to public URLs.
|
status Cascading health check from host to public URLs.
|
||||||
scorecard Compact pass/warn/fail operator scorecard.
|
scorecard Compact pass/warn/fail operator scorecard.
|
||||||
capacity Compact capacity and placement report.
|
capacity Compact capacity and placement report.
|
||||||
|
capacity-advisor Decide whether to add VMs or fix placement/resources.
|
||||||
|
capacity-limits [namespace] Recommend requests/limits YAML snippets.
|
||||||
recover-plan Print disaster recovery order and prerequisites.
|
recover-plan Print disaster recovery order and prerequisites.
|
||||||
recover-power [--dry-run] Run or preview post-outage recovery.
|
recover-power [--dry-run] Run or preview post-outage recovery.
|
||||||
|
impact [--since REF] [PATH...] Explain affected lab areas before changes.
|
||||||
|
review-last-change [REF] Summarize changed files, impact, validation.
|
||||||
map [--dot] Print the homelab dependency map.
|
map [--dot] Print the homelab dependency map.
|
||||||
change-journal {list|path} Show risky-command journal entries.
|
change-journal {list|path} Show risky-command journal entries.
|
||||||
release-snapshot Write a pre-change release snapshot.
|
release-snapshot Write a pre-change release snapshot.
|
||||||
|
|
@ -6181,6 +6208,7 @@ Security Learning
|
||||||
AI And Indexing
|
AI And Indexing
|
||||||
ai-index Build the local homelab RAG index.
|
ai-index Build the local homelab RAG index.
|
||||||
ai-check Query/check the local AI index.
|
ai-check Query/check the local AI index.
|
||||||
|
ask QUESTION... Find relevant docs/runbooks/commands.
|
||||||
|
|
||||||
Destructive
|
Destructive
|
||||||
nuke Guarded cluster state destruction path.
|
nuke Guarded cluster state destruction path.
|
||||||
|
|
@ -6216,6 +6244,12 @@ case "${1:-}" in
|
||||||
capacity)
|
capacity)
|
||||||
capacity_report "$@"
|
capacity_report "$@"
|
||||||
;;
|
;;
|
||||||
|
capacity-advisor)
|
||||||
|
capacity_advisor "$@"
|
||||||
|
;;
|
||||||
|
capacity-limits)
|
||||||
|
capacity_limits "$@"
|
||||||
|
;;
|
||||||
recover-plan)
|
recover-plan)
|
||||||
recover_plan
|
recover_plan
|
||||||
;;
|
;;
|
||||||
|
|
@ -6380,6 +6414,15 @@ case "${1:-}" in
|
||||||
ai-check)
|
ai-check)
|
||||||
ai_check
|
ai_check
|
||||||
;;
|
;;
|
||||||
|
ask)
|
||||||
|
ask_homelab "$@"
|
||||||
|
;;
|
||||||
|
impact)
|
||||||
|
impact "$@"
|
||||||
|
;;
|
||||||
|
review-last-change)
|
||||||
|
review_last_change "$@"
|
||||||
|
;;
|
||||||
security-scan)
|
security-scan)
|
||||||
security_scan
|
security_scan
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: ./jeannie ask QUESTION...
|
||||||
|
|
||||||
|
Search the homelab knowledge index for the closest docs, runbooks, scripts, and
|
||||||
|
Jeannie commands. Set LAB_AI_ASK_LLM=true to ask Ollama after retrieval.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
""|-h|--help|help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${LAB_AI_ASK_LLM:-false}" = "true" ]; then
|
||||||
|
exec "${REPO_ROOT}/scripts/query-homelab-ai-index" --ask "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "${REPO_ROOT}/scripts/query-homelab-ai-index" --context-only "$@"
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
run_check() {
|
||||||
|
local name="$1"
|
||||||
|
shift
|
||||||
|
echo "## ${name}"
|
||||||
|
set +e
|
||||||
|
"$@" 2>&1
|
||||||
|
local status=$?
|
||||||
|
set -e
|
||||||
|
echo "exit_status=${status}"
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp_file="$(mktemp "${TMPDIR:-/tmp}/jeannie-capacity-advisor.XXXXXX")"
|
||||||
|
trap 'rm -f "$tmp_file"' EXIT
|
||||||
|
|
||||||
|
{
|
||||||
|
run_check "capacity" "${REPO_ROOT}/scripts/capacity-report" --only problems
|
||||||
|
run_check "resource-budget" "${REPO_ROOT}/scripts/resource-budget"
|
||||||
|
run_check "control-plane" "${REPO_ROOT}/scripts/control-plane" status
|
||||||
|
} >"$tmp_file"
|
||||||
|
|
||||||
|
python3 - "$tmp_file" <<'PY'
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
text = open(sys.argv[1], encoding="utf-8").read()
|
||||||
|
lowered = text.lower()
|
||||||
|
|
||||||
|
missing_match = re.search(r"(\d+)\s+pods missing requests/limits", text)
|
||||||
|
missing_count = int(missing_match.group(1)) if missing_match else 0
|
||||||
|
|
||||||
|
pressure = any(token in lowered for token in ["diskpressure", "memorypressure", "node pressure"])
|
||||||
|
worker_storage_warn = "worker storage" in lowered and "warn" in lowered
|
||||||
|
non_system_control_plane = False
|
||||||
|
control_plane_section = text.split("## control-plane", 1)[-1]
|
||||||
|
for line in control_plane_section.splitlines():
|
||||||
|
if re.match(r"\s*(argocd|website-production|demos-static|heimdall|arr-stack|security-lab|monitoring)\s+", line):
|
||||||
|
non_system_control_plane = True
|
||||||
|
break
|
||||||
|
|
||||||
|
print("Jeannie Capacity Advisor")
|
||||||
|
print("========================")
|
||||||
|
|
||||||
|
recommendations = []
|
||||||
|
if missing_count:
|
||||||
|
recommendations.append(
|
||||||
|
(
|
||||||
|
"Fix resource requests/limits first",
|
||||||
|
f"{missing_count} pods are missing requests/limits, so scheduling and capacity math are not trustworthy yet.",
|
||||||
|
"./jeannie resource-budget --details",
|
||||||
|
"code/config: add resources to app manifests or Helm values",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if non_system_control_plane:
|
||||||
|
recommendations.append(
|
||||||
|
(
|
||||||
|
"Keep app/data-plane pods off Debian",
|
||||||
|
"Non-system workloads are still scheduled on the Debian control-plane host.",
|
||||||
|
"./jeannie control-plane taint",
|
||||||
|
"scheduling: taint Debian after Pimox workers are Ready",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if pressure or worker_storage_warn:
|
||||||
|
recommendations.append(
|
||||||
|
(
|
||||||
|
"Consider adding or recreating Pimox worker capacity",
|
||||||
|
"Cluster pressure or worker storage pressure is visible after current placement checks.",
|
||||||
|
"./jeannie workers list",
|
||||||
|
"capacity: increase LAB_PIMOX_WORKER_COUNT or recreate the pressured worker",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not recommendations:
|
||||||
|
recommendations.append(
|
||||||
|
(
|
||||||
|
"No new Pimox VM needed from current signals",
|
||||||
|
"No capacity warning, node pressure, or control-plane placement issue was detected in the sampled reports.",
|
||||||
|
"./jeannie capacity",
|
||||||
|
"watch: rerun after adding workloads",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for index, (title, why, command, fix) in enumerate(recommendations, start=1):
|
||||||
|
print(f"\n{index}. {title}")
|
||||||
|
print(f" Why: {why}")
|
||||||
|
print(f" Next command: {command}")
|
||||||
|
print(f" Fix path: {fix}")
|
||||||
|
|
||||||
|
print("\nRule of thumb")
|
||||||
|
print("- Add requests before deciding you need more VMs.")
|
||||||
|
print("- Taint Debian once Pimox workers are stable.")
|
||||||
|
print("- Add a Pimox VM only when real pressure remains after placement and requests are cleaned up.")
|
||||||
|
PY
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
KUBECONFIG_PATH="${KUBECONFIG:-${LAB_KUBECONFIG_PATH:-/home/jv/.kube/config}}"
|
||||||
|
PROFILE_FILE="${HOMELAB_RESOURCE_RECOMMENDATIONS:-${REPO_ROOT}/infra/resource-recommendations.yml}"
|
||||||
|
NAMESPACE="${1:-}"
|
||||||
|
PODS_JSON=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ -n "$PODS_JSON" ] && [ -f "$PODS_JSON" ]; then
|
||||||
|
rm -f "$PODS_JSON"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: ./jeannie capacity-limits [namespace]
|
||||||
|
|
||||||
|
Recommend Kubernetes requests/limits for containers missing them. This command
|
||||||
|
prints YAML snippets only; it does not edit manifests.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help|help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if ! command -v kubectl >/dev/null 2>&1; then
|
||||||
|
echo "kubectl is required." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -s "$KUBECONFIG_PATH" ]; then
|
||||||
|
echo "kubeconfig is missing: $KUBECONFIG_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PODS_JSON="$(mktemp "${TMPDIR:-/tmp}/jeannie-capacity-limits.XXXXXX")"
|
||||||
|
if [ -n "$NAMESPACE" ]; then
|
||||||
|
kubectl --kubeconfig "$KUBECONFIG_PATH" -n "$NAMESPACE" get pods -o json >"$PODS_JSON"
|
||||||
|
else
|
||||||
|
kubectl --kubeconfig "$KUBECONFIG_PATH" get pods -A -o json >"$PODS_JSON"
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "$PROFILE_FILE" "$PODS_JSON" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
profile_file, pods_json = sys.argv[1:3]
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
yaml = None
|
||||||
|
|
||||||
|
profiles = {
|
||||||
|
"defaults": {
|
||||||
|
"request_cpu_m": 50,
|
||||||
|
"request_memory_mib": 128,
|
||||||
|
"limit_cpu_multiplier": 4,
|
||||||
|
"limit_memory_multiplier": 2,
|
||||||
|
},
|
||||||
|
"namespaces": {},
|
||||||
|
}
|
||||||
|
if yaml:
|
||||||
|
with open(profile_file, encoding="utf-8") as handle:
|
||||||
|
loaded = yaml.safe_load(handle) or {}
|
||||||
|
profiles["defaults"].update(loaded.get("defaults", {}))
|
||||||
|
profiles["namespaces"].update(loaded.get("namespaces", {}))
|
||||||
|
|
||||||
|
with open(pods_json, encoding="utf-8") as handle:
|
||||||
|
pods = json.load(handle).get("items", [])
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
def profile_for(namespace):
|
||||||
|
profile = dict(profiles["defaults"])
|
||||||
|
profile.update(profiles["namespaces"].get(namespace, {}))
|
||||||
|
return profile
|
||||||
|
|
||||||
|
print("Jeannie Capacity Limits")
|
||||||
|
print("=======================")
|
||||||
|
print("mode: recommendations only")
|
||||||
|
print(f"profile_file: {profile_file}")
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for pod in pods:
|
||||||
|
namespace = pod["metadata"].get("namespace", "default")
|
||||||
|
owner = namespace
|
||||||
|
for ref in pod["metadata"].get("ownerReferences", []):
|
||||||
|
if ref.get("kind") in {"ReplicaSet", "StatefulSet", "DaemonSet", "Job"}:
|
||||||
|
owner = ref.get("name", owner)
|
||||||
|
break
|
||||||
|
for container in pod.get("spec", {}).get("containers", []):
|
||||||
|
resources = container.get("resources", {})
|
||||||
|
requests = resources.get("requests", {})
|
||||||
|
limits = resources.get("limits", {})
|
||||||
|
if requests.get("cpu") and requests.get("memory") and limits.get("cpu") and limits.get("memory"):
|
||||||
|
continue
|
||||||
|
key = (namespace, owner, container["name"])
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
profile = profile_for(namespace)
|
||||||
|
request_cpu = int(profile["request_cpu_m"])
|
||||||
|
request_memory = int(profile["request_memory_mib"])
|
||||||
|
limit_cpu = request_cpu * int(profile["limit_cpu_multiplier"])
|
||||||
|
limit_memory = request_memory * int(profile["limit_memory_multiplier"])
|
||||||
|
count += 1
|
||||||
|
print(f"\n{count}. {namespace}/{owner}:{container['name']}")
|
||||||
|
print(" Add or tune this resources block in the owning manifest/chart:")
|
||||||
|
print(" resources:")
|
||||||
|
print(" requests:")
|
||||||
|
print(f" cpu: {request_cpu}m")
|
||||||
|
print(f" memory: {request_memory}Mi")
|
||||||
|
print(" limits:")
|
||||||
|
print(f" cpu: {limit_cpu}m")
|
||||||
|
print(f" memory: {limit_memory}Mi")
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
print("\nNo containers missing requests/limits were found.")
|
||||||
|
else:
|
||||||
|
print("\nNotes:")
|
||||||
|
print("- Treat these as starting values, not truth.")
|
||||||
|
print("- Prefer app namespaces first; be careful with kube-system/control-plane pods.")
|
||||||
|
print("- After changes, run ./jeannie resource-budget and ./jeannie capacity.")
|
||||||
|
PY
|
||||||
|
|
@ -105,8 +105,12 @@ def read_tsv(path, key):
|
||||||
with open(path, encoding="utf-8", newline="") as handle:
|
with open(path, encoding="utf-8", newline="") as handle:
|
||||||
return {row[key].lower(): row for row in csv.DictReader(handle, delimiter="\t")}
|
return {row[key].lower(): row for row in csv.DictReader(handle, delimiter="\t")}
|
||||||
|
|
||||||
|
def read_tsv_rows(path):
|
||||||
|
with open(path, encoding="utf-8", newline="") as handle:
|
||||||
|
return list(csv.DictReader(handle, delimiter="\t"))
|
||||||
|
|
||||||
scorecard_info = read_tsv(scorecard_map, "label")
|
scorecard_info = read_tsv(scorecard_map, "label")
|
||||||
finding_info = read_tsv(findings_map, "pattern")
|
finding_rows = read_tsv_rows(findings_map)
|
||||||
|
|
||||||
with open(input_file, encoding="utf-8") as handle:
|
with open(input_file, encoding="utf-8") as handle:
|
||||||
text = handle.read()
|
text = handle.read()
|
||||||
|
|
@ -116,30 +120,32 @@ lines = text.splitlines()
|
||||||
def split_commands(value):
|
def split_commands(value):
|
||||||
return [item.strip() for item in (value or "").split(";") if item.strip() and item.strip() != "none"]
|
return [item.strip() for item in (value or "").split(";") if item.strip() and item.strip() != "none"]
|
||||||
|
|
||||||
def print_info(info):
|
def print_info(info, indent=" "):
|
||||||
diagnose = split_commands(info.get("diagnose_commands"))
|
diagnose = split_commands(info.get("diagnose_commands"))
|
||||||
jeannie_fix = split_commands(info.get("jeannie_fix_commands"))
|
jeannie_fix = split_commands(info.get("jeannie_fix_commands"))
|
||||||
other_fix = split_commands(info.get("other_fix_commands"))
|
other_fix = split_commands(info.get("other_fix_commands"))
|
||||||
|
|
||||||
print(f" means: {info.get('meaning') or 'No explain entry exists yet.'}")
|
print(f"{indent}Why: {info.get('meaning') or 'No explain entry exists yet.'}")
|
||||||
if diagnose:
|
if diagnose:
|
||||||
print(" diagnose:")
|
print(f"{indent}Next command: {diagnose[0]}")
|
||||||
for command in diagnose:
|
if len(diagnose) > 1:
|
||||||
print(f" {command}")
|
print(f"{indent}More checks:")
|
||||||
|
for command in diagnose[1:]:
|
||||||
|
print(f"{indent} {command}")
|
||||||
if jeannie_fix:
|
if jeannie_fix:
|
||||||
print(" Jeannie fix command exists: yes")
|
print(f"{indent}Jeannie fix command:")
|
||||||
for command in jeannie_fix:
|
for command in jeannie_fix:
|
||||||
print(f" {command}")
|
print(f"{indent} {command}")
|
||||||
else:
|
else:
|
||||||
print(" Jeannie fix command exists: no")
|
print(f"{indent}Jeannie fix command: none yet")
|
||||||
if other_fix:
|
if other_fix:
|
||||||
print(" non-Jeannie fix:")
|
print(f"{indent}Code/config fix:")
|
||||||
for command in other_fix:
|
for command in other_fix:
|
||||||
print(f" {command}")
|
print(f"{indent} {command}")
|
||||||
print(f" risk: {info.get('risk') or 'unknown'}")
|
print(f"{indent}Risk: {info.get('risk') or 'unknown'}")
|
||||||
print(f" auto-fix safe: {info.get('auto_fix') or 'no'}")
|
print(f"{indent}Auto-fix safe: {info.get('auto_fix') or 'no'}")
|
||||||
if info.get("notes"):
|
if info.get("notes"):
|
||||||
print(f" note: {info['notes']}")
|
print(f"{indent}Note: {info['notes']}")
|
||||||
|
|
||||||
def parse_scorecard():
|
def parse_scorecard():
|
||||||
rows = []
|
rows = []
|
||||||
|
|
@ -191,11 +197,35 @@ def interesting_lines():
|
||||||
def match_findings(line):
|
def match_findings(line):
|
||||||
lowered = line.lower()
|
lowered = line.lower()
|
||||||
matches = []
|
matches = []
|
||||||
for pattern, info in finding_info.items():
|
for info in finding_rows:
|
||||||
|
pattern = (info.get("pattern") or "").lower()
|
||||||
if pattern and pattern in lowered:
|
if pattern and pattern in lowered:
|
||||||
matches.append((pattern, info))
|
matches.append((pattern, info))
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
def best_finding(line):
|
||||||
|
matches = match_findings(line)
|
||||||
|
if not matches:
|
||||||
|
return None
|
||||||
|
return sorted(matches, key=lambda item: (len(item[0]), item[0]), reverse=True)[0]
|
||||||
|
|
||||||
|
def parse_report_line(line):
|
||||||
|
report_match = re.match(r"^(?P<status>ok|warn|fail|skip)\s+(?P<check>.+?)(?:\s{2,}(?P<summary>.+))?$", line)
|
||||||
|
if report_match:
|
||||||
|
return (
|
||||||
|
report_match.group("check").strip(),
|
||||||
|
report_match.group("status").strip(),
|
||||||
|
(report_match.group("summary") or "").strip(),
|
||||||
|
)
|
||||||
|
scorecard_match = re.match(r"^(?P<label>.{1,28}) (?P<status>pass|warn|fail)(?: - (?P<summary>.*))?$", line)
|
||||||
|
if scorecard_match:
|
||||||
|
return (
|
||||||
|
scorecard_match.group("label").strip(),
|
||||||
|
scorecard_match.group("status").strip(),
|
||||||
|
(scorecard_match.group("summary") or "").strip(),
|
||||||
|
)
|
||||||
|
return (line[:72], "", "")
|
||||||
|
|
||||||
print(f"Jeannie Explain: {topic}")
|
print(f"Jeannie Explain: {topic}")
|
||||||
print("=" * (17 + len(topic)))
|
print("=" * (17 + len(topic)))
|
||||||
print("mode: read-only")
|
print("mode: read-only")
|
||||||
|
|
@ -204,13 +234,13 @@ if topic == "scorecard":
|
||||||
rows = parse_scorecard()
|
rows = parse_scorecard()
|
||||||
if not rows:
|
if not rows:
|
||||||
print("\nNo scorecard warnings or failures found.")
|
print("\nNo scorecard warnings or failures found.")
|
||||||
for label, status, summary in rows:
|
for index, (label, status, summary) in enumerate(rows, start=1):
|
||||||
info = scorecard_info.get(label.lower(), {})
|
info = scorecard_info.get(label.lower(), {})
|
||||||
print(f"\n{label}")
|
print(f"\n{index}. {label}")
|
||||||
print(f" status: {status}")
|
print(f" Status: {status}")
|
||||||
if summary:
|
if summary:
|
||||||
print(f" observed: {summary}")
|
print(f" Observed: {summary}")
|
||||||
print_info(info)
|
print_info(info, " ")
|
||||||
raise SystemExit(0)
|
raise SystemExit(0)
|
||||||
|
|
||||||
found_lines = interesting_lines()
|
found_lines = interesting_lines()
|
||||||
|
|
@ -218,17 +248,26 @@ seen_patterns = set()
|
||||||
matched_any = False
|
matched_any = False
|
||||||
|
|
||||||
for line in found_lines:
|
for line in found_lines:
|
||||||
matches = match_findings(line)
|
title, status, summary = parse_report_line(line)
|
||||||
if not matches:
|
match = best_finding(summary) if summary else None
|
||||||
|
if not match:
|
||||||
|
match = best_finding(line)
|
||||||
|
if not match:
|
||||||
continue
|
continue
|
||||||
print(f"\nFinding: {line}")
|
pattern, info = match
|
||||||
for pattern, info in matches:
|
if pattern in seen_patterns:
|
||||||
if pattern in seen_patterns:
|
continue
|
||||||
continue
|
seen_patterns.add(pattern)
|
||||||
seen_patterns.add(pattern)
|
matched_any = True
|
||||||
matched_any = True
|
print(f"\n{len(seen_patterns)}. {title}")
|
||||||
print(f" matched: {pattern}")
|
if status:
|
||||||
print_info(info)
|
print(f" Status: {status}")
|
||||||
|
if summary:
|
||||||
|
print(f" Observed: {summary}")
|
||||||
|
else:
|
||||||
|
print(f" Observed: {line}")
|
||||||
|
print(f" Matched rule: {pattern}")
|
||||||
|
print_info(info, " ")
|
||||||
|
|
||||||
if not matched_any:
|
if not matched_any:
|
||||||
if found_lines:
|
if found_lines:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
REF="${1:-HEAD}"
|
||||||
|
changed_files=()
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: ./jeannie review-last-change [REF]
|
||||||
|
|
||||||
|
Summarize a commit, show touched files, run impact analysis, and print useful
|
||||||
|
validation and rollback hints. Defaults to HEAD.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$REF" in
|
||||||
|
-h|--help|help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if ! git -C "$REPO_ROOT" rev-parse --verify "$REF" >/dev/null 2>&1; then
|
||||||
|
echo "Unknown git ref: $REF" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Jeannie Last Change Review"
|
||||||
|
echo "=========================="
|
||||||
|
echo
|
||||||
|
git -C "$REPO_ROOT" show --no-renames --stat --oneline "$REF"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Changed files"
|
||||||
|
mapfile -t changed_files < <(git -C "$REPO_ROOT" diff-tree --no-commit-id --name-only -r "$REF")
|
||||||
|
printf ' %s\n' "${changed_files[@]}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
"${REPO_ROOT}/scripts/impact" "${changed_files[@]}"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Generic validation
|
||||||
|
./jeannie validate
|
||||||
|
./jeannie status
|
||||||
|
|
||||||
|
Rollback hint
|
||||||
|
git revert ${REF}
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- Prefer the impact-specific validation commands above before running ./jeannie up.
|
||||||
|
- For public route changes, validate edge and certs before app rollout.
|
||||||
|
EOF
|
||||||
Loading…
Reference in New Issue