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) <noreply@anthropic.com>
feat/v1-foundation v0.9.8
boban 2026-06-19 17:58:50 +02:00
parent 1c3d3d6a01
commit fceb0eef77
6 changed files with 90 additions and 5 deletions

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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' ""

View File

@ -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 "================"

55
update.sh Executable file
View File

@ -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 13 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 </dev/null