diff --git a/CLAUDE.md b/CLAUDE.md index 4df3d29..5a4d636 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,3 +27,8 @@ Integrations-Backend. - Keine neuen Laufzeit-Abhängigkeiten ohne Rückfrage (gilt in beiden Teilen). - Der Notationstext ist das führende Datenformat; kein Teil erfindet ein eigenes Speicherformat für die Struktur. +- Im mitgelieferten Plan `docs/examples/example-werkbaum.werkbaum` bekommt eine + fertige Funktion beim Mergen **`[x]`**, nicht `[^]`. Auf „in Produktion" + befördert erst der Deploy der stabilen Instanz + (`scripts/promote-shipped.sh`, von `deploy-prod.sh` aufgerufen) — nur er macht + die Aussage wahr. Siehe D30; SPEC §4 trennt beide Zustände ohnehin. diff --git a/README.md b/README.md index 9ccf26a..2efb37f 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,23 @@ repository, while the **version number** links to that exact commit "GitHub Actions". The repo must be **public** for this (GitHub Pages via Actions is only available for private repos on a paid plan). +### Stable instance + +`scripts/deploy-prod.sh` mirrors a badge-free production build to a server over +SSH (target in `.env`, template `.env.example`). Unlike Pages this is a +**deliberate** step, so it is also the moment a feature actually goes live. + +The script therefore starts by running `scripts/promote-shipped.sh`, which turns +`[x]` (done) into `[^]` (in production) in `docs/examples/example-werkbaum.werkbaum` +— the shipped plan describing Werkbaum itself — and records that as its own +commit. The convention is: mark a finished feature `[x]` when it is merged and +let the deploy promote it. That keeps the plan honest on both instances and +keeps the "what's new" highlighting meaningful. Skip it with `--no-promote`; see +[docs/DECISIONS.md](docs/DECISIONS.md) D30. + +The commit is **not** pushed automatically — run `git push` afterwards, or the +footer version link points at a commit GitHub does not know yet. + ## License MIT — see [LICENSE](LICENSE). © 2026 Michael Hönnig. The bundled IBM Plex fonts diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index f2f6b0e..4f10d32 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -904,3 +904,62 @@ wie am Bildschirm. Geprüft wurde im Browser — auch der Moduswechsel hin und zurück, mehrfach: Die Gruppe entsteht und löst sich rückstandsfrei auf, Knotenzahl und Dokumentordnung bleiben in allen drei Modi gleich. + +## D30 — `[x]` → `[^]` per Beförderungs-Commit vor dem Deploy +Der mitgelieferte Werkbaum-Plan (D27) behauptete `[^]` („in Produktion") für +Funktionen, die nur auf der automatisch deployten Pages-Instanz lagen, nicht auf +der stabilen Installation `werkbaum.javagil.de` (manueller Deploy, D16). Damit +war ausgerechnet das Dokument ungenau, das den Stand beschreiben soll — und die +„Was ist neu?"-Anzeige (D28) meldete Dinge als live, die es dort nicht waren. + +**Die Unterscheidung gibt es längst.** SPEC §4 trennt `[x]` fertig +(abgeschlossen) von `[^]` in Produktion (deployed/live). Der Plan hat diese +Trennung ausgerechnet für sich selbst nie benutzt und sprang direkt auf `[^]`. +Die Konvention lautet daher ab jetzt: **beim Mergen `[x]`**, und der Deploy +befördert. Nur der Deploy weiß, wann die Aussage wahr wird. + +**Umgesetzt als Commit, nicht als Rewrite beim Bauen.** `scripts/promote-shipped.sh` +schreibt die Statusboxen am Zeilenanfang von `[x]` auf `[^]` und hält das in +einem eigenen Commit fest; `scripts/deploy-prod.sh` ruft es als Schritt 0 auf +(abschaltbar mit `--no-promote`). Gründe: + +- Ein Rewrite beim Bauen macht **genau eine** Installation ehrlich. Pages + untertriebe dauerhaft, und die Neu-Anzeige wäre dort **für immer stumm** — + das Feature ließe sich nur noch künstlich prüfen. Der Commit dagegen wird von + **beiden** Pipelines gesehen: Pages baut ihn beim Push, prod beim nächsten + rsync. +- Das Deployment-Artefakt bleibt **inhaltsgleich mit dem Repo**. Die + vorhandenen `sed`-Regeln (D16) fassen nur Pfade und die Versionsnummer an — + Infrastruktur. Ein Status-Rewrite wäre der erste Schritt, der ändert, was das + Dokument *aussagt*; die per `?sourceUrl=` geladene Rohdatei wäre dann eine + dritte, wieder abweichende Fassung. +- Es folgt dem Präzedenzfall aus D16: Die Version wird per **bewusstem + Bump-Commit** gepflegt, „vollständig aus dem Repo reproduzierbar". Ein Deploy + ist ein bewusster Akt; ihn in der Historie festzuhalten passt dazu — und der + Plan bekommt nebenbei eine Chronik, wann was live ging. + +**Verworfen:** Rewrite nur im Prod-Deploy (siehe oben) und Rewrite in **beiden** +Pipelines. Letzteres wäre testbar, aber `[^]` hieße auf Pages faktisch nur +„gemerged", und das kollidiert damit, dass D16 die Pages-Instanz bewusst als +nicht-produktiven Build markiert (🚧-Badge). + +**Bekannter Preis:** Zwischen Beförderungs-Commit und rsync sagt Pages `[^]` für +etwas, das auf prod noch nicht liegt. Das Fenster ist kurz und liegt in der Hand +dessen, der deployt — gegenüber einer *dauerhaften* Ungenauigkeit bei der +Build-Rewrite-Variante der bessere Tausch. + +**Einmalige Nachholung.** Welche Knoten zu früh `[^]` trugen, ließ sich exakt +bestimmen statt zu schätzen: Der Footer der stabilen Instanz verlinkt den +deployten Commit (`4061362`), und alles danach ist dort nicht enthalten. Es +waren **genau zwei** Knoten — „Optional nodes" (D29) und „Show what is new since +your last visit" (D28) —, nicht das Dutzend, das vorher grob geschätzt worden +war. Beide stehen jetzt auf `[x]` und leuchten beim nächsten Prod-Deploy als neu +auf, was genau der Wahrheit entspricht. + +**Umfang:** Nur `docs/examples/example-werkbaum.werkbaum`. Die übrigen +Beispieldateien sind erfunden und sagen nichts über ein Deployment aus. + +**Nicht gepusht.** Das Skript committet, pusht aber nicht — Veröffentlichen +bleibt eine bewusste Handlung. `deploy-prod.sh` warnt stattdessen, wenn HEAD +noch nicht auf `origin` liegt: der Footer-Versionslink zeigt sonst auf einen +Commit, den GitHub nicht kennt. diff --git a/docs/examples/example-werkbaum.werkbaum b/docs/examples/example-werkbaum.werkbaum index 889b58c..5521efa 100644 --- a/docs/examples/example-werkbaum.werkbaum +++ b/docs/examples/example-werkbaum.werkbaum @@ -8,7 +8,7 @@ - [^] T-shirt size and the "decompose from M" rule (S) - [^] People tags, bare URLs, %% comments (XS) - [^] And/or decomposition (S) - - [^] Optional nodes — neither required nor an alternative (S) + - [x] Optional nodes — neither required nor an alternative (S) - [-] A separate storage format for the structure (L) %% the text is the format - [ ] Ticket references #123 (S) - [?] Benefit per node, not only cost (M) @@ -38,7 +38,7 @@ - [^] Load a document from ?sourceUrl= (S) - [^] Jump between diagram and text (S) - [ ] Open and save .werkbaum files (S) - + [^] Show what is new since your last visit (S) + + [x] Show what is new since your last visit (S) + [?] A pastel colour per person (S) - [?] Dates and milestones (M) | [?] An attribute in the line (S) diff --git a/scripts/deploy-prod.sh b/scripts/deploy-prod.sh index 7a3d7e2..da23918 100755 --- a/scripts/deploy-prod.sh +++ b/scripts/deploy-prod.sh @@ -2,16 +2,21 @@ # # Werkbaum — Produktions-Deploy via rsync/SSH. # -# Baut das badge-freie Prod-Bundle (`npm run build:prod`, ohne den -# Entwicklungs-Hinweis hinter dem Titel), stellt es lokal genauso zusammen wie -# der GitHub-Pages-Workflow (LICENSE-Link geradeziehen + Footer-Version/Commit) -# und spiegelt es per rsync in ein Zielverzeichnis. `--delete`: am Ziel bleibt -# nichts Altes stehen. +# Befördert zuerst die fertigen Knoten des mitgelieferten Werkbaum-Plans auf +# „in Produktion" (scripts/promote-shipped.sh, D30 — genau dieser Deploy macht +# die Aussage `[^]` wahr), baut dann das badge-freie Prod-Bundle +# (`npm run build:prod`, ohne den Entwicklungs-Hinweis hinter dem Titel), stellt +# es lokal genauso zusammen wie der GitHub-Pages-Workflow (LICENSE-Link +# geradeziehen + Footer-Version/Commit) und spiegelt es per rsync in ein +# Zielverzeichnis. `--delete`: am Ziel bleibt nichts Altes stehen. # # Verwendung: -# scripts/deploy-prod.sh [-y] [rsync-ziel] +# scripts/deploy-prod.sh [-y] [--no-promote] [rsync-ziel] # -# -y ohne Rückfrage spiegeln (sonst erst Vorschau via --dry-run + Nachfrage) +# -y ohne Rückfrage befördern und spiegeln (sonst erst Vorschau +# via --dry-run + Nachfrage) +# --no-promote Beförderungsschritt überspringen (z. B. Wiederholung eines +# Deploys, der schon befördert hat) # # Das Ziel ist entweder das Argument ODER — wenn keins angegeben ist — die # Variable DEPLOY_TARGET aus der git-ignorierten Datei .env im Repo-Wurzelordner @@ -26,16 +31,18 @@ # ACHTUNG: Das Zielverzeichnis wird als exklusiv für Werkbaum angenommen — # `--delete` entfernt dort ALLES, was nicht zum Bundle gehört. # -# Siehe README (Abschnitt Deployment) und docs/DECISIONS.md D16/D19. +# Siehe README (Abschnitt Deployment) und docs/DECISIONS.md D16/D19/D30. set -euo pipefail # ---- Argumente ---- YES=0 +PROMOTE=1 TARGET="" for arg in "$@"; do case "$arg" in -y|--yes) YES=1 ;; + --no-promote) PROMOTE=0 ;; -h|--help) awk 'NR>2 { if ($0 ~ /^#/) { sub(/^# ?/, ""); print } else exit }' "$0" exit 0 ;; @@ -72,6 +79,17 @@ if [ -z "$TARGET" ]; then exit 2 fi +# ---- 0) Fertiges auf „in Produktion" befördern (D30) ---- +# MUSS vor dem Build laufen: der Plan ist Build-Eingabe (?raw-Import, D27), und +# der Commit soll derjenige sein, auf den der Footer-Versionslink zeigt. +if [ "$PROMOTE" -eq 1 ]; then + PROMOTE_ARGS=() + [ "$YES" -eq 1 ] && PROMOTE_ARGS+=(-y) + "$ROOT/scripts/promote-shipped.sh" "${PROMOTE_ARGS[@]+"${PROMOTE_ARGS[@]}"}" +else + echo "==> Beförderung übersprungen (--no-promote)" +fi + # ---- 1) Prod-Build (ohne Build-Hinweis) ---- if [ ! -d frontend/node_modules ]; then echo "==> node_modules fehlt — npm ci" @@ -100,6 +118,12 @@ if git -C "$ROOT" rev-parse HEAD >/dev/null 2>&1; then BUILD_VERSION="${MAJORMINOR}.${MICRO}" COMMIT_URL="https://github.com/mhoennig/werkbaum/commit/$(git -C "$ROOT" rev-parse HEAD)" echo "==> Footer-Version ${BUILD_VERSION} -> ${COMMIT_URL}" + # Der Link zeigt ins Leere, solange der Commit nicht auf GitHub liegt — nach + # einer Beförderung (Schritt 0) ist das der Normalfall. + if [ -z "$(git -C "$ROOT" branch -r --contains HEAD 2>/dev/null)" ]; then + echo " ! HEAD liegt noch nicht auf origin — der Footer-Versionslink läuft" >&2 + echo " ins Leere, bis 'git push' nachgeholt ist." >&2 + fi SED_ARGS+=(-e "s#\(]*>\)[0-9.]\+#\1${BUILD_VERSION}#") else diff --git a/scripts/promote-shipped.sh b/scripts/promote-shipped.sh new file mode 100755 index 0000000..770e579 --- /dev/null +++ b/scripts/promote-shipped.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# +# Werkbaum — „fertig" auf „in Produktion" befördern. +# +# Der mitgelieferte Plan docs/examples/example-werkbaum.werkbaum beschreibt +# Werkbaum selbst. SPEC §4 unterscheidet `[x]` fertig (abgeschlossen) von `[^]` +# in Produktion (deployed/live) — und die zweite Aussage kann erst der Deploy +# wahr machen. Konvention deshalb: beim Mergen `[x]`, unmittelbar VOR dem +# Deploy dieser Lauf, der daraus `[^]` macht und das als Commit festhält. +# +# Warum ein Commit und kein Rewrite beim Bauen: siehe docs/DECISIONS.md D30. +# Kurz — der Commit wird von BEIDEN Pipelines gesehen (GitHub Pages baut ihn +# beim Push, prod beim nächsten rsync), das Deployment-Artefakt bleibt +# inhaltsgleich mit dem Repo, und die „Was ist neu?"-Anzeige (D28) bleibt auf +# der Pages-Instanz erlebbar statt dort für immer stumm zu sein. +# +# Verwendung: +# scripts/promote-shipped.sh [-n] [-y] +# +# -n, --dry-run nur zeigen, was befördert würde +# -y, --yes ohne Rückfrage befördern und committen +# +# Ohne zu befördernde Knoten endet der Lauf mit 0 und ändert nichts. +# Gepusht wird NICHT — das bleibt eine bewusste Handlung. + +set -euo pipefail + +DRY=0 +YES=0 +for arg in "$@"; do + case "$arg" in + -n|--dry-run) DRY=1 ;; + -y|--yes) YES=1 ;; + -h|--help) + awk 'NR>2 { if ($0 ~ /^#/) { sub(/^# ?/, ""); print } else exit }' "$0" + exit 0 ;; + *) echo "Unbekannte Option: $arg" >&2; exit 2 ;; + esac +done + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +PLAN="docs/examples/example-werkbaum.werkbaum" +[ -f "$PLAN" ] || { echo "Plan nicht gefunden: $PLAN" >&2; exit 1; } + +# Nur Statusboxen am Zeilenanfang (nach optionalem Zeichen -/+/|), damit ein +# „[x]" mitten im Label unangetastet bleibt. `x` auch als `X` (SPEC §4). +MATCH='^([[:space:]]*([-+|][[:space:]]*)?)\[[xX]\]' + +mapfile -t HITS < <(grep -nE "$MATCH" "$PLAN" || true) + +if [ "${#HITS[@]}" -eq 0 ]; then + echo "==> Nichts zu befördern — kein [x] im Plan." + exit 0 +fi + +echo "==> ${#HITS[@]} Knoten würden auf [^] befördert:" +printf ' %s\n' "${HITS[@]}" + +[ "$DRY" -eq 1 ] && exit 0 + +# Der Commit fasst genau diese eine Datei an. Ist sie schon geändert, lässt sich +# die Beförderung nicht sauber von der Änderung trennen — dann lieber abbrechen. +if git rev-parse HEAD >/dev/null 2>&1 && [ -n "$(git status --porcelain -- "$PLAN")" ]; then + echo " ! $PLAN hat uncommittete Änderungen — erst committen, dann befördern." >&2 + exit 1 +fi + +if [ "$YES" -ne 1 ]; then + printf '==> Befördern und committen? [y/N] ' + read -r ANS + case "$ANS" in y|Y|j|J) : ;; *) echo "Abgebrochen."; exit 1 ;; esac +fi + +# Labels für den Commit-Text sammeln (ohne Zeichen, Statusbox und Größe). +mapfile -t LABELS < <(printf '%s\n' "${HITS[@]}" \ + | sed -E 's/^[0-9]+:[[:space:]]*([-+|][[:space:]]*)?\[[xX]\][[:space:]]*//' \ + | sed -E 's/[[:space:]]*\((XS|S|M|L|XL|XXL)\)[[:space:]]*$//') + +sed -i -E "s/$MATCH/\1[^]/" "$PLAN" + +if ! git rev-parse HEAD >/dev/null 2>&1; then + echo "==> Kein Git-Repo — Datei geändert, nicht committet." + exit 0 +fi + +{ + echo "docs: Werkbaum-Plan — ${#HITS[@]} Knoten in Produktion" + echo + echo "Beim Deploy der stabilen Instanz gehen diese Knoten live; SPEC §4" + echo "unterscheidet [x] fertig von [^] in Produktion (D30):" + echo + printf -- '- %s\n' "${LABELS[@]}" +} | git commit -q -F - -- "$PLAN" + +echo "==> Committet: $(git log --oneline -1)" +echo " Hinweis: nicht gepusht. Ohne Push zeigt der Footer-Versionslink des" +echo " Deploys auf einen Commit, den GitHub noch nicht kennt."