Add GitOps validation promotion flow

This commit is contained in:
juvdiaz 2026-06-29 16:57:00 -06:00
parent 329a3d1abd
commit 0777bed10c
6 changed files with 172 additions and 12 deletions

View File

@ -4,10 +4,35 @@ name: Homelab Main
push: push:
branches: branches:
- main - main
- dev
jobs: 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: deploy:
runs-on: homelab-debian runs-on: homelab-debian
needs:
- validate
if: ${{ gitea.ref == 'refs/heads/main' }}
steps: steps:
- name: Check out repository - name: Check out repository
@ -21,12 +46,6 @@ jobs:
git checkout --force "${{ gitea.sha }}" git checkout --force "${{ gitea.sha }}"
git config --global --add safe.directory "$PWD" 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 - name: Block automatic deploy for external Gitea changes
run: | run: |
set -euo pipefail set -euo pipefail

View File

@ -760,8 +760,21 @@ ls -lh /home/jv/backups/gitea-restore-drills
## Gitea Actions ## Gitea Actions
This repo includes a Gitea Actions workflow at This repo includes a Gitea Actions workflow at
`.gitea/workflows/homelab-main.yml`. It runs only on pushes to `main` and targets `.gitea/workflows/homelab-main.yml`. It runs validation on pushes to `dev` and
a repository-scoped Debian host runner with the label `homelab-debian`. `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 The workflow only blocks automatic deploy for external Gitea service
changes: files under `infra/gitea/`, or edits inside the `deploy_gitea`, changes: files under `infra/gitea/`, or edits inside the `deploy_gitea`,

View File

@ -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, It reports host memory/disk, systemd services, Docker Compose stacks,
Kubernetes health when the API is reachable, Pimox worker VM status, RPi 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 Focused doctor commands run narrower read-only checks and print the most likely
next step: next step:
@ -758,8 +760,21 @@ ls -lh /home/jv/backups/gitea-restore-drills
## Gitea Actions ## Gitea Actions
This repo includes a Gitea Actions workflow at This repo includes a Gitea Actions workflow at
`.gitea/workflows/homelab-main.yml`. It runs only on pushes to `main` and targets `.gitea/workflows/homelab-main.yml`. It runs validation on pushes to `dev` and
a repository-scoped Debian host runner with the label `homelab-debian`. `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 The workflow only blocks automatic deploy for external Gitea service
changes: files under `infra/gitea/`, or edits inside the `deploy_gitea`, changes: files under `infra/gitea/`, or edits inside the `deploy_gitea`,

View File

@ -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 deployment readiness, pod restarts, node pressure, disk pressure, Traefik
5xx/404 log evidence, and recent Gitea errors. 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` `nuke`
: Destroy Kubernetes state and Pimox worker VMs. Requires : Destroy Kubernetes state and Pimox worker VMs. Requires
`LAB_CONFIRM_NUKE=homelab`. `LAB_CONFIRM_NUKE=homelab`.

View File

@ -5939,6 +5939,10 @@ security_attack_path() {
"${REPO_ROOT}/scripts/security-attack-path" "${@:2}" "${REPO_ROOT}/scripts/security-attack-path" "${@:2}"
} }
validate_homelab() {
"${REPO_ROOT}/scripts/validate-homelab"
}
case "${1:-}" in case "${1:-}" in
up) up)
up up
@ -5958,6 +5962,9 @@ case "${1:-}" in
status) status)
status_report status_report
;; ;;
validate)
validate_homelab
;;
apps) apps)
require_debian_server "apps" require_debian_server "apps"
jeannie_log_start "apps" jeannie_log_start "apps"
@ -6087,7 +6094,7 @@ case "${1:-}" in
echo "Log: ${JEANNIE_LOG_FILE}" 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 exit 1
;; ;;
esac esac

101
scripts/validate-homelab Executable file
View File

@ -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 "$@"