gwoe-antragspruefer/scripts/major-release-cut.sh

161 lines
5.9 KiB
Bash
Raw Normal View History

#!/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 <release-branch>
# scp /tmp/release.bundle vserver:/tmp/
# ssh vserver 'sudo git clone -b <branch> /tmp/release.bundle <neuer-pfad>'
set -euo pipefail
if [ $# -lt 2 ]; then
cat <<EOF
Usage: $0 <tag> <branch>
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)"