From cb6e0103e7c060590170284337d5be5e9534026f Mon Sep 17 00:00:00 2001 From: Stefan Mogeritsch Date: Tue, 2 Jun 2026 13:27:23 +0200 Subject: [PATCH] chore(ai): centralize guardrail scripts under .ai; add resolve_repo_root; add shims for .junie/.nolik; fix gitea contexts in release.yml Co-authored-by: Junie --- .ai/scripts/check-docs-drift.sh | 50 +++++++++++ .ai/scripts/lib/common.sh | 27 ++++++ .ai/scripts/render-plantuml.sh | 16 ++++ .ai/scripts/validate-links.sh | 127 ++++++++++++++++++++++++++ .gitea/workflows/release.yml | 32 +++---- .junie/scripts/check-docs-drift.sh | 44 +-------- .junie/scripts/render-plantuml.sh | 10 +-- .junie/scripts/validate-links.sh | 137 +---------------------------- .nolik/scripts/check-docs-drift.sh | 7 ++ .nolik/scripts/validate-links.sh | 7 ++ 10 files changed, 262 insertions(+), 195 deletions(-) create mode 100644 .ai/scripts/check-docs-drift.sh create mode 100644 .ai/scripts/lib/common.sh create mode 100644 .ai/scripts/render-plantuml.sh create mode 100644 .ai/scripts/validate-links.sh create mode 100755 .nolik/scripts/check-docs-drift.sh create mode 100755 .nolik/scripts/validate-links.sh diff --git a/.ai/scripts/check-docs-drift.sh b/.ai/scripts/check-docs-drift.sh new file mode 100644 index 00000000..2c3cc730 --- /dev/null +++ b/.ai/scripts/check-docs-drift.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/lib/common.sh" + +REPO_ROOT="$(resolve_repo_root)" +cd "$REPO_ROOT" + +# check-docs-drift.sh +# Zweck: sehr schlanke Drift-Checks gegen die neue Doku-Struktur. +# - Kein Guidelines-System mehr. +# - Single Source of Truth: `docs/` + +err=0 + +has() { grep -q "$2" "$1" || { echo "[DRIFT] '$2' fehlt in $1"; err=1; }; } +miss() { grep -q "$2" "$1" && { echo "[DRIFT] Veralteter Begriff '$2' in $1"; err=1; }; } + +# Harte Altlast-Pfade dürfen nicht mehr vorkommen +if git grep -n "docs/00_Domain/" -- docs >/dev/null 2>&1; then + echo "[DRIFT] Veralteter Pfad 'docs/00_Domain/' in docs/* gefunden" + err=1 +fi +if git grep -n "docs/adr/" -- docs >/dev/null 2>&1; then + echo "[DRIFT] Veralteter Pfad 'docs/adr/' in docs/* gefunden" + err=1 +fi +if git grep -n "docs/c4/" -- docs >/dev/null 2>&1; then + echo "[DRIFT] Veralteter Pfad 'docs/c4/' in docs/* gefunden" + err=1 +fi +if git grep -n "docs/how-to/" -- docs >/dev/null 2>&1; then + echo "[DRIFT] Veralteter Pfad 'docs/how-to/' in docs/* gefunden" + err=1 +fi +if git grep -n "docs/reference/" -- docs >/dev/null 2>&1; then + echo "[DRIFT] Veralteter Pfad 'docs/reference/' in docs/* gefunden" + err=1 +fi + +# Quelle der Wahrheit: Gateway-Technologie (sollte in Architektur/ADRs/C4 konsistent sein) +has docs/01_Architecture/ARCHITECTURE.md "Spring Cloud Gateway" +has docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Spring Cloud Gateway" +miss docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Ktor" +has docs/01_Architecture/c4/02-container-de.puml "Spring Cloud Gateway" +miss docs/01_Architecture/c4/02-container-de.puml "Ktor" + +exit $err diff --git a/.ai/scripts/lib/common.sh b/.ai/scripts/lib/common.sh new file mode 100644 index 00000000..2067a1d2 --- /dev/null +++ b/.ai/scripts/lib/common.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Common helpers for AI guardrail scripts + +# Robustly resolve the repository root directory. +# Strategy: prefer Git; fallback to marker search upwards; last resort: current dir. +resolve_repo_root() { + local start + start="${1:-$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)}" + if command -v git >/dev/null 2>&1; then + if git -C "$start" rev-parse --show-toplevel >/dev/null 2>&1; then + git -C "$start" rev-parse --show-toplevel + return 0 + fi + fi + local dir + dir="$(cd "$start" && pwd)" + while [ "$dir" != "/" ]; do + if [ -f "$dir/gradlew" ] || [ -f "$dir/settings.gradle.kts" ] || [ -d "$dir/.git" ]; then + echo "$dir" + return 0 + fi + dir="$(dirname "$dir")" + done + pwd +} diff --git a/.ai/scripts/render-plantuml.sh b/.ai/scripts/render-plantuml.sh new file mode 100644 index 00000000..dce0ff41 --- /dev/null +++ b/.ai/scripts/render-plantuml.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/lib/common.sh" + +REPO_ROOT="$(resolve_repo_root)" +cd "$REPO_ROOT" + +mkdir -p build/diagrams +shopt -s nullglob +for f in docs/architecture/c4/*.puml; do + docker run --rm -v "$PWD":/data plantuml/plantuml -tsvg "/data/$f" -o "/data/build/diagrams" + echo "Rendered build/diagrams/$(basename "${f%.puml}").svg" +done diff --git a/.ai/scripts/validate-links.sh b/.ai/scripts/validate-links.sh new file mode 100644 index 00000000..be984854 --- /dev/null +++ b/.ai/scripts/validate-links.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/lib/common.sh" + +REPO_ROOT="$(resolve_repo_root)" +cd "$REPO_ROOT" + +QUICK_MODE=false + +while [[ $# -gt 0 ]]; do + case $1 in + --quick) + QUICK_MODE=true + shift + ;; + --help|-h) + cat << 'EOF' +Docs Link-Validierung + +USAGE: + ./.ai/scripts/validate-links.sh [--quick] + +BESCHREIBUNG: + Prüft Markdown-Links in `docs/**/*.md` auf gebrochene relative Pfade. + Ignoriert externe Links (http/https/mailto) sowie reine Anchors (#...). + +OPTIONEN: + --quick Führt nur eine Teilmenge der Prüfungen durch (aktuell nicht implementiert). +EOF + exit 0 + ;; + *) + echo "[ERROR] Unbekannter Parameter: $1" >&2 + exit 2 + ;; + esac +done + +python3 - <<'PY' +import re +import sys +from pathlib import Path +from urllib.parse import unquote + +root = Path.cwd() +docs_dir = root / "docs" + +if not docs_dir.is_dir(): + print(f"[ERROR] docs-Verzeichnis nicht gefunden: {docs_dir}", file=sys.stderr) + sys.exit(2) + +# Veraltete Pfad-Prüfungen wurden entfernt; Fokus auf Link-Integrität. +FORBIDDEN_SUBSTRINGS = [] + +md_files = sorted(docs_dir.rglob("*.md")) + +link_pattern = re.compile(r"\]\(([^)]+)\)") + +errors = 0 + +def is_external(target: str) -> bool: + t = target.lower() + return t.startswith("http://") or t.startswith("https://") or t.startswith("mailto:") + +def strip_fragment_and_query(target: str) -> str: + target = target.split("#", 1)[0] + target = target.split("?", 1)[0] + return target + +for f in md_files: + text = f.read_text(encoding="utf-8", errors="replace") + + for forbidden in FORBIDDEN_SUBSTRINGS: + if forbidden in text: + print(f"[ERROR] Veralteter Pfad '{forbidden}' in {f}") + errors += 1 + + for match in link_pattern.finditer(text): + target = match.group(1).strip() + + if not target: + continue + if is_external(target): + continue + if target.startswith("#"): + continue + + if target.startswith("<") and target.endswith(">"): + target = target[1:-1] + + target = unquote(strip_fragment_and_query(target)) + + if target.startswith("/"): + continue + + if ":" in target.split("/", 1)[0]: + # z.B. "vscode:..." + continue + + resolved = (f.parent / target).resolve() + + try: + resolved.relative_to(root.resolve()) + except ValueError: + print(f"[ERROR] Link zeigt außerhalb des Repos: {f} -> {target}") + errors += 1 + continue + + if resolved.is_dir(): + if not (resolved / "README.md").is_file(): + print(f"[ERROR] Verlinktes Verzeichnis ohne README.md: {f} -> {target}") + errors += 1 + continue + + if not resolved.exists(): + print(f"[ERROR] Broken link: {f} -> {target}") + errors += 1 + +if errors: + print(f"[ERROR] Link-Validierung fehlgeschlagen: {errors} Fehler") + sys.exit(1) + +print(f"[OK] Link-Validierung erfolgreich: {len(md_files)} Markdown-Dateien geprüft") +PY diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index db1dad18..71487c75 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -23,7 +23,7 @@ jobs: tag-release: name: 🏷️ Git-Tag setzen # Für Plan-B-Builds überspringen: Commit-Message enthält [planb] - if: ${{ !contains(github.event.head_commit.message, '[planb]') }} + if: ${{ !contains(gitea.event.head_commit.message, '[planb]') }} runs-on: ubuntu-latest outputs: version: ${{ steps.read-version.outputs.version }} @@ -64,7 +64,7 @@ jobs: fi - name: Git-Tag erstellen & pushen - if: steps.check-tag.outputs.already_tagged == 'false' && github.event.inputs.dry_run != 'true' + if: steps.check-tag.outputs.already_tagged == 'false' && gitea.event.inputs.dry_run != 'true' run: | TAG="${{ steps.read-version.outputs.tag }}" VERSION="${{ steps.read-version.outputs.version }}" @@ -80,7 +80,7 @@ jobs: package-linux: name: 📦 Linux .deb Packaging # Nur ausführen, wenn Desktop-CI explizit aktiviert ist UND kein Plan‑B Commit - if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(github.event.head_commit.message, '[planb]') }} + if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(gitea.event.head_commit.message, '[planb]') }} runs-on: ubuntu-latest needs: tag-release @@ -88,11 +88,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup JDK 21 (Temurin) + - name: Setup JDK 25 (Temurin) uses: actions/setup-java@v4 with: distribution: temurin - java-version: '21' + java-version: '25' - name: Gradle cache uses: actions/cache@v4 @@ -128,7 +128,7 @@ jobs: package-windows: name: 📦 Windows .msi Packaging # Nur ausführen, wenn Desktop-CI explizit aktiviert ist UND kein Plan‑B Commit - if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(github.event.head_commit.message, '[planb]') }} + if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(gitea.event.head_commit.message, '[planb]') }} runs-on: windows-latest needs: tag-release @@ -136,11 +136,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup JDK 21 (Temurin) + - name: Setup JDK 25 (Temurin) uses: actions/setup-java@v4 with: distribution: temurin - java-version: '21' + java-version: '25' - name: Gradle cache uses: actions/cache@v4 @@ -179,11 +179,11 @@ jobs: steps: - name: Summary ausgeben run: | - echo "## 🚀 Release ${{ needs.tag-release.outputs.version }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Artefakt | Status |" >> $GITHUB_STEP_SUMMARY - echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Linux .deb | ${{ needs.package-linux.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Windows .msi | ${{ needs.package-windows.result }} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Git-Tag:** \`${{ needs.tag-release.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY + echo "## 🚀 Release ${{ needs.tag-release.outputs.version }}" >> $GITEA_STEP_SUMMARY + echo "" >> $GITEA_STEP_SUMMARY + echo "| Artefakt | Status |" >> $GITEA_STEP_SUMMARY + echo "|----------|--------|" >> $GITEA_STEP_SUMMARY + echo "| Linux .deb | ${{ needs.package-linux.result }} |" >> $GITEA_STEP_SUMMARY + echo "| Windows .msi | ${{ needs.package-windows.result }} |" >> $GITEA_STEP_SUMMARY + echo "" >> $GITEA_STEP_SUMMARY + echo "**Git-Tag:** \`${{ needs.tag-release.outputs.tag }}\`" >> $GITEA_STEP_SUMMARY diff --git a/.junie/scripts/check-docs-drift.sh b/.junie/scripts/check-docs-drift.sh index c4db519d..50adf4ba 100755 --- a/.junie/scripts/check-docs-drift.sh +++ b/.junie/scripts/check-docs-drift.sh @@ -1,43 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -# check-docs-drift.sh -# Zweck: sehr schlanke Drift-Checks gegen die neue Doku-Struktur. -# - Kein Guidelines-System mehr. -# - Single Source of Truth: `docs/` - -err=0 - -has() { grep -q "$2" "$1" || { echo "[DRIFT] '$2' fehlt in $1"; err=1; }; } -miss() { grep -q "$2" "$1" && { echo "[DRIFT] Veralteter Begriff '$2' in $1"; err=1; }; } - -# Harte Altlast-Pfade dürfen nicht mehr vorkommen -if git grep -n "docs/00_Domain/" -- docs >/dev/null 2>&1; then - echo "[DRIFT] Veralteter Pfad 'docs/00_Domain/' in docs/* gefunden" - err=1 -fi -if git grep -n "docs/adr/" -- docs >/dev/null 2>&1; then - echo "[DRIFT] Veralteter Pfad 'docs/adr/' in docs/* gefunden" - err=1 -fi -if git grep -n "docs/c4/" -- docs >/dev/null 2>&1; then - echo "[DRIFT] Veralteter Pfad 'docs/c4/' in docs/* gefunden" - err=1 -fi -if git grep -n "docs/how-to/" -- docs >/dev/null 2>&1; then - echo "[DRIFT] Veralteter Pfad 'docs/how-to/' in docs/* gefunden" - err=1 -fi -if git grep -n "docs/reference/" -- docs >/dev/null 2>&1; then - echo "[DRIFT] Veralteter Pfad 'docs/reference/' in docs/* gefunden" - err=1 -fi - -# Quelle der Wahrheit: Gateway-Technologie (sollte in Architektur/ADRs/C4 konsistent sein) -has docs/01_Architecture/ARCHITECTURE.md "Spring Cloud Gateway" -has docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Spring Cloud Gateway" -miss docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Ktor" -has docs/01_Architecture/c4/02-container-de.puml "Spring Cloud Gateway" -miss docs/01_Architecture/c4/02-container-de.puml "Ktor" - -exit $err +# Shim: Weiterleitung auf zentrale Guardrail in .ai/ +SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)" +ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")" +exec "$ROOT_DIR/.ai/scripts/check-docs-drift.sh" "$@" diff --git a/.junie/scripts/render-plantuml.sh b/.junie/scripts/render-plantuml.sh index 0f12d6aa..5817e538 100644 --- a/.junie/scripts/render-plantuml.sh +++ b/.junie/scripts/render-plantuml.sh @@ -1,9 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -mkdir -p build/diagrams -shopt -s nullglob -for f in docs/architecture/c4/*.puml; do - docker run --rm -v "$PWD":/data plantuml/plantuml -tsvg "/data/$f" -o "/data/build/diagrams" - echo "Rendered build/diagrams/$(basename "${f%.puml}").svg" -done +# Shim: Weiterleitung auf zentrale Guardrail in .ai/ +SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)" +ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")" +exec "$ROOT_DIR/.ai/scripts/render-plantuml.sh" "$@" diff --git a/.junie/scripts/validate-links.sh b/.junie/scripts/validate-links.sh index 963893be..c9983440 100755 --- a/.junie/scripts/validate-links.sh +++ b/.junie/scripts/validate-links.sh @@ -1,136 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -# validate-links.sh - Link-Validierung für Projektdokumentation (`docs/**`). -# Zweck: Guardrail für die "Docs-as-Code"-Strategie. - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -cd "$PROJECT_ROOT" - -QUICK_MODE=false - -while [[ $# -gt 0 ]]; do - case $1 in - --quick) - QUICK_MODE=true - shift - ;; - --help|-h) - cat << 'EOF' -Docs Link-Validierung - -USAGE: - ./.junie/scripts/validate-links.sh [--quick] - -BESCHREIBUNG: - Prüft Markdown-Links in `docs/**/*.md` auf gebrochene relative Pfade. - Ignoriert externe Links (http/https/mailto) sowie reine Anchors (#...). - -OPTIONEN: - --quick Führt nur eine Teilmenge der Prüfungen durch (aktuell nicht implementiert). -EOF - exit 0 - ;; - *) - echo "[ERROR] Unbekannter Parameter: $1" >&2 - exit 2 - ;; - esac -done - -python3 - <<'PY' -import os -import re -import sys -from pathlib import Path -from urllib.parse import unquote - -root = Path.cwd() -docs_dir = root / "docs" - -if not docs_dir.is_dir(): - print(f"[ERROR] docs-Verzeichnis nicht gefunden: {docs_dir}", file=sys.stderr) - sys.exit(2) - -# Veraltete Pfad-Prüfungen wurden entfernt, da sie zu wartungsintensiv waren. -# Das Skript konzentriert sich nun auf die Validierung der Link-Integrität. -FORBIDDEN_SUBSTRINGS = [] - -md_files = sorted(docs_dir.rglob("*.md")) - -link_pattern = re.compile(r"\]\(([^)]+)\)") - -errors = 0 - -def is_external(target: str) -> bool: - t = target.lower() - return t.startswith("http://") or t.startswith("https://") or t.startswith("mailto:") - -def strip_fragment_and_query(target: str) -> str: - # remove fragment and query parts - target = target.split("#", 1)[0] - target = target.split("?", 1)[0] - return target - -for f in md_files: - text = f.read_text(encoding="utf-8", errors="replace") - - for forbidden in FORBIDDEN_SUBSTRINGS: - if forbidden in text: - print(f"[ERROR] Veralteter Pfad '{forbidden}' in {f}") - errors += 1 - - for match in link_pattern.finditer(text): - target = match.group(1).strip() - - if not target: - continue - if is_external(target): - continue - if target.startswith("#"): - continue - - # drop angle brackets <...> used in markdown for urls with spaces - if target.startswith("<") and target.endswith(">"): - target = target[1:-1] - - target = unquote(strip_fragment_and_query(target)) - - # ignore absolute paths in the repo (we treat them as doc-style links; validate only if relative) - if target.startswith("/"): - continue - - # ignore non-file targets (e.g. empty or protocol-less anchors) - if ":" in target.split("/", 1)[0]: - # things like "vscode:..." etc. - continue - - # treat as file path relative to markdown file - resolved = (f.parent / target).resolve() - - # keep validation within repo - try: - resolved.relative_to(root.resolve()) - except ValueError: - print(f"[ERROR] Link zeigt außerhalb des Repos: {f} -> {target}") - errors += 1 - continue - - # allow directories if they contain README.md - if resolved.is_dir(): - if not (resolved / "README.md").is_file(): - print(f"[ERROR] Verlinktes Verzeichnis ohne README.md: {f} -> {target}") - errors += 1 - continue - - if not resolved.exists(): - print(f"[ERROR] Broken link: {f} -> {target}") - errors += 1 - -if errors: - print(f"[ERROR] Link-Validierung fehlgeschlagen: {errors} Fehler") - sys.exit(1) - -print(f"[OK] Link-Validierung erfolgreich: {len(md_files)} Markdown-Dateien geprüft") -PY +# Shim: Weiterleitung auf zentrale Guardrail in .ai/ +SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)" +ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")" +exec "$ROOT_DIR/.ai/scripts/validate-links.sh" "$@" diff --git a/.nolik/scripts/check-docs-drift.sh b/.nolik/scripts/check-docs-drift.sh new file mode 100755 index 00000000..50adf4ba --- /dev/null +++ b/.nolik/scripts/check-docs-drift.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Shim: Weiterleitung auf zentrale Guardrail in .ai/ +SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)" +ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")" +exec "$ROOT_DIR/.ai/scripts/check-docs-drift.sh" "$@" diff --git a/.nolik/scripts/validate-links.sh b/.nolik/scripts/validate-links.sh new file mode 100755 index 00000000..c9983440 --- /dev/null +++ b/.nolik/scripts/validate-links.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Shim: Weiterleitung auf zentrale Guardrail in .ai/ +SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)" +ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")" +exec "$ROOT_DIR/.ai/scripts/validate-links.sh" "$@"