feat(deploy): TrueNAS Custom-App + Auto-Deploy, plus aufgelaufene Arbeit
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 1m11s
CI / Frontend Tests (Node/Vite) (push) Failing after 4m59s
CI / Docker Build & Push (push) Has been skipped
CI / Deploy auf TrueNAS (Custom App) (push) Has been skipped

Deployment:
- custom-app.compose.yaml: self-contained Compose fuer TrueNAS "Custom App"
  (absolute Host-Bind-Pfade, postgres:18, pull_policy always, Port 8090)
- scripts/truenas-deploy.sh: Host-Skript create/redeploy via midclt (App
  bleibt unter Apps sichtbar) inkl. Image-Pull + Health-Check
- ci.yml Deploy-Job: laeuft auf ubuntu-latest-Runner, kopiert Deploy-Dateien
  per SSH auf den NAS-Host und triggert truenas-deploy.sh (statt runs-on goldeye)
- compose.yaml/.env.example: postgres:18 (Locale-Match zur Quell-DB), Port 8090
- .gitignore: .agents/, tools/rag/, deploy/truenas/.env (Secrets/Scratch)

Aufgelaufene Feature-Arbeit (verified/Freeze, Migrationen, Import-Triage):
- GerbilOverride/VerifiedGerbil-Endpoints + GerbilSnapshotService + Tests
- EF-Migrationen (ShowInChronicle, Stillborn, BirthOrder, ManualFlag, DSGVO)
- Frontend VerifizierteTierePage + verified-API + e2e-Spec
- diverse Import-/Triage-Skripte und -Tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-19 09:19:11 +02:00
parent 84365bba7f
commit 45b8533f18
93 changed files with 20477 additions and 432 deletions

View File

@@ -0,0 +1,91 @@
#!/bin/sh
# GerbilManager Deploy-Skript (laeuft auf der TrueNAS Goldeye)
# ===========================================================
# Holt die neuen Images aus der Gitea-Registry und rollt den Compose-Stack neu aus.
# Wird vom Gitea-Actions 'deploy'-Job (self-hosted Runner auf der NAS) aufgerufen
# UND kann jederzeit manuell auf der NAS ausgefuehrt werden:
#
# sh /opt/gerbilmanager/deploy/truenas/scripts/deploy.sh
# TAG=<git-sha> sh .../deploy.sh # Rollback auf ein bestimmtes Image
# GIT_PULL=0 sh .../deploy.sh # ohne 'git pull' (nur Images neu ziehen)
#
# Konfiguration ueber Umgebungsvariablen (alle optional, sinnvolle Defaults):
# COMPOSE_DIR Installationsverzeichnis (Default: /opt/gerbilmanager)
# TAG Image-Tag (Default: latest; fuer Rollback z. B. ein git-SHA)
# GIT_PULL 1 = vorher 'git pull' (Default), 0 = ueberspringen
# HEALTH_TIMEOUT Sekunden auf api-Healthcheck warten (Default: 180)
# REGISTRY / REGISTRY_USER / REGISTRY_TOKEN optionaler docker login vor dem Pull
set -eu
COMPOSE_DIR="${COMPOSE_DIR:-/opt/gerbilmanager}"
COMPOSE_FILE="$COMPOSE_DIR/deploy/truenas/compose.yaml"
TAG="${TAG:-latest}"
GIT_PULL="${GIT_PULL:-1}"
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-180}"
REGISTRY="${REGISTRY:-git.rismer.de}"
log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"; }
fail() { log "FEHLER: $1"; exit 1; }
export TAG # compose interpoliert ${TAG} in den Image-Referenzen
log "=== Deploy gestartet (TAG=$TAG, dir=$COMPOSE_DIR) ==="
[ -f "$COMPOSE_FILE" ] || fail "compose.yaml nicht gefunden: $COMPOSE_FILE"
command -v docker >/dev/null 2>&1 || fail "docker nicht im PATH des Runners"
docker compose version >/dev/null 2>&1 || fail "'docker compose' (v2) nicht verfuegbar"
cd "$COMPOSE_DIR"
# --- 1. Repo aktualisieren (compose.yaml / Skripte / Docs) ---
if [ "$GIT_PULL" = "1" ] && [ -d "$COMPOSE_DIR/.git" ]; then
log "git pull --ff-only ..."
git -C "$COMPOSE_DIR" pull --ff-only || log "WARNUNG: git pull fehlgeschlagen — fahre mit vorhandenem Stand fort"
fi
# --- 2. Registry-Login (nur wenn Credentials uebergeben wurden) ---
if [ -n "${REGISTRY_USER:-}" ] && [ -n "${REGISTRY_TOKEN:-}" ]; then
log "docker login $REGISTRY ..."
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin >/dev/null \
|| fail "docker login fehlgeschlagen"
fi
# --- 3. Neue Images ziehen ---
log "Ziehe Images (Tag: $TAG) ..."
docker compose -f "$COMPOSE_FILE" pull || fail "docker compose pull fehlgeschlagen"
# --- 4. Stack neu ausrollen (nur geaenderte Container werden neu erstellt) ---
log "Rolle Stack aus (up -d) ..."
docker compose -f "$COMPOSE_FILE" up -d --remove-orphans || fail "docker compose up fehlgeschlagen"
# --- 5. Auf api-Healthcheck warten (impliziert db gesund) ---
log "Warte auf api-Healthcheck (max ${HEALTH_TIMEOUT}s) ..."
API_CID="$(docker compose -f "$COMPOSE_FILE" ps -q api)"
[ -n "$API_CID" ] || fail "api-Container nicht gefunden"
elapsed=0
while :; do
state="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$API_CID" 2>/dev/null || echo unknown)"
case "$state" in
healthy) log "api ist gesund."; break ;;
none|unknown)
# Kein Healthcheck definiert/lesbar -> auf laufenden Zustand zurueckfallen.
running="$(docker inspect --format '{{.State.Running}}' "$API_CID" 2>/dev/null || echo false)"
[ "$running" = "true" ] && { log "api laeuft (kein Healthcheck-Status lesbar)."; break; }
;;
esac
if [ "$elapsed" -ge "$HEALTH_TIMEOUT" ]; then
log "Letzte api-Logs:"; docker compose -f "$COMPOSE_FILE" logs --tail 40 api || true
fail "api wurde nicht innerhalb ${HEALTH_TIMEOUT}s gesund (Status: $state)"
fi
sleep 5; elapsed=$((elapsed + 5))
done
# --- 6. Alte/ungenutzte Images aufraeumen ---
log "Raeume verwaiste Images auf ..."
docker image prune -f >/dev/null 2>&1 || true
log "Laufende Dienste:"
docker compose -f "$COMPOSE_FILE" ps --format 'table {{.Service}}\t{{.Status}}' 2>/dev/null \
|| docker compose -f "$COMPOSE_FILE" ps
log "=== Deploy erfolgreich abgeschlossen (TAG=$TAG) ==="

View File

@@ -0,0 +1,103 @@
#!/bin/sh
# GerbilManager — TrueNAS Custom-App Deploy/Redeploy (laeuft auf dem NAS-HOST)
# ===========================================================================
# Legt die App als TrueNAS "Custom App" an (sichtbar unter Apps) bzw. rollt sie
# neu aus. Wird vom Gitea-Actions-Deploy-Job per SSH aufgerufen UND kann jederzeit
# manuell auf dem NAS-Host ausgefuehrt werden:
#
# sh /opt/gerbilmanager/deploy/truenas/scripts/truenas-deploy.sh
#
# Voraussetzungen auf dem Host:
# * /opt/gerbilmanager/deploy/truenas/custom-app.compose.yaml (Vorlage)
# * /opt/gerbilmanager/deploy/truenas/.env (mit POSTGRES_PASSWORD etc. — NICHT im Repo)
# * midclt + docker im PATH (auf TrueNAS SCALE gegeben)
#
# Env-Overrides: COMPOSE_DIR, ENV_FILE, HEALTH_TIMEOUT
set -eu
APP_NAME=gerbilmanager
COMPOSE_DIR="${COMPOSE_DIR:-/opt/gerbilmanager}"
DEPLOY_DIR="$COMPOSE_DIR/deploy/truenas"
ENV_FILE="${ENV_FILE:-$DEPLOY_DIR/.env}"
TEMPLATE="$DEPLOY_DIR/custom-app.compose.yaml"
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-300}"
BASE=/mnt/JailStorage/DockerVolumes/gerbilmanager
log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"; }
fail() { log "FEHLER: $*"; exit 1; }
[ -f "$TEMPLATE" ] || fail "Vorlage fehlt: $TEMPLATE"
[ -f "$ENV_FILE" ] || fail ".env fehlt: $ENV_FILE (aus .env.example erstellen)"
command -v midclt >/dev/null 2>&1 || fail "midclt nicht im PATH"
command -v docker >/dev/null 2>&1 || fail "docker nicht im PATH"
# --- .env laden ---
set -a; . "$ENV_FILE"; set +a
: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in .env}"
export REGISTRY="${REGISTRY:-git.rismer.de/gulum}"
export TAG="${TAG:-latest}"
export PORT="${PORT:-8090}"
log "=== Deploy $APP_NAME (TAG=$TAG, PORT=$PORT) ==="
# --- Host-Verzeichnisse sicherstellen ---
for d in pgdata photos keys backups publicsite scripts; do mkdir -p "$BASE/$d"; done
chown -R 999:999 "$BASE/pgdata" 2>/dev/null || true
chmod 700 "$BASE/pgdata" 2>/dev/null || true
# Backup-Sidecar-Skripte an den absoluten Mount-Pfad kopieren
cp "$DEPLOY_DIR/scripts/backup.sh" "$DEPLOY_DIR/scripts/entrypoint.sh" \
"$DEPLOY_DIR/scripts/restore.sh" "$BASE/scripts/" 2>/dev/null || true
# --- Frische Images ziehen (garantiert :latest, unabhaengig vom pull_policy) ---
log "Ziehe Images ..."
docker pull "$REGISTRY/gerbilmanager-api:$TAG" || fail "docker pull api fehlgeschlagen"
docker pull "$REGISTRY/gerbilmanager-frontend:$TAG" || fail "docker pull frontend fehlgeschlagen"
# --- Vorlage rendern + midclt-Payload bauen (python: sichere Escapes) ---
PAYLOAD_FILE="$(mktemp)"
trap 'rm -f "$PAYLOAD_FILE"' EXIT
python3 - "$TEMPLATE" > "$PAYLOAD_FILE" <<'PY'
import sys, os, json
tmpl = open(sys.argv[1], encoding="utf-8").read()
repl = {
"__POSTGRES_PASSWORD__": os.environ["POSTGRES_PASSWORD"],
"__REGISTRY__": os.environ.get("REGISTRY", "git.rismer.de/gulum"),
"__TAG__": os.environ.get("TAG", "latest"),
"__PORT__": os.environ.get("PORT", "8090"),
"__AI_BASEURL__": os.environ.get("AI__BaseUrl", ""),
"__AI_APIKEY__": os.environ.get("AI__ApiKey", ""),
"__AI_MODEL__": os.environ.get("AI__Model", "gemini-flash-latest"),
}
for k, v in repl.items():
tmpl = tmpl.replace(k, v)
print(json.dumps({"custom_app": True, "app_name": "gerbilmanager",
"custom_compose_config_string": tmpl}))
PY
# --- Existiert die App schon? ---
EXISTS="$(midclt call app.query "[[\"name\",\"=\",\"$APP_NAME\"]]" 2>/dev/null | python3 -c 'import sys,json; print(len(json.load(sys.stdin)))' 2>/dev/null || echo 0)"
if [ "$EXISTS" -ge 1 ]; then
log "App existiert -> redeploy (frische Images sind bereits gezogen)"
midclt call app.redeploy "$APP_NAME" >/dev/null || fail "app.redeploy fehlgeschlagen"
else
log "App fehlt -> app.create (Custom App)"
midclt call app.create "$(cat "$PAYLOAD_FILE")" >/dev/null || fail "app.create fehlgeschlagen"
fi
# --- Auf Gesundheit warten (Frontend proxyt /api/health -> API) ---
log "Warte auf /api/health (max ${HEALTH_TIMEOUT}s) ..."
elapsed=0
while :; do
if curl -fsS "http://localhost:$PORT/api/health" 2>/dev/null | grep -qi 'healthy'; then
log "App ist gesund."; break
fi
[ "$elapsed" -ge "$HEALTH_TIMEOUT" ] && { \
log "Letzte App-Logs:"; midclt call app.query "[[\"name\",\"=\",\"$APP_NAME\"]]" >/dev/null 2>&1 || true; \
fail "App wurde nicht innerhalb ${HEALTH_TIMEOUT}s gesund"; }
sleep 5; elapsed=$((elapsed + 5))
done
log "Aufraeumen ungenutzter Images ..."
docker image prune -f >/dev/null 2>&1 || true
log "=== Deploy erfolgreich ($APP_NAME, TAG=$TAG) ==="