rank 3. shared/services.yml is the single source of truth for every deployable service (hosts, unit dirs, units, path claims, tests, scrape/ dns/probe facts, aliases, stacks); scripts/service-map.py is its only reader (hosts/services/allows/plan/seams/owner/units/stack/get/render/ check). deploy-plan.py becomes a thin CLI wrapper; both --service allow-lists (deploy.sh, deploy-remote.sh) and the changed-test mapper are the inventory. Selection semantics: host scoping (a host's tree selects only its own services), the quadlet netns/network coupling graph resolved across directories (gluetun's riders live in their own dirs), stacks, and canonical restart order. Against the pinned old-planner fixture: 921 raw deltas reduced to 17, all intentional (anubis now maps; cross-host paths select nothing; the retired qwen-sharp template plans nothing and the live froggeric template plans frank-archon; dankserver/romm/romm.container, missed by the old filename rule, plans the romm trio). The fixture is regenerated as the new pin. Integrity gates: schema, unit claims, quadlet-file ownership, path-glob liveness, Pi-hole hosts / Prometheus jobs / DNS / homepage probes drift. tests/service_inventory.bats pins all of it plus the plan fixture and the deploy-plan CLI. Allow-list grep seams in hermes_webui/observability_quadlet become behavioral allows calls.
175 lines
5.7 KiB
Bash
Executable File
175 lines
5.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Run only the Bats tests affected by the staged paths.
|
|
#
|
|
# Usage:
|
|
# scripts/test-changed.sh # inspect the staged commit
|
|
# scripts/test-changed.sh --list # print selected test files
|
|
# scripts/test-changed.sh --paths FILE ...
|
|
#
|
|
# --paths is intended for tests and local experimentation. The normal mode is
|
|
# deliberately staged-only so a pre-commit hook cannot accidentally validate
|
|
# an unrelated dirty working-tree file.
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
LIST_ONLY=0
|
|
USE_PATHS=0
|
|
declare -a CHANGED=()
|
|
|
|
die() {
|
|
printf 'ERROR: %s\n' "$*" >&2
|
|
exit 2
|
|
}
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--list)
|
|
LIST_ONLY=1
|
|
shift
|
|
;;
|
|
--paths)
|
|
USE_PATHS=1
|
|
shift
|
|
while [ "$#" -gt 0 ] && [[ "$1" != --* ]]; do
|
|
CHANGED+=("$1")
|
|
shift
|
|
done
|
|
;;
|
|
--)
|
|
shift
|
|
CHANGED+=("$@")
|
|
break
|
|
;;
|
|
*)
|
|
die "unknown option '$1' (use --list or --paths FILE ...)"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
cd "$REPO_ROOT"
|
|
if [ "$USE_PATHS" -eq 0 ]; then
|
|
while IFS= read -r -d '' path; do
|
|
CHANGED+=("$path")
|
|
done < <(git diff --cached --name-only -z --diff-filter=ACMRTUXB)
|
|
fi
|
|
|
|
[ "${#CHANGED[@]}" -gt 0 ] || {
|
|
printf 'No staged paths; no focused tests to run.\n'
|
|
exit 0
|
|
}
|
|
|
|
declare -a TESTS=()
|
|
add_test() {
|
|
local test="$1"
|
|
local existing
|
|
MAPPED=1
|
|
for existing in "${TESTS[@]:-}"; do
|
|
[ "$existing" != "$test" ] || return 0
|
|
done
|
|
TESTS+=("$test")
|
|
}
|
|
|
|
# The path->test mapping has two layers:
|
|
# 1. cross-cutting seams that no single service owns (compose validity,
|
|
# env consistency, the deployer itself, docs, syntax);
|
|
# 2. the service inventory (shared/services.yml via service-map.py
|
|
# "seams"): every service the path selects on ANY host contributes its
|
|
# declared tests. Service seams are the services' own — the former
|
|
# shotgun fallbacks (every dankclaw quadlet unit ran the open-webui,
|
|
# pihole, and searxng seams) are retired; the inventory names each
|
|
# service's real seams, and adding a service is a data edit.
|
|
for path in "${CHANGED[@]}"; do
|
|
MAPPED=0
|
|
case "$path" in
|
|
tests/*.bats)
|
|
add_test "$path"
|
|
continue
|
|
;;
|
|
tests/helpers.bash|tests/run.sh)
|
|
die "$path is shared test infrastructure; run ./tests/run.sh explicitly"
|
|
;;
|
|
tests/e2e/*)
|
|
add_test tests/e2e_scaffold.bats
|
|
continue
|
|
;;
|
|
.githooks/*)
|
|
add_test tests/changed_tests.bats
|
|
continue
|
|
;;
|
|
# Archive material and host-level facts: a path under any
|
|
# .AGENTTRASH/ directory is retired-by-policy (the planner plans
|
|
# nothing from it), udev rules are applied by manual sudo install,
|
|
# reports are frozen artifacts, and ignore-lists change nothing at
|
|
# runtime. They select no focused test; without this case they would
|
|
# trip the mapping gate below.
|
|
.AGENTTRASH/*|*/.AGENTTRASH/*|dankclaw/udev/*|report/*|.gitignore|.env.example)
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
# Cross-cutting seams.
|
|
case "$path" in
|
|
dankserver/.env.example|dankclaw/.env.example)
|
|
add_test tests/env_consistency.bats
|
|
;;
|
|
dankserver/Caddyfile|dankclaw/Caddyfile)
|
|
add_test tests/caddyfile.bats
|
|
add_test tests/deployment_scoping.bats
|
|
;;
|
|
scripts/deploy.sh|scripts/deploy-remote.sh|scripts/deploy-plan.py|scripts/deploy-*-quadlet.sh)
|
|
add_test tests/deployment_flow.bats
|
|
add_test tests/deployment_scoping.bats
|
|
add_test tests/scripts_syntax.bats
|
|
;;
|
|
scripts/service-map.py|shared/services.yml)
|
|
add_test tests/service_inventory.bats
|
|
add_test tests/deployment_scoping.bats
|
|
add_test tests/scripts_syntax.bats
|
|
;;
|
|
scripts/lib/*.sh)
|
|
add_test tests/llm_gates.bats
|
|
add_test tests/scripts_syntax.bats
|
|
;;
|
|
serverAppConfigs-docs/*|README.md|AGENTS.md)
|
|
add_test tests/docs_completeness.bats
|
|
add_test tests/deployment_scoping.bats
|
|
;;
|
|
scripts/mount-entropy.sh)
|
|
add_test tests/mount_entropy.bats
|
|
add_test tests/scripts_syntax.bats
|
|
;;
|
|
old/scripts/*)
|
|
add_test tests/scripts_syntax.bats
|
|
;;
|
|
scripts/*)
|
|
add_test tests/scripts_syntax.bats
|
|
;;
|
|
esac
|
|
|
|
# The inventory's declared seams for every service this path selects.
|
|
while IFS= read -r seam; do
|
|
[ -n "$seam" ] && add_test "$seam"
|
|
done < <(python3 "$REPO_ROOT/scripts/service-map.py" seams -- "$path" 2>/dev/null || true)
|
|
|
|
# Mapping gate: a path that selected no test at all — no service seam,
|
|
# no cross-cutting rule — is an unmapped area of the repo; refuse to
|
|
# commit it silently (the old catch-all `die`).
|
|
if [ "$MAPPED" -eq 0 ]; then
|
|
die "no focused-test mapping for '$path'; map it in the inventory (shared/services.yml) before committing"
|
|
fi
|
|
done
|
|
|
|
# Mapped-but-untestable paths (udev rules, trash moves) legitimately produce an
|
|
# empty selection; that is a pass, not a bats usage error.
|
|
[ "${#TESTS[@]}" -gt 0 ] || exit 0
|
|
|
|
if [ "$LIST_ONLY" -eq 1 ]; then
|
|
printf '%s\n' "${TESTS[@]}"
|
|
exit 0
|
|
fi
|
|
|
|
command -v bats >/dev/null 2>&1 || die 'bats is not installed'
|
|
printf 'Running focused tests for %d changed path(s):\n' "${#CHANGED[@]}"
|
|
printf ' %s\n' "${TESTS[@]}"
|
|
bats --print-output-on-failure "${TESTS[@]}"
|