From fceb0eef77515358b5144b20fe24b1d537c6393f Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 19 Jun 2026 17:58:50 +0200 Subject: [PATCH] feat(deploy): one-command update.sh + preserve config on install re-run Add update.sh (sudo ./update.sh): git pull --ff-only (never discards local changes), self-update via re-exec if the script itself changed, then hand off to the idempotent install.sh non-interactively. Sets safe.directory so a root-run pull works on the clusev-owned tree. Fix install.sh so a non-interactive re-run (an update) no longer wipes a configured domain / ACME e-mail: both now default to the existing .env value when not supplied. First install (empty = bare IP) is unchanged. README documents 'sudo ./update.sh'; the MOTD shows the update command. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 20 ++++++++++++++++ README.md | 7 ++++-- config/clusev.php | 2 +- docker/motd/00-clusev | 1 + install.sh | 10 ++++++-- update.sh | 55 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 5 deletions(-) create mode 100755 update.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 292c29b..bef1dee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,26 @@ getaggte Releases (Kanal `stable`, optional `beta`) — niemals Entwicklungs-Bui _Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `vX.Y.Z` getaggt._ +## [0.9.8] - 2026-06-19 + +### Hinzugefügt +- **`update.sh` — Ein-Befehl-Update (`sudo ./update.sh`).** Holt den neuesten Stand + (`git pull --ff-only`, verwirft nie lokale Änderungen), **aktualisiert sich bei Bedarf selbst** + (hat sich `update.sh` im Pull geändert, startet es die neue Version neu) und übergibt dann an den + idempotenten `install.sh` (Build, Neustart, Migration, MOTD). Setzt für root automatisch + `safe.directory` (das Verzeichnis gehört dem `clusev`-Nutzer, sonst verweigert ein root-`git pull`) + und ruft `install.sh` **nicht-interaktiv** auf, sodass nichts neu abgefragt wird. + +### Behoben +- **Re-Run von `install.sh` bewahrt jetzt Domain & ACME-E-Mail.** Bislang schrieb der Installer + `APP_DOMAIN`/`ACME_EMAIL` bei jedem Lauf — ein nicht-interaktiver Re-Run (also ein Update) ohne + gesetzte `CLUSEV_DOMAIN` hätte eine konfigurierte Domain **gelöscht**. Beide defaulten jetzt auf den + bestehenden `.env`-Wert; Erst-Installation (leer = Bare-IP) unverändert. + +### Geändert +- **README-Update-Abschnitt** auf `sudo ./update.sh` umgestellt (der alte Zweischritt + `git pull && sudo ./install.sh` funktioniert weiterhin); MOTD zeigt den Update-Befehl an. + ## [0.9.7] - 2026-06-19 ### Behoben diff --git a/README.md b/README.md index 8a33646..6b2dde6 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,13 @@ terminal needed; a small, scoped host service performs the restart). ```bash cd clusev -git pull -sudo ./install.sh # idempotent: rebuilds, restarts and migrates — secrets are preserved +sudo ./update.sh # pulls the latest code, then rebuilds, restarts and migrates ``` +`update.sh` fast-forwards the repo (it never discards local changes), re-runs the idempotent +installer non-interactively, and updates itself if the script changed. Secrets and the configured +domain / e-mail are preserved. (The older two-step `git pull && sudo ./install.sh` still works.) + ## Account recovery The **forgot-password** screen offers self-service recovery: an **e-mail reset link** (valid 15 diff --git a/config/clusev.php b/config/clusev.php index 657da12..b84438e 100644 --- a/config/clusev.php +++ b/config/clusev.php @@ -3,7 +3,7 @@ return [ // First tagged release is v0.1.0 (semantic, not -dev). The live build hash // is resolved from .git at runtime (see App\Livewire\Versions\Index). - 'version' => '0.9.7', + 'version' => '0.9.8', // Default user channel. Only 'stable' and 'beta' are ever offered to users. 'channel' => 'stable', diff --git a/docker/motd/00-clusev b/docker/motd/00-clusev index 0e8aaec..8208108 100644 --- a/docker/motd/00-clusev +++ b/docker/motd/00-clusev @@ -42,5 +42,6 @@ printf ' %sDashboard%s %s%s%s\n' "$D" "$R" "$O" "$URL" "$R" printf ' %sStack%s %s\n' "$D" "$R" "$(stack_status)" printf ' %sLogin%s %sStandard-Passwort: clusev — beim ersten Login aendern · Reset: clusev:reset-admin%s\n' "$D" "$R" "$D" "$R" printf ' %sVerwalten%s %sdocker compose -f docker-compose.prod.yml ps | logs -f | restart%s\n' "$D" "$R" "$D" "$R" +printf ' %sUpdate%s %ssudo ./update.sh (in %s)%s\n' "$D" "$R" "$D" "$(dirname "$COMPOSE")" "$R" printf ' %s──────────────────────────────────────────────────────────%s\n' "$D" "$R" printf '%s\n' "" diff --git a/install.sh b/install.sh index c6ae311..d49860f 100755 --- a/install.sh +++ b/install.sh @@ -45,6 +45,9 @@ set_kv() { fi } rand_hex() { openssl rand -hex "${1:-32}"; } +# env_get: read a key's current value from .env (empty if the file or key is absent). Lets a +# re-run / update default to the EXISTING domain & ACME e-mail instead of wiping them. +env_get() { [ -f "$ENV_FILE" ] && grep -E "^${1}=" "$ENV_FILE" | head -n1 | cut -d= -f2- || true; } # ── root + OS detection (before everything) ────────────────────────── [ "$(id -u)" = 0 ] || die "Bitte mit sudo ausfuehren: sudo ./install.sh" @@ -106,8 +109,11 @@ chown -R clusev:clusev . info "Installationsverzeichnis gehoert clusev" # ── inputs (interactive at a TTY, else env / bare-IP fallback) ─────── -DOMAIN="${CLUSEV_DOMAIN:-}" -ADMIN_EMAIL="${CLUSEV_ADMIN_EMAIL:-}" +# Default to the EXISTING .env values so a non-interactive re-run (i.e. an update via +# update.sh) preserves the configured domain / ACME e-mail instead of clearing them. On a +# first install .env does not exist yet, so these resolve empty → bare-IP, unchanged. +DOMAIN="${CLUSEV_DOMAIN:-$(env_get APP_DOMAIN)}" +ADMIN_EMAIL="${CLUSEV_ADMIN_EMAIL:-$(env_get ACME_EMAIL)}" HTTP_PORT="${CLUSEV_HTTP_PORT:-80}" if [ -t 0 ]; then echo; bold "Clusev Installer"; echo "================" diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..474c3ec --- /dev/null +++ b/update.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Clusev updater — pull the latest code and re-run the idempotent installer (root required). +# One command to update an existing install: +# +# sudo ./update.sh +# +# In order: +# 1. Mark the repo a safe git dir for root. install.sh chowns the tree to the `clusev` user, +# so a root-run `git pull` would otherwise refuse with "dubious ownership". +# 2. git pull --ff-only on the tracked branch. Aborts cleanly on any conflict / divergence — +# it NEVER discards local changes. +# 3. Self-update: if update.sh itself changed in the pull, re-exec the NEW copy once (guarded +# against loops) so improvements to this very script take effect on the same run. +# 4. Hand off to ./install.sh — idempotent: rebuilds the image, restarts the stack, migrates, +# refreshes the MOTD and fixes ownership. Run NON-interactively (stdin from /dev/null) so +# it never re-prompts; secrets and the configured domain / ACME e-mail are preserved. +set -euo pipefail + +cd "$(dirname "$0")" +REPO_DIR="$(pwd)" + +bold() { printf '\033[1m%s\033[0m\n' "$*"; } +info() { printf ' %s\n' "$*"; } +die() { printf '\033[31m x %s\033[0m\n' "$*" >&2; exit 1; } + +[ "$(id -u)" = 0 ] || die "Bitte mit sudo ausfuehren: sudo ./update.sh" +[ -f docker-compose.prod.yml ] || die "Keine Clusev-Installation hier (docker-compose.prod.yml fehlt)." +[ -x ./install.sh ] || [ -f ./install.sh ] || die "install.sh fehlt — Repo unvollstaendig." +command -v git >/dev/null 2>&1 || die "git ist nicht installiert." + +bold "Clusev Update" +echo "=============" + +# Steps 1–3 run only on the first pass; a re-exec (after a self-update) jumps straight to step 4. +if [ "${CLUSEV_UPDATE_REEXEC:-0}" != 1 ]; then + # 1. let root operate on the clusev-owned working tree + git config --global --add safe.directory "$REPO_DIR" 2>/dev/null || true + + # 2. fast-forward pull only — never auto-merge or throw away local edits + before_hash="$(sha256sum -- "$0" | cut -d' ' -f1)" + info "Hole Aktualisierungen (git pull --ff-only) ..." + git pull --ff-only \ + || die "git pull fehlgeschlagen (lokale Aenderungen oder divergierter Branch). Mit 'git status' pruefen und erneut versuchen." + after_hash="$(sha256sum -- "$0" | cut -d' ' -f1)" + + # 3. self-update: relaunch the new update.sh if it changed + if [ "$before_hash" != "$after_hash" ]; then + info "update.sh wurde aktualisiert — starte die neue Version ..." + exec env CLUSEV_UPDATE_REEXEC=1 "$0" "$@" + fi +fi + +# 4. apply via the idempotent installer, non-interactively (no prompts; config preserved) +info "Wende Update an (install.sh) ..." +exec ./install.sh