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:
71
scripts/ai_explain.py
Normal file
71
scripts/ai_explain.py
Normal 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())
|
||||
Reference in New Issue
Block a user