scripts: gitea_issues.py drives the tea CLI; 38 decision issues opened on Gitea
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
+41
-41
@@ -1,4 +1,4 @@
|
||||
# ruff: noqa: E501 S310
|
||||
# ruff: noqa: E501
|
||||
"""Create one Gitea issue per open decision in docs/PLAN.md.
|
||||
|
||||
Usage (token from a file, never from the command line or chat):
|
||||
@@ -12,13 +12,11 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
BASE = "https://gt.pescoinc.biz/api/v1"
|
||||
REPO = "spencerm/PacketStation"
|
||||
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):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(BASE + path, data=data, method=method)
|
||||
req.add_header("Authorization", "token " + token)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
req.add_header("Accept", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return json.loads(r.read().decode() or "null")
|
||||
except urllib.error.HTTPError as e:
|
||||
raise SystemExit(f"{method} {path}: HTTP {e.code} {e.read().decode()[:300]}") from None
|
||||
def tea(*args: str, output: str | None = "json") -> list | dict | str:
|
||||
cmd = ["tea", *args, "--repo", REPO]
|
||||
if output:
|
||||
cmd += ["--output", output]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8")
|
||||
if r.returncode != 0:
|
||||
raise SystemExit(f"tea {' '.join(args)} failed: {r.stderr.strip()[:400]}")
|
||||
if output == "json":
|
||||
return json.loads(r.stdout or "[]")
|
||||
return r.stdout
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--token-file", required=True)
|
||||
ap.add_argument("--dry-run", action="store_true")
|
||||
a = ap.parse_args()
|
||||
if a.dry_run:
|
||||
@@ -317,42 +313,46 @@ def main() -> int:
|
||||
print(f"{len(ISSUES)} issues")
|
||||
return 0
|
||||
|
||||
token = Path(a.token_file).read_text(encoding="utf-8").strip()
|
||||
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)}
|
||||
existing = {lb["name"] for lb in tea("labels", "list")}
|
||||
for name, color in LABELS.items():
|
||||
if name not in existing:
|
||||
lb = api("POST", f"/repos/{REPO}/labels", token, {"name": name, "color": "#" + color})
|
||||
existing[name] = lb["id"]
|
||||
tea("labels", "create", "--name", name, "--color", color, output=None)
|
||||
print("label created:", name)
|
||||
|
||||
titles = set()
|
||||
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
|
||||
titles = {i["title"] for i in tea("issues", "list", "--state", "all", "--limit", "500")}
|
||||
|
||||
made = 0
|
||||
for key, title, labels, body in ISSUES:
|
||||
if title in titles:
|
||||
print("exists:", title)
|
||||
continue
|
||||
full = (
|
||||
body
|
||||
+ f"\n\n---\nPlan reference: {key} in [docs/PLAN.md]({PLAN}) (open questions). Answer here; the build takes the default until then."
|
||||
footer = (
|
||||
"Plan reference: "
|
||||
+ key
|
||||
+ " in [docs/PLAN.md]("
|
||||
+ PLAN
|
||||
+ ") (open questions). Answer here; the build takes the default until then."
|
||||
)
|
||||
issue = api(
|
||||
"POST",
|
||||
f"/repos/{REPO}/issues",
|
||||
token,
|
||||
{"title": title, "body": full, "labels": [existing[lb] for lb in labels]},
|
||||
full = body + "\n\n---\n" + footer + "\n"
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as f:
|
||||
f.write(full)
|
||||
path = f.name
|
||||
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
|
||||
print(f"{made} created, {len(ISSUES) - made} already existed")
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user