chore(deploy): entkoppelte Self-Update-Skripte pro Server
Security Audit / Python Dependency Audit (push) Has been cancelled
Security Audit / Node.js Dependency Audit (push) Has been cancelled

Jeder Server aktualisiert sich eigenstaendig aus dem Repo – kein
Cross-Server-SSH, kein "both". 137 und 164 komplett getrennt.

- server-update.sh: laeuft AUF dem Server (git pull --ff-only → alembic
  upgrade head → restart → health). Guard: bricht ab wenn venv fehlt.
- push-frontend.sh: lokal, baut Frontend + rsync dist/ an EINEN Server
  (Node laeuft nicht auf den Servern, dist ist gitignored). Guard gegen
  Ausfuehrung auf einem Zielserver.
- deploy.sh + update.sh stillgelegt (Abbruch-Stubs): deploy.sh rsyncte den
  Arbeitsbaum in den Server-git-Baum und verschmutzte ihn (git pull brach);
  update.sh koppelte beide Server ("both").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 16:29:43 +02:00
co-authored by Claude Opus 4.8
parent 43ebc83905
commit f24b25b22d
4 changed files with 142 additions and 656 deletions
+11 -549
View File
@@ -1,554 +1,16 @@
#!/usr/bin/env bash
# =============================================================================
# TimeMaster Deployment & Update Script
# Lokal ausführen verbindet sich per SSH auf Produktivserver
# STILLGELEGT nicht mehr verwenden.
#
# Usage:
# ./update.sh [OPTIONS]
# Alte Variante: lokal starten, SSHt in BEIDE Server ("both") und deployt sie
# gekoppelt. Nicht gewünscht 137 und 164 sollen entkoppelt sein, jeder Server
# aktualisiert sich selbst.
#
# Options:
# --no-tests Tests überspringen
# --no-frontend Frontend-Build und -Sync überspringen
# --server 137 Nur Server 192.168.1.137
# --server 164 Nur Server 192.168.1.164
# --server both Beide Server (Standard)
# --dry-run Alle Befehle ausgeben, aber NICHT ausführen
# --help Diese Hilfe anzeigen
#
# Schritte (Reihenfolge ist kritisch):
# 1. Tests auf Server 137 ausführen (via SSH)
# 2. Frontend bauen (lokal, npm run build)
# 3. git pull origin main auf Server(n)
# 4. Alembic-Migration auf Server ausführen
# 5. Service neustarten
# 6. Health-Check (3 Versuche)
# 7. Frontend-Dist synchronisieren
# 8. Abschlussbericht
# Neuer, entkoppelter Ablauf:
# 1. Lokal: git add -A && git commit && git push origin main
# 2. Server: ssh root@<host> 'cd /opt/timemaster && ./server-update.sh'
# (jeder Server einzeln, unabhängig kein Cross-Server-SSH)
# 3. Frontend (falls geändert, lokal): ./push-frontend.sh <137|164>
# =============================================================================
set -euo pipefail
# =============================================================================
# KONFIGURATION
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="$SCRIPT_DIR/backend"
FRONTEND_DIR="$SCRIPT_DIR/frontend"
SERVER_137="root@192.168.1.137"
SERVER_164="root@192.168.1.164"
REMOTE_PATH="/opt/timemaster"
REMOTE_VENV="$REMOTE_PATH/backend/venv"
HEALTH_RETRIES=3
HEALTH_WAIT=3
# =============================================================================
# FARBEN & AUSGABE
# =============================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
RESET='\033[0m'
step() { echo -e "\n${BLUE}${BOLD}$*${RESET}"; }
ok() { echo -e " ${GREEN}$*${RESET}"; }
fail() { echo -e " ${RED}$*${RESET}"; }
warn() { echo -e " ${YELLOW}$*${RESET}"; }
info() { echo -e " ${RESET}$*${RESET}"; }
hr() { echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"; }
die() {
echo ""
fail "$1"
echo ""
hr
echo -e "${RED}${BOLD} ABBRUCH: $1${RESET}"
hr
print_summary
exit 1
}
# =============================================================================
# ARGUMENT-PARSING
# =============================================================================
OPT_TESTS=true
OPT_FRONTEND=true
OPT_SERVER="both"
OPT_DRYRUN=false
while [[ $# -gt 0 ]]; do
case "$1" in
--no-tests)
OPT_TESTS=false
shift ;;
--no-frontend)
OPT_FRONTEND=false
shift ;;
--server)
if [[ -z "${2:-}" ]]; then
die "--server benötigt einen Wert: 137, 164 oder both"
fi
OPT_SERVER="$2"
if [[ "$OPT_SERVER" != "137" && "$OPT_SERVER" != "164" && "$OPT_SERVER" != "both" ]]; then
die "--server muss 137, 164 oder both sein (nicht: $OPT_SERVER)"
fi
shift 2 ;;
--dry-run)
OPT_DRYRUN=true
shift ;;
--help|-h)
# Zeige nur den Header-Kommentarblock (bis zur ersten Leerzeile im Kommentar-Block)
awk '/^#!/{next} /^#/{sub(/^# ?/,""); print; next} /^[^#]/{exit}' "$0"
exit 0 ;;
*)
die "Unbekanntes Argument: $1 (--help für Hilfe)"
;;
esac
done
# Server-Liste aufbauen
SERVERS=()
case "$OPT_SERVER" in
137) SERVERS=("$SERVER_137") ;;
164) SERVERS=("$SERVER_164") ;;
both) SERVERS=("$SERVER_137" "$SERVER_164") ;;
esac
# =============================================================================
# ZUSAMMENFASSUNGS-TRACKING
# =============================================================================
START_TIME=$(date +%s)
declare -A STEP_STATUS # name → "OK" | "FAIL" | "SKIP" | "WARN"
declare -a STEP_ORDER=()
step_record() {
local name="$1" status="$2"
STEP_STATUS["$name"]="$status"
STEP_ORDER+=("$name")
}
print_summary() {
local end_time elapsed
end_time=$(date +%s)
elapsed=$(( end_time - START_TIME ))
echo ""
hr
echo -e "${BOLD} Abschlussbericht (Dauer: ${elapsed}s)${RESET}"
hr
for name in "${STEP_ORDER[@]}"; do
local status="${STEP_STATUS[$name]}"
case "$status" in
OK) echo -e " ${GREEN}$name${RESET}" ;;
FAIL) echo -e " ${RED}$name${RESET}" ;;
SKIP) echo -e " ${YELLOW} $name (übersprungen)${RESET}" ;;
WARN) echo -e " ${YELLOW}$name${RESET}" ;;
esac
done
hr
}
# =============================================================================
# HILFSFUNKTIONEN
# =============================================================================
# Führt einen Befehl aus oder gibt ihn im Dry-Run nur aus
run_cmd() {
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} $*"
return 0
fi
"$@"
}
# SSH-Befehl mit Fehlerbehandlung
run_ssh() {
local server="$1"
shift
local cmd="$*"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} ssh $server \"$cmd\""
return 0
fi
if ! ssh -o ConnectTimeout=10 -o BatchMode=yes "$server" "$cmd"; then
return 1
fi
}
# rsync mit Fehlerbehandlung
run_rsync() {
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} rsync $*"
return 0
fi
rsync "$@"
}
# SSH-Erreichbarkeit prüfen
check_ssh() {
local server="$1"
if $OPT_DRYRUN; then
info "Dry-Run: SSH-Check für $server übersprungen"
return 0
fi
if ! ssh -o ConnectTimeout=10 -o BatchMode=yes "$server" "true" 2>/dev/null; then
return 1
fi
}
# =============================================================================
# HEADER
# =============================================================================
hr
echo -e "${BOLD} TimeMaster Deployment Script${RESET}"
echo -e " Datum: $(date '+%Y-%m-%d %H:%M:%S')"
echo -e " Server: ${SERVERS[*]}"
echo -e " Tests: $(if $OPT_TESTS; then echo "ja"; else echo "nein (--no-tests)"; fi)"
echo -e " Frontend: $(if $OPT_FRONTEND; then echo "ja"; else echo "nein (--no-frontend)"; fi)"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}${BOLD}Modus: DRY-RUN (keine Änderungen werden durchgeführt)${RESET}"
fi
hr
# =============================================================================
# PRE-FLIGHT: Lokale Verzeichnisse prüfen
# =============================================================================
step "Pre-Flight: Lokale Verzeichnisse prüfen"
if [[ ! -d "$BACKEND_DIR" ]]; then
die "Backend-Verzeichnis nicht gefunden: $BACKEND_DIR"
fi
ok "Backend-Verzeichnis vorhanden: $BACKEND_DIR"
if $OPT_FRONTEND; then
if [[ ! -d "$FRONTEND_DIR" ]]; then
die "Frontend-Verzeichnis nicht gefunden: $FRONTEND_DIR"
fi
if [[ ! -f "$FRONTEND_DIR/package.json" ]]; then
die "package.json nicht gefunden: $FRONTEND_DIR/package.json"
fi
ok "Frontend-Verzeichnis vorhanden: $FRONTEND_DIR"
fi
# SSH-Erreichbarkeit aller Zielserver prüfen
for server in "${SERVERS[@]}"; do
step "Pre-Flight: SSH-Verbindung zu $server prüfen"
if check_ssh "$server"; then
ok "SSH-Verbindung zu $server erfolgreich"
else
die "SSH-Verbindung zu $server fehlgeschlagen.\n Prüfe: ssh $server\n SSH-Key konfiguriert?"
fi
done
step_record "Pre-Flight" "OK"
# =============================================================================
# SCHRITT 1: TESTS (auf Server 137)
# =============================================================================
if ! $OPT_TESTS; then
step "Schritt 1: Tests (übersprungen via --no-tests)"
step_record "1. Tests" "SKIP"
else
step "Schritt 1: Tests auf $SERVER_137 ausführen"
info "Befehl: cd /opt/timemaster/backend && source venv/bin/activate && python -m pytest -x -q"
TEST_CMD="cd $REMOTE_PATH/backend && source $REMOTE_VENV/bin/activate && python -m pytest -x -q 2>&1"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} ssh $SERVER_137 \"$TEST_CMD\""
step_record "1. Tests" "SKIP"
else
TEST_OUTPUT=$(ssh -o ConnectTimeout=30 -o BatchMode=yes "$SERVER_137" "$TEST_CMD" 2>&1) || true
TEST_RC=${PIPESTATUS[0]:-$?}
# Ausgabe der Tests anzeigen
echo "$TEST_OUTPUT" | while IFS= read -r line; do
info "$line"
done
# Prüfen ob Tests fehlgeschlagen (pytest gibt RC != 0 bei Fehler)
if echo "$TEST_OUTPUT" | grep -qE "^(FAILED|ERROR|[0-9]+ failed)"; then
fail "Tests FEHLGESCHLAGEN"
echo ""
echo -e " ${RED}Fehlerdetails:${RESET}"
echo "$TEST_OUTPUT" | grep -A 5 -E "^(FAILED|ERROR)" | while IFS= read -r line; do
echo -e " $line"
done
step_record "1. Tests" "FAIL"
die "Tests fehlgeschlagen Deploy abgebrochen. Keine Dateien wurden auf den Server übertragen."
fi
# Erfolgsmeldung aus pytest-Ausgabe extrahieren
PASSED_LINE=$(echo "$TEST_OUTPUT" | grep -E "passed" | tail -1 || echo "")
if [[ -n "$PASSED_LINE" ]]; then
ok "Tests bestanden: $PASSED_LINE"
else
ok "Tests abgeschlossen ohne Fehler"
fi
step_record "1. Tests" "OK"
fi
fi
# =============================================================================
# SCHRITT 2: FRONTEND BAUEN
# =============================================================================
if ! $OPT_FRONTEND; then
step "Schritt 2: Frontend-Build (übersprungen via --no-frontend)"
step_record "2. Frontend-Build" "SKIP"
else
step "Schritt 2: Frontend bauen (npm run build)"
info "Verzeichnis: $FRONTEND_DIR"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} cd $FRONTEND_DIR && npm run build"
step_record "2. Frontend-Build" "SKIP"
else
BUILD_OUTPUT=$(cd "$FRONTEND_DIR" && npm run build 2>&1) || BUILD_RC=$?
BUILD_RC=${BUILD_RC:-0}
if [[ $BUILD_RC -ne 0 ]]; then
fail "Frontend-Build FEHLGESCHLAGEN (Exit-Code: $BUILD_RC)"
echo ""
echo -e " ${RED}Build-Ausgabe:${RESET}"
echo "$BUILD_OUTPUT" | tail -30 | while IFS= read -r line; do
echo -e " $line"
done
step_record "2. Frontend-Build" "FAIL"
die "Frontend-Build fehlgeschlagen Deploy abgebrochen."
fi
if [[ ! -d "$FRONTEND_DIR/dist" ]]; then
step_record "2. Frontend-Build" "FAIL"
die "Frontend-Build scheinbar erfolgreich, aber dist/-Verzeichnis fehlt: $FRONTEND_DIR/dist"
fi
DIST_SIZE=$(du -sh "$FRONTEND_DIR/dist" | cut -f1)
ok "Frontend gebaut: $FRONTEND_DIR/dist ($DIST_SIZE)"
step_record "2. Frontend-Build" "OK"
fi
fi
# =============================================================================
# SCHRITT 3, 4, 5, 6, 7: PRO SERVER AUSFÜHREN
# =============================================================================
SERVER_RESULTS=()
ALL_SERVERS_OK=true
for server in "${SERVERS[@]}"; do
SERVER_SHORT="${server##*@}" # "192.168.1.137" aus "root@192.168.1.137"
echo ""
hr
echo -e "${BOLD} Server: $server${RESET}"
hr
SERVER_OK=true
# -------------------------------------------------------------------------
# SCHRITT 3: git pull vom Gitea
# -------------------------------------------------------------------------
step "[$SERVER_SHORT] Schritt 3: git pull (origin/main)"
GIT_PULL_CMD="git config --global --add safe.directory $REMOTE_PATH 2>/dev/null; cd $REMOTE_PATH && git pull --ff-only origin main 2>&1"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} ssh $server \"$GIT_PULL_CMD\""
step_record "3. git pull [$SERVER_SHORT]" "SKIP"
else
GIT_OUTPUT=$(ssh -o ConnectTimeout=30 -o BatchMode=yes "$server" "$GIT_PULL_CMD" 2>&1)
GIT_RC=$?
echo "$GIT_OUTPUT" | while IFS= read -r line; do info "$line"; done
if [ $GIT_RC -eq 0 ]; then
ok "git pull erfolgreich"
step_record "3. git pull [$SERVER_SHORT]" "OK"
else
fail "git pull FEHLGESCHLAGEN (Exit-Code: $GIT_RC)"
step_record "3. git pull [$SERVER_SHORT]" "FAIL"
SERVER_OK=false
ALL_SERVERS_OK=false
warn "Server $server wird übersprungen (git pull fehlgeschlagen)"
continue
fi
fi
# -------------------------------------------------------------------------
# SCHRITT 4: Alembic-Migration
# -------------------------------------------------------------------------
step "[$SERVER_SHORT] Schritt 4: Alembic-Migration"
MIGRATE_CMD="cd $REMOTE_PATH/backend && source $REMOTE_VENV/bin/activate && alembic upgrade head 2>&1"
info "Befehl: $MIGRATE_CMD"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} ssh $server \"$MIGRATE_CMD\""
step_record "4. Migration [$SERVER_SHORT]" "SKIP"
else
MIGRATE_OUTPUT=$(ssh -o ConnectTimeout=30 -o BatchMode=yes "$server" "$MIGRATE_CMD" 2>&1) || MIGRATE_RC=$?
MIGRATE_RC=${MIGRATE_RC:-0}
echo "$MIGRATE_OUTPUT" | while IFS= read -r line; do
info "$line"
done
if [[ $MIGRATE_RC -ne 0 ]]; then
fail "Alembic-Migration FEHLGESCHLAGEN (Exit-Code: $MIGRATE_RC)"
echo ""
warn "Service wird NICHT neugestartet (DB-Schema könnte inkonsistent sein)"
echo ""
echo -e " ${RED}Letzte 20 Server-Log-Zeilen:${RESET}"
ssh -o ConnectTimeout=10 -o BatchMode=yes "$server" \
"journalctl -u timemaster -n 20 --no-pager 2>/dev/null || echo '(keine Logs verfügbar)'" \
| while IFS= read -r line; do echo -e " $line"; done
echo ""
echo -e " ${YELLOW}Diagnose-Befehle:${RESET}"
echo -e " ssh $server 'cd $REMOTE_PATH/backend && source $REMOTE_VENV/bin/activate && alembic current'"
echo -e " ssh $server 'cd $REMOTE_PATH/backend && source $REMOTE_VENV/bin/activate && alembic history --verbose'"
step_record "4. Migration [$SERVER_SHORT]" "FAIL"
SERVER_OK=false
ALL_SERVERS_OK=false
warn "Server $server wird übersprungen (Migration fehlgeschlagen, Service NICHT neugestartet)"
continue
fi
# Prüfe ob Migration "up to date" ist (kein Fehler, aber auch keine Änderung)
if echo "$MIGRATE_OUTPUT" | grep -qi "already up to date\|no migrations"; then
ok "Migration: bereits aktuell (keine neuen Migrationen)"
else
ok "Migration erfolgreich"
fi
step_record "4. Migration [$SERVER_SHORT]" "OK"
fi
# -------------------------------------------------------------------------
# SCHRITT 5: Service neustarten
# -------------------------------------------------------------------------
step "[$SERVER_SHORT] Schritt 5: Service neustarten"
if run_ssh "$server" "systemctl restart timemaster"; then
ok "Service neugestartet"
step_record "5. Service-Restart [$SERVER_SHORT]" "OK"
else
RESTART_RC=$?
fail "systemctl restart timemaster FEHLGESCHLAGEN (Exit-Code: $RESTART_RC)"
echo ""
echo -e " ${RED}Letzte 30 Journal-Zeilen:${RESET}"
ssh -o ConnectTimeout=10 -o BatchMode=yes "$server" \
"journalctl -u timemaster -n 30 --no-pager 2>/dev/null || echo '(keine Logs verfügbar)'" \
| while IFS= read -r line; do echo -e " $line"; done
echo ""
echo -e " ${YELLOW}Diagnose-Befehle:${RESET}"
echo -e " ssh $server 'systemctl status timemaster'"
echo -e " ssh $server 'journalctl -u timemaster -n 100 --no-pager'"
step_record "5. Service-Restart [$SERVER_SHORT]" "FAIL"
SERVER_OK=false
ALL_SERVERS_OK=false
warn "Server $server: Service-Start fehlgeschlagen"
continue
fi
# -------------------------------------------------------------------------
# SCHRITT 6: Health-Check
# -------------------------------------------------------------------------
step "[$SERVER_SHORT] Schritt 6: Health-Check (${HEALTH_RETRIES} Versuche, ${HEALTH_WAIT}s Pause)"
if $OPT_DRYRUN; then
echo -e " ${YELLOW}[DRY-RUN]${RESET} ssh $server 'curl -sf http://localhost:8000/health'"
step_record "6. Health-Check [$SERVER_SHORT]" "SKIP"
else
HEALTH_OK=false
for attempt in $(seq 1 $HEALTH_RETRIES); do
info "Versuch $attempt/$HEALTH_RETRIES..."
HEALTH_OUTPUT=$(ssh -o ConnectTimeout=10 -o BatchMode=yes "$server" \
"curl -sf --connect-timeout 5 --max-time 10 http://localhost:8000/health 2>&1") \
&& HEALTH_RC=0 || HEALTH_RC=$?
if [[ $HEALTH_RC -eq 0 ]]; then
ok "Health-Check erfolgreich (Versuch $attempt): $HEALTH_OUTPUT"
HEALTH_OK=true
break
else
warn "Versuch $attempt fehlgeschlagen"
if [[ $attempt -lt $HEALTH_RETRIES ]]; then
info "Warte ${HEALTH_WAIT}s..."
sleep "$HEALTH_WAIT"
fi
fi
done
if ! $HEALTH_OK; then
fail "Health-Check nach $HEALTH_RETRIES Versuchen FEHLGESCHLAGEN"
echo ""
warn "Service läuft möglicherweise nicht korrekt. Log-Befehle:"
echo -e " ${YELLOW}ssh $server 'journalctl -u timemaster -n 50 --no-pager'${RESET}"
echo -e " ${YELLOW}ssh $server 'systemctl status timemaster'${RESET}"
echo -e " ${YELLOW}ssh $server 'curl -v http://localhost:8000/health'${RESET}"
step_record "6. Health-Check [$SERVER_SHORT]" "WARN"
SERVER_OK=false
ALL_SERVERS_OK=false
# KEIN Abbruch hier nur Warnung, Frontend noch synchronisieren
else
step_record "6. Health-Check [$SERVER_SHORT]" "OK"
fi
fi
# -------------------------------------------------------------------------
# SCHRITT 7: Frontend-Dist synchronisieren
# -------------------------------------------------------------------------
if ! $OPT_FRONTEND; then
step "[$SERVER_SHORT] Schritt 7: Frontend-Sync (übersprungen via --no-frontend)"
step_record "7. Frontend-Sync [$SERVER_SHORT]" "SKIP"
else
step "[$SERVER_SHORT] Schritt 7: Frontend-Dist synchronisieren"
info "rsync: $FRONTEND_DIR/dist/ → $server:$REMOTE_PATH/frontend/dist/"
if [[ ! -d "$FRONTEND_DIR/dist" ]] && ! $OPT_DRYRUN; then
warn "dist/-Verzeichnis nicht gefunden: $FRONTEND_DIR/dist"
warn "Frontend-Build wurde eventuell nicht ausgeführt (--no-frontend?)"
step_record "7. Frontend-Sync [$SERVER_SHORT]" "WARN"
else
if run_rsync -avz --delete \
"$FRONTEND_DIR/dist/" \
"$server:$REMOTE_PATH/frontend/dist/" ; then
ok "Frontend-Dist synchronisiert"
step_record "7. Frontend-Sync [$SERVER_SHORT]" "OK"
else
RSYNC_FE_RC=$?
fail "Frontend-rsync FEHLGESCHLAGEN (Exit-Code: $RSYNC_FE_RC)"
fail "Befehl war: rsync -avz --delete $FRONTEND_DIR/dist/ $server:$REMOTE_PATH/frontend/dist/"
step_record "7. Frontend-Sync [$SERVER_SHORT]" "FAIL"
SERVER_OK=false
ALL_SERVERS_OK=false
fi
fi
fi
if $SERVER_OK; then
SERVER_RESULTS+=("${GREEN}$server${RESET}")
else
SERVER_RESULTS+=("${YELLOW}$server (mit Warnungen/Fehlern)${RESET}")
fi
done # Ende Server-Schleife
# =============================================================================
# ABSCHLUSSBERICHT
# =============================================================================
echo ""
hr
echo -e "${BOLD} Server-Ergebnisse:${RESET}"
for result in "${SERVER_RESULTS[@]}"; do
echo -e " $result"
done
print_summary
if $ALL_SERVERS_OK; then
echo -e "${GREEN}${BOLD} DEPLOYMENT ERFOLGREICH${RESET}"
else
echo -e "${YELLOW}${BOLD} DEPLOYMENT MIT WARNUNGEN ABGESCHLOSSEN${RESET}"
echo -e "${YELLOW} Bitte die Fehlermeldungen oben prüfen.${RESET}"
fi
hr
exit 0
echo "update.sh ist stillgelegt. Nutze: git push → server-update.sh (auf dem Server) → push-frontend.sh (lokal)." >&2
exit 1