open_notebook/scripts/update_stack.sh
dschlueter 4805fe75ed Pin images by digest; add update, smoke-test and status scripts
Goal: at every session start, run exactly the application the repo says — and be able
to move to a new upstream release deliberately, with a way back.

The old setup (v1-latest + pull_policy: always) was unreliable in both directions.
`docker compose up -d` silently pulled a new application, including an irreversible
SurrealDB schema migration, while a reboot (restart: always) or `docker compose start`
kept running the old image without pulling anything. The running version was effectively
unpredictable.

Note v1-latest tracks releases, not main: the current image (v1.10.0, 2026-06-18) IS the
latest release — the repo being "ahead" is unreleased code, which is explicitly not
wanted here. So this is about guaranteeing and detecting, not catching up.

- docker-compose.yml: both images pinned by digest, pull_policy: missing.
- scripts/check_updates.sh: reports running version/digest vs the latest release and
  registry digest. Changes nothing; meant for the session-start ritual.
- scripts/update_stack.sh: resolve new digest -> stop -> back up surreal_data,
  notebook_data and docker-compose.yml -> repin -> start -> smoke test -> roll back data
  AND compose file if the smoke test fails. The data backup is the point: the app migrates
  the DB on startup and an older app cannot read a migrated DB, so a bad update would
  otherwise be a one-way door. tar runs inside a container because the data dirs are
  root-owned and the host user cannot restore over them.
- scripts/smoke_test.sh: checks what actually breaks here, not just "does it start".
  Every local adaptation leans on upstream internals and can break silently: the
  prompts/podcast directory mount masks the image's directory (a new template upstream
  would be invisible), config/content_core.yaml freezes content-core's defaults (because
  CCORE_CONFIG_PATH replaces rather than merges), and the env-var workarounds depend on
  current content-core/esperanto/podcast_creator behaviour. So it verifies those
  assumptions explicitly, plus API, TTS audio, STT and a real YouTube transcript.

Both scripts read the digest from the lfnovo/open_notebook line specifically. A naive
"first sha256 in the file" grep matches surrealdb (listed first) — caught while testing:
check_updates.sh falsely reported an update, and update_stack.sh would have rewritten the
database image instead of the application.

Verified: smoke test green against the current stack, and red (exit 1) when failures are
injected (dead TTS port, removed template variable). Backup/restore mechanics exercised
separately: 44 MB in 3.3s, restore yields 13 DB files and 91 notebook files. The update
path itself cannot be exercised end to end until a newer image exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 14:12:31 +02:00

104 lines
3.9 KiB
Bash
Executable file

#!/usr/bin/env bash
# Aktualisiert Open Notebook auf das neueste veroeffentlichte Image — mit Rueckfallpunkt.
#
# Ablauf:
# 1. Neuen Digest aus der Registry holen (nichts zu tun, wenn schon aktuell).
# 2. Stack stoppen und Daten sichern (surreal_data + notebook_data + docker-compose.yml).
# 3. Digest in docker-compose.yml eintragen, Stack starten.
# 4. Rauchtest (scripts/smoke_test.sh).
# 5. Rot -> alles zurueckrollen (Daten UND Compose-Datei). Gruen -> fertig.
#
# Der Rollback der Daten ist der Grund fuer das Backup: Open Notebook migriert die
# SurrealDB beim Start, und ein Downgrade der Anwendung kann eine migrierte Datenbank
# nicht wieder lesen. Ohne Backup waere ein missgluecktes Update eine Einbahnstrasse.
#
# ./scripts/update_stack.sh # auf neuestes Image
# ./scripts/update_stack.sh --dry-run # nur zeigen, was passieren wuerde
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO"
TAG="lfnovo/open_notebook:v1-latest"
BACKUP_DIR="$REPO/backups"
KEEP_BACKUPS=3
DRY_RUN=0
[ "${1:-}" = "--dry-run" ] && DRY_RUN=1
say() { printf '\n\033[1m%s\033[0m\n' "$1"; }
# Gezielt aus der open_notebook-Zeile. Ein blindes grep auf den ersten Digest der Datei
# erwischt surrealdb (steht weiter oben) — das sed weiter unten wuerde dann den falschen
# Digest ersetzen und die Datenbank austauschen statt der Anwendung.
pinned="$(grep -oP 'lfnovo/open_notebook\S*@\Ksha256:[0-9a-f]+' docker-compose.yml | head -1)"
[ -n "$pinned" ] || { echo "FEHLER: kein open_notebook-Digest in docker-compose.yml gepinnt." >&2; exit 1; }
say "1/5 Registry abfragen"
remote="$(docker buildx imagetools inspect "$TAG" --format '{{.Manifest.Digest}}')"
echo " gepinnt: $pinned"
echo " neu : $remote"
if [ "$remote" = "$pinned" ]; then
echo " Bereits aktuell — nichts zu tun."
exit 0
fi
if [ "$DRY_RUN" = 1 ]; then
echo " (--dry-run: hier wuerde das Update starten)"
exit 0
fi
ts="$(date +%Y%m%d-%H%M%S)"
backup="$BACKUP_DIR/$ts"
mkdir -p "$backup"
say "2/5 Stack stoppen und sichern -> backups/$ts"
docker compose down
cp docker-compose.yml "$backup/docker-compose.yml"
# Die Datenverzeichnisse gehoeren root (der Container schreibt als root), deshalb wird
# im Container gepackt statt auf dem Host — spart sudo.
docker run --rm -v "$REPO:/work" -w /work --entrypoint tar \
"lfnovo/open_notebook@$pinned" -czf "backups/$ts/data.tgz" surreal_data notebook_data
echo " $(du -sh "$backup/data.tgz" | cut -f1) gesichert"
restore() {
say "ROLLBACK"
docker compose down || true
cp "$backup/docker-compose.yml" docker-compose.yml
docker run --rm -v "$REPO:/work" -w /work --entrypoint sh \
"lfnovo/open_notebook@$pinned" -c \
"rm -rf surreal_data notebook_data && tar -xzf backups/$ts/data.tgz"
docker compose up -d
echo " Alter Stand ($pinned) ist wiederhergestellt, Daten aus backups/$ts."
echo " Das neue Image bleibt lokal liegen — Ursache pruefen, dann erneut versuchen."
}
say "3/5 Auf neues Image umstellen"
sed -i "s|@$pinned|@$remote|" docker-compose.yml
docker compose up -d
echo " gestartet, warte auf die API..."
for _ in $(seq 1 30); do
curl -sf -m 3 http://127.0.0.1:5055/api/models/providers >/dev/null 2>&1 && break
sleep 5
done
say "4/5 Rauchtest"
if ./scripts/smoke_test.sh; then
say "5/5 Fertig"
newver="$(docker compose exec -T open_notebook grep -m1 '^version' /app/pyproject.toml |
grep -oP '(?<=")[^"]+' || echo '?')"
echo " Update auf $newver erfolgreich, Rauchtest gruen."
echo " Backup: backups/$ts (bleibt liegen)"
echo
echo " Bitte den neuen Digest committen:"
echo " git add docker-compose.yml && git commit -m 'Update Open Notebook auf $newver'"
else
echo
echo " Rauchtest ROT — das Update wird zurueckgenommen."
restore
exit 1
fi
# Alte Backups aufraeumen (Platte ist knapp).
ls -1dt "$BACKUP_DIR"/*/ 2>/dev/null | tail -n +$((KEEP_BACKUPS + 1)) | while read -r old; do
echo " raeume altes Backup weg: $(basename "$old")"
rm -rf "$old"
done