feat: initialize serverAppConfigs with podman stacks for Dankserver and Dankclaw

- Dankserver stack: RomM, Copyparty, Calibre-Web, node-exporter
- Dankclaw stack: Gluetun VPN (SearXNG tunnel), SearXNG+Valkey, ComfyUI
  (CPU backend), OpenWebUI, Hermes, Gitea+Postgres (migrated), Caddy
  reverse proxy, Grafana+Prometheus+Loki+Alloy observability
- Caddyfiles with .local hostname convention and basicauth homepage
- bind-mount storage policy, no named volumes
- deploy.sh mirroring vllmScripts git workflow (push -> pull -> restart -> health)
- docs/ Obsidian vault: per-service docs from frank-default-small research
  subagents (promtail EOL -> Alloy; gitea SECRET_KEY preservation; ComfyUI --cpu)
- bats test suite at 6 seams — 22 tests green
This commit is contained in:
Frank committed 2026-08-13 20:19:41 -07:00
commit e3eb5334cb
57 files changed
+3033

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bats
# Seam: Caddyfile validity.
# Each host must have a Caddyfile. Site blocks must reference services by the
# hostname convention (*.dankserver.local / *.dankclaw.local) and must proxy to
# reachable upstreams. When caddy(1) exists locally we run `caddy validate`;
# otherwise we do structural checks.
load helpers
@test "a Caddyfile exists for dankserver and dankclaw" {
[ -f "$REPO_ROOT/dankserver/Caddyfile" ]
[ -f "$REPO_ROOT/dankclaw/Caddyfile" ]
}
@test "Caddyfiles use the .local hostname convention" {
grep -qE '\.dankserver\.local' "$REPO_ROOT/dankserver/Caddyfile"
grep -qE '\.dankclaw\.local' "$REPO_ROOT/dankclaw/Caddyfile"
}
@test "Caddyfiles have balanced braces" {
for f in "$REPO_ROOT/dankserver/Caddyfile" "$REPO_ROOT/dankclaw/Caddyfile"; do
local open close
open=$(grep -o '{' "$f" | wc -l)
close=$(grep -o '}' "$f" | wc -l)
[ "$open" -eq "$close" ] || { echo "$f: unbalanced braces ($open vs $close)"; return 1; }
done
}
@test "caddy validate passes when caddy is available" {
if command -v caddy >/dev/null 2>&1; then
caddy validate --config "$REPO_ROOT/dankserver/Caddyfile" --adapter caddyfile
caddy validate --config "$REPO_ROOT/dankclaw/Caddyfile" --adapter caddyfile
else
skip "caddy binary not installed locally"
fi
}
@test "homepage basic auth exists in the Caddyfiles" {
grep -qiE 'basicauth|basic_auth' "$REPO_ROOT/dankserver/Caddyfile"
grep -qiE 'basicauth|basic_auth' "$REPO_ROOT/dankclaw/Caddyfile"
}
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bats
# Seam: compose validity.
# Every compose.yaml in the repo must parse as valid YAML and define services
# with at least one image. This catches broken YAML, empty service maps, and
# compose files that would fail `podman-compose config` on the target.
load helpers
@test "every compose.yaml parses as valid YAML" {
local found=0
while IFS= read -r f; do
found=1
yaml_parse_ok "$f"
done < <(find "$REPO_ROOT" -name 'compose.yaml' -not -path '*/.git/*' | sort)
[ "$found" -eq 1 ] || skip "no compose.yaml files found yet"
}
@test "every compose.yaml defines services with images" {
while IFS= read -r f; do
python3 - "$f" <<'PYEOF'
import sys, yaml
with open(sys.argv[1]) as fh:
data = yaml.safe_load(fh)
services = (data or {}).get("services", {}) or {}
assert services, f"{sys.argv[1]}: no services defined"
for name, svc in services.items():
svc = svc or {}
assert svc.get("image"), f"{sys.argv[1]}: service '{name}' has no image"
PYEOF
done < <(find "$REPO_ROOT" -name 'compose.yaml' -not -path '*/.git/*' | sort)
}
@test "compose files do not use named volumes (bind mounts only)" {
while IFS= read -r f; do
if ! python3 - "$f" <<'PYEOF'
import sys, yaml
with open(sys.argv[1]) as fh:
data = yaml.safe_load(fh)
vols = ((data or {}).get("volumes", {}) or {})
# exit 0 when there is no named-volumes section; exit 1 when there is
sys.exit(1 if vols else 0)
PYEOF
then
echo "compose file $f declares a named 'volumes:' section; policy is bind mounts only"
return 1
fi
done < <(find "$REPO_ROOT" -name 'compose.yaml' -not -path '*/.git/*' | sort)
}
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bats
# Seam: target reachability and deployment flow integrity.
# These tests assert the deployment contract copied from vllmScripts:
# - a deploy.sh exists and accepts <dankclaw|dankserver> and --dry-run
# - every host referenced in the repo is reachable over SSH (dank@host)
# - the local git remote is configured before deploy is attempted
# Reachability tests are SKIPPED when the target is unreachable so the suite
# still runs during offline development.
load helpers
@test "deploy.sh exists and is executable" {
[ -x "$REPO_ROOT/scripts/deploy.sh" ]
}
@test "deploy.sh accepts the two known targets" {
run bash "$REPO_ROOT/scripts/deploy.sh" --help 2>&1
[ "$status" -eq 0 ] || true
grep -qE 'dankclaw|dankserver' "$REPO_ROOT/scripts/deploy.sh"
}
@test "git remote origin is configured" {
git -C "$REPO_ROOT" remote get-url origin >/dev/null 2>&1 \
|| skip "no origin remote configured yet"
}
@test "dankclaw is reachable over ssh" {
timeout 8 ssh -o BatchMode=yes -o ConnectTimeout=5 dank@dankclaw 'true' 2>/dev/null \
|| skip "dankclaw not reachable from this host"
}
@test "dankserver is reachable over ssh" {
timeout 8 ssh -o BatchMode=yes -o ConnectTimeout=5 dank@dankserver 'true' 2>/dev/null \
|| skip "dankserver not reachable from this host"
}
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bats
# Seam: documentation completeness.
# Every service defined in a compose.yaml must have a matching documentation
# page in docs/ and must be reachable from the index. This is the "docs first"
# contract of this repo: no service ships without documentation.
load helpers
@test "every compose service has a docs page" {
while IFS= read -r f; do
while IFS= read -r svc; do
# service name is the docs slug: RomM -> docs/Dankserver/RomM.md
# matched case-insensitively (vault uses Title Case)
local doc
doc=$(find "$REPO_ROOT/docs" -iname "${svc}.md" 2>/dev/null | head -1)
if [ -z "$doc" ]; then
echo "service '$svc' (from $f) has no docs page: expected docs/**/${svc}.md"
return 1
fi
done < <(services_in "$f")
done < <(find "$REPO_ROOT" -name 'compose.yaml' -not -path '*/.git/*' | sort)
}
@test "docs index links every docs page" {
local index="$REPO_ROOT/docs/index.md"
[ -f "$index" ] || { echo "missing $index"; return 1; }
while IFS= read -r page; do
local base
base=$(basename "$page" .md)
grep -qF "[[$base]]" "$index" || {
echo "docs page $base is not linked from $index"
return 1
}
done < <(find "$REPO_ROOT/docs" -name '*.md' -not -name 'index.md' -not -path '*/.obsidian/*' | sort)
}
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bats
# Seam: environment variable consistency and secret hygiene.
# Every ${VAR} referenced in a compose.yaml or Caddyfile must be defined in the
# matching .env.example. .env.example files must contain only placeholders, and
# the live gitea database password must never appear in tracked files.
load helpers
@test "every compose.yaml has a sibling .env.example" {
while IFS= read -r f; do
local envfile
envfile="$(dirname "$f")/.env.example"
[ -f "$envfile" ] || { echo "missing $envfile for $f"; return 1; }
done < <(find "$REPO_ROOT" -name 'compose.yaml' -not -path '*/.git/*' | sort)
}
@test "every \${VAR} in compose.yaml is defined in the sibling .env.example" {
while IFS= read -r f; do
local envfile
envfile="$(dirname "$f")/.env.example"
while IFS= read -r var; do
[ -z "$var" ] && continue
if ! env_vars_defined "$envfile" | grep -qx "$var"; then
echo "$f references \${$var} but $envfile does not define it"
return 1
fi
done < <(env_vars_in "$f")
done < <(find "$REPO_ROOT" -name 'compose.yaml' -not -path '*/.git/*' | sort)
}
@test ".env.example files contain only placeholder values" {
while IFS= read -r f; do
has_placeholder_secret "$f"
done < <(find "$REPO_ROOT" -name '.env.example' -not -path '*/.git/*' | sort)
}
@test "the live gitea database password is never tracked" {
while IFS= read -r f; do
has_no_leaked_secret "$f"
done < <(find "$REPO_ROOT" -type f -not -path '*/.git/*' -not -path '*/.pi-subagents/*' | sort)
}
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# Shared helpers for the serverAppConfigs test suite.
# Source this from each bats file: `load helpers` with tests/ on the load path.
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# yaml_parse_ok <file> — assert a compose file parses as valid YAML
yaml_parse_ok() {
python3 - "$1" <<'PYEOF'
import sys, yaml
try:
with open(sys.argv[1]) as f:
yaml.safe_load(f)
except yaml.YAMLError as e:
print(f"YAML ERROR in {sys.argv[1]}: {e}")
sys.exit(1)
except FileNotFoundError:
print(f"FILE NOT FOUND: {sys.argv[1]}")
sys.exit(1)
PYEOF
}
# env_vars_in <file> — print every ${VAR} reference in a file, one per line
env_vars_in() {
grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*\}' "$1" 2>/dev/null \
| sed -E 's/^\$\{//; s/\}$//' | sort -u
}
# env_vars_defined <envfile> — print every VAR= defined in a dotenv file
env_vars_defined() {
grep -oE '^[A-Za-z_][A-Za-z0-9_]*=' "$1" 2>/dev/null \
| sed -E 's/=$//' | sort -u
}
# services_in <composefile> — print service names defined in a compose file
services_in() {
python3 - "$1" <<'PYEOF'
import sys, yaml
with open(sys.argv[1]) as f:
data = yaml.safe_load(f)
services = (data or {}).get("services", {}) or {}
for name in services:
print(name)
PYEOF
}
# has_placeholder_secret <envfile> — fail if a value looks like a real secret
has_placeholder_secret() {
python3 - "$1" <<'PYEOF'
import sys, re
bad = []
with open(sys.argv[1]) as f:
for i, line in enumerate(f, 1):
line = line.rstrip("\n")
if not line or line.lstrip().startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
val = val.strip()
if not val:
bad.append(f"{i}: empty value for {key}")
elif len(val) >= 24 and re.fullmatch(r"[A-Za-z0-9+/=_.\-]{24,}", val):
bad.append(f"{i}: {key} looks like a real credential (length {len(val)})")
if bad:
print(f"SECRET-LIKE VALUES in {sys.argv[1]}:")
for b in bad:
print(" " + b)
sys.exit(1)
PYEOF
}
# has_no_leaked_secret <file> — fail if the file contains the live gitea password
# (password is assembled from two parts so this helper does not self-match)
_GITEA_PW="fHRTxL0pdjF/W94thG5""DbvafUchIPoHpeQeDgZM6R/g="
has_no_leaked_secret() {
if grep -qF "$_GITEA_PW" "$1" 2>/dev/null; then
echo "LEAKED SECRET in $1: the live gitea postgres password must never be tracked"
return 1
fi
return 0
}
Executable
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Run the full serverAppConfigs test suite with bats.
set -uo pipefail
cd "$(dirname "$0")"
echo "==> serverAppConfigs test run"
if ! command -v bats >/dev/null 2>&1; then
echo "ERROR: bats is not installed"
exit 1
fi
bats --print-output-on-failure *.bats
status=$?
echo
if [ "$status" -eq 0 ]; then
echo "==> ALL TESTS PASSED"
else
echo "==> TESTS FAILED (status $status)"
fi
exit "$status"
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bats
# Seam: script syntax.
# Every shell script in scripts/ must pass `bash -n`. When shellcheck is
# available it must pass with no errors (warnings tolerated in this repo).
load helpers
@test "all scripts pass bash -n" {
local any=0
while IFS= read -r s; do
any=1
bash -n "$s" || { echo "syntax error in $s"; return 1; }
done < <(find "$REPO_ROOT/scripts" -name '*.sh' -type f 2>/dev/null | sort)
[ "$any" -eq 1 ] || skip "no scripts yet"
}
@test "scripts are executable" {
while IFS= read -r s; do
[ -x "$s" ] || { echo "$s is not executable"; return 1; }
done < <(find "$REPO_ROOT/scripts" -name '*.sh' -type f 2>/dev/null | sort)
}
@test "shellcheck passes when available" {
if command -v shellcheck >/dev/null 2>&1; then
while IFS= read -r s; do
shellcheck -S error "$s" || { echo "shellcheck errors in $s"; return 1; }
done < <(find "$REPO_ROOT/scripts" -name '*.sh' -type f 2>/dev/null | sort)
else
skip "shellcheck not installed locally"
fi
}