#!/usr/bin/env python3 """import_issues.py — Einmal-Import der offenen management-Issues. Migrationsakte, kein Dauerwerkzeug (Design 2026-08-11, Slice 4; ADR-0012). Liest die offenen Issues des Projekts axion1337.chat/management read-only von git.lab (Token nur per Dateipfad, Wert erscheint nirgends) und schreibt je Issue eine kanonische Datei docs/issues/-.md. GitLab-iid = Datei-id — keine dritte Nummernwelt. Kommentare und Verlauf bleiben auf GitLab; der Dateikopf verlinkt dorthin. Abbildung (alt → Schema): ohne status-Label → open · status:next → next · status:doing → in-progress · status:wartet → waiting (wartegrund: Verweis auf den GitLab-Verlauf; Präzisierung im nächsten Refinement) · Meilenstein "Mn — …" → Mn · priority:x → x · due_date → due · host:x → host · area:x → area. Fehlt Meilenstein oder Priorität, bricht der Import ab — das wäre ein Befund, kein Füllwert. Relative Upload-Pfade in Beschreibungen werden auf absolute git.lab-URLs umgeschrieben, damit der Link-Check nicht ins Leere prüft. Usage: python3 import_issues.py [tokenpfad] """ from __future__ import annotations import json import re import sys import urllib.request from pathlib import Path API = "https://git.lab/api/v4/projects/axion1337.chat%2Fmanagement/issues" UPLOADS = "https://git.lab/axion1337.chat/management" UMLAUTE = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss", "Ä": "ae", "Ö": "oe", "Ü": "ue", "é": "e"}) def slug(titel: str) -> str: s = titel.translate(UMLAUTE).lower() s = re.sub(r"[^a-z0-9]+", "-", s).strip("-") if len(s) > 48: s = s[:48].rsplit("-", 1)[0] return s or "ohne-titel" def hole(token: str): issues, seite = [], 1 while True: req = urllib.request.Request( f"{API}?state=opened&per_page=100&page={seite}", headers={"PRIVATE-TOKEN": token}) with urllib.request.urlopen(req) as antwort: batch = json.load(antwort) issues += batch if len(batch) < 100: return sorted(issues, key=lambda i: i["iid"]) seite += 1 def main() -> int: root = Path(sys.argv[1]) tokenpfad = Path(sys.argv[2] if len(sys.argv) > 2 else Path.home() / ".config/gitlab-lab/token") token = tokenpfad.read_text(encoding="utf-8").strip() ziel = root / "docs/issues" geschrieben = [] for i in hole(token): iid, titel, labels = i["iid"], i["title"].strip(), i["labels"] ms = (i.get("milestone") or {}).get("title", "") m = re.match(r"^(M\d)\b", ms) prio = [l.split(":")[1] for l in labels if l.startswith("priority:")] if not m or len(prio) != 1: sys.exit(f"ABBRUCH: #{iid} ohne eindeutigen Meilenstein/" f"Priorität ({ms!r}, {prio!r}) — Befund, kein Füllwert.") status = "open" wartegrund = "" if "status:doing" in labels: status = "in-progress" elif "status:next" in labels: status = "next" elif "status:wartet" in labels: status = "waiting" wartegrund = ("Grund im GitLab-Verlauf benannt (Import " "2026-08-11); im nächsten Refinement präzisieren") host = [l.split(":")[1] for l in labels if l.startswith("host:")] area = [l.split(":")[1] for l in labels if l.startswith("area:")] zeilen = ["---", "type: issue", f'id: "{iid:04d}"', f"status: {status}", f"created: {i['created_at'][:10]}", f"milestone: {m.group(1)}", f"priority: {prio[0]}"] if i.get("due_date"): zeilen.append(f"due: {i['due_date']}") if host: zeilen.append(f"host: {host[0]}") if area: zeilen.append(f"area: {area[0]}") if wartegrund: zeilen.append(f"wartegrund: {wartegrund}") zeilen += [f'gitlab_iid: "{iid}"', "related: []", "---", ""] beschreibung = (i.get("description") or "").replace("\r\n", "\n") beschreibung = beschreibung.replace("](/uploads/", f"]({UPLOADS}/uploads/") kopf = (f"# {titel}\n\n" f"> Import aus [management#{iid}]({i['web_url']}) " f"(2026-08-11). Kommentare und Verlauf bleiben dort; " f"kanonisch ist ab jetzt diese Datei (ADR-0012).\n\n") datei = ziel / f"{iid:04d}-{slug(titel)}.md" datei.write_text("\n".join(zeilen) + kopf + beschreibung.rstrip() + "\n", encoding="utf-8") geschrieben.append(datei.name) for name in geschrieben: print(name) print(f"import: {len(geschrieben)} Issues geschrieben") return 0 if __name__ == "__main__": sys.exit(main())