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:
2026-06-09 15:38:53 +02:00
commit daf89ff8b9
10 changed files with 592 additions and 0 deletions

71
scripts/ai_explain.py Normal file
View File

@@ -0,0 +1,71 @@
"""Features B & C — Klartext-Erklärung von Scan-Funden / CI-Fehlern.
Aufruf:
python ai_explain.py <kind> <textdatei>
kind = "security" -> erklärt Trivy/Gitleaks-Funde (GitLab-Duo-Parität)
kind = "ci-failure" -> erklärt ein fehlgeschlagenes Job-Log
Postet das Ergebnis als idempotenten PR-Kommentar (eigener Marker je kind).
Ohne PR-Kontext (z.B. push ohne PR) wird nur ins Job-Log geschrieben.
"""
import sys
import context
import gitea_api
import llm
KINDS = {
"security": {
"marker": "<!-- ci-ai:security -->",
"heading": "## 🛡️ Security-Scan — Erklärung der Funde",
"system": (
"Du bist ein Security-Engineer. Erkläre die folgenden Scanner-Funde "
"(Trivy/Gitleaks) auf Deutsch in Klartext: was wurde gefunden, warum "
"ist es ein Risiko, und wie behebt man es konkret. Priorisiere nach "
"Schweregrad. Keine Funde erfinden."
),
"intro": "Der Security-Scan ist fehlgeschlagen. Hier die Funde im Klartext:",
},
"ci-failure": {
"marker": "<!-- ci-ai:ci-failure -->",
"heading": "## 🔧 CI-Fehler — Analyse",
"system": (
"Du bist ein erfahrener CI/CD-Engineer. Analysiere das folgende Log "
"eines fehlgeschlagenen CI-Jobs auf Deutsch: nenne die wahrscheinliche "
"Ursache und einen konkreten Fix. Knapp und konkret."
),
"intro": "Ein CI-Job ist fehlgeschlagen. Wahrscheinliche Ursache:",
},
}
LOG_CAP = 16000 # Zeichen aus dem Log (Ende ist meist am aussagekräftigsten)
def main() -> int:
if len(sys.argv) != 3 or sys.argv[1] not in KINDS:
print("Usage: ai_explain.py <security|ci-failure> <textdatei>", file=sys.stderr)
return 2
kind = KINDS[sys.argv[1]]
with open(sys.argv[2], encoding="utf-8", errors="replace") as fh:
text = fh.read()
if len(text) > LOG_CAP:
text = "[... Anfang gekürzt ...]\n" + text[-LOG_CAP:]
answer = llm.complete(kind["system"], f"{kind['intro']}\n\n```\n{text}\n```", max_tokens=1200)
body = f"{kind['heading']}\n\n{answer}"
owner, repo = context.repo_slug()
pr = context.pull_request()
if pr and pr["index"]:
cid = gitea_api.upsert_comment(owner, repo, pr["index"], kind["marker"], body)
print(f"[ai_explain] Kommentar gepostet/aktualisiert (id={cid}).")
else:
print("[ai_explain] Kein PR-Kontext — Ausgabe nur im Log:\n")
print(body)
return 0
if __name__ == "__main__":
sys.exit(main())