Fix: update.sh State-Dateien, Lock, Version-Datei + Installer-Integration

update.sh:
- State-Dateien (/var/lib/mailwolt/update/state + rc) werden geschrieben
- Lock-Datei verhindert parallele Update-Prozesse
- write_version_files() aktualisiert auch /var/lib/mailwolt/version
- Kombinierter _cleanup-Trap ersetzt cleanup_maintenance
- LATEST_TAG via git rev-list statt sort -V (zuverlässiger)
- Update-Log nach /var/log/mailwolt-update.log

installer.sh:
- update.sh wird als /usr/local/sbin/mailwolt-update installiert
- Sudoers-Eintrag für mailwolt-update ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-04-24 13:38:27 +02:00
parent f8f30d57f7
commit fbce5dc8ba
2 changed files with 294 additions and 1 deletions

View File

@ -787,9 +787,13 @@ nginx -t && systemctl reload nginx
HELPER
chmod 755 /usr/local/sbin/mailwolt-apply-domains
# ===== Sudoers für www-data (helper) =====
# ===== mailwolt-update installieren =====
install -m 755 "${APP_DIR}/update.sh" /usr/local/sbin/mailwolt-update
# ===== Sudoers für www-data (helper + update) =====
cat > /etc/sudoers.d/mailwolt-certbot <<'SUDOERS'
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-apply-domains
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update
SUDOERS
chmod 440 /etc/sudoers.d/mailwolt-certbot

289
update.sh Normal file
View File

@ -0,0 +1,289 @@
#!/usr/bin/env bash
set -euo pipefail
# -------- Konfiguration -------------------------------------------------------
APP_USER="${APP_USER:-mailwolt}"
APP_GROUP="${APP_GROUP:-www-data}"
APP_DIR="${APP_DIR:-/var/www/mailwolt}"
BRANCH="${BRANCH:-main}"
MODE="${UPDATE_MODE:-tags}"
ALLOW_DIRTY="${ALLOW_DIRTY:-0}"
STATE_DIR="/var/lib/mailwolt/update"
LOCK_FILE="/var/run/mailwolt-update.lock"
LOG_FILE="/var/log/mailwolt-update.log"
# npm / CI Defaults
export CI=1
export NPM_CONFIG_FUND=false
export NPM_CONFIG_AUDIT=false
export npm_config_loglevel=warn
# -------- Helper --------------------------------------------------------------
as_app(){ sudo -u "$APP_USER" -H bash -lc "$*"; }
restart_if_exists(){
local u="$1"
systemctl list-unit-files | grep -q "^${u}\.service" && systemctl restart "$u" || true
}
reload_if_active(){
local u="$1"
systemctl is-active --quiet "$u" && systemctl reload "$u" || true
}
restart_php_fpm(){
for u in php8.3-fpm php8.2-fpm php8.1-fpm php-fpm; do
if systemctl list-unit-files | grep -q "^${u}\.service"; then
# reload = graceful: laufende Requests fertig, dann Workers tauschen
systemctl reload "$u" 2>/dev/null || systemctl restart "$u"
return 0
fi
done
}
artisan_down(){
as_app "cd ${APP_DIR} && php artisan down --retry=10 --render=errors.503" 2>/dev/null || true
echo "[i] Wartungsmodus aktiviert"
}
artisan_up(){
as_app "cd ${APP_DIR} && php artisan up" 2>/dev/null || true
echo "[i] Wartungsmodus beendet"
}
git_safe(){
# Falls nötig: Repo als safe markieren (z.B. wenn Root den Wrapper aufruft)
as_app "git -C ${APP_DIR} config --global --add safe.directory ${APP_DIR} >/dev/null 2>&1 || true"
}
git_dirty_check(){
if [[ "$ALLOW_DIRTY" != "1" ]]; then
local dirty
dirty="$(as_app "git -C ${APP_DIR} status --porcelain")"
if [[ -n "$dirty" ]]; then
echo "[!] Arbeitsbaum hat uncommitted Änderungen. Abbruch (ALLOW_DIRTY=1 zum Überschreiben)."
exit 2
fi
fi
}
get_version(){
as_app "cd ${APP_DIR} && (git describe --tags --always 2>/dev/null || git rev-parse --short=7 HEAD)"
}
write_version_files(){
local ver="$1" rev="$2"
install -d -m 0755 /etc/mailwolt /var/lib/mailwolt || true
printf "version=%s\nrev=%s\nupdated=%s\n" "$ver" "$rev" "$(date -Is)" > /etc/mailwolt/build.info || true
printf '%s\n' "${ver#v}" > /var/lib/mailwolt/version || true
printf '%s\n' "$ver" > /var/lib/mailwolt/version_raw || true
}
write_build_info(){ write_version_files "$@"; }
# --- Frontend build (quiet & robust) ------------------------------------------
frontend_build_quiet() {
local LOG="/var/log/mailwolt-frontend-build.log"
local NPM_ENV="CI=1 NPM_CONFIG_FUND=false NPM_CONFIG_AUDIT=false npm_config_loglevel=warn"
# Logfile vorbereiten
install -d -m 0755 "$(dirname "$LOG")"
: > "$LOG" || true
chmod 0644 "$LOG"
echo "[i] Frontend: vorbereiten …"
# Build-Ziele & Temp besitzbar machen
as_app "mkdir -p '${APP_DIR}/public/build' '${APP_DIR}/node_modules' '${APP_DIR}/.vite' '${APP_DIR}/.npm-cache'"
chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}/public/build" "${APP_DIR}/node_modules" "${APP_DIR}/.vite" "${APP_DIR}/.npm-cache" || true
chmod -R g+rwX "${APP_DIR}/public/build" "${APP_DIR}/node_modules" "${APP_DIR}/.vite" || true
# evtl. störrische Reste entfernen (vermeidet EACCES beim Unlink/Rimraf)
rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true
# npm Config (kein Fund/Audit, eigener Cache unter App-User)
as_app "printf 'fund=false\naudit=false\ncache=${APP_DIR}/.npm-cache\n' > ~/.npmrc" >>"$LOG" 2>&1 || true
echo "[i] Frontend: Dependencies … (Details: $LOG)"
if ! as_app "cd '${APP_DIR}' && ${NPM_ENV} npm ci --no-audit --no-fund --no-progress" >>"$LOG" 2>&1; then
if ! as_app "cd '${APP_DIR}' && ${NPM_ENV} npm install --no-audit --no-fund --no-progress" >>"$LOG" 2>&1; then
echo "[!] npm install fehlgeschlagen. Letzte 60 Zeilen:"
tail -n 60 "$LOG" || true
return 1
fi
fi
echo "[i] Frontend: Build … (Details: $LOG)"
if ! as_app "cd '${APP_DIR}' && ${NPM_ENV} npm run build --silent --loglevel=warn" >>"$LOG" 2>&1; then
echo "[!] Build fehlgeschlagen. Letzte 80 Zeilen:"
tail -n 80 "$LOG" || true
return 1
fi
# Nach dem Build noch einmal Rechte begradigen
chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}/public/build" || true
find "${APP_DIR}/public/build" -type d -exec chmod 2775 {} \; 2>/dev/null || true
find "${APP_DIR}/public/build" -type f -exec chmod 0664 {} \; 2>/dev/null || true
echo "[✓] Frontend gebaut."
}
# -------- Guards --------------------------------------------------------------
[[ "$(id -u)" -eq 0 ]] || { echo "[!] Bitte als root ausführen"; exit 1; }
[[ -d "$APP_DIR/.git" ]] || { echo "[!] $APP_DIR scheint kein Git-Repo zu sein"; exit 1; }
# -------- Lock ----------------------------------------------------------------
mkdir -p "$STATE_DIR"
if [[ -f "$LOCK_FILE" ]]; then
LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null || true)
if [[ -n "$LOCK_PID" ]] && kill -0 "$LOCK_PID" 2>/dev/null; then
echo "[!] Update läuft bereits (PID $LOCK_PID) — Abbruch."
exit 1
fi
rm -f "$LOCK_FILE"
fi
echo $$ > "$LOCK_FILE"
# -------- State & Cleanup-Trap -----------------------------------------------
MAINTENANCE_ACTIVE=0
_cleanup() {
local rc=$?
[[ "$MAINTENANCE_ACTIVE" -eq 1 ]] && artisan_up
mkdir -p "$STATE_DIR"
echo "done" > "$STATE_DIR/state"
echo "$rc" > "$STATE_DIR/rc"
rm -f "$LOCK_FILE"
}
trap '_cleanup' EXIT INT TERM
# Update als laufend markieren
echo "running" > "$STATE_DIR/state"
rm -f "$STATE_DIR/rc"
: > "$LOG_FILE"
git_safe
git_dirty_check
# -------- Git: neuen Stand holen ---------------------------------------------
echo "[i] Prüfe Repository …"
OLD_REV="$(as_app "git -C ${APP_DIR} rev-parse HEAD")"
OLD_VER="$(get_version)"
NEW_REV="$OLD_REV"
if [[ "$MODE" = "tags" ]]; then
# → Neueste Tags holen
as_app "git -C ${APP_DIR} fetch --quiet origin && git -C ${APP_DIR} fetch --tags --quiet origin || true"
LATEST_TAG="$(as_app "git -C ${APP_DIR} describe --tags --abbrev=0 \$(git -C ${APP_DIR} rev-list --tags --max-count=1 2>/dev/null) 2>/dev/null || echo ''")"
if [[ -z "$LATEST_TAG" ]]; then
echo "[!] Keine Tags gefunden falle auf origin/${BRANCH} zurück"
as_app "git -C ${APP_DIR} checkout -q ${BRANCH} && git -C ${APP_DIR} pull --ff-only origin ${BRANCH}"
else
TARGET_REV="$(as_app "git -C ${APP_DIR} rev-list -n1 ${LATEST_TAG}")"
if [[ "$TARGET_REV" = "$OLD_REV" ]]; then
echo "[✓] Bereits auf neuestem Release (${LATEST_TAG}) nichts zu tun."
write_build_info "$(get_version)" "$OLD_REV"
exit 0
fi
echo "[i] Checkout auf Release ${LATEST_TAG} (${TARGET_REV:0:7}) …"
as_app "git -C ${APP_DIR} checkout -q ${LATEST_TAG}"
fi
NEW_REV="$(as_app "git -C ${APP_DIR} rev-parse HEAD")"
else
# Rolling: branch folgen
as_app "git -C ${APP_DIR} fetch --quiet origin ${BRANCH}"
BEHIND="$(as_app "git -C ${APP_DIR} rev-list --count HEAD..origin/${BRANCH} || echo 0")"
if [[ "$BEHIND" -eq 0 ]]; then
echo "[✓] Branch origin/${BRANCH} ist bereits aktuell nichts zu tun."
write_build_info "$(get_version)" "$OLD_REV"
exit 0
fi
echo "[i] Es gibt ${BEHIND} neue Commit(s) ziehe Änderungen …"
as_app "git -C ${APP_DIR} checkout -q ${BRANCH} && git -C ${APP_DIR} pull --ff-only origin ${BRANCH}"
NEW_REV="$(as_app "git -C ${APP_DIR} rev-parse HEAD")"
fi
# -------- Änderungstypen ermitteln -------------------------------------------
CHANGED_FILES="$(as_app "git -C ${APP_DIR} diff --name-only ${OLD_REV}..${NEW_REV}")"
NEED_COMPOSER=0
NEED_MIGRATIONS=0
NEED_FRONTEND=0
NEED_PHP_RESTART=0
echo "$CHANGED_FILES" | grep -qE '(^|/)composer\.(json|lock)$' && NEED_COMPOSER=1
echo "$CHANGED_FILES" | grep -qE '^database/migrations/' && NEED_MIGRATIONS=1
echo "$CHANGED_FILES" | grep -qE '^(package(-lock)?\.json|vite\.config(\.ts|\.js)?|resources/|public/.*\.(js|css))' && NEED_FRONTEND=1
echo "$CHANGED_FILES" | grep -qE '^(app/|routes/|config/|resources/views/)' && NEED_PHP_RESTART=1
echo "[i] Zusammenfassung:"
echo " Composer : $([[ $NEED_COMPOSER -eq 1 ]] && echo JA || echo nein)"
echo " Migrations : $([[ $NEED_MIGRATIONS -eq 1 ]] && echo JA || echo nein)"
echo " Frontend : $([[ $NEED_FRONTEND -eq 1 ]] && echo JA || echo nein)"
echo " PHP restart : $([[ $NEED_PHP_RESTART -eq 1 ]] && echo JA || echo nein)"
# Wenn gar nichts relevantes geändert wurde → sauber beenden
if [[ $NEED_COMPOSER -eq 0 && $NEED_MIGRATIONS -eq 0 && $NEED_FRONTEND -eq 0 && $NEED_PHP_RESTART -eq 0 ]]; then
echo "[✓] Code-Stand aktualisiert, aber keine Build/Runtime-Änderungen keine Neustarts nötig."
NEW_VER="$(get_version)"
write_build_info "$NEW_VER" "$NEW_REV"
echo "[i] Version: ${OLD_VER}${NEW_VER}"
exit 0
fi
# -------- Composer (ohne Downtime) -------------------------------------------
if [[ $NEED_COMPOSER -eq 1 ]]; then
echo "[i] Composer …"
as_app "cd ${APP_DIR} && composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev"
fi
# -------- Frontend (ohne Downtime — Assets sind statisch) --------------------
if [[ $NEED_FRONTEND -eq 1 ]]; then
echo "[i] Frontend-Änderungen erkannt baue Assets …"
if ! frontend_build_quiet; then
echo "[!] Frontend-Build ist fehlgeschlagen (siehe /var/log/mailwolt-frontend-build.log)."
exit 1
fi
fi
# -------- Wartungsmodus: Migrations + Cache + PHP-FPM reload -----------------
if [[ $NEED_MIGRATIONS -eq 1 || $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 ]]; then
artisan_down
MAINTENANCE_ACTIVE=1
fi
if [[ $NEED_MIGRATIONS -eq 1 ]]; then
echo "[i] DB-Migrationen …"
as_app "cd ${APP_DIR} && php artisan migrate --force"
fi
if [[ $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 || $NEED_MIGRATIONS -eq 1 ]]; then
echo "[i] Cache/Optimierungen …"
as_app "cd ${APP_DIR} && php artisan optimize:clear || true"
as_app "cd ${APP_DIR} && php artisan config:cache || true"
as_app "cd ${APP_DIR} && php artisan route:cache || true"
as_app "cd ${APP_DIR} && php artisan queue:restart || true"
fi
# -------- Dienste neu laden (gezielt) ----------------------------------------
echo "[i] Dienste neu laden …"
if [[ $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 || $NEED_MIGRATIONS -eq 1 ]]; then
restart_php_fpm
restart_if_exists "${APP_USER}-queue"
restart_if_exists "${APP_USER}-schedule"
restart_if_exists "${APP_USER}-ws"
fi
if [[ $NEED_FRONTEND -eq 1 || $NEED_PHP_RESTART -eq 1 ]]; then
reload_if_active nginx
fi
# -------- Wartungsmodus beenden ----------------------------------------------
if [[ "$MAINTENANCE_ACTIVE" -eq 1 ]]; then
artisan_up
MAINTENANCE_ACTIVE=0
fi
# -------- Version + Build-Info ablegen ----------------------------------------
NEW_VER="$(get_version)"
write_version_files "$NEW_VER" "$NEW_REV"
echo "[✓] Update abgeschlossen: ${OLD_VER}${NEW_VER} (${OLD_REV:0:7}${NEW_REV:0:7})"