From 107406e3c038cd184092704db2ee8a4be20e5469 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Mon, 24 Aug 2026 00:36:08 +0200 Subject: [PATCH] Update brach ohne Meldung ab Beim Sprung von 1.5.0 auf 1.10.1 endete scripts/update.sh mitten in der Ausgabe. Kein Fehler, keine Zeile, einfach zurueck zur Eingabeaufforderung. Ursache war eine abgeschnittene Leitung: git log --oneline "HEAD..$ZIEL" | head -15 | sed ... head schliesst die Leitung nach 15 Zeilen, git log schreibt weiter und bekommt SIGPIPE (141). Mit "set -o pipefail" gilt die ganze Kette als gescheitert, "set -e" beendet das Skript - stumm. Zeitabhaengig, deshalb monatelang unauffaellig: Solange git log fertig wurde, bevor head zumachte, ging es gut. Die Schwelle liegt exakt bei 15 Commits, und v1.5.0..v1.10.0 waren die ersten 16. Behoben, indem der Erzeuger selbst begrenzt (git log -15) statt die Leitung zu kappen. Dieselbe Behandlung fuer die beiden Tag-Listen. Dieselbe Falle steckte in der Selbstpruefung von sicherung.sh: openssl enc -d ... | head -c 5 | grep -q "PGDMP" Beide Leser schliessen frueh. Bei kleiner Datenbank steht alles im Puffer und es faellt nicht auf; mit wachsender Datenbank kippt es - und dann meldet das Skript eine INTAKTE Sicherung als kaputt und verweigert das Update. Jetzt liest dd eine feste Menge und endet regulaer. Damit so etwas nicht wieder stumm passiert: - Eine ERR-Falle nennt Zeilennummer und Exitcode und erklaert bei 141, dass es SIGPIPE ist und ein Fehler im Skript, nicht beim Benutzer. - Ein Test verbietet "| head" in Skripten mit pipefail und nennt die Zeile. Gegenprobe gemacht: Er schlaegt an. - Bei mehr als 15 Commits steht "... und N weitere" darunter, statt sie wortlos wegzulassen. Nachgestellt: der Ablauf von 1.5.0 auf 1.10.1 laeuft durch. --- CHANGELOG.md | 48 ++++++++++++++++++++++++++++++++++++++++- docs/INSTALLATION.md | 14 ++++++------ docs/ROADMAP.md | 2 +- package.json | 2 +- scripts/sicherung.sh | 16 +++++++++++--- scripts/update.sh | 50 ++++++++++++++++++++++++++++++++++++++----- src/lib/version.ts | 2 +- tests/skripte.test.ts | 47 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 162 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eecf44c..85c4020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,51 @@ Wiederherstellung*. _Noch nichts._ +## [1.10.2] – 2026-08-24 + +### Behoben + +- **Das Update brach ohne Meldung ab.** Beim Sprung von 1.5.0 auf 1.10.1 + endete `scripts/update.sh` mitten in der Ausgabe — kein Fehler, keine + Zeile, einfach zurück zur Eingabeaufforderung. Ursache war eine + abgeschnittene Leitung: + + ``` + git log --oneline "HEAD..$ZIEL" | head -15 | sed ... + ``` + + `head` schließt die Leitung nach 15 Zeilen, `git log` schreibt weiter + und bekommt SIGPIPE. Weil das Skript mit `set -o pipefail` und + `set -e` läuft, gilt die ganze Kette als gescheitert und das Skript + beendet sich — stumm. + + Das ist zeitabhängig und deshalb monatelang nicht aufgefallen: Solange + `git log` fertig war, bevor `head` zumachte, ging alles gut. Genau bei + 1.10.0 lagen zum ersten Mal **mehr als 15 Commits** dazwischen. + +- **Dieselbe Falle in der Selbstprüfung der Sicherung.** `sicherung.sh` + prüfte mit `openssl … | head -c 5 | grep -q "PGDMP"`, ob sich die + frische Sicherung wieder entschlüsseln lässt. Bei kleiner Datenbank + ging das gut, weil alles im Puffer stand. Mit wachsender Datenbank + hätte es gekippt — und dann hätte das Skript eine **völlig intakte + Sicherung als kaputt gemeldet** und das Update verweigert. + +### Geändert + +- **Ein stummer Abbruch ist nicht mehr möglich.** `update.sh` meldet + jetzt bei jedem unerwarteten Ende die Zeilennummer und den Exitcode, + und erklärt bei 141 ausdrücklich, dass es sich um SIGPIPE handelt und + um einen Fehler im Skript, nicht um einen Bedienfehler. +- Stehen mehr als 15 Commits an, steht jetzt „… und N weitere" darunter, + statt sie wortlos wegzulassen. + +### Geprüft + +- Ein Test verbietet das Muster künftig in allen Betriebsskripten: + Wer wieder `| head` in eine Leitung schreibt, bekommt die Zeilennummer + genannt. Gegenprobe gemacht — der Test schlägt an. +- Der Ablauf von 1.5.0 auf 1.10.1 einmal nachgestellt: läuft durch. + ## [1.10.1] – 2026-08-23 ### Geändert @@ -1126,7 +1171,8 @@ Was davon wann angegangen wird, steht in [`docs/ROADMAP.md`](docs/ROADMAP.md). --- -[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.1...HEAD +[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.2...HEAD +[1.10.2]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.1...v1.10.2 [1.10.1]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.0...v1.10.1 [1.10.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.9.0...v1.10.0 [1.9.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.8.0...v1.9.0 diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index c9e8bd8..d5c2955 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -131,14 +131,14 @@ cd /opt/betreuungs-cockpit # Auf die letzte veröffentlichte Fassung wechseln, # nicht auf den laufenden Entwicklungsstand -git checkout v1.10.1 +git checkout v1.10.2 ``` ### Kontrolle ```bash ls docker-compose.yml Dockerfile .env.example # alle drei da -git describe --tags # v1.10.1 +git describe --tags # v1.10.2 ``` --- @@ -299,7 +299,7 @@ betreuungs-cockpit-sicherung-1 Up ```bash # Antwortet die Anwendung? (Port anpassen, falls du APP_PORT gesetzt hast) curl -s http://127.0.0.1:3000/api/gesundheit -# {"status":"bereit","version":"1.10.1","commit":"..."} +# {"status":"bereit","version":"1.10.2","commit":"..."} ``` Kommt hier nichts, sieh in die Logs: @@ -649,7 +649,7 @@ Erst **vom Proxy-Server aus** — kommt er überhaupt an? ```bash curl -s http://10.0.0.5:3000/api/gesundheit -# {"status":"bereit","version":"1.10.1",...} +# {"status":"bereit","version":"1.10.2",...} ``` Kommt hier nichts, liegt es an 7a: falsche `APP_BIND`, Firewall, oder der @@ -700,7 +700,7 @@ Zum Schluss: **Fristen → Pflichtfristen ableiten**. - Du kommst mit E-Mail, Passwort und dem Code aus der App herein. - Unter **Fristen** stehen Einträge mit Paragraphenbezug. - Unter **Protokoll** siehst du deine eigene Anmeldung. -- Unten links in der Seitenleiste steht `Version 1.10.1 · `. +- Unten links in der Seitenleiste steht `Version 1.10.2 · `. --- @@ -875,7 +875,7 @@ alles so vor wie zuvor. | Aufruf | Wirkung | |---|---| | `./scripts/update.sh` | auf die neueste veröffentlichte Fassung | -| `./scripts/update.sh v1.10.1` | auf eine bestimmte Fassung | +| `./scripts/update.sh v1.10.2` | auf eine bestimmte Fassung | | `./scripts/update.sh --pruefen` | nur nachsehen, nichts ändern | | `./scripts/update.sh --ja` | ohne Rückfrage (für cron) | | `./scripts/update.sh --verwerfen` | eigene Änderungen wegwerfen statt beiseitelegen | @@ -913,7 +913,7 @@ git fetch --tags git log --oneline HEAD..origin/main # 3. Umschalten -git checkout v1.10.1 +git checkout v1.10.2 # 4. Neu bauen und starten GIT_COMMIT=$(git rev-parse --short HEAD) BUILD_ZEIT=$(date -Iseconds) docker compose up -d --build diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 2606f75..b19c4df 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -9,7 +9,7 @@ Zeit spart. Er steht weit unten, wenn er nur schön wäre. Diese Datei ist eine Absichtserklärung, kein Vertrag. Was sich im Betrieb als wichtiger herausstellt, rutscht nach oben. -Stand: 23.08.2026 · Aktuelle Fassung: **1.10.1** ([CHANGELOG](../CHANGELOG.md)) +Stand: 23.08.2026 · Aktuelle Fassung: **1.10.2** ([CHANGELOG](../CHANGELOG.md)) --- diff --git a/package.json b/package.json index 153f30b..41daa25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "betreuungs-cockpit", - "version": "1.10.1", + "version": "1.10.2", "private": true, "description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung", "engines": { diff --git a/scripts/sicherung.sh b/scripts/sicherung.sh index 90e2f13..c5d320f 100755 --- a/scripts/sicherung.sh +++ b/scripts/sicherung.sh @@ -75,9 +75,19 @@ done # --- Selbstpruefung ---------------------------------------------------------- # Ein Backup, das sich nicht entschluesseln laesst, ist kein Backup. # Deshalb wird sofort geprueft, ob die Datei wieder lesbar ist. -if openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \ - -pass env:SICHERUNG_PASSWORT -in "$DATEI" 2>/dev/null \ - | head -c 5 | grep -q "PGDMP"; then +# Kein "| head -c 5 | grep -q": Beide schliessen die Leitung, sobald sie +# genug haben. openssl schreibt dann in ein geschlossenes Rohr, bekommt +# SIGPIPE - und mit "set -o pipefail" gilt die ganze Kette als +# gescheitert. Bei einer kleinen Datenbank faellt das nicht auf, weil +# alles schon im Puffer steht; mit wachsender Datenbank kippt es, und +# dann meldet die Selbstpruefung eine INTAKTE Sicherung als kaputt. +# +# "dd" liest eine feste Menge und beendet sich regulaer. +KOPF="$(openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \ + -pass env:SICHERUNG_PASSWORT -in "$DATEI" 2>/dev/null \ + | dd bs=1 count=5 2>/dev/null || true)" + +if [ "$KOPF" = "PGDMP" ]; then echo "$(date -Iseconds) Selbstpruefung bestanden." else echo "$(date -Iseconds) FEHLER: Die Sicherung laesst sich nicht entschluesseln!" >&2 diff --git a/scripts/update.sh b/scripts/update.sh index 3cc67a8..95d5b47 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -52,6 +52,32 @@ gut() { printf ' %s%s%s\n' "$GRUEN" "$1" "$AUS"; } warnung() { printf ' %s%s%s\n' "$GELB" "$1" "$AUS"; } abbruch() { printf '\n%sAbbruch: %s%s\n\n' "$ROT" "$1" "$AUS" >&2; exit 1; } +# --- Unerwarteter Abbruch ---------------------------------------------------- +# +# "set -e" beendet das Skript bei jedem Befehl, der nicht 0 zurueckgibt - +# stumm. Auf dem Bildschirm bricht die Ausgabe dann einfach ab, und man +# sucht an der falschen Stelle. +# +# Genau das ist beim Sprung von v1.5.0 auf v1.10.1 passiert: Eine Leitung +# der Form "git log ... | head -15" hat den Erzeuger mit SIGPIPE beendet, +# "pipefail" hat das an die Kette weitergereicht, und das Skript war weg. +# +# Diese Falle sagt jetzt wenigstens, WO es passiert ist. Exitcode 141 +# heisst SIGPIPE - dann wurde irgendwo wieder eine Leitung zu frueh +# geschlossen. +unerwartet() { + code=$1 + zeile=$2 + printf '%sAbbruch in Zeile %s (Exitcode %s).%s\n' "$ROT" "$zeile" "$code" "$AUS" >&2 + if [ "$code" = "141" ]; then + printf '%sExitcode 141 ist SIGPIPE: eine Leitung wurde zu frueh geschlossen,%s\n' "$ROT" "$AUS" >&2 + printf '%setwa durch "| head". Das ist ein Fehler im Skript, nicht bei dir.%s\n' "$ROT" "$AUS" >&2 + fi + printf '%sKam der Abbruch vor Schritt 5, wurde nichts veraendert.%s\n' "$ROT" "$AUS" >&2 +} +trap 'unerwartet $? $LINENO' ERR + + # --- Aufrufparameter --------------------------------------------------------- ZIEL="" @@ -113,7 +139,11 @@ if ! git fetch --tags --quiet origin; then fi JETZT="$(git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD)" -NEUESTE="$(git tag --list 'v*' --sort=-v:refname | head -1)" +# Kein "| head -1": Siehe den Hinweis zu SIGPIPE weiter unten. Die erste +# Zeile holt die Shell selbst. +ALLE_TAGS="$(git tag --list 'v*' --sort=-v:refname)" +NEUESTE="${ALLE_TAGS%%$' +'*}" [ -n "$NEUESTE" ] || abbruch "Es gibt keine veroeffentlichte Fassung (kein Tag)." @@ -123,7 +153,8 @@ fi git rev-parse --verify --quiet "$ZIEL^{commit}" >/dev/null \ || abbruch "Die Fassung '$ZIEL' gibt es nicht. Vorhanden: -$(git tag --list 'v*' --sort=-v:refname | head -5 | sed 's/^/ /')" +$(printf '%s +' "$ALLE_TAGS" | sed -n '1,5p' | sed 's/^/ /')" printf ' Jetzt: %s\n' "$JETZT" printf ' Ziel: %s%s%s\n' "$FETT" "$ZIEL" "$AUS" @@ -152,14 +183,23 @@ if [ -n "$CHANGELOG" ]; then if (nummer == jetzt) exit drucken = (nummer != "Unveröffentlicht") } - drucken { print " " $0 } - ' | head -60 + drucken && zeilen < 60 { print " " $0; zeilen++ } + ' else warnung "Kein CHANGELOG.md in $ZIEL gefunden." fi printf '\n' -git log --oneline --no-decorate "HEAD..$ZIEL" | head -15 | sed 's/^/ /' +# "-15" statt "| head -15": Bei mehr Commits als der Grenze schliesst head +# die Leitung, git bekommt SIGPIPE, und mit "set -o pipefail" gilt die ganze +# Kette als gescheitert - "set -e" beendet das Skript dann OHNE Meldung. +# Genau das ist beim Sprung von v1.5.0 auf v1.10.1 passiert. +ANZAHL="$(git rev-list --count "HEAD..$ZIEL")" +git log --oneline --no-decorate -15 "HEAD..$ZIEL" | sed 's/^/ /' +if [ "$ANZAHL" -gt 15 ]; then + printf ' ... und %s weitere +' "$((ANZAHL - 15))" +fi if [ "$NUR_PRUEFEN" = "1" ]; then printf '\n' diff --git a/src/lib/version.ts b/src/lib/version.ts index 94a0cee..d393081 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -17,7 +17,7 @@ */ /** Muss zur "version" in der package.json passen. */ -export const VERSION = '1.10.1' +export const VERSION = '1.10.2' /** * Kurzer Git-Stand, beim Bauen hineingereicht: diff --git a/tests/skripte.test.ts b/tests/skripte.test.ts index 6a2fdbf..a09af41 100644 --- a/tests/skripte.test.ts +++ b/tests/skripte.test.ts @@ -85,3 +85,50 @@ describe.skipIf(!mitGit)('Betriebsskripte', () => { } }) }) + +/** + * Abschneidende Leitungen in Skripten mit "pipefail". + * + * Anlass ist ein Abbruch im Betrieb: Beim Sprung von v1.5.0 auf v1.10.1 + * endete update.sh mitten in der Ausgabe, ohne jede Meldung. Ursache war + * + * git log --oneline "HEAD..$ZIEL" | head -15 | sed ... + * + * "head" schliesst die Leitung nach 15 Zeilen, "git log" schreibt weiter + * und bekommt SIGPIPE (Exitcode 141). Mit "set -o pipefail" gilt die + * ganze Kette als gescheitert, "set -e" beendet das Skript - stumm. + * + * Es ist zeitabhaengig: Solange der Erzeuger fertig wird, bevor "head" + * zumacht, faellt nichts auf. Deshalb lief es monatelang gut und riss + * genau dann, als zum ersten Mal mehr als 15 Commits dazwischenlagen. + * + * Dieselbe Falle steckte in der Selbstpruefung der Sicherung, wo sie + * eine intakte Sicherung als kaputt gemeldet haette, sobald die + * Datenbank gross genug ist. + * + * Der Test verbietet das Muster deshalb ganz. Wer eine Ausgabe kuerzen + * will, nimmt "git log -15", "sed -n '1,15p'" oder einen Zaehler in awk - + * alles drei liest bis zum Ende und schliesst nichts zu frueh. + */ +describe('Skripte mit pipefail', () => { + const skripte = ['scripts/update.sh', 'scripts/sicherung.sh', 'scripts/wiederherstellung.sh'] + + it.each(skripte)('%s schneidet keine Leitung mit head ab', (pfad) => { + const inhalt = readFileSync(pfad, 'utf8') + if (!/set -[a-z]*o pipefail|set -o pipefail/.test(inhalt)) return + + const treffer = inhalt + .split('\n') + .map((zeile, nummer) => ({ zeile, nummer: nummer + 1 })) + // Kommentarzeilen erklaeren das Muster gerade - sie sind erlaubt. + .filter(({ zeile }) => !zeile.trim().startsWith('#')) + .filter(({ zeile }) => !zeile.includes('printf')) + .filter(({ zeile }) => /\|\s*head\b/.test(zeile)) + + expect( + treffer.map((t) => `Zeile ${t.nummer}: ${t.zeile.trim()}`), + 'Abschneidende Leitung: der Erzeuger bekommt SIGPIPE, pipefail reicht ' + + 'es weiter, set -e beendet das Skript stumm.', + ).toEqual([]) + }) +}) -- 2.54.0