Deploy-Race: rebreak-idle-staging wurde direkt nach pm2-restart von rebreak-staging gestartet, bevor der Nitro-Server auf Port 3016 lauschte. Der Daemon startete sofort Initial-Scans fuer alle Accounts -- jeder triggerScan()-Call scheiterte mit "fetch failed" (ECONNREFUSED). Kein Crash, aber Error-Log-Burst (N Fehler pro Mail-Account) und verpasster Initial-Sweep. Fix: curl-Preflight in Step 5b wartet bis Port 3016 antwortet (max 60s, 12x alle 5s, --retry-connrefused). Bei Timeout: WARN im Log, kein Deploy-Abbruch (best-effort fuer optionalen Service). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
203 lines
8.1 KiB
Bash
Executable File
203 lines
8.1 KiB
Bash
Executable File
#!/bin/bash
|
|
# deploy-from-artifact.sh -- Server-side Deploy nach GitHub-Actions-Artifact-Upload.
|
|
#
|
|
# Wird via SSH von .github/workflows/deploy-staging.yml aufgerufen.
|
|
# Erwartet: /srv/rebreak/backend/.output-incoming.tar.gz (vom GA-Runner via scp gepusht).
|
|
#
|
|
# Diff zu scripts/deploy.sh:
|
|
# - KEIN pnpm build hier (das macht der GA-Runner mit 7 GB RAM)
|
|
# - KEIN pnpm install --frozen-lockfile fuer Build-Deps -- nur Runtime-Deps via prod-flag
|
|
# - Migration-Detection bleibt (Pattern aus deploy.sh)
|
|
# - Atomic .output-staging-Replacement bleibt
|
|
#
|
|
# Failure-Mode: Bei Migration-Fehler kein pm2-restart (Daten-Konsistenz-Schutz).
|
|
#
|
|
# Lockfile: setzt /srv/rebreak/.deploy-ga.lock waehrend Deploy laeuft.
|
|
# deploy.sh (webhook-trigger) respektiert diesen Lock und bricht ab.
|
|
# Verhindert Race-Condition wenn Webhook und GH-Actions gleichzeitig deployen.
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="/srv/rebreak"
|
|
APP_DIR="${REPO_ROOT}/backend"
|
|
ARTIFACT="${APP_DIR}/.output-incoming.tar.gz"
|
|
PM2_BIN="/root/.nvm/versions/node/v24.11.1/bin/pm2"
|
|
PNPM_BIN="/root/.nvm/versions/node/v24.11.1/bin/pnpm"
|
|
GA_LOCK="${REPO_ROOT}/.deploy-ga.lock"
|
|
|
|
log() { echo "[deploy-artifact] $(date '+%H:%M:%S') $*"; }
|
|
log_err() { echo "[deploy-artifact:err] $(date '+%H:%M:%S') $*" >&2; }
|
|
|
|
log "=== Rebreak Deploy-from-Artifact gestartet ==="
|
|
|
|
export PATH="/root/.nvm/versions/node/v24.11.1/bin:$PATH"
|
|
|
|
# 0a. Exklusiv-Lock setzen (verhindert parallelen Webhook-Deploy)
|
|
if ! ( set -o noclobber; echo "$$" > "$GA_LOCK" ) 2>/dev/null; then
|
|
LOCK_PID=$(cat "$GA_LOCK" 2>/dev/null || echo "?")
|
|
# Stale lock (Prozess tot)? Dann ueberschreiben.
|
|
if ! kill -0 "$LOCK_PID" 2>/dev/null; then
|
|
log "Staler Lock von PID $LOCK_PID gefunden -- ueberschreibe"
|
|
echo "$$" > "$GA_LOCK"
|
|
else
|
|
log_err "Ein anderer Deploy laeuft bereits (PID $LOCK_PID) -- abort"
|
|
exit 1
|
|
fi
|
|
fi
|
|
# Lock beim Beenden immer aufraumen
|
|
trap 'rm -f "$GA_LOCK"' EXIT
|
|
|
|
# 0b. Sanity-Check Artifact
|
|
[[ -f "$ARTIFACT" ]] || { log_err "Artifact $ARTIFACT fehlt -- abort"; exit 1; }
|
|
|
|
# 1. Git pull (fuer scripts/-Updates + prisma/migrations + .last-deployed-sha)
|
|
log "Step 1: git pull..."
|
|
cd "${REPO_ROOT}"
|
|
git fetch origin main
|
|
git reset --hard origin/main
|
|
log "Git updated to $(git rev-parse --short HEAD)"
|
|
|
|
# 2. Migration-Detection (1:1 aus scripts/deploy.sh uebernommen)
|
|
log "Step 2: Migration-Check..."
|
|
PREV_SHA=$(cat "${REPO_ROOT}/.last-deployed-sha" 2>/dev/null || echo "")
|
|
CUR_SHA=$(git -C "${REPO_ROOT}" rev-parse HEAD)
|
|
|
|
run_migration=false
|
|
if [[ -z "$PREV_SHA" ]]; then
|
|
log "Kein .last-deployed-sha gefunden -- first-deploy: Migration sicherheitshalber ausfuehren"
|
|
run_migration=true
|
|
elif ! git -C "${REPO_ROOT}" diff --quiet "$PREV_SHA"..HEAD -- backend/prisma/migrations/ backend/prisma/schema.prisma; then
|
|
log "Migration-Changes detected zwischen ${PREV_SHA} und ${CUR_SHA}"
|
|
run_migration=true
|
|
else
|
|
log "Keine Migration-Changes seit ${PREV_SHA} -- skip migrate deploy"
|
|
fi
|
|
|
|
if $run_migration; then
|
|
log "Running prisma migrate deploy..."
|
|
cd "${APP_DIR}"
|
|
|
|
source /etc/environment
|
|
if [[ -z "${INFISICAL_CLIENT_ID:-}" || -z "${INFISICAL_CLIENT_SECRET:-}" ]]; then
|
|
log_err "INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET fehlt -- Migration abgebrochen"
|
|
exit 1
|
|
fi
|
|
|
|
INFISICAL_TOKEN=$(infisical login \
|
|
--method=universal-auth \
|
|
--client-id="${INFISICAL_CLIENT_ID}" \
|
|
--client-secret="${INFISICAL_CLIENT_SECRET}" \
|
|
--silent --plain 2>/dev/null)
|
|
|
|
[[ -z "$INFISICAL_TOKEN" ]] && { log_err "Infisical login fehlgeschlagen"; exit 1; }
|
|
|
|
infisical run \
|
|
--projectId="${INFISICAL_PROJECT_ID:-14b11b35-ef59-4b8a-a16b-398f0cc3ad93}" \
|
|
--env=staging \
|
|
--token="$INFISICAL_TOKEN" \
|
|
-- bash -c '
|
|
set -e
|
|
export DATABASE_URL="${DATABASE_URL:-${NUXT_DATABASE_URL:-}}"
|
|
if [[ -z "$DATABASE_URL" ]]; then
|
|
echo "[deploy-artifact:err] DATABASE_URL nicht in Infisical-staging" >&2
|
|
exit 1
|
|
fi
|
|
"'"${PNPM_BIN}"'" prisma migrate deploy --schema prisma/schema.prisma
|
|
' 2>&1 || {
|
|
log_err "Migration-Deploy fehlgeschlagen -- pm2-restart ABGEBROCHEN"
|
|
exit 1
|
|
}
|
|
log "Migration done"
|
|
fi
|
|
|
|
# 3. Runtime-Deps installieren (nur falls package.json/lockfile changed)
|
|
# Prisma-Client ist schon im Artifact baked-in via `prisma generate` auf dem Runner,
|
|
# aber Runtime-Module (z.B. @prisma/client native binaries) muessen lokal sein.
|
|
# --prefer-offline: nutzt pnpm-Store-Cache wenn moeglich (kein neuer Download).
|
|
# Store wächst unbegrenzt -- prunen via: pnpm store prune (z.B. monatlich als Cron).
|
|
log "Step 3: pnpm install (runtime-deps)..."
|
|
cd "${REPO_ROOT}"
|
|
CI=true "${PNPM_BIN}" install --frozen-lockfile --prefer-offline 2>&1 || {
|
|
log_err "frozen-lockfile fehlgeschlagen, fallback ohne frozen..."
|
|
CI=true "${PNPM_BIN}" install --no-frozen-lockfile --prefer-offline 2>&1
|
|
}
|
|
log "pnpm install done"
|
|
|
|
# 3b. imap-idle Runtime-Deps installieren (imapflow + pg, standalone package.json)
|
|
IDLE_DIR="${APP_DIR}/imap-idle"
|
|
if [[ -d "$IDLE_DIR" && -f "$IDLE_DIR/package.json" ]]; then
|
|
log "Step 3b: npm install (imap-idle runtime-deps)..."
|
|
cd "$IDLE_DIR"
|
|
npm install --production --prefer-offline 2>&1
|
|
# scp preserviert Permissions nicht immer -- explizit setzen
|
|
[[ -f "$IDLE_DIR/start-idle-staging.sh" ]] && chmod +x "$IDLE_DIR/start-idle-staging.sh"
|
|
log "imap-idle npm install done"
|
|
else
|
|
log "Step 3b: imap-idle-Verzeichnis nicht gefunden -- skip (wird via GH-Actions deployed)"
|
|
fi
|
|
|
|
# 4. Artifact extrahieren -- atomisches mv (gleicher Pattern wie deploy.sh)
|
|
log "Step 4: Artifact extrahieren..."
|
|
cd "${APP_DIR}"
|
|
rm -rf .output-staging-new
|
|
mkdir -p .output-staging-new
|
|
tar xzf "$ARTIFACT" -C .output-staging-new
|
|
|
|
# Sanity-Check: server/index.mjs muss drin sein
|
|
[[ -f .output-staging-new/server/index.mjs ]] || {
|
|
log_err "Ungueltiges Artifact -- .output-staging-new/server/index.mjs fehlt"
|
|
rm -rf .output-staging-new
|
|
exit 1
|
|
}
|
|
|
|
rm -rf .output-staging
|
|
mv .output-staging-new .output-staging
|
|
rm -f "$ARTIFACT"
|
|
log ".output-staging aktualisiert"
|
|
|
|
# 5. pm2 restart (--update-env zieht neue Infisical-Secrets)
|
|
log "Step 5: pm2 restart rebreak-staging..."
|
|
"${PM2_BIN}" restart rebreak-staging --update-env 2>/dev/null || \
|
|
"${PM2_BIN}" start "${REPO_ROOT}/ecosystem.config.js" --only rebreak-staging
|
|
log "rebreak-staging restarted"
|
|
|
|
# 5b. Warten bis Backend-Port ready ist (max 60s).
|
|
# Verhindert Deploy-Race: rebreak-idle-staging startet Initial-Scan-Welle sofort
|
|
# beim Hochfahren -- wenn Port 3016 noch nicht lauscht, schlaegt jeder
|
|
# triggerScan-Call mit "fetch failed" fehl und der idle-Daemon loggt einen
|
|
# Burst (1 Fehler pro aktivem Mail-Account). Das ist kein Crash (fetch-Fehler
|
|
# werden im catch absorbiert), aber es produziert Larm im Error-Log und
|
|
# verpasst den Initial-Sweep.
|
|
# curl --retry: 12 Versuche alle 5s = 60s Timeout. --retry-connrefused: auch
|
|
# bei ECONNREFUSED (Port noch nicht offen) weiterversuchen, nicht sofort abbrechen.
|
|
# --silent --output /dev/null: kein Response-Body in die Logs.
|
|
# --fail: Exit-Code != 0 bei HTTP 4xx/5xx -- wir wollen nur "Port lauscht".
|
|
# Nitro antwortet auf / mit 200 (oder redirect), auf unbekannte Routen mit 404 --
|
|
# beides bedeutet "Backend laeuft". Daher kein --fail hier.
|
|
log "Step 5b: Warte auf Backend-Port 3016 (max 60s)..."
|
|
if curl --silent --output /dev/null \
|
|
--retry 12 --retry-delay 5 --retry-connrefused \
|
|
"http://127.0.0.1:3016/"; then
|
|
log "Backend-Port 3016 ready"
|
|
else
|
|
log "WARN: Backend-Port 3016 nicht erreichbar nach 60s -- idle-Restart trotzdem fortsetzen"
|
|
fi
|
|
|
|
# 6. Optional services (best-effort, Mo's Scope)
|
|
log "Step 6: Optional services restart..."
|
|
"${PM2_BIN}" restart rebreak-imap-staging 2>/dev/null || true
|
|
# rebreak-idle-staging: startOrReload (erster Deploy hat keinen laufenden Prozess)
|
|
"${PM2_BIN}" restart rebreak-idle-staging --update-env 2>/dev/null || \
|
|
"${PM2_BIN}" start "${REPO_ROOT}/ecosystem.config.js" --only rebreak-idle-staging
|
|
"${PM2_BIN}" restart dns-rebreak-staging 2>/dev/null || true
|
|
"${PM2_BIN}" restart dns-rebreak 2>/dev/null || true
|
|
|
|
# 7. pm2 save
|
|
"${PM2_BIN}" save 2>/dev/null || true
|
|
|
|
# 8. Last-deployed-SHA persistieren
|
|
echo "${CUR_SHA}" > "${REPO_ROOT}/.last-deployed-sha"
|
|
log "Last-deployed-SHA gespeichert: ${CUR_SHA}"
|
|
|
|
log "=== Deploy erfolgreich: $(git -C ${REPO_ROOT} rev-parse --short HEAD) ==="
|