diff --git a/.gitea/workflows/homelab-main.yml b/.gitea/workflows/homelab-main.yml index ba91b18..2b40465 100644 --- a/.gitea/workflows/homelab-main.yml +++ b/.gitea/workflows/homelab-main.yml @@ -4,10 +4,35 @@ name: Homelab Main push: branches: - main + - dev jobs: + validate: + runs-on: homelab-debian + + steps: + - name: Check out repository + run: | + set -euo pipefail + + find . -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + + git init + git remote add origin ssh://git@192.168.100.73:32222/jv/my-homelab-configs.git + git fetch --prune origin +refs/heads/main:refs/remotes/origin/main +refs/heads/dev:refs/remotes/origin/dev +refs/tags/*:refs/tags/* + git checkout --force "${{ gitea.sha }}" + git config --global --add safe.directory "$PWD" + + - name: Validate repository + run: | + set -euo pipefail + + ./jeannie validate + deploy: runs-on: homelab-debian + needs: + - validate + if: ${{ gitea.ref == 'refs/heads/main' }} steps: - name: Check out repository @@ -21,12 +46,6 @@ jobs: git checkout --force "${{ gitea.sha }}" git config --global --add safe.directory "$PWD" - - name: Check generated docs - run: | - set -euo pipefail - - scripts/render-docs --check - - name: Block automatic deploy for external Gitea changes run: | set -euo pipefail diff --git a/README.md b/README.md index ec05a89..6989937 100644 --- a/README.md +++ b/README.md @@ -760,8 +760,21 @@ ls -lh /home/jv/backups/gitea-restore-drills ## Gitea Actions This repo includes a Gitea Actions workflow at -`.gitea/workflows/homelab-main.yml`. It runs only on pushes to `main` and targets -a repository-scoped Debian host runner with the label `homelab-debian`. +`.gitea/workflows/homelab-main.yml`. It runs validation on pushes to `dev` and +`main`, and deploys only from `main`. That keeps the promotion path simple: + +```text +dev -> ./jeannie validate in Gitea Actions -> main -> Argo CD sync +``` + +The workflow targets a repository-scoped Debian host runner with the label +`homelab-debian`. + +Run the same validation locally before promoting: + +```bash +./jeannie validate +``` The workflow only blocks automatic deploy for external Gitea service changes: files under `infra/gitea/`, or edits inside the `deploy_gitea`, diff --git a/README.md.tmpl b/README.md.tmpl index 9e1c92a..68fded6 100644 --- a/README.md.tmpl +++ b/README.md.tmpl @@ -274,7 +274,9 @@ Run a read-only health snapshot from the Debian server with: It reports host memory/disk, systemd services, Docker Compose stacks, Kubernetes health when the API is reachable, Pimox worker VM status, RPi -services, Tailscale, and key local/public HTTP endpoints. +services, Tailscale, key local/public HTTP endpoints, and "what broke" signals +such as deployment readiness, pod restarts, node pressure, disk pressure, +Traefik 5xx/404 log evidence, and recent Gitea errors. Focused doctor commands run narrower read-only checks and print the most likely next step: @@ -758,8 +760,21 @@ ls -lh /home/jv/backups/gitea-restore-drills ## Gitea Actions This repo includes a Gitea Actions workflow at -`.gitea/workflows/homelab-main.yml`. It runs only on pushes to `main` and targets -a repository-scoped Debian host runner with the label `homelab-debian`. +`.gitea/workflows/homelab-main.yml`. It runs validation on pushes to `dev` and +`main`, and deploys only from `main`. That keeps the promotion path simple: + +```text +dev -> ./{{ main_script }} validate in Gitea Actions -> main -> Argo CD sync +``` + +The workflow targets a repository-scoped Debian host runner with the label +`homelab-debian`. + +Run the same validation locally before promoting: + +```bash +./{{ main_script }} validate +``` The workflow only blocks automatic deploy for external Gitea service changes: files under `infra/gitea/`, or edits inside the `deploy_gitea`, diff --git a/docs/jeannie.1.md b/docs/jeannie.1.md index c4caba0..fd542c8 100644 --- a/docs/jeannie.1.md +++ b/docs/jeannie.1.md @@ -52,6 +52,11 @@ the older detailed tables. The cascade includes "what broke" signals for recent deployment readiness, pod restarts, node pressure, disk pressure, Traefik 5xx/404 log evidence, and recent Gitea errors. +`validate` +: Run non-mutating repository checks used by the GitOps promotion flow: +inventory, generated docs, Bash syntax, shellcheck, YAML parsing, OpenTofu +formatting, tailnet policy validation, and optional static security checks. + `nuke` : Destroy Kubernetes state and Pimox worker VMs. Requires `LAB_CONFIRM_NUKE=homelab`. diff --git a/jeannie b/jeannie index 93f5467..b07643e 100755 --- a/jeannie +++ b/jeannie @@ -5939,6 +5939,10 @@ security_attack_path() { "${REPO_ROOT}/scripts/security-attack-path" "${@:2}" } +validate_homelab() { + "${REPO_ROOT}/scripts/validate-homelab" +} + case "${1:-}" in up) up @@ -5958,6 +5962,9 @@ case "${1:-}" in status) status_report ;; + validate) + validate_homelab + ;; apps) require_debian_server "apps" jeannie_log_start "apps" @@ -6087,7 +6094,7 @@ case "${1:-}" in echo "Log: ${JEANNIE_LOG_FILE}" ;; *) - echo "Usage: $0 {up|plan [all|provisioning|cluster|platform|apps|edge]|rebuild-cluster|stop-cluster|start-cluster|status|apps|website-translation-model|website-ollama-listen|ollama-setup|deploy-gitea|rpi-services|bootstrap-gitea-repo|backup-gitea|drill-gitea-restore|install-gitea-runner|move-prometheus-stack-workers|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|state-backup|fix-debian-docker-root|secrets-init|secrets-check|tailnet-policy-check|ai-index|ai-check|security-scan|security-prepare|security-zap|security-k8s|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-runtime|security-attack-path|openwrt|nuke}" + echo "Usage: $0 {up|plan [all|provisioning|cluster|platform|apps|edge]|rebuild-cluster|stop-cluster|start-cluster|status|validate|apps|website-translation-model|website-ollama-listen|ollama-setup|deploy-gitea|rpi-services|bootstrap-gitea-repo|backup-gitea|drill-gitea-restore|install-gitea-runner|move-prometheus-stack-workers|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|state-backup|fix-debian-docker-root|secrets-init|secrets-check|tailnet-policy-check|ai-index|ai-check|security-scan|security-prepare|security-zap|security-k8s|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-runtime|security-attack-path|openwrt|nuke}" exit 1 ;; esac diff --git a/scripts/validate-homelab b/scripts/validate-homelab new file mode 100755 index 0000000..707d43c --- /dev/null +++ b/scripts/validate-homelab @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +run_step() { + local label="$1" + shift + + printf '\n== %s ==\n' "${label}" + "$@" +} + +shell_files() { + local path + + git -C "${REPO_ROOT}" ls-files | while IFS= read -r path; do + case "${path}" in + jeannie | *.sh) + printf '%s\n' "${path}" + ;; + scripts/*) + if head -n 1 "${REPO_ROOT}/${path}" | grep -Eq 'sh|bash'; then + printf '%s\n' "${path}" + fi + ;; + esac + done +} + +validate_bash_syntax() { + local files=() + local file + + while IFS= read -r file; do + files+=("${REPO_ROOT}/${file}") + done < <(shell_files) + + if ((${#files[@]} == 0)); then + echo "no shell files found" + return 0 + fi + bash -n "${files[@]}" +} + +validate_shellcheck() { + local files=() + local file + + while IFS= read -r file; do + files+=("${REPO_ROOT}/${file}") + done < <(shell_files) + + if ! command -v shellcheck >/dev/null 2>&1; then + echo "shellcheck not installed; skipping" + return 0 + fi + if ((${#files[@]} == 0)); then + echo "no shell files found" + return 0 + fi + shellcheck "${files[@]}" +} + +validate_yaml() { + if command -v ruby >/dev/null 2>&1; then + ruby -e 'require "yaml"; (Dir["**/*.yml"] + Dir["**/*.yaml"]).each { |path| next if path.start_with?(".git/"); YAML.load_stream(File.read(path)) }; puts "yaml parse ok"' + return 0 + fi + echo "ruby not installed; skipping generic YAML parse" +} + +validate_tofu_fmt() { + if ! command -v tofu >/dev/null 2>&1; then + echo "tofu not installed; skipping fmt check" + return 0 + fi + tofu fmt -check -recursive "${REPO_ROOT}/bootstrap" +} + +validate_security_static() { + "${REPO_ROOT}/scripts/validate-tailnet-policy" + if command -v trivy >/dev/null 2>&1; then + trivy config --quiet "${REPO_ROOT}" + else + echo "trivy not installed; skipping IaC security static scan" + fi +} + +main() { + cd "${REPO_ROOT}" + run_step "Inventory" "${REPO_ROOT}/scripts/validate-homelab-inventory" "${HOMELAB_INVENTORY_FILE:-${REPO_ROOT}/homelab.yml}" + run_step "Generated docs" "${REPO_ROOT}/scripts/render-docs" --check + run_step "Bash syntax" validate_bash_syntax + run_step "Shellcheck" validate_shellcheck + run_step "YAML parse" validate_yaml + run_step "OpenTofu formatting" validate_tofu_fmt + run_step "Security static checks" validate_security_static +} + +main "$@"