diff --git a/scripts/deploy.sh b/scripts/deploy.sh index e32d81e..2f5623e 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,12 +1,22 @@ #!/bin/bash -# Deploy-Script mit Uptime-Kuma-Wartungsmodus -# Usage: ./scripts/deploy.sh [files...] -# Ohne Argumente: alles deployen +# Standard-Deploy auf gwoe.toppyr.de (Prod). # -# Setzt den GWÖ-Monitor in Uptime Kuma auf Wartung, -# deployed, und aktiviert den Monitor wieder. +# Workflow ab v2.0.0: prod laeuft als sauberer git-checkout, der Server +# zieht den release/2.0-Branch direkt aus dem Gitea-Repo. Tar-Upload- +# Pfad ist obsolet (siehe scripts/major-release-cut.sh fuer den +# Spezialfall eines neuen Major-Cuts). # -# Benötigt: UPTIME_KUMA_USER + UPTIME_KUMA_PASS in ~/.env oder als ENV +# Setzt den Uptime-Kuma-Monitor auf Wartung, deployed, reaktiviert. +# +# Usage: +# ./scripts/deploy.sh # Deploy aktuellen release/2.0-Stand +# ./scripts/deploy.sh --force # Branch-Guard ueberspringen (Notfall) +# +# Voraussetzungen: +# - Lokaler Branch ist release/2.0 (oder --force) +# - Remote release/2.0 ist gepusht +# - SSH-Zugang zu vserver +# - UPTIME_KUMA_USER + UPTIME_KUMA_PASS in ~/.env (optional) set -euo pipefail @@ -14,20 +24,19 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" SERVER="vserver" REMOTE_DIR="/opt/gwoe-antragspruefer" +PUBLIC_URL="https://gwoe.toppyr.de" UPTIME_KUMA_URL="https://status.toppyr.de" -MONITOR_ID=9 # GWÖ-Antragsprüfer +MONITOR_ID=9 +EXPECTED_BRANCH="release/2.0" -# Credentials laden if [ -f ~/.env ]; then source ~/.env fi cd "$PROJECT_DIR" -# Branch-Guard: Prod (gwoe.toppyr.de) ist auf release/1.0 festgelegt. -# 1.x-Entwicklung laeuft auf gwoe-dev.toppyr.de via Cron-Auto-Deploy aus main. +# Branch-Guard CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "") -EXPECTED_BRANCH="release/1.0" if [ "${1:-}" = "--force" ]; then shift echo "⚠ --force aktiv: Branch-Guard übersprungen ($CURRENT_BRANCH)" @@ -35,20 +44,43 @@ elif [ "$CURRENT_BRANCH" != "$EXPECTED_BRANCH" ]; then cat </dev/null || echo "") +if [ "$LOCAL_HEAD" != "$REMOTE_HEAD" ]; then + echo "⚠ Lokal ($LOCAL_HEAD) != origin/$EXPECTED_BRANCH ($REMOTE_HEAD)" + echo " Erst pushen, dann deployen." + exit 1 +fi + +echo "=== Deploy GWÖ-Antragsprüfer auf $PUBLIC_URL ===" +echo "Branch: $EXPECTED_BRANCH @ ${LOCAL_HEAD:0:8}" + +# 1. Uptime Kuma auf Wartung if [ -n "${UPTIME_KUMA_USER:-}" ] && [ -n "${UPTIME_KUMA_PASS:-}" ]; then echo "⏸ Setze Monitor auf Wartung..." python3 -c " @@ -59,39 +91,34 @@ api.pause_monitor($MONITOR_ID) api.disconnect() print(' Monitor pausiert') " 2>/dev/null || echo " (Uptime Kuma nicht erreichbar, überspringe)" -else - echo "⚠ UPTIME_KUMA_USER/PASS nicht gesetzt, überspringe Wartungsmodus" fi -# 2. Build + Deploy -if [ $# -gt 0 ]; then - # Spezifische Files - echo "📦 Packe: $@" - tar czf /tmp/gwoe-deploy.tar.gz "$@" -else - # Alles - echo "📦 Packe gesamtes Projekt (ohne venv/data/reports)..." - tar czf /tmp/gwoe-deploy.tar.gz \ - --exclude='venv' --exclude='__pycache__' \ - --exclude='data' --exclude='reports' --exclude='.env' . -fi +# 2. Pre-Deploy DB-Snapshot fuer Schnell-Rollback +echo "💾 Pre-Deploy DB-Backup..." +ssh "$SERVER" "$REMOTE_DIR/scripts/backup-db.sh" 2>&1 | tail -1 -echo "🚀 Upload + Build..." -scp /tmp/gwoe-deploy.tar.gz "$SERVER:/tmp/" -ssh "$SERVER" "cd $REMOTE_DIR && tar xzf /tmp/gwoe-deploy.tar.gz && docker compose up -d --build" 2>&1 | tail -5 +# 3. Pull + Build +echo "🚀 git pull + docker compose up -d --build..." +ssh "$SERVER" "cd $REMOTE_DIR && git fetch --quiet && git reset --hard origin/$EXPECTED_BRANCH && docker compose up -d --build" 2>&1 | tail -8 -# 3. Warte auf Health +# 4. Warte auf Health echo "⏳ Warte auf Health-Check..." -for i in $(seq 1 30); do - code=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 3 "https://gwoe.toppyr.de/health" 2>/dev/null || echo "000") - if [ "$code" = "200" ]; then - echo "✅ Health OK nach ${i}s" +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 "?") + echo "✅ Health OK nach ${i}s — version $version" break fi sleep 1 + if [ "$i" = "60" ]; then + echo "✗ Health-Check nach 60s nicht OK — pruefe Logs:" + echo " ssh $SERVER 'docker logs gwoe-antragspruefer --tail 50'" + exit 1 + fi done -# 4. Uptime Kuma wieder aktivieren +# 5. Uptime Kuma reaktivieren if [ -n "${UPTIME_KUMA_USER:-}" ] && [ -n "${UPTIME_KUMA_PASS:-}" ]; then echo "▶ Reaktiviere Monitor..." python3 -c " diff --git a/scripts/major-release-cut.sh b/scripts/major-release-cut.sh new file mode 100755 index 0000000..a4ed84e --- /dev/null +++ b/scripts/major-release-cut.sh @@ -0,0 +1,160 @@ +#!/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)"