107 lines
3.3 KiB
Python
107 lines
3.3 KiB
Python
"""Feature A — KI-PR-Review + Summary.
|
||
|
||
Holt den PR-Diff über die Gitea-API, lässt ihn vom konfigurierten LLM
|
||
zusammenfassen + reviewen und postet das Ergebnis als EINEN idempotenten
|
||
Kommentar (Marker -> editiert sich bei jedem Push, statt zu spammen).
|
||
|
||
Erwartete Env-Vars: siehe llm.py + gitea_api.py, plus die Actions-Kontext-
|
||
Variablen (GITHUB_REPOSITORY, GITHUB_EVENT_PATH).
|
||
"""
|
||
import sys
|
||
|
||
import context
|
||
import gitea_api
|
||
import llm
|
||
|
||
MARKER = "<!-- ci-ai:review -->"
|
||
DIFF_CAP = 40000 # Zeichen — darüber wird der Diff gekürzt (Token-/Kostenschutz)
|
||
|
||
# Dateien, die für ein Review wenig Sinn ergeben (Lockfiles, Binär, generiert).
|
||
SKIP_TOKENS = (
|
||
".lock", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
||
".min.js", ".min.css", ".map",
|
||
".png", ".jpg", ".jpeg", ".gif", ".ico", ".pdf", ".woff", ".woff2", ".ttf",
|
||
"/vendor/", "/node_modules/", "/dist/", "/build/",
|
||
)
|
||
|
||
SYSTEM = (
|
||
"Du bist ein erfahrener, pragmatischer Code-Reviewer. Antworte auf Deutsch, "
|
||
"knapp und konkret. Erfinde nichts, was nicht im Diff steht."
|
||
)
|
||
|
||
PROMPT = """\
|
||
Reviewe den folgenden Pull-Request.
|
||
|
||
PR-Titel: {title}
|
||
PR-Beschreibung: {body}
|
||
|
||
Gib genau diese zwei Abschnitte aus (Markdown):
|
||
|
||
### Zusammenfassung
|
||
2–4 Sätze: Was ändert dieser PR und warum.
|
||
|
||
### Review
|
||
Stichpunkte zu: möglichen Bugs, Sicherheits-/Edge-Cases, fehlenden Tests,
|
||
Stil/Lesbarkeit. Nur echte, im Diff belegbare Punkte — wenn alles gut aussieht,
|
||
sag das ehrlich. Markiere ernste Probleme mit ⚠️.
|
||
|
||
--- DIFF ---
|
||
{diff}
|
||
"""
|
||
|
||
|
||
def filter_diff(diff: str) -> str:
|
||
"""Entfernt uninteressante Dateien (per `diff --git`-Sektion)."""
|
||
if not diff:
|
||
return ""
|
||
sections = diff.split("diff --git ")
|
||
kept = []
|
||
for sec in sections[1:]: # sections[0] ist der Vorspann (leer)
|
||
header = sec.split("\n", 1)[0].lower()
|
||
if any(tok in header for tok in SKIP_TOKENS):
|
||
continue
|
||
kept.append("diff --git " + sec)
|
||
return "".join(kept).strip()
|
||
|
||
|
||
def main() -> int:
|
||
owner, repo = context.repo_slug()
|
||
pr = context.pull_request()
|
||
if not pr or not pr["index"]:
|
||
print("[ai_review] Kein Pull-Request im Event — übersprungen.")
|
||
return 0
|
||
if pr["author"].lower().endswith("-bot") or pr["author"].lower() == "ci-ai":
|
||
print("[ai_review] PR vom Bot selbst — übersprungen.")
|
||
return 0
|
||
|
||
ok, why = llm.configured()
|
||
if not ok:
|
||
print(f"[ai_review] KI inaktiv ({why}) — Schritt sauber übersprungen, kein Fehler.")
|
||
return 0
|
||
|
||
diff = filter_diff(gitea_api.get_pr_diff(owner, repo, pr["index"]))
|
||
if not diff:
|
||
print("[ai_review] Leerer/gefilterter Diff — nichts zu reviewen.")
|
||
return 0
|
||
|
||
truncated = False
|
||
if len(diff) > DIFF_CAP:
|
||
diff = diff[:DIFF_CAP]
|
||
truncated = True
|
||
|
||
answer = llm.complete(
|
||
SYSTEM,
|
||
PROMPT.format(title=pr["title"], body=pr["body"][:1000], diff=diff),
|
||
max_tokens=1800,
|
||
)
|
||
|
||
note = "\n\n_⚠️ Diff war groß und wurde gekürzt — Review nur über den Anfang._" if truncated else ""
|
||
body = f"## 🤖 KI-Review\n\n{answer}{note}"
|
||
cid = gitea_api.upsert_comment(owner, repo, pr["index"], MARKER, body)
|
||
print(f"[ai_review] Kommentar gepostet/aktualisiert (id={cid}).")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|