Files
HiveGrid-Wireframes/hivegrid-cloud/.planning/plane-sync/link_ticketdocs.py
T

73 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""
link_ticketdocs.py — Phase 1 der Review-Plane-Operation.
Fügt in jedes bestehende Ticket als ERSTE Doku-Zeile den granularen Link auf
.planning/ticket-docs/<WorkingID>.md ein (Plane-styled <a>). Idempotent (Working-ID-Match,
skippt wenn schon verlinkt). Sonderfälle (Rename/Auflösung) bleiben Phase 3 überlassen.
Default DRY-RUN. Scharf: --apply
"""
import json, os, re, sys, time, urllib.request, urllib.error
APPLY = "--apply" in sys.argv
TOK = {}
for line in open(os.path.expanduser("~/.cli-tokens/plane")):
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1); TOK[k.strip()] = v.strip()
BASE = TOK["PLANE_BASE_URL"].rstrip("/"); WS = TOK["PLANE_WORKSPACE"]; KEY = TOK["PLANE_API_KEY"]
PID = "35d0d968-8ab3-40cc-97b8-b81d917dc5df"
API = f"{BASE}/api/v1/workspaces/{WS}/projects/{PID}"
DOC_BASE = "https://git.tool-ctrl.de/toolctrl-labs/HiveGrid-Cloud/src/branch/main/.planning/ticket-docs/"
LINK = ('<a target="_blank" class="text-accent-secondary underline underline-offset-[3px] '
'hover:text-accent-primary transition-colors cursor-pointer" href="{url}" '
'rel="noopener noreferrer">📄 Ticket-Doku ({wid})</a><br>')
MARK = re.compile(r'(📚 Doku</(?:b|strong)><br\s*/?>)')
# Sonderfälle → Phase 3 (Rename/Auflösung), hier NICHT anfassen:
SKIP = {"AK-P1c", "D-REST", "VD-1", "VD-2", "VD-3", "VD-4", "VD-5"}
def patch(iid, html):
body = json.dumps({"description_html": html}).encode()
r = urllib.request.Request(API + f"/issues/{iid}/", data=body, method="PATCH")
r.add_header("X-API-Key", KEY); r.add_header("Content-Type", "application/json")
for attempt in range(4):
try:
with urllib.request.urlopen(r, timeout=30) as resp:
return resp.status
except urllib.error.HTTPError as e:
if e.code == 429:
time.sleep(5); continue
return e.code
return 999
bk = json.load(open(".planning/plane-sync/plane-backup-review.json"))
todo, skip_done, skip_special, miss_doc, miss_mark = [], 0, 0, [], []
for i in bk["issues"]:
wid = re.split(r'\s*·\s*', i["name"])[0].strip()
if wid in SKIP:
skip_special += 1; continue
if not os.path.exists(f".planning/ticket-docs/{wid}.md"):
miss_doc.append(wid); continue
html = i["description_html"]
if f"ticket-docs/{wid}.md" in html:
skip_done += 1; continue
link = LINK.format(url=DOC_BASE + wid + ".md", wid=wid)
new = MARK.sub(lambda m: m.group(1) + link, html, count=1)
if new == html:
miss_mark.append(wid); continue
todo.append((i["id"], wid, new))
print(f"=== Phase 1 Link-Update {'(SCHARF)' if APPLY else '(DRY-RUN)'} ===")
print(f"zu verlinken: {len(todo)} | schon verlinkt: {skip_done} | Sonderfälle(P3): {skip_special}")
if miss_doc: print(f"⚠ ohne Doc-Datei: {miss_doc}")
if miss_mark: print(f"⚠ ohne Doku-Marker (manuell prüfen): {miss_mark}")
print("Beispiele:", [w for _, w, _ in todo[:12]])
if APPLY:
ok = 0
for iid, wid, new in todo:
st = patch(iid, new)
if st < 300: ok += 1
else: print(f" ✗ {wid}: HTTP {st}")
time.sleep(1.1)
print(f"✅ gepatcht: {ok}/{len(todo)}")
else:
print("\n→ scharf: python3 .planning/plane-sync/link_ticketdocs.py --apply")