KI-PR-Review-Agent auf eigenem VPS selbst hosten
Betreiben Sie einen KI-Reviewer auf Ihrem VPS: Self-hosted Runner, Diff-Prompts, Pfad- und Größenfilter, Inline-Kommentare sowie die echten Kosten pro PR.
Was ein selbst gehosteter PR-Review-Agent tut
Ein selbst gehosteter PR-Review-Agent ist ein kleines Programm auf einem Server, den Sie selbst besitzen. Er liest das Diff eines Pull Requests (PR) und sendet nur die geänderten Zeilen an ein Modell. Die Antwort wird als Inline-Review-Kommentar veröffentlicht. Der Agent checkt Ihren Branch niemals aus und liest keine Datei, die der Pull Request nicht geändert hat. Er besitzt nur zwei Zugangsdaten: einen API-Key für ein Modell und ein Token, das Kommentare erstellen kann und keine weiteren Berechtigungen hat.
Ein Modell kann ein Diff lesen. Dieser Teil ist gelöst. Entscheidend ist, wohin das Diff gesendet wird und wer den Key besitzt. Bei einem gehosteten Review-Bot verlässt jedes Diff aus jedem privaten Repository Ihr Netzwerk, landet in den Logs eines Drittanbieters und unterliegt dessen Aufbewahrungsrichtlinie. Auf einem VPS, den Sie selbst besitzen, geht das Diff von GitHub zu Ihrem Server und anschließend an die Model API. Außerdem können Sie die vierzig Codezeilen prüfen, die festlegen, was gesendet wird.
Was Sie vor dem Start benötigen
- Einen VPS mit Ubuntu 24.04, auf dem bereits ein selbst gehosteter GitHub-Actions-Runner für das Repository registriert ist. Weisen Sie ihm bei der Registrierung zusätzlich das Label
pr-reviewzu, da der folgende Workflow anhand dieses Labels ausgewählt wird. - Einen Anthropic-API-Schlüssel aus der Claude Console.
- Ein Repository, in dem Sie kontrollieren, wer einen Pull Request öffnen darf. Ein privates Repository ist der einfachere Fall. Der folgende Abschnitt zu Forks behandelt den öffentlichen Fall. Die dortige Antwort ist weniger komfortabel.
Reviewer auf dem VPS installieren
Der Runner-Dienst läuft unter dem Benutzerkonto ohne privilegierte Rechte, das Sie beim Ausführen von ./svc.sh install erstellt haben. Installieren Sie den Reviewer unter demselben Konto, damit der Job ihn ohne sudo ausführen kann. Ersetzen Sie runner unten durch den Namen Ihres Kontos.
sudo apt update && sudo apt install -y gh python3-venv
sudo install -d -m 755 -o runner -g runner /opt/pr-review
sudo -u runner python3 -m venv /opt/pr-review/venv
sudo -u runner /opt/pr-review/venv/bin/pip install anthropic
gh --versiongh --version gibt unter Ubuntu 24.04 mit Stand August 2026 gh version 2.45.0 aus. Jede Version ab 2.20 unterstützt das unten verwendete Flag --input. Eine Command 'gh' not found-Meldung bedeutet, dass die Komponente universe nicht aktiviert ist. Führen Sie daher sudo add-apt-repository universe aus und versuchen Sie es erneut.
Wo der Schlüssel und das Token liegen
Zwei Geheimnisse, zwei unterschiedliche Gültigkeitszeiträume. Keines davon gehört in das Repository.
ANTHROPIC_API_KEY ist ein Repository-Secret. Sie legen es unter Settings, dann Secrets and variables und anschließend Actions fest. GitHub verschlüsselt es und injiziert es zur Laufzeit in die Umgebung des Schritts. Es liegt niemals als Datei auf der Festplatte und erscheint niemals in der Git-Historie.
GITHUB_TOKEN funktioniert anders. Actions stellt für jeden Job ein neues Token aus und verwirft es, sobald der Job endet. Welche Aktionen dieses Token ausführen darf, wird durch den permissions:-Block im Workflow festgelegt. Genau dort wird das Prinzip der geringsten Berechtigungen umgesetzt:
permissions:
contents: read
pull-requests: writeDieses Token kann eine Review veröffentlichen. Es kann keinen Commit pushen, keinen Branch mergen, keine Workflow-Datei bearbeiten und nicht auf ein anderes Repository zugreifen. Ein Agent, der Kommentare schreiben kann, ist ein Reviewer. Ein Agent, der pushen kann, ist ein Committer. Dem hat niemand zugestimmt. Behandeln Sie den Modellschlüssel ebenso sorgfältig, weil er Kosten über Ihr Konto verursacht. Weitere Informationen zu diesem Problem finden Sie unter Geheimnisse außerhalb der Reichweite eines KI-Agenten halten.
Actions ersetzt die exakte Secret-Zeichenfolge in Job-Logs durch ***. Die Ersetzung erkennt nur die exakte Zeichenfolge. Wenn Sie einen Schlüssel base64-kodieren, auf zwei Zeilen aufteilen oder zeichenweise ausgeben, erscheint er im Klartext. Fügen Sie keinen Debug-Schritt hinzu, der die Umgebung ausgibt.
Warum ein Pull Request aus einem Fork Ihren API-Schlüssel nie sieht
Die Regel von GitHub ist kurz: Mit Ausnahme von GITHUB_TOKEN werden Secrets nicht an den Runner übergeben, wenn ein Workflow aus einem geforkten Repository ausgelöst wird. Ein pull_request-Lauf aus einem Fork startet Ihr Skript daher ohne ANTHROPIC_API_KEY, und der erste API-Aufruf schlägt mit invalid x-api-key fehl.
Die naheliegende Lösung besteht darin, den Trigger auf pull_request_target umzustellen. Dieser läuft im Kontext des Basis-Repositorys und erhält die Secrets. Tun Sie das hier nicht. Die Sicherheitshinweise von GitHub besagen, dass diese Workflows „privilegiert sind. Das bedeutet, dass sie denselben Cache des Haupt-Branches wie andere privilegierte Workflow-Trigger verwenden und möglicherweise Schreibzugriff auf das Repository sowie Zugriff auf referenzierte Secrets haben.“ Das Ergebnis „kann ausgenutzt werden, um ein Repository zu übernehmen“.
Auch beim Runner sind die Hinweise eindeutig: „Self-hosted Runner sollten für öffentliche Repositorys auf GitHub fast nie verwendet werden, weil jeder Benutzer Pull Requests gegen das Repository öffnen und die Umgebung kompromittieren kann.“
Daraus ergeben sich zwei Designentscheidungen. Der Job enthält eine Bedingung, sodass er nur für Branches ausgeführt wird, die in Ihr eigenes Repository gepusht wurden. Außerdem enthält der Workflow überhaupt keinen actions/checkout-Schritt. Der Agent hat den Branch nie auf der Festplatte. Ein schädlicher Pull Request ist daher nur Text, der an ein Modell gesendet wird. Er kann kein Build-Skript auf Ihrem VPS ausführen, weil auf Ihrem VPS nichts davon ausgeführt wird. Text ist deshalb trotzdem nicht automatisch harmlos: Ein von einem Fremden erstellter Diff ist nicht vertrauenswürdige Eingabe, die bei einem Modell ankommt. Das ist dieselbe Vertrauensgrenze wie bei der Websuche durch einen Agenten. Die einzige Begrenzung besteht hier darin, dass dieser Agent nichts anderes tun kann, als einen Kommentar zu posten.
Den Diff abrufen, nicht das Repository
Eine Anfrage liefert den vollständigen Diff als Klartext.
export GH_TOKEN=your_token # in the workflow this comes from secrets.GITHUB_TOKEN
gh api /repos/OWNER/REPO/pulls/42 -H "Accept: application/vnd.github.diff"Der Medientyp Accept: application/vnd.github.diff wandelt die Antwort von einem JSON-Objekt mit der Beschreibung des Pull Requests in den eigentlichen Unified Diff um. gh api gibt diesen Inhalt unverändert aus. Die erste angezeigte Zeile sollte mit diff --git a/ beginnen. Ein gh: Not Found (HTTP 404) bedeutet, dass das Token keinen Zugriff auf das Repository hat. Bei einem fein abgestuften persönlichen Token ist fast immer die fehlende Berechtigung Pull requests die Ursache.
Filtern Sie, bevor Sie ein Token ausgeben
Dieser Abschnitt entscheidet darüber, ob Menschen Ihren Bot lesen oder stummschalten. Jeder der folgenden Filter wird ausgeführt, bevor das Modell ein einziges Byte sieht.
- Pfadfilter. Sperren Sie Lock-Dateien, vendored Verzeichnisse, minifizierte Bundles und generierten Code. Ein Modellkommentar zu
package-lock.jsonist reines Rauschen. Außerdem machen diese Dateien häufig den größten Teil eines Diffs aus. - Eine Größenobergrenze. Wird die Obergrenze überschritten, überspringen Sie das Review und beenden Sie den Prozess mit dem Status erfolgreich. Für ein Refactoring mit 4,000 Zeilen gibt es dann eine ehrliche Zeile mit dem Hinweis, dass es für ein automatisches Review zu groß war, statt sechzig Vermutungen.
- Ein Schweregrad-Schwellenwert und eine Kommentarobergrenze. Melden Sie Findings mit hohem und mittlerem Schweregrad, maximal zehn, absteigend nach Schweregrad sortiert. Den Kommentar Nummer elf liest niemand.
Das Skript
Speichern Sie dies als /opt/pr-review/review.py. Das Skript liest seine Konfiguration aus der Umgebung. Der Workflow kann dadurch Modelle ändern, ohne den Code anzupassen.
#!/usr/bin/env python3
"""Review only the changed lines of one pull request."""
import json
import os
import subprocess
import sys
import anthropic
REPO = os.environ["GITHUB_REPOSITORY"]
PR = os.environ["PR_NUMBER"]
MODEL = os.environ.get("REVIEW_MODEL", "claude-haiku-4-5-20251001")
MAX_DIFF_BYTES = int(os.environ.get("MAX_DIFF_BYTES", "120000"))
MIN_SEVERITY = os.environ.get("MIN_SEVERITY", "medium")
MAX_COMMENTS = 10
RANK = {"low": 0, "medium": 1, "high": 2}
SKIP = ("package-lock.json", "poetry.lock", "/vendor/", "/node_modules/", ".min.js")
raw_diff = subprocess.run(
["gh", "api", f"/repos/{REPO}/pulls/{PR}",
"-H", "Accept: application/vnd.github.diff"],
check=True, capture_output=True, text=True,
).stdoutDie Aufteilung des Diffs nach Datei ermöglicht die Pfadfilterung. Die Nummerierung jeder Zeile sorgt dafür, dass die Review-Kommentare an der richtigen Stelle erscheinen. GitHub akzeptiert einen Inline-Kommentar nur für eine Zeile, die Bestandteil des Diffs ist. Das Modell muss daher eine tatsächlich vorhandene Zeilennummer angeben. Durch die Übergabe der Nummern kann es eine davon übernehmen, statt eine zu erfinden.
def per_file(diff_text):
"""Split a unified diff into one string per file."""
sections, current = [], []
for line in diff_text.splitlines():
if line.startswith("diff --git ") and current:
sections.append("\n".join(current))
current = []
current.append(line)
if current:
sections.append("\n".join(current))
return sections
def annotate(section):
"""Prefix every line that exists in the new file with its line number."""
out, n, in_hunk = [], 0, False
for line in section.splitlines():
if line.startswith("@@"):
n = int(line.split("+")[1].split(",")[0].split(" ")[0])
in_hunk = True
out.append(line)
elif not in_hunk or line.startswith(("-", "\\")):
out.append(line)
else:
out.append(f"{n}\t{line}")
n += 1
return "\n".join(out)
kept = [s for s in per_file(raw_diff)
if not any(p in s.split("\n", 1)[0] for p in SKIP)]
payload = "\n".join(annotate(s) for s in kept)
if not payload.strip():
print("every changed file was filtered out")
raise SystemExit(0)
if len(payload) > MAX_DIFF_BYTES:
print(f"diff is {len(payload)} bytes, over the {MAX_DIFF_BYTES} cap")
raise SystemExit(0)Der Hunk-Header enthält die Nummerierung. @@ -12,7 +12,9 @@ gibt an, dass der Hunk der neuen Datei bei Zeile 12 beginnt. Der Zähler startet daher dort und wird nur für hinzugefügte und unveränderte Zeilen erhöht. Entfernte Zeilen werden ohne Nummer durchlaufen, weil sie in der neuen Datei nicht vorhanden sind. Die Prüfung auf Zeilen, die mit einem Backslash beginnen, überspringt die Markierung für das fehlende Zeilenende, die git am Ende einer Datei schreibt. Andernfalls würde sich jede folgende Nummer um eins verschieben.
Beide Beendigungen verwenden den Status 0, nicht 1. Ein gefilterter oder zu großer Pull Request soll einen grünen Check anzeigen. Ein roter Check, auf den Menschen nicht reagieren können, wird ignoriert. Sobald ein Check ignoriert wird, werden alle ignoriert.
SYSTEM = (
"You review one pull request diff. Every line that exists in the new file is "
"prefixed with its line number and a tab character. "
"Report only defects you can see in the lines shown: a crash, a resource leak, "
"a security mistake, a wrong boundary condition, a broken contract with code "
"that is visible in this diff. Do not comment on style, naming or formatting. "
"Do not guess about code you cannot see. Leave out anything you are not "
"certain about. An empty findings list is a normal and common answer. "
'Reply with JSON only, in this shape: {"findings": [{"path": "src/app.py", '
'"line": 42, "severity": "high", "comment": "what is wrong, then why"}]} '
"Every line number must be one you can see in the left column of that file."
)
client = anthropic.Anthropic()
message = client.messages.create(
model=MODEL,
max_tokens=2000,
system=SYSTEM,
messages=[{"role": "user", "content": payload}],
)
print(f"stop={message.stop_reason} in={message.usage.input_tokens} "
f"out={message.usage.output_tokens}", file=sys.stderr)
text = message.content[0].text
findings = json.loads(text[text.find("{"):text.rfind("}") + 1])["findings"]
findings = [f for f in findings if RANK.get(f["severity"], 0) >= RANK[MIN_SEVERITY]]
findings.sort(key=lambda f: -RANK.get(f["severity"], 0))
del findings[MAX_COMMENTS:]
if not findings:
print("nothing above the severity threshold; posting no comment")
raise SystemExit(0)
review = {
"event": "COMMENT",
"body": f"Automated review of the changed lines. {len(findings)} finding(s).",
"comments": [
{"path": f["path"].removeprefix("b/"), "line": f["line"], "side": "RIGHT",
"body": f"**{f['severity']}** {f['comment']}"}
for f in findings
],
}
subprocess.run(
["gh", "api", "-X", "POST", f"/repos/{REPO}/pulls/{PR}/reviews", "--input", "-"],
input=json.dumps(review), text=True, check=True,
)Drei Details in diesem Block sind entscheidend. Das JSON wird zwischen dem ersten { und dem letzten } ausgeschnitten, weil ein Modell seine Antwort manchmal in einen Codeblock einschließt und json.loads an diesem Codeblock scheitert. Aus path wird ein führendes b/ entfernt, weil dieses Präfix aus dem Diff-Header stammt und GitHub einen relativ zum Repository angegebenen Pfad erwartet. --input - sendet das gesamte Review mit einem API-Aufruf. Dadurch treffen zehn Findings als eine Benachrichtigung statt als zehn ein.
Wenn es nichts zu melden gibt, sendet das Skript nichts. Ein Bot, der bei jedem Pull Request „keine Probleme gefunden“ schreibt, bringt die Menschen dazu, solche Meldungen zu überfliegen. Dann überfliegen sie auch die Meldung, auf die es ankommt.
Workflow einbinden
Speichern Sie dies als .github/workflows/pr-review.yml:
name: pr-review
on:
pull_request:
types: [opened, synchronize, reopened]
paths-ignore:
- '**.md'
- 'docs/**'
permissions:
contents: read
pull-requests: write
concurrency:
group: pr-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review:
if: github.event.pull_request.head.repo.full_name == github.repository && !contains(github.event.pull_request.labels.*.name, 'no-ai-review')
runs-on: [self-hosted, linux, pr-review]
steps:
- name: Review the changed lines
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REVIEW_MODEL: claude-haiku-4-5-20251001
MIN_SEVERITY: medium
run: /opt/pr-review/venv/bin/python /opt/pr-review/review.pyGITHUB_REPOSITORY steht nicht in diesem env:-Block, weil Actions es bereits für jeden Job setzt. Die Gruppe concurrency ist für die Abrechnung relevant: Ohne sie führen drei schnelle Korrekturen an einem Branch zu drei vollständigen Prüfungen, und alle drei werden berechnet. Mit ihr bleibt nur die letzte Prüfung bestehen.
Die Zeile if: erfüllt zwei Aufgaben. Die erste Hälfte überspringt Pull Requests aus Forks, die ohne Schlüssel ohnehin fehlschlagen würden. Die zweite Hälfte bietet Ihrem Team eine Abschaltmöglichkeit: Fügen Sie einem Pull Request das Label no-ai-review hinzu, wird der Job nicht ausgeführt.
Öffnen Sie einen Pull Request und beobachten Sie das Ergebnis:
gh run list --workflow=pr-review.yml --limit 3
gh run view --log
gh pr view 42 --commentsEin Lauf, der innerhalb weniger Sekunden mit nothing above the severity threshold; posting no comment im Log endet, funktioniert korrekt. Bei einem kleinen, sauberen Pull Request ist das das erwartete Ergebnis.
Was kostet eine automatisierte Pull-Request-Prüfung?
Der Diff macht fast den gesamten Input aus. Daher bestimmt die Größe des Diffs den Preis. Unten sehen Sie einen gemessenen Diff mit 500 Zeilen einschließlich des System-Prompts. Die Token wurden mit dem Token-Zähl-Endpunkt gezählt und nicht geschätzt.
The data behind this chart
[
{
"label": "Haiku 4.5",
"input_tokens": "8,000",
"output_tokens": "1,200"
},
{
"label": "Sonnet 5",
"input_tokens": "10,400",
"output_tokens": "1,560"
},
{
"label": "Opus 5",
"input_tokens": "10,400",
"output_tokens": "1,560"
}
]Dieser Diff umfasste bei Haiku 4.5 8,000 Input-Tokens und bei Sonnet 5 10,400. Der Text war identisch, die Anzahl jedoch unterschiedlich. Claude-Modelle ab 4.7 verwenden einen neueren Tokenizer, der für denselben Input ungefähr 30% mehr Tokens erzeugt. Anthropic dokumentiert dies auf der Preisseite. Berücksichtigen Sie das, wenn Sie ein neueres Modell allein anhand des Preises pro 1 Million Tokens mit einem älteren Modell vergleichen.
Listenpreise im August 2026: Haiku 4.5 kostet $1 pro 1 Million Input-Tokens und $5 pro 1 Million Output-Tokens. Sonnet 5 kostet im Rahmen der Einführungsaktion bis zum 31. August 2026 $2 beziehungsweise $10. Danach gelten $3 beziehungsweise $15. Opus 5 kostet $5 beziehungsweise $25.
The data behind this chart
[
{
"label": "Haiku 4.5",
"cost_per_pr_cents": 1.4,
"cost_200_prs_usd": "2.80"
},
{
"label": "Sonnet 5",
"cost_per_pr_cents": 3.64,
"cost_200_prs_usd": "7.28"
},
{
"label": "Opus 5",
"cost_per_pr_cents": 9.1,
"cost_200_prs_usd": "18.20"
}
]Das entspricht 1.4 Cent pro Pull Request bei Haiku 4.5 und 9.1 Cent bei Opus 5. Ein Team, das monatlich 200 Pull Requests zusammenführt, zahlt bei Haiku 4.5 ungefähr $2.80, bei Sonnet 5 $7.28 oder bei Opus 5 $18.20. Ab dem 1. September 2026 multiplizieren Sie den Wert für Sonnet 5 mit 1.5.
Zwei Faktoren erhöhen die tatsächlichen Kosten gegenüber dieser Schätzung. Der synchronize-Trigger prüft jeden Push. Ein aktiver Branch mit acht Pushes verursacht daher acht Prüfungen. Die Parallelitätsregel hilft nur, wenn Pushes in kurzer Folge eintreffen. Außerdem wird vorausgesetzt, dass die Pfadfilter funktionieren. Eine einzige nicht gefilterte Lock-Datei kann den Input allein verdoppeln.
Prompt-Caching hilft in diesem Fall nicht. Das gecachte Präfix muss zwischen den Aufrufen bytegenau identisch sein. Der Diff ist jedoch bei jedem Aufruf anders. Der System-Prompt ist der einzige stabile Teil und liegt deutlich unter der erforderlichen Mindestlänge für das Caching. Die allgemeine Regel finden Sie unter wann sich Prompt-Caching selbst bezahlt, und eine Entscheidungshilfe für die drei oben genannten Modelle unter welches Claude-Modell für welche Aufgabe verwendet werden sollte.
Messen Sie Ihre eigenen Diffs, bevor Sie die Funktion aktivieren
Fügen Sie diese Zeile ein, nachdem payload erstellt wurde, und führen Sie das Script anschließend manuell für einige Pull Requests des letzten Monats aus:
print(client.messages.count_tokens(
model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)Der Zähl-Endpunkt führt das Modell nicht aus. Daher verbraucht er keine Input- oder Output-Tokens. Er verwendet den Tokenizer des von Ihnen angegebenen Modells. Führen Sie ihn für zehn echte Pull Requests aus Ihrem eigenen Repository aus. Verwenden Sie den Median statt des Mittelwerts, damit eine sehr große Migration die Schätzung nicht verzerrt.
Warum Review-Bots stummgeschaltet werden und wie Sie das vermeiden
Zwei Verhaltensweisen zerstören das Vertrauen in diese Bots. Für beide gibt es im obigen Code eine Lösung.
Alles gleichzeitig prüfen. Ein Bot, der vierzig Kommentare hinterlässt, wird davon keinen einzigen vollständig gelesen. Die Schwelle für den Schweregrad und die Begrenzung auf zehn Kommentare sind keine Frage der Höflichkeit. Sie sorgen dafür, dass die wichtigen Befunde sichtbar bleiben. Wenn die Befunde vor dem Kürzen nach Schweregrad sortiert werden, entfernt die Begrenzung die unwichtigsten Befunde und nicht zufällig zehn davon.
Mit Sicherheit über etwas kommentieren, das der Bot nicht prüfen kann. Dieses Verhalten führt dazu, dass Entwickler den Bot endgültig deaktivieren. Ein Modell, dem 200 Zeilen aus einer Codebasis mit 40,000 Zeilen gezeigt werden, schreibt trotzdem: „Das beschädigt die Cache-Invalidierung in redis_client.py“ – und bezieht sich dabei auf eine Datei, die es nie gesehen hat. Der System-Prompt wirkt dem in klarer Sprache entgegen: Melden Sie nur Fehler, die in den gezeigten Zeilen sichtbar sind, und lassen Sie alles weg, bei dem Sie nicht sicher sind. Die konkrete Benennung dieses Fehlers funktioniert besser als eine allgemeine Aufforderung zu Genauigkeit. Der Hinweis, dass ein leeres Ergebnis normal ist, verhindert außerdem, dass das Modell bei einer Änderung von zwei Zeilen etwas erfindet, das es kommentieren kann.
Veröffentlichen Sie das Review als COMMENT, niemals als REQUEST_CHANGES. Die Einschätzung eines Modells darf einen Merge nicht blockieren können. Sobald dies möglich ist, wird jemand unter Zeitdruck den gesamten Workflow entfernen, statt darüber zu diskutieren.
Fehlerbilder und die dabei angezeigten Zeichenfolgen
HTTP 422 beim Übermitteln des Reviews. gh gibt gh: Unprocessable Entity (HTTP 422) aus, und im Antworttext wird das Feld genannt: Pull request review thread line must be part of the diff. GitHub kann diesen Kommentar nicht verankern. Häufige Ursachen sind eine vom Modell erfundene Zeilennummer, ein path mit weiterhin vorhandenem b/-Präfix oder ein Kommentar zu einer entfernten Zeile. In diesem Fall muss side statt RIGHT auf LEFT gesetzt werden. Geben Sie die Review-JSON vor dem Übermitteln aus und prüfen Sie einen Kommentar manuell anhand des Diffs.
invalid x-api-key von der Model API. Der Schritt schlägt beim ersten Aufruf von messages.create fehl. Entweder ist das Secret ANTHROPIC_API_KEY im Repository nicht gesetzt, oder der Pull Request stammt aus einem Fork, sodass Actions überhaupt keine Secrets übergeben hat. Die Fork-Prüfung in der Zeile if: hätte den Schritt überspringen müssen. Prüfen Sie daher zuerst diese Zeile.
gh: Resource not accessible by integration (HTTP 403). Das Job-Token darf nicht in Pull Requests schreiben. Fügen Sie pull-requests: write in den Block permissions: ein. Wenn es bereits enthalten ist, öffnen Sie Settings, dann Actions und anschließend General. Dort kann eine Organisationsrichtlinie begrenzen, was ein Workflow-Token an Berechtigungen anfordern darf.
json.decoder.JSONDecodeError. Das Modell hat kein parsebares JSON zurückgegeben. Häufig ist die Antwort an die Token-Grenze gestoßen und mitten in einem Objekt abgebrochen. Die Logzeile gibt hierfür stop_reason aus: Ein Wert von max_tokens bedeutet, dass Sie max_tokens erhöhen oder MAX_COMMENTS verringern müssen.
Der Workflow wird nie ausgeführt. gh run list zeigt für den Pull Request nichts an. Prüfen Sie, ob paths-ignore nicht alle geänderten Dateien herausfiltert. Prüfen Sie anschließend die Fork-Prüfung und die Label-Prüfung. Kontrollieren Sie danach mit sudo systemctl status 'actions.runner.*' auf dem VPS, ob der Runner aktiv ist. Bei einem nicht erreichbaren Runner bleibt der Job ohne Fehlermeldung im Pull Request in der Warteschlange.
Jedes Review bleibt leer. Setzen Sie MIN_SEVERITY für einen Lauf auf low. Wenn Befunde erscheinen, arbeitet der Schwellenwert wie vorgesehen. Wenn nichts erscheint, geben Sie payload aus und prüfen Sie, dass die Filter nicht den gesamten Diff entfernt haben.
Betrieb zusammen mit anderen Agents
Der Reviewer ist klein. Daher liegt es nahe, ihn auf dem Host zu installieren, auf dem bereits alle anderen Dienste laufen. Wenn das Repository relevant ist, sollten Sie ihn trotzdem getrennt betreiben. Dieser Prozess verwaltet ein Token, mit dem er Ihren Code kommentieren kann, sowie einen Schlüssel, der Ausgaben autorisiert. Ein selbst gehosteter Runner ist außerdem grundsätzlich ein Ort, an dem Workflow-Code ausgeführt wird. Ein eigenes unprivilegiertes Konto ohne sudo-Berechtigungen auf einem Host, auf dem keine anderen Dienste laufen, bildet die Mindestanforderung. Wenn Sie zusätzlich interaktive Agents betreiben, die Code auschecken, hat sich eine verworfene VM pro Agent als belastbares Muster erwiesen. Einen Coding-Agent auf einem VPS betreiben behandelt die allgemeine Einrichtung. Wenn die Anthropic API für Sie neu ist, bietet sich eine erste Claude-API-Anwendung auf einem VPS als kleinerer Einstieg an.
FAQ
Benötigt ein KI-Agent für PR-Reviews Schreibzugriff auf mein Repository?
Nein. Er benötigt pull-requests: write, um ein Review zu veröffentlichen, und contents: read, um den Diff abzurufen. Das ist die vollständige Liste. Sie legen diese Berechtigungen im Block permissions: des Workflows fest. Dieser begrenzt, was das auftragsbezogene GITHUB_TOKEN ausführen darf. Mit diesen beiden Zeilen kann der Agent einen Pull Request kommentieren, aber weder einen Commit pushen noch einen Branch mergen. Veröffentlichen Sie Reviews mit event: COMMENT statt mit REQUEST_CHANGES. Dadurch kann der Agent einen Merge ebenfalls nicht blockieren.
Warum schlägt mein Review-Kommentar mit HTTP 422 fehl?
GitHub akzeptiert einen Inline-Review-Kommentar nur für eine Zeile, die Bestandteil des Pull-Request-Diffs ist. Andernfalls gibt GitHub Pull request review thread line must be part of the diff zurück. Prüfen Sie, dass path relativ zum Repository angegeben ist und keinen b/-Präfix aus dem Diff-Header enthält. Die Zeilennummer muss innerhalb eines Hunk dieser Datei liegen. side muss für eine hinzugefügte oder unveränderte Zeile RIGHT und für eine entfernte Zeile LEFT sein. Wenn Sie jeder Diff-Zeile vor der Übergabe an das Modell die Zeilennummer der neuen Datei voranstellen, verhindert das von vornherein, dass das Modell Zeilennummern erfindet.
Kann ich dies in einem öffentlichen Repository mit Pull Requests aus Forks ausführen?
Nicht mit diesem Design. GitHub übergibt keine Secrets an einen Workflow, der von einem Fork ausgelöst wird. Daher fehlt der Modellschlüssel und der Lauf schlägt fehl. GitHub weist außerdem darauf hin, dass selbst gehostete Runner „should almost never be used for public repositories“ sind, weil jeder einen Pull Request öffnen kann, der die Ausführung von Code auf Ihrer Maschine auslöst. Beschränken Sie den Reviewer bei einem öffentlichen Projekt entweder auf Branches, die direkt in das Repository gepusht wurden. Genau das bewirkt die if:-Bedingung. Oder verschieben Sie den Review-Schritt auf einen von GitHub gehosteten Runner und akzeptieren Sie, dass der Diff Ihre eigene Infrastruktur verlässt.
Welches Modell sollte ich für Pull-Request-Reviews verwenden?
Beginnen Sie mit Haiku 4.5. Einen begrenzten Diff anhand einer festen Liste von Fehlertypen zu prüfen, ist kein schwieriges Reasoning-Problem. Das günstigste Modell hält die monatlichen Kosten auf einem Betrag, über den niemand diskutiert. Wechseln Sie zu Sonnet 5, wenn Sie feststellen, dass es in Ihrer Sprache oder Ihrem Framework echte Fehler übersieht. Messen Sie dies, statt es lediglich anzunehmen. Opus 5 ist pro Pull Request mit großem Abstand das teuerste der drei Modelle. Das lässt sich für einen Release-Branch eher rechtfertigen als für jeden Push auf jeden Feature-Branch.