A small persistent service (bot/app.py) that reuses the ci-ai modules (llm.py->Jetson, gitea_api.py, ai_review prompt). Handles issue_comment @ci-ai mentions as threaded chat and pull_request events as idempotent reviews — answering in seconds via webhook, with NO runner/clone/hairpin. Runs on the Pi next to Gitea (internal http://gitea:3000 API). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
3.7 KiB
Python
100 lines
3.7 KiB
Python
"""Schlanke Gitea-API-Helfer (stdlib only).
|
|
|
|
- PR-Diff holen
|
|
- Kommentare IDEMPOTENT posten: ein versteckter HTML-Marker identifiziert den
|
|
Bot-Kommentar; existiert er schon, wird er editiert statt neu angelegt
|
|
(kein Zuspammen bei jedem Push).
|
|
|
|
Env-Vars:
|
|
GITEA_API Basis-URL der API, Default https://git.eulernest.eu/api/v1
|
|
GITEA_TOKEN Token fürs Kommentieren (Actions-Auto-Token ODER PAT).
|
|
Fallback: GITEA_BOT_TOKEN.
|
|
"""
|
|
import json
|
|
import os
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
_BASE = os.environ.get("GITEA_API", "https://git.eulernest.eu/api/v1").rstrip("/")
|
|
_TOKEN = os.environ.get("GITEA_TOKEN") or os.environ.get("GITEA_BOT_TOKEN", "")
|
|
|
|
|
|
def _request(method: str, path: str, *, data=None, raw: bool = False):
|
|
url = f"{_BASE}{path}"
|
|
headers = {}
|
|
if _TOKEN:
|
|
headers["Authorization"] = f"token {_TOKEN}"
|
|
body = None
|
|
if data is not None:
|
|
body = json.dumps(data).encode("utf-8")
|
|
headers["Content-Type"] = "application/json"
|
|
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
content = resp.read().decode("utf-8", "replace")
|
|
if raw:
|
|
return content
|
|
return json.loads(content) if content.strip() else None
|
|
except urllib.error.HTTPError as exc:
|
|
detail = exc.read().decode("utf-8", "replace")
|
|
raise SystemExit(f"[gitea] {method} {path} -> HTTP {exc.code}: {detail[:400]}")
|
|
except urllib.error.URLError as exc:
|
|
raise SystemExit(f"[gitea] {method} {path} fehlgeschlagen: {exc.reason}")
|
|
|
|
|
|
def get_pr_diff(owner: str, repo: str, index: int) -> str:
|
|
"""Unified Diff eines PRs (Gitea unterstützt den .diff-Suffix)."""
|
|
return _request("GET", f"/repos/{owner}/{repo}/pulls/{index}.diff", raw=True)
|
|
|
|
|
|
def get_issue_thread(owner: str, repo: str, index: int) -> str:
|
|
"""Titel + Body + alle MENSCHLICHEN Kommentare eines Issues/PRs als Text.
|
|
|
|
Eigene Bot-Kommentare (Marker `<!-- ci-ai:`) werden übersprungen, damit das
|
|
Summary nicht die eigene Ausgabe mitliest.
|
|
"""
|
|
iss = _request("GET", f"/repos/{owner}/{repo}/issues/{index}")
|
|
parts = [f"# {iss.get('title', '')}\n\n{iss.get('body') or '(kein Text)'}"]
|
|
comments = _request("GET", f"/repos/{owner}/{repo}/issues/{index}/comments") or []
|
|
for c in comments:
|
|
body = c.get("body") or ""
|
|
if "<!-- ci-ai:" in body:
|
|
continue
|
|
author = (c.get("user") or {}).get("login", "?")
|
|
parts.append(f"--- @{author}:\n{body}")
|
|
return "\n\n".join(parts)
|
|
|
|
|
|
def upsert_comment(owner: str, repo: str, index: int, marker: str, body: str) -> int:
|
|
"""Legt einen Kommentar an ODER editiert den vorhandenen mit gleichem Marker.
|
|
|
|
PR-Kommentare sind in Gitea Issue-Kommentare (gleicher Endpunkt).
|
|
"""
|
|
full = f"{body}\n\n{marker}"
|
|
existing = _request("GET", f"/repos/{owner}/{repo}/issues/{index}/comments") or []
|
|
for comment in existing:
|
|
if marker in (comment.get("body") or ""):
|
|
_request(
|
|
"PATCH",
|
|
f"/repos/{owner}/{repo}/issues/comments/{comment['id']}",
|
|
data={"body": full},
|
|
)
|
|
return comment["id"]
|
|
created = _request(
|
|
"POST",
|
|
f"/repos/{owner}/{repo}/issues/{index}/comments",
|
|
data={"body": full},
|
|
)
|
|
return created["id"]
|
|
|
|
|
|
def post_comment(owner: str, repo: str, index: int, body: str) -> int:
|
|
"""Legt einen NEUEN Kommentar an (nicht idempotent — für Chat-Antworten,
|
|
wo jede @mention ihre eigene Antwort bekommen soll)."""
|
|
created = _request(
|
|
"POST",
|
|
f"/repos/{owner}/{repo}/issues/{index}/comments",
|
|
data={"body": body},
|
|
)
|
|
return created["id"]
|