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.
|
"""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
|
||||||
|
|||||||
Reference in New Issue
Block a user