diff --git a/README.md b/README.md index 03f74f5..6f252de 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ vorbereitet. ## Inhalt - [Schnellstart](#schnellstart) +- [Aktualisieren](#aktualisieren) - [Konfiguration](#konfiguration) - [Bedienung](#bedienung) - [Technischer Aufbau](#technischer-aufbau) @@ -45,6 +46,53 @@ docker compose down -v # Stack stoppen und Datenbank löschen --- +## Aktualisieren + +Für neue Versionen liegt ein Update-Skript bei — einmal für Linux/macOS und +einmal für Windows. Es holt den aktuellen Stand aus Git, baut den Stack neu +und wartet, bis die Anwendung wieder antwortet. + +```bash +./update.sh # Linux / macOS +``` + +```powershell +.\update.ps1 # Windows +``` + +Beim ersten Aufruf unter Linux muss das Skript einmal ausführbar gemacht +werden: `chmod +x update.sh`. + +Was das Skript tut: + +1. prüft, ob `git`, `docker compose` und die `.env` vorhanden sind, +2. bricht ab, wenn es **lokale Änderungen** gibt — sonst würde `git pull` im + Konflikt enden, +3. holt den neuen Stand mit `git merge --ff-only` und zeigt die neuen Commits, +4. baut den Stack nur neu, wenn es tatsächlich Änderungen gab, +5. wartet, bis die Anwendung auf `APP_PORT` antwortet, und zeigt sonst das + Protokoll, +6. gibt die Migrationszeilen aus dem Start-Protokoll und den Containerstatus + aus. + +Optionen: + +| Option | Wirkung | +| ------------------- | -------------------------------------------------------------- | +| `--force` / `-Force` | Auch ohne neue Commits neu bauen | +| `--prune` / `-Prune` | Danach verwaiste Docker-Images entfernen (`docker image prune`) | +| `--help` | Hilfe anzeigen | + +Die Datenbank bleibt beim Update erhalten — neue Migrationen wendet der +App-Container beim Start automatisch an. Wer lieber von Hand aktualisiert: + +```bash +git pull +docker compose up -d --build +``` + +--- + ## Konfiguration Alle Einstellungen stehen in der `.env` im Projektverzeichnis (Vorlage: @@ -171,6 +219,7 @@ prisma/ migrations/ Versionierte Migrationen docker/ entrypoint.sh Migration + Serverstart im Container +update.sh / update.ps1 Update-Skript (git pull + Neubau + Prüfung) src/ app/ page.tsx Startseite mit dem Kachel-Grid diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh old mode 100644 new mode 100755 diff --git a/update.ps1 b/update.ps1 new file mode 100644 index 0000000..9e4d3f2 --- /dev/null +++ b/update.ps1 @@ -0,0 +1,149 @@ +<# +.SYNOPSIS + QR-Code-Generator aktualisieren: neuen Stand holen, Stack neu bauen, starten. + +.DESCRIPTION + Windows-Gegenstück zu update.sh. Holt den aktuellen Stand aus dem Git-Remote + und baut den Docker-Stack neu, wenn es Änderungen gab. + +.PARAMETER Force + Auch dann neu bauen, wenn es keine neuen Commits gibt. + +.PARAMETER Prune + Nach dem Bauen verwaiste Docker-Images entfernen. + +.EXAMPLE + .\update.ps1 + .\update.ps1 -Force -Prune +#> +[CmdletBinding()] +param( + [switch]$Force, + [switch]$Prune +) + +$ErrorActionPreference = 'Stop' + +function Write-Info { param([string]$Text) Write-Host "→ $Text" } +function Write-Ok { param([string]$Text) Write-Host "✓ $Text" -ForegroundColor Green } +function Stop-WithError { + param([string]$Text) + Write-Host "✗ $Text" -ForegroundColor Red + exit 1 +} + +# Immer im Projektverzeichnis arbeiten, egal von wo das Skript gestartet wird. +Set-Location -Path $PSScriptRoot + +# --- Voraussetzungen -------------------------------------------------------- + +foreach ($tool in @('git', 'docker')) { + if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { + Stop-WithError "$tool ist nicht installiert." + } +} + +docker compose version *> $null +if (-not $?) { + Stop-WithError 'docker compose ist nicht verfügbar (Docker Compose v2 wird benötigt).' +} + +if (-not (Test-Path .env)) { + Stop-WithError '.env fehlt. Anlegen mit: Copy-Item .env.example .env' +} + +# Ein "git pull" auf einem veränderten Arbeitsverzeichnis endet im Konflikt. +$dirty = git status --porcelain +if ($dirty) { + Write-Host '✗ Es gibt lokale Änderungen im Arbeitsverzeichnis:' -ForegroundColor Red + git status --short + Write-Host '' + Stop-WithError "Bitte zuerst committen oder mit 'git stash' beiseitelegen." +} + +# --- Neuen Stand holen ------------------------------------------------------ + +$before = git rev-parse HEAD +$branch = git rev-parse --abbrev-ref HEAD + +Write-Info "Hole neuen Stand von origin/$branch …" +git fetch --quiet origin +if (-not $?) { Stop-WithError 'Der Remote ist nicht erreichbar.' } + +# --ff-only: lieber abbrechen als eine Merge-Situation erzeugen. +git merge --ff-only "origin/$branch" +if (-not $?) { + Stop-WithError 'Der lokale Stand lässt sich nicht ohne Merge aktualisieren. Bitte von Hand prüfen.' +} + +$after = git rev-parse HEAD + +if ($before -eq $after) { + Write-Ok "Bereits auf dem neuesten Stand ($(git rev-parse --short HEAD))." +} else { + Write-Ok "Aktualisiert: $(git rev-parse --short $before) → $(git rev-parse --short $after)" + Write-Host '' + git --no-pager log --oneline "$before..$after" + Write-Host '' +} + +# --- Stack neu bauen -------------------------------------------------------- + +$running = @(docker compose ps --status running --quiet).Count + +if (($before -eq $after) -and (-not $Force) -and ($running -gt 0)) { + Write-Ok 'Der Stack läuft und es gibt nichts Neues — kein Neubau nötig.' + Write-Host ' (Mit .\update.ps1 -Force lässt sich ein Neubau erzwingen.)' + exit 0 +} + +Write-Info 'Baue und starte den Stack neu — das dauert einige Minuten …' +docker compose up -d --build +if (-not $?) { Stop-WithError 'Der Neubau ist fehlgeschlagen.' } + +# --- Warten, bis die Anwendung antwortet ------------------------------------ + +# Nur die Ziffern behalten — so stören weder Anführungszeichen noch Leerzeichen. +$portLine = Select-String -Path .env -Pattern '^APP_PORT=' | Select-Object -First 1 +$appPort = if ($portLine) { ($portLine.Line -replace '\D', '') } else { '' } +if (-not $appPort) { $appPort = '3000' } + +Write-Info 'Warte darauf, dass die Anwendung antwortet …' +$ready = $false +for ($attempt = 0; $attempt -lt 60; $attempt++) { + try { + Invoke-WebRequest -Uri "http://localhost:$appPort/" -UseBasicParsing -TimeoutSec 5 | Out-Null + $ready = $true + break + } catch { + Start-Sleep -Seconds 2 + } +} + +if (-not $ready) { + docker compose logs --tail 40 app + Stop-WithError 'Die Anwendung antwortet nach 2 Minuten nicht. Protokoll siehe oben.' +} + +Write-Ok "Die Anwendung antwortet auf http://localhost:$appPort" + +# --- Ergebnis --------------------------------------------------------------- + +Write-Host '' +Write-Info 'Datenbankmigrationen beim Start:' +docker compose logs app | + Select-String -Pattern 'migration', 'Migration', 'pending' | + Select-Object -Last 5 | + ForEach-Object { " $($_.Line)" } + +Write-Host '' +docker compose ps + +if ($Prune) { + Write-Host '' + Write-Info 'Räume verwaiste Docker-Images auf …' + docker image prune --force +} + +Write-Host '' +Write-Ok "Fertig — Version $(git rev-parse --short HEAD)" diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..7c581d7 --- /dev/null +++ b/update.sh @@ -0,0 +1,152 @@ +#!/bin/sh +# ============================================================================= +# QR-Code-Generator aktualisieren +# +# ./update.sh neuen Stand holen und, falls es Änderungen gibt, +# den Stack neu bauen und starten +# ./update.sh --force auch ohne neue Commits neu bauen +# ./update.sh --prune danach verwaiste Docker-Images aufräumen +# ./update.sh --help diese Hilfe +# ============================================================================= +set -eu + +FORCE=0 +PRUNE=0 + +usage() { + cat <<'HILFE' +QR-Code-Generator aktualisieren. + + ./update.sh neuen Stand holen und, falls es Änderungen gibt, + den Stack neu bauen und starten + ./update.sh --force auch ohne neue Commits neu bauen + ./update.sh --prune danach verwaiste Docker-Images aufräumen + ./update.sh --help diese Hilfe +HILFE +} + +while [ $# -gt 0 ]; do + case "$1" in + --force) FORCE=1 ;; + --prune) PRUNE=1 ;; + -h|--help) usage; exit 0 ;; + *) + echo "Unbekannte Option: $1" >&2 + echo "Aufruf: ./update.sh [--force] [--prune]" >&2 + exit 2 + ;; + esac + shift +done + +# Immer im Projektverzeichnis arbeiten, egal von wo das Skript gestartet wird. +cd "$(dirname "$0")" + +info() { echo "→ $*"; } +ok() { echo "✓ $*"; } +fail() { echo "✗ $*" >&2; exit 1; } + +# --- Voraussetzungen -------------------------------------------------------- + +command -v git >/dev/null 2>&1 || fail "git ist nicht installiert." +command -v docker >/dev/null 2>&1 || fail "docker ist nicht installiert." +docker compose version >/dev/null 2>&1 || + fail "docker compose ist nicht verfügbar (Docker Compose v2 wird benötigt)." + +[ -f .env ] || fail ".env fehlt. Anlegen mit: cp .env.example .env" + +# Ein "git pull" auf einem verändertem Arbeitsverzeichnis endet im Konflikt. +if [ -n "$(git status --porcelain)" ]; then + echo "✗ Es gibt lokale Änderungen im Arbeitsverzeichnis:" >&2 + git status --short >&2 + echo "" >&2 + fail "Bitte zuerst committen oder mit 'git stash' beiseitelegen." +fi + +# --- Neuen Stand holen ------------------------------------------------------ + +BEFORE="$(git rev-parse HEAD)" +BRANCH="$(git rev-parse --abbrev-ref HEAD)" + +info "Hole neuen Stand von origin/$BRANCH …" +git fetch --quiet origin +# --ff-only: lieber abbrechen als eine Merge-Situation erzeugen, die auf einem +# Server niemand auflösen will. +git merge --ff-only "origin/$BRANCH" || + fail "Der lokale Stand lässt sich nicht ohne Merge aktualisieren. Bitte von Hand prüfen." + +AFTER="$(git rev-parse HEAD)" + +if [ "$BEFORE" = "$AFTER" ]; then + ok "Bereits auf dem neuesten Stand ($(git rev-parse --short HEAD))." +else + ok "Aktualisiert: $(git rev-parse --short "$BEFORE") → $(git rev-parse --short "$AFTER")" + echo "" + git --no-pager log --oneline "$BEFORE..$AFTER" + echo "" +fi + +# --- Stack neu bauen -------------------------------------------------------- + +RUNNING="$(docker compose ps --status running --quiet 2>/dev/null | wc -l | tr -d ' ')" + +if [ "$BEFORE" = "$AFTER" ] && [ "$FORCE" -eq 0 ] && [ "$RUNNING" -gt 0 ]; then + ok "Der Stack läuft und es gibt nichts Neues — kein Neubau nötig." + echo " (Mit ./update.sh --force lässt sich ein Neubau erzwingen.)" + exit 0 +fi + +info "Baue und starte den Stack neu — das dauert einige Minuten …" +docker compose up -d --build + +# --- Warten, bis die Anwendung antwortet ------------------------------------ + +# Nur die Ziffern behalten — so stören weder Anführungszeichen noch ein +# Wagenrücklauf aus einer unter Windows bearbeiteten .env. +APP_PORT="$(grep -m1 '^APP_PORT=' .env | cut -d= -f2 | tr -cd '0-9')" +[ -n "$APP_PORT" ] || APP_PORT=3000 + +probe() { + if command -v curl >/dev/null 2>&1; then + curl --silent --fail --max-time 5 --output /dev/null "http://localhost:$APP_PORT/" + elif command -v wget >/dev/null 2>&1; then + wget --quiet --timeout=5 --tries=1 --spider "http://localhost:$APP_PORT/" + else + return 0 # Kein Prüfwerkzeug vorhanden — dann eben ohne Prüfung. + fi +} + +info "Warte darauf, dass die Anwendung antwortet …" +ATTEMPT=0 +until probe; do + ATTEMPT=$((ATTEMPT + 1)) + if [ "$ATTEMPT" -ge 60 ]; then + echo "" + docker compose logs --tail 40 app >&2 + fail "Die Anwendung antwortet nach 2 Minuten nicht. Protokoll siehe oben." + fi + sleep 2 +done + +ok "Die Anwendung antwortet auf http://localhost:$APP_PORT" + +# --- Ergebnis --------------------------------------------------------------- + +echo "" +info "Datenbankmigrationen beim Start:" +docker compose logs app 2>/dev/null | + grep -E "migration|Migration|pending" | + tail -n 5 | + sed 's/^/ /' + +echo "" +docker compose ps + +if [ "$PRUNE" -eq 1 ]; then + echo "" + info "Räume verwaiste Docker-Images auf …" + docker image prune --force +fi + +echo "" +ok "Fertig — Version $(git rev-parse --short HEAD)"