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:
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

View File

@ -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`,

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,
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`,

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
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`.

View File

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

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