73 lines
3.3 KiB
Python
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")
|