refactor(scripts): Frontmatter-Validator überarbeiten; kleine Typ-Korrektur im YouTrack-KB-Sync
- validate-frontmatter.py: - In ein sauberes, idiomatisches Skript mit Funktionen (load_schema, extract_frontmatter, validate_file, main) refaktoriert. - Pfadbehandlung auf pathlib umgestellt; robustere Frontmatter-Erkennung via Regex (unterstützt LF/CRLF, nur am Datei‑Anfang). - Verbesserte, klare Fehlermeldungen; Exit-Code jetzt 0/1 über sys.exit(main()). - Typannotationen und Module‑Docstring ergänzt; __future__ für |‑Unions hinzugefügt. - Sichere Schema-Ladung mit Fehlerbehandlung (Datei fehlt / ungültiges JSON). - youtrack-sync-kb.py: - Kleinere, idiomatische Typkorrektur: parent_id als Optional (str | None) in create_article, keine Verhaltensänderung. Ergebnis - Die beiden Python-Skripte folgen nun einer sauberen Syntax und idiomatischen Python‑Praktiken (klare Funktionen, Typen, robuste Fehlerbehandlung). Das Verhalten der bestehenden YouTrack‑Synchronisation bleibt unverändert.
This commit is contained in:
@@ -25,3 +25,9 @@ logs/
|
|||||||
|
|
||||||
# Generated diagrams
|
# Generated diagrams
|
||||||
build/diagrams/
|
build/diagrams/
|
||||||
|
|
||||||
|
# Python virtual environment
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
attrs==25.4.0
|
||||||
|
certifi==2025.10.5
|
||||||
|
charset-normalizer==3.4.4
|
||||||
|
idna==3.11
|
||||||
|
jsonschema==4.25.1
|
||||||
|
jsonschema-specifications==2025.9.1
|
||||||
|
PyYAML==6.0.3
|
||||||
|
referencing==0.37.0
|
||||||
|
requests==2.32.5
|
||||||
|
rpds-py==0.28.0
|
||||||
|
urllib3==2.5.0
|
||||||
@@ -1,36 +1,78 @@
|
|||||||
import os, re, yaml, json
|
"""
|
||||||
from glob import glob
|
Validiert YAML-Frontmatter in Markdown-Dateien unterhalb von docs/ gegen ein JSON-Schema.
|
||||||
|
|
||||||
|
- Erwartet das Schema in docs/.frontmatter.schema.json
|
||||||
|
- Meldet fehlendes oder ungültiges Frontmatter pro Datei
|
||||||
|
- Exit-Code 0 bei Erfolg, 1 wenn mindestens ein Fehler auftritt
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import jsonschema
|
import jsonschema
|
||||||
except ImportError:
|
except ImportError: # pragma: no cover
|
||||||
# GitHub Actions step will install this before running; provide friendlier message if missing
|
# GitHub Actions step installiert dies vor dem Lauf; freundlichere Fehlermeldung
|
||||||
raise SystemExit("[FM] jsonschema package not installed. Please run: pip install jsonschema pyyaml")
|
raise SystemExit("[FM] jsonschema nicht installiert. Bitte ausführen: pip install jsonschema pyyaml")
|
||||||
|
|
||||||
SCHEMA_PATH = 'docs/.frontmatter.schema.json'
|
|
||||||
FM_REGEX = re.compile(r'^---\n(.*?)\n---', re.S)
|
|
||||||
|
|
||||||
with open(SCHEMA_PATH, encoding='utf-8') as f:
|
SCHEMA_PATH = Path("docs/.frontmatter.schema.json")
|
||||||
schema = json.load(f)
|
# Erlaubt LF und CRLF, verlangt Frontmatter am Datei-Anfang
|
||||||
|
FM_REGEX = re.compile(r"\A---\r?\n(.*?)\r?\n---(?:\r?\n|$)", re.S)
|
||||||
|
|
||||||
errors = 0
|
|
||||||
for path in glob('docs/**/*.md', recursive=True):
|
def load_schema(path: Path) -> dict[str, Any]:
|
||||||
# ADRs und ggf. generierte Inhalte vorerst ausnehmen (separater Rollout für FM)
|
|
||||||
if path.startswith('docs/architecture/adr/'):
|
|
||||||
continue
|
|
||||||
# Skip generated or non-content files if any (none by default)
|
|
||||||
with open(path, 'r', encoding='utf-8') as fh:
|
|
||||||
content = fh.read()
|
|
||||||
m = FM_REGEX.search(content)
|
|
||||||
if not m:
|
|
||||||
print(f"[FM] fehlt: {path}")
|
|
||||||
errors = 1
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
fm = yaml.safe_load(m.group(1)) or {}
|
with path.open(encoding="utf-8") as f:
|
||||||
jsonschema.validate(fm, schema)
|
return json.load(f)
|
||||||
except Exception as e:
|
except FileNotFoundError:
|
||||||
print(f"[FM] invalid in {path}: {e}")
|
print(f"[FM] Schema-Datei fehlt: {path.as_posix()}")
|
||||||
errors = 1
|
sys.exit(1)
|
||||||
|
except json.JSONDecodeError as e: # pragma: no cover
|
||||||
|
print(f"[FM] Ungültiges JSON-Schema in {path.as_posix()}: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
exit(errors)
|
|
||||||
|
def extract_frontmatter(text: str) -> str | None:
|
||||||
|
m = FM_REGEX.search(text)
|
||||||
|
return m.group(1) if m else None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_file(path: Path, schema: dict[str, Any]) -> bool:
|
||||||
|
content = path.read_text(encoding="utf-8")
|
||||||
|
fm_text = extract_frontmatter(content)
|
||||||
|
if fm_text is None:
|
||||||
|
print(f"[FM] fehlt: {path.as_posix()}")
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
fm = yaml.safe_load(fm_text) or {}
|
||||||
|
jsonschema.validate(fm, schema)
|
||||||
|
return True
|
||||||
|
except Exception as e: # jsonschema.ValidationError u.a.
|
||||||
|
print(f"[FM] ungültig in {path.as_posix()}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
schema = load_schema(SCHEMA_PATH)
|
||||||
|
|
||||||
|
had_error = False
|
||||||
|
for path in Path("docs").rglob("*.md"):
|
||||||
|
# ADRs und ggf. generierte Inhalte vorerst ausnehmen (separater Rollout für FM)
|
||||||
|
if path.as_posix().startswith("docs/architecture/adr/"):
|
||||||
|
continue
|
||||||
|
if not validate_file(path, schema):
|
||||||
|
had_error = True
|
||||||
|
|
||||||
|
return 1 if had_error else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ def http(method: str, url: str, **kw):
|
|||||||
# *** KORRIGIERTE FUNKTION ***
|
# *** KORRIGIERTE FUNKTION ***
|
||||||
def get_project_by_short_name(name: str):
|
def get_project_by_short_name(name: str):
|
||||||
"""Sucht die interne ID des Projekts anhand des Kürzels (z.B. 'MP').
|
"""Sucht die interne ID des Projekts anhand des Kürzels (z.B. 'MP').
|
||||||
Versucht zuerst /api/projects, fällt bei Fehler auf /api/admin/projects zurück.
|
Versucht zuerst /api/projects, fällt bei Fehlern auf /api/admin/projects zurück.
|
||||||
"""
|
"""
|
||||||
# 1) Primärer, nicht-admin Endpunkt
|
# 1) Primärer, nicht-admin Endpunkt
|
||||||
url1 = yt_url("/api/projects?fields=id,shortName")
|
url1 = yt_url("/api/projects?fields=id,shortName")
|
||||||
@@ -160,7 +160,7 @@ def find_article_in_kb_by_title(title: str, parent_id: str | None = None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# *** KORRIGIERTE FUNKTION ***
|
# *** KORRIGIERTE FUNKTION ***
|
||||||
def create_article(title: str, markdown: str, parent_id: str = None):
|
def create_article(title: str, markdown: str, parent_id: str | None = None):
|
||||||
"""Erstellt einen neuen Artikel in der Knowledge Base des Projekts.
|
"""Erstellt einen neuen Artikel in der Knowledge Base des Projekts.
|
||||||
Fallback: Wenn keine KB existiert (KB_ID is None), wird der Artikel dem Projekt zugeordnet.
|
Fallback: Wenn keine KB existiert (KB_ID is None), wird der Artikel dem Projekt zugeordnet.
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user