#!/bin/bash # Major-Release-Cut auf Prod (z.B. 1.x → 2.0). # # Sonderfall — laeuft EINMAL pro Major. Fuer normale Updates auf demselben # Major-Branch: scripts/deploy.sh. # # Was hier passiert: # 1. Pre-flight: Tag + release/X.0-Branch existieren? Lokales Repo clean? # 2. DB-Dumps prod + dev frisch ziehen # 3. Manuelles Restic-Backup # 4. Frischer git-clone auf vserver in Parallel-Pfad # 5. Praeparierte data/-Dir vorbereiten: # - dev's data als Basis (enthaelt aktuelles Schema, embeddings, votes) # - assessments/jobs/news/PM/monitoring leeren (Frischstart) # - Behalten: plenum_vote_results, abgeordnetenwatch_votes/polls # 6. backups/, reports/, .env aus altem Pfad kopieren # 7. Container down → Pfad-Switch → Container up # 8. Smoke-Tests # # Usage: # ./scripts/major-release-cut.sh v2.0.0 release/2.0 # # Voraussetzungen: # - Tag + Branch sind bereits gepusht # - Lokal $RELEASE_BRANCH ist ausgecheckt und aktuell # - SSH-Zugang zu vserver # # Falls Gitea-seitig "early EOF / repository corruption": # git bundle create /tmp/release.bundle # scp /tmp/release.bundle vserver:/tmp/ # ssh vserver 'sudo git clone -b /tmp/release.bundle ' set -euo pipefail if [ $# -lt 2 ]; then cat < Beispiel: $0 v2.0.0 release/2.0 EOF exit 1 fi TAG="$1" BRANCH="$2" SERVER="vserver" PROD_DIR="/opt/gwoe-antragspruefer" DEV_DIR="/opt/gwoe-antragspruefer-dev" NEW_DIR="/opt/gwoe-antragspruefer-${TAG#v}" # z.B. /opt/gwoe-antragspruefer-2.0.0 ARCHIVE_DIR="/opt/gwoe-antragspruefer-$(date +%Y%m%d-%H%M%S)-archive" PUBLIC_URL="https://gwoe.toppyr.de" REPO_URL="https://repo.toppyr.de/tobias/gwoe-antragspruefer.git" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" cd "$PROJECT_DIR" echo "=== Major-Release-Cut auf $PUBLIC_URL ===" echo "Tag: $TAG" echo "Branch: $BRANCH" echo "Neu: $NEW_DIR" echo "Alt: $ARCHIVE_DIR (Archiv)" echo # Pre-flight if [ -n "$(git status --porcelain)" ]; then echo "✗ Lokales Repo nicht clean. Erst commiten/stashen." exit 1 fi if ! git rev-parse "$TAG" >/dev/null 2>&1; then echo "✗ Tag $TAG existiert lokal nicht." exit 1 fi read -p "⚠ Major-Cut auf prod jetzt durchfuehren? [yes/N] " ans [ "$ans" = "yes" ] || exit 1 # 1. Frische DB-Dumps prod + dev echo echo "1️⃣ Frische DB-Dumps..." ssh "$SERVER" "$PROD_DIR/scripts/backup-db.sh && $DEV_DIR/scripts/backup-db.sh" 2>&1 | tail -2 # 2. Manuelles Restic-Backup echo echo "2️⃣ Restic-Backup..." ssh "$SERVER" "sudo /home/dotty/backup-scripts/vserver-backup.sh" 2>&1 | tail -3 # 3. Frischer Clone auf vserver (mit Bundle-Fallback bei Gitea-Korruption) echo echo "3️⃣ Klone $BRANCH nach $NEW_DIR..." if ! ssh "$SERVER" "sudo git clone -b '$BRANCH' --single-branch '$REPO_URL' '$NEW_DIR' 2>&1" | tail -3 | grep -q "done"; then echo " Gitea-Clone fehlgeschlagen — versuche Bundle-Fallback..." git bundle create "/tmp/${TAG}.bundle" "$BRANCH" 2>&1 | tail -1 scp "/tmp/${TAG}.bundle" "$SERVER:/tmp/" ssh "$SERVER" "sudo rm -rf '$NEW_DIR' 2>/dev/null; sudo git clone -b '$BRANCH' '/tmp/${TAG}.bundle' '$NEW_DIR' 2>&1 | tail -3" ssh "$SERVER" "cd '$NEW_DIR' && sudo git remote set-url origin '$REPO_URL'" fi ssh "$SERVER" "sudo chown -R dotty:dotty '$NEW_DIR' && cd '$NEW_DIR' && git log --oneline -1" # 4. Praeparierte data/-Dir: dev's Stand als Basis, ausgewaehlte Tabellen leeren echo echo "4️⃣ Bereite leere data/-Dir vor (basiert auf dev, assessments leer, votes behalten)..." ssh "$SERVER" "cp -a '$DEV_DIR/data/.' '$NEW_DIR/data/'" ssh "$SERVER" "python3 - <<'PY' import sqlite3 db = sqlite3.connect('$NEW_DIR/data/gwoe-antraege.db') TO_CLEAR = [ 'assessments', 'assessment_versions', 'presse_drafts', 'news_articles', 'auto_rate_runs', 'jobs', 'monitoring_scans', 'monitoring_daily_summary', 'auth_bypass_uses', 'comments', 'merkliste', 'bookmarks', 'email_subscriptions', 'votes', ] # Behalten: plenum_vote_results, abgeordnetenwatch_votes, abgeordnetenwatch_polls for t in TO_CLEAR: try: db.execute(f'DELETE FROM {t}') except Exception: pass try: db.execute('DELETE FROM sqlite_sequence') except Exception: pass db.commit() db.execute('VACUUM') print(' votes:', db.execute('SELECT COUNT(*) FROM plenum_vote_results').fetchone()[0]) print(' assessments:', db.execute('SELECT COUNT(*) FROM assessments').fetchone()[0]) db.close() PY" # 5. .env, backups/, reports/ aus altem Pfad echo echo "5️⃣ Kopiere .env, backups/, reports/ aus altem Pfad..." ssh "$SERVER" "cp '$PROD_DIR/.env' '$NEW_DIR/.env'" ssh "$SERVER" "cp -a '$PROD_DIR/backups' '$NEW_DIR/backups'" ssh "$SERVER" "cp -a '$PROD_DIR/reports' '$NEW_DIR/reports'" # 6. Switchover echo echo "6️⃣ Container down + Pfad-Switch..." ssh "$SERVER" "cd '$PROD_DIR' && docker compose down" 2>&1 | tail -3 ssh "$SERVER" "sudo mv '$PROD_DIR' '$ARCHIVE_DIR' && sudo mv '$NEW_DIR' '$PROD_DIR' && sudo chown -R dotty:dotty '$PROD_DIR'" ssh "$SERVER" "cd '$PROD_DIR' && docker compose up -d --build" 2>&1 | tail -5 # 7. Smoke-Tests echo echo "7️⃣ Smoke-Tests..." for i in $(seq 1 60); do response=$(curl -sS --max-time 3 "$PUBLIC_URL/health" 2>/dev/null || echo "") if echo "$response" | grep -q '"status":"ok"'; then version=$(echo "$response" | python3 -c "import json,sys; print(json.load(sys.stdin).get('version','?'))" 2>/dev/null) echo "✅ Health OK nach ${i}s — version $version" break fi sleep 1 done echo " Score-Histogram: $(curl -sk "$PUBLIC_URL/api/auswertungen/score-histogram" | python3 -c 'import json,sys; print(json.load(sys.stdin)["total"])')" echo " Bundeslaender: $(curl -sk "$PUBLIC_URL/api/bundeslaender" | python3 -c 'import json,sys; print(len(json.load(sys.stdin)))')" echo echo "=== Cut abgeschlossen ===" echo "Alt-Stand: $ARCHIVE_DIR (kann nach paar Tagen geloescht werden)" echo "Neuer Workflow: scripts/deploy.sh (auf Branch $BRANCH)"