scripts: gitea_issues.py drives the tea CLI; 38 decision issues opened on Gitea
ci / frontend (push) Successful in 43s
ci / server (push) Successful in 11m6s
ci / image (push) Successful in 14s
ci / e2e (push) Failing after 4m3s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-10-18 20:50:34 -06:00
co-authored by Claude Fable 5.1
parent 5ca83f48c3
commit 3623dca77b
+41 -41
View File
@@ -1,4 +1,4 @@
# ruff: noqa: E501 S310 # ruff: noqa: E501
"""Create one Gitea issue per open decision in docs/PLAN.md. """Create one Gitea issue per open decision in docs/PLAN.md.
Usage (token from a file, never from the command line or chat): Usage (token from a file, never from the command line or chat):
@@ -12,13 +12,11 @@ from __future__ import annotations
import argparse import argparse
import json import json
import subprocess
import sys import sys
import urllib.error import tempfile
import urllib.parse
import urllib.request
from pathlib import Path from pathlib import Path
BASE = "https://gt.pescoinc.biz/api/v1"
REPO = "spencerm/PacketStation" REPO = "spencerm/PacketStation"
PLAN = "https://gt.pescoinc.biz/spencerm/PacketStation/src/branch/main/docs/PLAN.md" PLAN = "https://gt.pescoinc.biz/spencerm/PacketStation/src/branch/main/docs/PLAN.md"
@@ -293,22 +291,20 @@ ISSUES: list[tuple[str, str, list[str], str]] = [
] ]
def api(method: str, path: str, token: str, body: dict | None = None): def tea(*args: str, output: str | None = "json") -> list | dict | str:
data = json.dumps(body).encode() if body is not None else None cmd = ["tea", *args, "--repo", REPO]
req = urllib.request.Request(BASE + path, data=data, method=method) if output:
req.add_header("Authorization", "token " + token) cmd += ["--output", output]
req.add_header("Content-Type", "application/json") r = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8")
req.add_header("Accept", "application/json") if r.returncode != 0:
try: raise SystemExit(f"tea {' '.join(args)} failed: {r.stderr.strip()[:400]}")
with urllib.request.urlopen(req, timeout=30) as r: if output == "json":
return json.loads(r.read().decode() or "null") return json.loads(r.stdout or "[]")
except urllib.error.HTTPError as e: return r.stdout
raise SystemExit(f"{method} {path}: HTTP {e.code} {e.read().decode()[:300]}") from None
def main() -> int: def main() -> int:
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("--token-file", required=True)
ap.add_argument("--dry-run", action="store_true") ap.add_argument("--dry-run", action="store_true")
a = ap.parse_args() a = ap.parse_args()
if a.dry_run: if a.dry_run:
@@ -317,42 +313,46 @@ def main() -> int:
print(f"{len(ISSUES)} issues") print(f"{len(ISSUES)} issues")
return 0 return 0
token = Path(a.token_file).read_text(encoding="utf-8").strip() existing = {lb["name"] for lb in tea("labels", "list")}
if not token:
raise SystemExit("token file is empty")
existing = {lb["name"]: lb["id"] for lb in api("GET", f"/repos/{REPO}/labels?limit=100", token)}
for name, color in LABELS.items(): for name, color in LABELS.items():
if name not in existing: if name not in existing:
lb = api("POST", f"/repos/{REPO}/labels", token, {"name": name, "color": "#" + color}) tea("labels", "create", "--name", name, "--color", color, output=None)
existing[name] = lb["id"]
print("label created:", name) print("label created:", name)
titles = set() titles = {i["title"] for i in tea("issues", "list", "--state", "all", "--limit", "500")}
page = 1
while True:
batch = api("GET", f"/repos/{REPO}/issues?state=all&type=issues&limit=50&page={page}", token)
if not batch:
break
titles.update(i["title"] for i in batch)
page += 1
made = 0 made = 0
for key, title, labels, body in ISSUES: for key, title, labels, body in ISSUES:
if title in titles: if title in titles:
print("exists:", title) print("exists:", title)
continue continue
full = ( footer = (
body "Plan reference: "
+ f"\n\n---\nPlan reference: {key} in [docs/PLAN.md]({PLAN}) (open questions). Answer here; the build takes the default until then." + key
+ " in [docs/PLAN.md]("
+ PLAN
+ ") (open questions). Answer here; the build takes the default until then."
) )
issue = api( full = body + "\n\n---\n" + footer + "\n"
"POST", with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as f:
f"/repos/{REPO}/issues", f.write(full)
token, path = f.name
{"title": title, "body": full, "labels": [existing[lb] for lb in labels]}, try:
out = tea(
"issues",
"create",
"--title",
title,
"--labels",
",".join(labels),
"--description-file",
path,
output=None,
) )
print(f"created #{issue['number']}: {title}") finally:
Path(path).unlink(missing_ok=True)
first = (out or "").strip().splitlines()[0] if out else ""
print(f"created: {title} {first[:60]}")
made += 1 made += 1
print(f"{made} created, {len(ISSUES) - made} already existed") print(f"{made} created, {len(ISSUES) - made} already existed")
return 0 return 0