SSD Nodes Learn 🎉 VPS ab $4.99/Monat
Anleitungen Matt ConnorVon Matt Connor · Aktualisiert 2026-08-07

KI-PR-Review-Agent auf eigenem VPS selbst hosten

Betreiben Sie einen KI-Reviewer für Pull Requests auf Ihrem VPS: mit Self-hosted Runner, Diff-Filterung, Inline-Kommentaren und berechneten Kosten pro PR.

Was ein selbst gehosteter PR-Review-Agent macht

Ein selbst gehosteter PR-Review-Agent ist ein kleines Programm auf einem Server, den Sie selbst betreiben. 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. Er liest auch keine Datei, die vom Pull Request nicht geändert wurde. Er besitzt nur zwei Zugangsdaten: einen API-Schlüssel (Application Programming Interface) für ein Modell und ein Token, das ausschließlich Kommentare erstellen kann.

Ein Modell kann ein Diff lesen. Dieser Teil ist gelöst. Entscheidend ist, wohin das Diff gesendet wird und wer den Schlüssel 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 (Virtual Private Server), den Sie selbst betreiben, geht das Diff von GitHub zu Ihrem Server und von dort an die Model-API. Sie können außerdem die vierzig Codezeilen prüfen, die festlegen, welche Daten gesendet werden.

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-review zu, da der folgende Workflow dieses Label verwendet.
  • Einen Anthropic-API-Schlüssel aus der Claude Console.
  • Ein Repository, in dem Sie steuern können, wer einen Pull Request öffnen darf. Ein privates Repository ist der einfachste Fall. Der folgende Abschnitt zu Forks behandelt den öffentlichen Fall. Die dortige Antwort ist weniger komfortabel.

Reviewer auf der VPS installieren

Der Runner-Dienst läuft unter dem unprivilegierten Konto, 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 --version

gh --version gibt unter Ubuntu 24.04 im 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 mit zwei unterschiedlichen Gültigkeitsdauern. 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 dem Datenträger 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 Block permissions: im Workflow festgelegt. Genau dort wird das Prinzip der geringsten Berechtigungen umgesetzt:

permissions:
  contents: read
  pull-requests: write

Mit diesem Token kann eine Review veröffentlicht werden. Es kann keinen Commit pushen, keinen Branch mergen, keine Workflow-Datei ändern und auf kein anderes Repository zugreifen. Ein Agent, der kommentieren kann, ist ein Reviewer. Ein Agent, der pushen kann, ist ein Committer, und dem hat niemand zugestimmt. Behandeln Sie den Modellschlüssel ebenso sorgfältig, da er Kosten auf Ihrem Konto verursacht. Weitere Informationen zu diesem Problem finden Sie unter Geheimnisse außerhalb der Reichweite eines AI-Agenten halten.

Actions ersetzt die exakte Secret-Zeichenfolge in den Job-Logs durch ***. Dabei wird nur die exakte Zeichenfolge erkannt. Wenn Sie einen Schlüssel mit base64 codieren, auf zwei Zeilen aufteilen oder zeichenweise ausgeben, erscheint er ungeschützt. Fügen Sie keinen Debug-Schritt hinzu, der die Umgebung ausgibt.

Warum ein Pull Request aus einem Fork niemals Ihren API-Schlüssel erhält

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 sagen ausdrücklich, dass diese Workflows „privilegiert sind. Das bedeutet, dass sie denselben Cache des Main-Branches wie andere privilegierte Workflow-Trigger verwenden können 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 formuliert GitHub die Empfehlung 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 Prüfung, sodass er nur auf 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. Für ein Modell ist ein bösartiger Pull Request daher nur Text, der übergeben wird. Er kann kein Build-Skript auf Ihrem VPS ausführen, weil auf Ihrem VPS niemals etwas daraus ausgeführt wird.

Den Diff abrufen, nicht das Repository

Eine Anfrage ruft den gesamten Diff als Klartext ab.

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 Unified Diff selbst 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 feingranularen persönlichen Token bedeutet das fast immer, dass die Berechtigung Pull requests nicht aktiviert wurde.

Filtern Sie, bevor Sie ein Token verbrauchen

Dieser Abschnitt entscheidet darüber, ob Menschen die Ausgaben eines Bots lesen oder ihn stummschalten. Jeder der folgenden Filter wird angewendet, bevor das Modell ein einziges Byte sieht.

  • Pfadfilter. Sperren Sie Lock-Dateien, vendored Verzeichnisse, minifizierte Bundles und generierten Code. Ein Modellkommentar zu package-lock.json ist reines Rauschen. Außerdem enthalten diese Dateien häufig den größten Teil eines Diffs.
  • Eine Größenbegrenzung. Überschreitet der Diff diese Begrenzung, überspringen Sie die Prüfung und beenden Sie den Prozess mit Erfolg. Bei einem Refactoring mit 4,000 Zeilen erhalten Sie dadurch eine eindeutige Zeile, dass die Änderung für eine automatische Prüfung zu groß war, statt sechzig Vermutungen.
  • Ein Schweregrad-Schwellenwert und eine Kommentarobergrenze. Melden Sie Befunde mit hohem und mittlerem Schweregrad, höchstens zehn davon, 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. Dadurch kann der Workflow die Modelle ohne Codeänderung wechseln.

#!/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,
).stdout

Die 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 vorangestellten 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 erhalten keine Nummer, weil sie in der neuen Datei nicht vorhanden sind. Die Prüfung auf Zeilen, die mit einem umgekehrten Schrägstrich beginnen, überspringt die Markierung für das fehlende Zeilenende, die git am Ende einer Datei schreibt. Andernfalls würden alle folgenden Zeilennummern um eins verschoben.

Beide Ausgänge verwenden den Status 0, nicht 1. Ein gefilterter oder zu großer Pull Request sollte einen grünen Check anzeigen. Ein roter Check, auf den Menschen nicht reagieren können, wird ignoriert. Sobald ein Check ignoriert wird, werden alle Checks 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. Die JSON-Ausgabe wird zwischen dem ersten { und dem letzten } ausgeschnitten, weil ein Modell seine Antwort gelegentlich in einen Codeblock einschließt und json.loads an diesem Codeblock scheitert. Beim 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 die gesamte Review mit einem einzigen API-Aufruf. Dadurch treffen zehn Befunde als eine Benachrichtigung statt als zehn einzelne Benachrichtigungen ein.

Wenn es nichts zu melden gibt, veröffentlicht das Skript nichts. Ein Bot, der bei jedem Pull Request „keine Probleme gefunden“ schreibt, bringt die Menschen dazu, darüber hinwegzulesen. Dann übersehen sie auch den einen Befund, der relevant war.

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.py

GITHUB_REPOSITORY steht nicht in diesem env:-Block, weil Actions die Variable bereits für jeden Job setzt. Die Gruppe concurrency ist für die Abrechnung relevant: Ohne sie starten drei schnelle Korrekturen in einem Branch drei vollständige Reviews, und alle drei werden berechnet. Mit ihr bleibt nur die letzte Ausführung 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 Möglichkeit, den Job zu deaktivieren: Fügen Sie einem Pull Request das Label no-ai-review hinzu, und der Job wird 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 --comments

Eine Ausführung, die innerhalb weniger Sekunden mit nothing above the severity threshold; posting no comment im Log endet, funktioniert korrekt. Bei einem kleinen, unveränderten Pull Request ist das das erwartete Ergebnis.

Was kostet eine automatisierte Pull-Request-Prüfung?

Der Diff macht fast die gesamte Eingabe aus. Daher bestimmt die Größe des Diffs den Preis. Unten sehen Sie einen gemessenen Diff mit 500 Zeilen sowie den System-Prompt. Die Zählung erfolgte über den Token-Zählendpunkt und nicht über eine Schätzung.

ChartTokens for one 500 line pull request diff, measured August 2026
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 8,000 Eingabetoken bei Haiku 4.5 und 10,400 bei Sonnet 5. Der Text war identisch, die Zählung jedoch unterschiedlich. Claude-Modelle ab 4.7 verwenden einen neueren Tokenizer, der für dieselbe Eingabe ungefähr 30% mehr Token erzeugt. Anthropic dokumentiert dies auf seiner Preisseite. Berücksichtigen Sie diesen Unterschied, wenn Sie ein neueres Modell allein anhand des Preises pro 1 Million Token mit einem älteren Modell vergleichen.

Listenpreise im August 2026: Haiku 4.5 kostet 1 $ pro 1 Million Eingabetoken und 5 $ pro 1 Million Ausgabetoken. Sonnet 5 kostet im Einführungszeitraum bis zum 31. August 2026 2 $ beziehungsweise 10 $, danach 3 $ beziehungsweise 15 $. Opus 5 kostet 5 $ beziehungsweise 25 $.

ChartCost of that one review at list prices, August 2026
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 200 Pull Requests pro Monat zusammenführt, zahlt ungefähr $2.80 bei Haiku 4.5, $7.28 bei Sonnet 5 oder $18.20 bei Opus 5. 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 die Pushes in kurzer Folge eingehen. Die Zahlen setzen außerdem voraus, dass die Pfadfilter funktionieren. Eine einzige ungefilterte Lock-Datei kann die Eingabe allein verdoppeln.

Prompt-Caching hilft hier 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 Mindestlänge für das Caching. Die allgemeine Regel finden Sie unter wann sich Prompt-Caching rechnet. Informationen zur Auswahl zwischen den drei oben genannten Modellen finden Sie unter welches Claude-Modell Sie für welche Aufgabe verwenden.

Messen Sie Ihre eigenen Diffs, bevor Sie die Funktion aktivieren

Fügen Sie diese Zeile ein, nachdem payload erstellt wurde. Führen Sie das Skript 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ählendpunkt führt das Modell nicht aus. Er verbraucht daher keine Eingabe- oder Ausgabetoken. Außerdem verwendet er den Tokenizer des 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 verfälscht.

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 auf einmal prüfen. Ein Bot, der vierzig Kommentare hinterlässt, wird mit hoher Wahrscheinlichkeit gar nicht gelesen. Die Schweregrenze und die Begrenzung auf zehn Kommentare sind keine Höflichkeitsmaßnahmen. Sie sorgen dafür, dass die tatsächlichen Findings sichtbar bleiben. Wenn vor dem Kürzen nach Schweregrad sortiert wird, entfernt die Begrenzung die am wenigsten wichtigen Findings statt beliebiger zehn.

Mit Sicherheit über etwas kommentieren, das der Bot nicht prüfen kann. Das ist der Grund, aus dem Entwickler solche Bots dauerhaft deaktivieren. Einem Modell werden 200 Zeilen aus einer Codebasis mit 40,000 Zeilen gezeigt. Trotzdem schreibt es möglicherweise: „Das bricht die Cache-Invalidierung in redis_client.py“, obwohl es die betreffende Datei nie gesehen hat. Der System-Prompt wirkt dem in klarer Sprache entgegen: Melden Sie nur Fehler, die in den gezeigten Zeilen erkennbar sind, und lassen Sie alles weg, bei dem Sie nicht sicher sind. Es funktioniert besser, den Fehler direkt zu benennen, als allgemein um Genauigkeit zu bitten. Außerdem verhindert der Hinweis, dass ein leeres Ergebnis normal ist, dass das Modell zu einer Änderung von zwei Zeilen etwas erfindet.

Veröffentlichen Sie das Review als COMMENT, niemals als REQUEST_CHANGES. Die Einschätzung eines Modells darf einen Merge nicht blockieren. Sobald sie das kann, wird jemand unter Zeitdruck den gesamten Workflow entfernen, statt mit dem Bot darüber zu diskutieren.

Fehlerbilder und die dabei ausgegebenen Zeichenfolgen

HTTP 422 beim Absenden des Reviews. gh gibt gh: Unprocessable Entity (HTTP 422) aus, und der Antworttext nennt das Feld: 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, das weiterhin das Präfix b/ enthält, oder ein Kommentar zu einer entfernten Zeile. In diesem Fall muss side auf LEFT statt auf RIGHT gesetzt werden. Geben Sie das Review-JSON vor dem Absenden aus und prüfen Sie einen Kommentar manuell anhand des Diffs.

invalid x-api-key von der Modell-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-Sperre 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 zum Block permissions: hinzu. Wenn es bereits vorhanden ist, öffnen Sie Settings, dann Actions und anschließend General. Dort kann eine Organisationsrichtlinie begrenzen, welche Berechtigungen ein Workflow-Token anfordern darf.

json.decoder.JSONDecodeError. Das Modell hat kein analysierbares JSON zurückgegeben. Die häufigste Ursache ist eine Antwort, die das Token-Limit erreicht hat und mitten in einem Objekt abgebrochen wurde. Die Logzeile gibt dafü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 danach die Fork-Sperre und die Label-Sperre. Kontrollieren Sie anschließend mit sudo systemctl status 'actions.runner.*' auf dem VPS, ob der Runner aktiv ist. Ein nicht erreichbarer Runner lässt den Job ohne Fehlermeldung im Pull Request in der Warteschlange.

Jedes Review bleibt leer. Setzen Sie MIN_SEVERITY für einen Durchlauf auf low. Wenn Befunde erscheinen, arbeitet der Schwellenwert wie vorgesehen. Wenn nichts erscheint, geben Sie payload aus und prüfen Sie, ob die Filter nicht den gesamten Diff entfernt haben.

Gemeinsamer Betrieb mit anderen Agents

Der Reviewer ist klein. Daher liegt es nahe, ihn auf dem Server zu installieren, der bereits alle anderen Dienste ausführt. Wenn das Repository wichtig ist, sollten Sie ihn getrennt betreiben. Dieser Prozess enthält ein Token, mit dem er Ihren Code kommentieren kann, sowie einen Schlüssel, der Ausgaben verursacht. Ein selbst gehosteter Runner ist außerdem grundsätzlich ein Ort, an dem Workflow-Code ausgeführt wird. Ein eigener Benutzer ohne privilegierte Rechte und ohne sudo-Berechtigungen auf einem Host, der keine anderen Dienste ausführt, bildet die Mindestanforderung. Wenn Sie zusätzlich interaktive Agents betreiben, die Code auschecken, ist eine kurzlebige VM pro Agent das belastbare Muster. Einen Coding-Agent auf einem VPS betreiben behandelt die allgemeine Einrichtung. Wenn die Anthropic API für Sie neu ist, bietet eine erste Claude-API-Anwendung auf einem VPS einen kleineren Einstieg als dieses Setup.

FAQ

Benötigt ein KI-Agent für PR-Reviews Schreibzugriff auf mein Repository?

Nein. Für das Veröffentlichen eines Reviews benötigt er pull-requests: write und zum Abrufen des Diffs contents: read. Das ist die vollständige Liste. Sie legen sie im Block permissions: des Workflows fest. Dieser begrenzt, was der auf den jeweiligen Job bezogene 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. Verwenden Sie zum Veröffentlichen von Reviews event: COMMENT statt 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. Außerdem muss die Zeilennummer 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, erfindet das Modell von vornherein keine Zeilennummern.

Kann ich dies mit einem öffentlichen Repository und Pull Requests aus Forks verwenden?

Nicht mit diesem Design. GitHub übergibt keine Secrets an einen Workflow, der von einem Fork ausgelöst wird. Daher fehlt der Model-Key und der Lauf schlägt fehl. GitHub weist außerdem darauf hin, dass selbstgehostete Runner „fast nie für öffentliche Repositories verwendet werden sollten“, weil jeder einen Pull Request öffnen kann, der Code auf Ihrer Maschine ausführt. 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. Alternativ verlagern Sie den Review-Schritt auf einen von GitHub gehosteten Runner und akzeptieren, 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 Niveau, über das niemand diskutiert. Wechseln Sie zu Sonnet 5, wenn das Modell in Ihrer Sprache oder Ihrem Framework tatsächliche 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 leichter rechtfertigen als für jeden Push in jeden Feature-Branch.