Enhance deployment script to handle symlinks and ensure proper data restoration from backup
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 48s

This commit updates the deploy-production.sh script to check if the server/data directory is a symlink and removes it if necessary, ensuring that data is copied to a real directory. It also modifies the data restoration process to follow symlinks when copying from the backup, improving the reliability of data recovery during deployment.
This commit is contained in:
Torsten Schulz (local)
2026-01-17 18:54:05 +01:00
parent 5cf12d1838
commit 3577831149
3 changed files with 508 additions and 2 deletions

View File

@@ -293,8 +293,20 @@ if [ ! -d "$BACKUP_DIR/server-data" ]; then
exit 1
fi
# WICHTIG: Prüfe, ob server/data ein Symlink ist (z.B. auf Backup-Verzeichnis)
# Falls ja, entferne den Symlink, damit wir in das echte Verzeichnis kopieren können
if [ -L "server/data" ]; then
echo " WARNING: server/data ist ein Symlink. Entferne Symlink..."
rm "server/data"
fi
# Stelle sicher, dass server/data ein echtes Verzeichnis ist
if [ ! -d "server/data" ]; then
mkdir -p server/data
cp -a "$BACKUP_DIR/server-data/." server/data/
fi
# Kopiere Daten vom Backup (verwende -L, um Symlinks zu folgen, falls nötig)
cp -aL "$BACKUP_DIR/server-data/." server/data/
echo " Restored server/data from backup ($BACKUP_DIR/server-data)."
# Stelle alle CSVs wieder her

432
deploy-test.sh Executable file
View File

@@ -0,0 +1,432 @@
#!/bin/bash
set -euo pipefail
# Deployment Script für Harheimer TC Test-Instanz
# Verwendet separate Backup- und Data-Verzeichnisse
# Immer im Repo-Verzeichnis arbeiten (wichtig für Backup/Restore mit relativen Pfaden)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
echo "=== Harheimer TC Test-Instanz Deployment ==="
echo ""
echo "Working directory: $(pwd)"
echo ""
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "ERROR: Dieses Script muss im Git-Repository ausgeführt werden (kein .git gefunden)."
exit 1
fi
# Separate Verzeichnisse für Test-Instanz
DEPLOY_HOME="${DEPLOY_HOME:-${HOME:-/tmp}}"
DATA_ROOT="${DATA_ROOT:-$DEPLOY_HOME/harheimertc.test-data}"
BACKUP_ROOT="${BACKUP_ROOT:-$DEPLOY_HOME/harheimertc.test-backups}"
mkdir -p "$DATA_ROOT" "$BACKUP_ROOT"
ensure_symlink_dir() {
local src="$1" # z.B. server/data
local target="$2" # z.B. /var/lib/harheimertc.test/server-data
mkdir -p "$(dirname "$src")"
mkdir -p "$target"
if [ -L "$src" ]; then
return 0
fi
if [ -d "$src" ]; then
echo " Moving $src -> $target (first-time migration)"
# Merge existing content into target
cp -a "$src/." "$target/" || true
rm -rf "$src"
fi
ln -s "$target" "$src"
echo " Linked $src -> $target"
}
has_tracked_files_under() {
local prefix="$1" # e.g. public/data
# If any file is tracked under this path, symlinking the directory will break git operations
git ls-files "$prefix" | head -n 1 | grep -q .
}
echo "0. Ensuring persistent data directories (recommended)..."
# IMPORTANT: Only symlink server/data if it's not tracked by git.
if has_tracked_files_under "server/data"; then
echo " Skipping symlink for server/data (tracked files detected in git)."
echo " Recommendation: remove server/data/** from git history and keep them only as production data."
else
ensure_symlink_dir "server/data" "$DATA_ROOT/server-data"
fi
# IMPORTANT: Only symlink public/data if it's not tracked by git.
# Otherwise git will error with "path is beyond a symbolic link".
if has_tracked_files_under "public/data"; then
echo " Skipping symlink for public/data (tracked files detected in git)."
echo " Recommendation: remove public/data/*.csv from git history and keep them only as production data."
else
ensure_symlink_dir "public/data" "$DATA_ROOT/public-data"
fi
ensure_symlink_dir "public/uploads" "$DATA_ROOT/public-uploads"
echo ""
# 1. BACKUP: Laufende Produktivdaten VOR allen Git-Operationen sichern
echo "1. Backing up current production data (pre-git)..."
# Human readable timestamp (lokal)
BACKUP_TS="$(date +"%Y-%m-%d_%H-%M-%S")"
BACKUP_DIR="$BACKUP_ROOT/backup_$BACKUP_TS"
mkdir -p "$BACKUP_DIR"
echo " Backup directory: $BACKUP_DIR"
# Backup server data (JSON) und CSVs immer vom Dateisystem, nicht aus 'stash'
if [ -d server/data ]; then
cp -a server/data "$BACKUP_DIR/server-data"
echo " Backed up server/data -> $BACKUP_DIR/server-data"
else
echo "ERROR: server/data existiert nicht. Abbruch, damit wir keine Repo-Defaults ausrollen."
exit 1
fi
if ls public/data/*.csv >/dev/null 2>&1; then
mkdir -p "$BACKUP_DIR/public-data"
cp -a public/data/*.csv "$BACKUP_DIR/public-data/"
echo " Backed up public/data/*.csv -> $BACKUP_DIR/public-data/"
else
echo " No public CSVs to backup (public/data/*.csv not found)"
fi
# 2. Handle local changes and Git Pull
echo "2. Handling local changes and pulling latest from git..."
# Check if there are merge conflicts first
if [ -n "$(git status --porcelain | grep '^UU\|^AA\|^DD')" ]; then
echo " Resolving existing merge conflicts..."
git reset --hard HEAD
fi
# Ensure git operations can run even if we have untracked local artifacts (e.g. backups/)
# We avoid `git stash` (breaks with symlinked tracked paths). Instead, hard-reset and clean safely.
echo " Resetting working tree to HEAD (production data will be restored from backup)..."
git reset --hard HEAD
echo " Cleaning untracked files (excluding data dirs/backups/.env)..."
git clean -fd \
-e server/data \
-e public/data \
-e public/uploads \
-e backups \
-e .env || true
# Pull latest changes
echo " Pulling latest changes..."
git pull
# Reset any accidental changes from stash restore (should be none now)
git reset --hard HEAD >/dev/null 2>&1
# WICHTIG: Entferne public/data Dateien aus Git-Index, falls sie getrackt sind
# (Sie sollten in .gitignore sein, aber falls sie historisch getrackt wurden)
if git ls-files --error-unmatch public/data/*.csv >/dev/null 2>&1; then
echo " WARNING: public/data/*.csv Dateien sind im Git getrackt!"
echo " Entferne sie aus dem Git-Index (Dateien bleiben erhalten)..."
git rm --cached public/data/*.csv 2>/dev/null || true
fi
# 3. Install dependencies
echo ""
echo "3. Installing dependencies..."
npm install
# 4. Remove old build (but keep data!)
echo ""
echo "4. Cleaning build artifacts..."
# Sicherstellen, dass .output vollständig gelöscht wird
if [ -d ".output" ]; then
echo " Removing .output directory..."
rm -rf .output
# Prüfen, ob wirklich gelöscht wurde
if [ -d ".output" ]; then
echo "WARNING: .output konnte nicht vollständig gelöscht werden. Versuche erneut..."
sleep 2
rm -rf .output
if [ -d ".output" ]; then
echo "ERROR: .output konnte auch nach erneutem Versuch nicht gelöscht werden!"
echo "Bitte manuell prüfen und löschen: rm -rf .output"
exit 1
fi
fi
echo " ✓ .output gelöscht"
fi
# Auch .nuxt Cache löschen für sauberen Build
if [ -d ".nuxt" ]; then
echo " Removing .nuxt cache..."
rm -rf .nuxt
echo " ✓ .nuxt gelöscht"
fi
# Prüfe, ob node_modules vorhanden ist (für npm run build)
if [ ! -d "node_modules" ]; then
echo ""
echo "WARNING: node_modules fehlt. Installiere Dependencies..."
npm install
fi
# 5. Build
echo ""
echo "5. Building application..."
echo " Running: npm run build"
echo " (This may take a few minutes...)"
# Clean-Build: Stelle sicher, dass node_modules aktuell ist
echo " Checking dependencies..."
if [ ! -f "node_modules/.package-lock.json" ] && [ ! -f "package-lock.json" ]; then
echo " WARNING: package-lock.json fehlt. Führe npm install aus..."
npm install
fi
# Build mit expliziter Fehlerbehandlung und Output-Capture
BUILD_OUTPUT=$(npm run build 2>&1)
BUILD_EXIT_CODE=$?
# Zeige Build-Output
echo "$BUILD_OUTPUT"
if [ "$BUILD_EXIT_CODE" -ne 0 ]; then
echo ""
echo "ERROR: Build fehlgeschlagen mit Exit-Code $BUILD_EXIT_CODE"
echo "Bitte prüfen Sie die Build-Ausgabe oben auf Fehler."
exit 1
fi
# Prüfe auf Warnungen im Build-Output, die auf Probleme hinweisen
if echo "$BUILD_OUTPUT" | grep -qi "error\|failed\|missing"; then
echo ""
echo "WARNING: Build-Output enthält möglicherweise Fehler oder Warnungen."
echo "Bitte prüfen Sie die Ausgabe oben."
fi
# Prüfe, ob der Build erfolgreich war - mehrere Checks
echo ""
echo " Verifying build output..."
BUILD_FAILED=0
# Check 1: _nuxt Verzeichnis
if [ ! -d ".output/public/_nuxt" ]; then
echo "ERROR: .output/public/_nuxt Verzeichnis fehlt!"
BUILD_FAILED=1
else
NUXT_FILES=$(find .output/public/_nuxt -type f 2>/dev/null | wc -l)
echo " ✓ .output/public/_nuxt vorhanden ($NUXT_FILES Dateien)"
if [ "$NUXT_FILES" -eq 0 ]; then
echo "ERROR: .output/public/_nuxt ist leer!"
BUILD_FAILED=1
else
# Prüfe, ob wichtige Dateien vorhanden sind
JS_FILES=$(find .output/public/_nuxt -name "*.js" 2>/dev/null | wc -l)
CSS_FILES=$(find .output/public/_nuxt -name "*.css" 2>/dev/null | wc -l)
echo " ✓ JS-Dateien: $JS_FILES, CSS-Dateien: $CSS_FILES"
if [ "$JS_FILES" -eq 0 ]; then
echo "ERROR: Keine JS-Dateien in .output/public/_nuxt gefunden!"
BUILD_FAILED=1
fi
fi
fi
# Check 1b: _nuxt/builds Verzeichnis (für Meta-Dateien)
if [ -d ".output/public/_nuxt/builds" ]; then
BUILD_META_FILES=$(find .output/public/_nuxt/builds -type f 2>/dev/null | wc -l)
echo " ✓ .output/public/_nuxt/builds vorhanden ($BUILD_META_FILES Meta-Dateien)"
else
echo "WARNING: .output/public/_nuxt/builds fehlt (kann in manchen Nuxt-Versionen normal sein)"
fi
# Check 2: Server index.mjs
if [ ! -f ".output/server/index.mjs" ]; then
echo "ERROR: .output/server/index.mjs fehlt!"
BUILD_FAILED=1
else
echo " ✓ .output/server/index.mjs vorhanden"
fi
# Check 3: Public Verzeichnis
if [ ! -d ".output/public" ]; then
echo "ERROR: .output/public Verzeichnis fehlt!"
BUILD_FAILED=1
else
echo " ✓ .output/public vorhanden"
fi
# Check 4: Server Verzeichnis
if [ ! -d ".output/server" ]; then
echo "ERROR: .output/server Verzeichnis fehlt!"
BUILD_FAILED=1
else
echo " ✓ .output/server vorhanden"
fi
if [ "$BUILD_FAILED" -eq 1 ]; then
echo ""
echo "ERROR: Build-Verifikation fehlgeschlagen!"
echo "Bitte führen Sie manuell aus:"
echo " rm -rf .output .nuxt"
echo " npm run build"
exit 1
fi
echo " ✓ Build erfolgreich verifiziert"
# 6. Restore Production Data (überschreibe Repo-Defaults mit Backup)
echo ""
echo "6. Restoring production data..."
# Stelle server/data vollständig wieder her (inkl. config.json, users.json, news.json, sessions.json, members.json, membership-applications)
if [ ! -d "$BACKUP_DIR/server-data" ]; then
echo "ERROR: Backup-Verzeichnis $BACKUP_DIR/server-data fehlt. Abbruch."
exit 1
fi
# WICHTIG: Prüfe, ob server/data ein Symlink ist (z.B. auf Backup-Verzeichnis)
# Falls ja, entferne den Symlink, damit wir in das echte Verzeichnis kopieren können
if [ -L "server/data" ]; then
echo " WARNING: server/data ist ein Symlink. Entferne Symlink..."
rm "server/data"
fi
# Stelle sicher, dass server/data ein echtes Verzeichnis ist
if [ ! -d "server/data" ]; then
mkdir -p server/data
fi
# Kopiere Daten vom Backup (verwende -L, um Symlinks zu folgen, falls nötig)
cp -aL "$BACKUP_DIR/server-data/." server/data/
echo " Restored server/data from backup ($BACKUP_DIR/server-data)."
# Stelle alle CSVs wieder her
if ls "$BACKUP_DIR/public-data"/*.csv >/dev/null 2>&1; then
mkdir -p public/data
# WICHTIG: Überschreibe auch Dateien, die aus dem Git-Repository kommen
# Verwende cp mit -f (force) um sicherzustellen, dass Backup-Dateien Vorrang haben
for csv_file in "$BACKUP_DIR/public-data"/*.csv; do
filename=$(basename "$csv_file")
# Überschreibe explizit, auch wenn Datei bereits existiert
cp -f "$csv_file" "public/data/$filename"
# Stelle sicher, dass die Datei wirklich überschrieben wurde
if [ -f "public/data/$filename" ]; then
# Prüfe, ob die Datei wirklich vom Backup kommt (Größenvergleich)
backup_size=$(stat -f%z "$csv_file" 2>/dev/null || stat -c%s "$csv_file" 2>/dev/null || echo "0")
restored_size=$(stat -f%z "public/data/$filename" 2>/dev/null || stat -c%s "public/data/$filename" 2>/dev/null || echo "0")
if [ "$backup_size" = "$restored_size" ] && [ "$backup_size" != "0" ]; then
echo " ✓ Restored public/data/$filename from backup ($backup_size bytes)"
else
echo " ⚠ WARNING: public/data/$filename Größe stimmt nicht überein (Backup: $backup_size, Restored: $restored_size)"
fi
else
echo " ❌ ERROR: Konnte public/data/$filename nicht wiederherstellen!"
fi
done
echo " ✓ All public/data/*.csv files restored from backup ($BACKUP_DIR/public-data)."
# Zusätzliche Sicherheit: Entferne public/data Dateien aus Git-Index, falls sie getrackt sind
# (nach dem Restore, damit sie nicht beim nächsten git reset überschrieben werden)
if git ls-files --error-unmatch public/data/*.csv >/dev/null 2>&1; then
echo " WARNING: public/data/*.csv Dateien sind noch im Git getrackt!"
echo " Entferne sie aus dem Git-Index (Dateien bleiben erhalten)..."
git rm --cached public/data/*.csv 2>/dev/null || true
echo " ✓ public/data/*.csv aus Git-Index entfernt"
fi
else
echo " No public CSVs to restore"
fi
# Sanity Check: users.json muss existieren und darf nicht leer sein
if [ ! -s server/data/users.json ]; then
echo "ERROR: server/data/users.json fehlt oder ist leer nach Restore. Abbruch."
exit 1
fi
# 7. Cleanup (Backups werden bewusst behalten)
echo ""
echo "7. Keeping backups in $BACKUP_ROOT (no git stash used)."
# 8. Restart PM2
echo ""
echo "8. Restarting PM2..."
# Prüfe, ob PM2 installiert ist
if ! command -v pm2 &> /dev/null; then
echo "ERROR: PM2 ist nicht installiert oder nicht im PATH!"
echo "Bitte installieren Sie PM2: npm install -g pm2"
exit 1
fi
# Funktion zum Starten/Neustarten einer PM2-Instanz
restart_pm2_instance() {
local instance_name=$1
local config_file="$SCRIPT_DIR/harheimertc.test.config.cjs"
# Prüfe, ob die Konfigurationsdatei existiert
if [ ! -f "$config_file" ]; then
echo " ERROR: PM2-Konfigurationsdatei nicht gefunden: $config_file"
return 1
fi
if ! pm2 describe "$instance_name" &> /dev/null; then
echo " WARNING: PM2-Prozess '$instance_name' existiert nicht."
echo " Versuche, den Prozess zu starten..."
echo " Verwende Konfiguration: $config_file"
if pm2 start "$config_file" --update-env; then
echo " ✓ PM2-Prozess '$instance_name' gestartet"
return 0
else
echo " ERROR: Konnte PM2-Prozess '$instance_name' nicht starten."
echo " Bitte manuell prüfen: pm2 logs $instance_name"
return 1
fi
else
echo " Restarting $instance_name with --update-env..."
if pm2 restart "$instance_name" --update-env; then
echo " ✓ PM2-Prozess '$instance_name' neu gestartet"
return 0
else
echo " ERROR: PM2-Restart für '$instance_name' fehlgeschlagen!"
return 1
fi
fi
}
# Starte/Neustarte Test-Instanz
if ! restart_pm2_instance "harheimertc.test"; then
echo ""
echo "WARNING: PM2-Instanz konnte nicht gestartet werden. Bitte manuell prüfen:"
echo " pm2 status"
echo " pm2 logs harheimertc.test"
fi
# Prüfe, ob der Prozess läuft
sleep 2
echo ""
echo " Checking PM2 instance status..."
if pm2 describe harheimertc.test | grep -q "online"; then
echo " ✓ PM2-Prozess 'harheimertc.test' läuft (online)"
else
echo " WARNING: PM2-Prozess 'harheimertc.test' ist nicht online. Prüfe Logs: pm2 logs harheimertc.test"
fi
echo ""
echo "=== Test-Instanz Deployment completed successfully! ==="
echo "The test application is now running with the latest code and your test data preserved."
echo ""
echo "Useful commands:"
echo " pm2 logs harheimertc.test # View logs"
echo " pm2 status # View status"
echo " pm2 restart harheimertc.test # Restart manually"

View File

@@ -0,0 +1,62 @@
// Load environment variables from .env (production secrets)
// Für Test-Instanz: /var/www/harheimertc.test/.env
try {
// eslint-disable-next-line global-require
require('dotenv').config({ path: '/var/www/harheimertc.test/.env' })
} catch (_e) {
// If dotenv isn't available or .env missing, continue (process.env may be set elsewhere)
}
// Helper function to create env object
function createEnv(port) {
return {
NODE_ENV: 'production',
PORT: port,
// Secrets/Config (loaded from .env above, if present)
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
JWT_SECRET: process.env.JWT_SECRET,
SMTP_HOST: process.env.SMTP_HOST,
SMTP_PORT: process.env.SMTP_PORT,
SMTP_USER: process.env.SMTP_USER,
SMTP_PASS: process.env.SMTP_PASS,
SMTP_FROM: process.env.SMTP_FROM,
SMTP_ADMIN: process.env.SMTP_ADMIN,
NUXT_PUBLIC_BASE_URL: process.env.NUXT_PUBLIC_BASE_URL,
COOKIE_SECURE: process.env.COOKIE_SECURE,
COOKIE_SAMESITE: process.env.COOKIE_SAMESITE,
CSP_ENABLED: process.env.CSP_ENABLED,
CSP_REPORT_ONLY: process.env.CSP_REPORT_ONLY,
CSP_VALUE: process.env.CSP_VALUE,
HIBP_ENABLED: process.env.HIBP_ENABLED,
HIBP_USER_AGENT: process.env.HIBP_USER_AGENT,
HIBP_TIMEOUT_MS: process.env.HIBP_TIMEOUT_MS,
HIBP_CACHE_TTL_MS: process.env.HIBP_CACHE_TTL_MS,
HIBP_FAIL_CLOSED: process.env.HIBP_FAIL_CLOSED,
AUDIT_LOG_ENABLED: process.env.AUDIT_LOG_ENABLED,
WEBAUTHN_ORIGIN: process.env.WEBAUTHN_ORIGIN,
WEBAUTHN_RP_ID: process.env.WEBAUTHN_RP_ID,
WEBAUTHN_RP_NAME: process.env.WEBAUTHN_RP_NAME,
WEBAUTHN_REQUIRE_UV: process.env.WEBAUTHN_REQUIRE_UV
}
}
module.exports = {
apps: [
{
name: 'harheimertc.test',
// Nuxt 4 production build: direkt den Node-Server starten (kein "preview mode")
script: 'node',
args: '.output/server/index.mjs',
cwd: '/var/www/harheimertc.test',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: createEnv(process.env.PORT || 3102),
error_file: '/var/log/pm2/harheimertc.test-error.log',
out_file: '/var/log/pm2/harheimertc.test-out.log',
log_file: '/var/log/pm2/harheimertc.test-combined.log',
time: true
}
]
}