159 lines
7.6 KiB
Bash
Executable File
159 lines
7.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =====================================================================
|
|
# TeamVis — Update einer Self-Host-Instanz
|
|
# =====================================================================
|
|
# Aktualisiert eine bestehende Instanz in einem Rutsch:
|
|
# 1. Pre-Update-Backup der Datenbank (Modus 2)
|
|
# 2. `git pull` im Bundle → neue Migrationen + Skripte
|
|
# 3. Fehlende Migrationen einspielen — jede in EINER Transaktion zusammen
|
|
# mit ihrem Eintrag in public.teamvis_schema_migrations (Modus 2)
|
|
# 4. neues App-Image ziehen + Container neu starten
|
|
#
|
|
# Warum ein Ledger in der DB? Früher diente `git diff OLD NEW` als Ledger.
|
|
# Schlug dabei eine Migration fehl, war HEAD trotzdem schon vorgerückt — der
|
|
# nächste Lauf meldete „keine neuen Migrationen", die restlichen fehlten
|
|
# dauerhaft, und eine halb angewendete Migration blieb unbemerkt (Review
|
|
# 04.10.2026). Jetzt gilt: eingespielt ist, was in der Tabelle steht; eine
|
|
# fehlgeschlagene Migration rollt komplett zurück und wird beim nächsten
|
|
# Lauf erneut versucht.
|
|
#
|
|
# Bestandsinstanzen ohne Tabelle: Beim ersten Lauf wird sie angelegt und mit
|
|
# allen Migrationen des Stands VOR dem Pull befüllt — genau die galten nach
|
|
# der alten Logik als eingespielt.
|
|
#
|
|
# Aufruf: bash deploy/selfhost/update.sh [instanz-verzeichnis]
|
|
# (ohne Argument: das Instanz-Verzeichnis wird automatisch erkannt, wenn
|
|
# es genau eines gibt.)
|
|
set -euo pipefail
|
|
|
|
c_bold=$'\033[1m'; c_grn=$'\033[32m'; c_red=$'\033[31m'; c_yel=$'\033[33m'; c_rst=$'\033[0m'
|
|
say() { printf '%s\n' "$*"; }
|
|
head() { printf '\n%s%s%s\n' "$c_bold" "$*" "$c_rst"; }
|
|
ok() { printf '%s✓%s %s\n' "$c_grn" "$c_rst" "$*"; }
|
|
warn() { printf '%s!%s %s\n' "$c_yel" "$c_rst" "$*"; }
|
|
die() { printf '%s✗ %s%s\n' "$c_red" "$*" "$c_rst" >&2; exit 1; }
|
|
|
|
BUNDLE="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
cd "$BUNDLE"
|
|
[ -d .git ] || die "Kein git-Repo in $BUNDLE — Updates laufen über 'git pull'. Bitte das Bundle per 'git clone' beziehen."
|
|
command -v docker >/dev/null || die "docker fehlt."
|
|
|
|
# ── Instanz-Verzeichnis bestimmen (enthält docker-compose.yml + .env) ──
|
|
INSTANCE="${1:-}"
|
|
if [ -z "$INSTANCE" ]; then
|
|
mapfile -t cands < <(
|
|
for d in "$BUNDLE"/*/; do
|
|
[ -f "${d}docker-compose.yml" ] && [ -f "${d}.env" ] && printf '%s\n' "${d%/}"
|
|
done
|
|
)
|
|
if [ "${#cands[@]}" -eq 1 ]; then
|
|
INSTANCE="${cands[0]}"
|
|
elif [ "${#cands[@]}" -eq 0 ]; then
|
|
die "Kein Instanz-Verzeichnis gefunden. Bitte als Argument angeben: bash deploy/selfhost/update.sh <verzeichnis>"
|
|
else
|
|
say "Mehrere Instanzen gefunden:"; printf ' %s\n' "${cands[@]}"
|
|
die "Bitte das gewünschte Verzeichnis als Argument angeben."
|
|
fi
|
|
fi
|
|
[ -f "$INSTANCE/docker-compose.yml" ] || die "In $INSTANCE liegt keine docker-compose.yml."
|
|
ok "Instanz: $INSTANCE"
|
|
|
|
# Modus erkennen: eigener db-Service (Modus 2) oder externe Supabase (Modus 1).
|
|
MODE2=0
|
|
grep -qE '^[[:space:]]+db:' "$INSTANCE/docker-compose.yml" && MODE2=1
|
|
|
|
POSTGRES_DB="$(grep -E '^POSTGRES_DB=' "$INSTANCE/.env" 2>/dev/null | cut -d= -f2- || true)"
|
|
POSTGRES_DB="${POSTGRES_DB:-postgres}"
|
|
PGPW="$(grep -E '^POSTGRES_PASSWORD=' "$INSTANCE/.env" 2>/dev/null | cut -d= -f2- || true)"
|
|
dbsql() { ( cd "$INSTANCE" && docker compose exec -T -e PGPASSWORD="$PGPW" db psql -v ON_ERROR_STOP=1 -U supabase_admin -d "$POSTGRES_DB" "$@" ); }
|
|
|
|
# ── 1. Pre-Update-Backup (nur Modus 2 — lokale DB) ────────────────────
|
|
if [ "$MODE2" = "1" ]; then
|
|
head "1) Datenbank-Backup"
|
|
ts="$(date +%Y%m%d-%H%M%S)"
|
|
dump="$INSTANCE/backup-db-$ts.sql"
|
|
# Der Dump enthält alle Personendaten — nur für den Eigentümer lesbar
|
|
# anlegen (vorher landete er mit 644 im Instanz-Verzeichnis).
|
|
install -m 600 /dev/null "$dump"
|
|
if ( cd "$INSTANCE" && docker compose exec -T -e PGPASSWORD="$PGPW" db pg_dump -U supabase_admin "$POSTGRES_DB" ) > "$dump" 2>/dev/null; then
|
|
ok "Backup: $dump ($(wc -l < "$dump") Zeilen)"
|
|
else
|
|
warn "Backup fehlgeschlagen — trotzdem fortfahren? (Strg-C zum Abbrechen)"; read -r _ || true
|
|
fi
|
|
else
|
|
warn "Externe Supabase (Modus 1): Backup macht dein Supabase-Anbieter. Kein lokaler Dump."
|
|
fi
|
|
|
|
# ── 2. git pull → neue Migrationen/Skripte ────────────────────────────
|
|
head "2) Bundle aktualisieren (git pull)"
|
|
OLD="$(git rev-parse HEAD)"
|
|
git pull --ff-only origin main || die "git pull fehlgeschlagen (abweichende Branches?). Notfalls: git fetch && git reset --hard origin/main"
|
|
NEW="$(git rev-parse HEAD)"
|
|
if [ "$OLD" = "$NEW" ]; then
|
|
ok "Schon aktuell (keine neuen Commits)."
|
|
else
|
|
ok "Aktualisiert: ${OLD:0:8} → ${NEW:0:8}"
|
|
fi
|
|
|
|
# ── 3. Fehlende Migrationen einspielen ────────────────────────────────
|
|
head "3) Migrationen"
|
|
if [ "$MODE2" = "1" ]; then
|
|
LEDGER="public.teamvis_schema_migrations"
|
|
if [ "$(dbsql -tAc "select to_regclass('$LEDGER') is not null")" != "t" ]; then
|
|
say "Migrations-Ledger fehlt — wird angelegt und mit dem bisherigen Stand (${OLD:0:8}) befüllt."
|
|
{
|
|
echo "begin;"
|
|
echo "create table $LEDGER (filename text primary key, applied_at timestamptz not null default now());"
|
|
echo "revoke all on $LEDGER from anon, authenticated;"
|
|
git ls-tree --name-only "$OLD" -- supabase/migrations/ | grep -E '\.sql$' | sort \
|
|
| sed -E "s#^supabase/migrations/(.*)\$#insert into $LEDGER (filename) values ('\\1');#"
|
|
echo "commit;"
|
|
} | dbsql >/dev/null
|
|
ok "Ledger angelegt ($(dbsql -tAc "select count(*) from $LEDGER") Einträge)."
|
|
fi
|
|
|
|
mapfile -t APPLIED < <(dbsql -tAc "select filename from $LEDGER")
|
|
declare -A DONE=()
|
|
for f in "${APPLIED[@]}"; do [ -n "$f" ] && DONE["$f"]=1; done
|
|
|
|
PENDING=()
|
|
for m in $(ls supabase/migrations/*.sql | sort); do
|
|
[ -z "${DONE[$(basename "$m")]:-}" ] && PENDING+=("$m")
|
|
done
|
|
|
|
if [ "${#PENDING[@]}" -eq 0 ]; then
|
|
ok "Keine offenen Migrationen."
|
|
else
|
|
for m in "${PENDING[@]}"; do
|
|
name="$(basename "$m")"
|
|
say "→ $name"
|
|
# Migration + Ledger-Eintrag atomar: scheitert die Migration, bleibt
|
|
# weder ein halber Schema-Stand noch ein Eintrag zurück.
|
|
{ cat "$BUNDLE/$m"; printf "\ninsert into %s (filename) values ('%s');\n" "$LEDGER" "$name"; } \
|
|
| dbsql --single-transaction >/dev/null \
|
|
|| die "Migration $name fehlgeschlagen und vollständig zurückgerollt. Fehler beheben und update.sh erneut starten — bereits eingespielte Migrationen bleiben erhalten, die App wurde NICHT aktualisiert."
|
|
done
|
|
ok "${#PENDING[@]} Migration(en) eingespielt."
|
|
fi
|
|
else
|
|
mapfile -t NEWMIG < <(git diff --diff-filter=A --name-only "$OLD" "$NEW" -- 'supabase/migrations/*.sql' 2>/dev/null | sort)
|
|
if [ "${#NEWMIG[@]}" -eq 0 ]; then
|
|
ok "Keine neuen Migrationen in diesem Update."
|
|
else
|
|
warn "Externe Supabase: bitte diese neuen Migrationen EINMAL in Supabase Studio (SQL-Editor) ausführen:"
|
|
for m in "${NEWMIG[@]}"; do say " $BUNDLE/$m"; done
|
|
warn "Schlägt eine fehl: nicht weitermachen, sondern Fehler beheben — update.sh zeigt sie beim nächsten Lauf NICHT erneut an."
|
|
read -r -p " Enter drücken, sobald sie eingespielt sind … " _ || true
|
|
fi
|
|
fi
|
|
|
|
# ── 4. Neues Image ziehen + Container neu starten ─────────────────────
|
|
head "4) App aktualisieren"
|
|
( cd "$INSTANCE" && docker compose pull && docker compose up -d )
|
|
ok "Container neu gestartet."
|
|
|
|
head "Fertig 🎉"
|
|
SITE_URL="$(grep -E '^SITE_URL=|^NEXT_PUBLIC_SITE_URL=' "$INSTANCE/.env" 2>/dev/null | head -1 | cut -d= -f2-)"
|
|
[ -n "$SITE_URL" ] && say " $SITE_URL"
|
|
say " Bei Problemen Logs prüfen: ( cd $INSTANCE && docker compose logs -f app )"
|