ci-ai: KI-Schicht für die Gitea-CI (PR-Review, Security-Erklärung, CI-Failure-Analyse)
Provider-agnostische LLM-Anbindung (Anthropic jetzt, Jetson/Ollama später per Env-Var umschaltbar). Stdlib-only Skripte + Beispiel-Workflows. GitLab-Duo-Parität für self-hosted Gitea + act_runner. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
70
scripts/gitea_api.py
Normal file
70
scripts/gitea_api.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""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 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"]
|
||||
Reference in New Issue
Block a user