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([]) + }) +})