SSD Nodes Learn 🎉 VPS od $5.50/mies.
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-08-13

Własny agent AI do przeglądu PR na serwerze VPS

Dowiedz się, jak samodzielnie wdrożyć agenta AI do analizy pull requestów na własnym serwerze VPS. Zapewnij prywatność kodu, kontrolę nad danymi i zredukuj koszty operacyjne.

Działanie własnego agenta do przeglądu PR

Własny agent do przeglądu PR to niewielki program uruchomiony na posiadanym serwerze. Odczytuje on różnice (diff) w pull request (PR) i przesyła do modelu wyłącznie zmienione linie kodu. Otrzymana odpowiedź jest publikowana w formie komentarzy wewnątrz kodu. Agent nigdy nie pobiera gałęzi (branch) i nie odczytuje plików, których pull request nie dotyczy. Jedyne posiadane przez niego dane uwierzytelniające to klucz API modelu oraz token, który posiada uprawnienia wyłącznie do dodawania komentarzy.

Model potrafi odczytać diff. Ten problem jest rozwiązany. Kluczowe znaczenie ma to, dokąd trafia diff i kto przechowuje klucz. Korzystanie z hostowanego bota do przeglądu oznacza, że diffy z każdego prywatnego repozytorium opuszczają sieć, trafiają do logów strony trzeciej i podlegają jej polityce retencji danych. Na posiadanym VPS (virtual private server) diff przesyłany jest z GitHub bezpośrednio na własną maszynę, a następnie do API modelu. Użytkownik może samodzielnie zweryfikować czterdzieści linii kodu, które decydują o tym, jakie dane są wysyłane.

Wymagania wstępne

  • Serwer VPS z systemem Ubuntu 24.04 oraz własnym runnerem GitHub Actions zarejestrowanym w repozytorium. Podczas rejestracji należy nadać mu dodatkową etykietę pr-review, ponieważ poniższy workflow dokonuje wyboru na podstawie tego znacznika.
  • Klucz API Anthropic uzyskany w konsoli Claude.
  • Repozytorium, w którym użytkownik posiada uprawnienia do kontroli nad tym, kto może otwierać pull requesty. Repozytorium prywatne jest najprostszym przypadkiem. Sekcja dotycząca forków poniżej opisuje przypadek publiczny, gdzie rozwiązanie jest mniej wygodne.

Instalacja reviewer na serwerze VPS

Usługa runner działa na koncie z ograniczonymi uprawnieniami, utworzonym podczas wykonywania ./svc.sh install. Zainstaluj reviewer na tym samym koncie, aby zadanie mogło go uruchamiać bez użycia sudo. Zastąp runner poniżej nazwą swojego konta.

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 wyświetla gh version 2.45.0 w systemie Ubuntu 24.04 według stanu na sierpień 2026. Każde wydanie od 2.20 wzwyż obsługuje flagę --input używaną poniżej. Komunikat Command 'gh' not found oznacza, że komponent universe nie jest włączony, więc uruchom sudo add-apt-repository universe i spróbuj ponownie.

Lokalizacja klucza i tokena

Dwa sekrety, dwa różne cykle życia. Żaden z nich nie powinien trafić do repozytorium.

ANTHROPIC_API_KEY to sekret repozytorium, ustawiany w sekcji Settings, następnie Secrets and variables, a potem Actions. GitHub szyfruje go i wstrzykuje do środowiska kroku w czasie wykonywania. Nie jest on plikiem na dysku ani nie pojawia się w historii git.

GITHUB_TOKEN działa inaczej. Actions generuje świeży token dla każdego zadania i niszczy go po zakończeniu pracy. Uprawnienia tego tokena są definiowane przez blok permissions: w pliku workflow, dlatego to właśnie tutaj realizowana jest zasada najmniejszych uprawnień:

permissions:
  contents: read
  pull-requests: write

Ten token może opublikować recenzję. Nie może wypchnąć commita, scalić gałęzi, edytować pliku workflow ani modyfikować innego repozytorium. Agent, który może komentować, jest recenzentem. Agent, który może wypychać zmiany, jest osobą zatwierdzającą kod, a na to nikt nie wyraził zgody. Traktuj klucz modelu z taką samą ostrożnością, ponieważ generuje on koszty na Twoim koncie. Więcej informacji na temat tego typu problemów znajduje się w ochrona sekretów przed dostępem agenta AI.

Actions zastępuje dokładny ciąg znaków sekretu wartością *** w dziennikach zadań. Mechanizm ten dopasowuje tylko dokładny ciąg, więc klucz zakodowany w base64, podzielony na dwie linie lub wypisywany znak po znaku pojawi się w dzienniku w postaci jawnej. Nie dodawaj kroku debugowania, który zrzuca zawartość środowiska.

Dlaczego pull request z forka nie widzi klucza API

Zasada GitHub jest krótka: z wyjątkiem GITHUB_TOKEN, sekrety nie są przekazywane do runnera, gdy workflow jest wyzwalany z forkowanego repozytorium. Zatem pull_request uruchomiony z forka rozpoczyna skrypt bez ANTHROPIC_API_KEY, a pierwsze wywołanie API kończy się błędem invalid x-api-key.

Kuszącym rozwiązaniem jest zmiana wyzwalacza na pull_request_target, który działa w kontekście repozytorium bazowego i otrzymuje sekrety. Nie należy tego robić. Własne wytyczne bezpieczeństwa GitHub wskazują, że takie workflow „są uprzywilejowane, co oznacza, że współdzielą tę samą pamięć podręczną głównej gałęzi z innymi uprzywilejowanymi wyzwalaczami workflow, mogą mieć dostęp do zapisu w repozytorium oraz dostęp do przywołanych sekretów”, a wynik „może zostać wykorzystany do przejęcia repozytorium”.

Te same wytyczne jasno wypowiadają się na temat runnera: „Self-hosted runners prawie nigdy nie powinny być używane w publicznych repozytoriach na GitHub, ponieważ każdy użytkownik może otworzyć pull request do repozytorium i skompromitować środowisko”.

To determinuje dwa wybory projektowe. Zadanie posiada zabezpieczenie, dzięki któremu uruchamia się tylko na gałęziach wypchniętych do własnego repozytorium. Workflow nie posiada również żadnego kroku actions/checkout. Agent nigdy nie posiada gałęzi na dysku, więc wrogi pull request jest tylko tekstem wysyłanym do modelu. Nie może on uruchomić skryptu budowania na VPS, ponieważ nic na VPS go nie uruchamia. Tekst nie jest jednak nieszkodliwy: diff napisany przez nieznajomą osobę to niezaufane dane wejściowe docierające do modelu; jest to ta sama granica zaufania, którą napotykasz, gdy pozwalasz agentowi na wyszukiwanie w sieci, a jedyną rzeczą, która go tutaj ogranicza, jest fakt, że ten agent nie może zrobić nic poza opublikowaniem komentarza.

Pobieranie diffa zamiast całego repozytorium

Jedno żądanie pozwala uzyskać cały diff w formacie zwykłego tekstu.

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"

Typ mediów Accept: application/vnd.github.diff zmienia odpowiedź z obiektu JSON opisującego pull request na sam ujednolicony diff, a gh api wypisuje treść odpowiedzi bez zmian. Pierwsza linia powinna zaczynać się od diff --git a/. Błąd gh: Not Found (HTTP 404) oznacza, że token nie ma uprawnień dostępu do repozytorium, co w przypadku tokenów typu fine-grained personal access token niemal zawsze oznacza brak nadanego uprawnienia Pull requests.

Filtrowanie przed zużyciem tokenów

Ta sekcja stanowi różnicę między botem, którego użytkownicy czytają, a takim, który jest wyciszany. Każdy z poniższych filtrów uruchamia się, zanim model otrzyma choćby jeden bajt danych.

  • Filtry ścieżek. Zablokuj pliki blokad (lock files), katalogi z zewnętrznymi bibliotekami (vendored directories), zminimalizowane pakiety oraz wygenerowany kod. Komentarz modelu dotyczący package-lock.json to czysty szum, a pliki te często stanowią większość bajtów w diffie.
  • Limit rozmiaru. Po przekroczeniu limitu pomiń przegląd i zakończ działanie z kodem sukcesu. Refaktoryzacja na 4000 linii otrzyma jeden rzetelny komunikat o tym, że zmiana była zbyt duża do automatycznej analizy, zamiast sześćdziesięciu nietrafionych sugestii.
  • Próg istotności i limit komentarzy. Raportuj znalezienia o wysokim i średnim priorytecie, maksymalnie dziesięć, zaczynając od tych o najwyższej istotności. Nikt nie czyta jedenastego komentarza.

Skrypt

Zapisz to jako /opt/pr-review/review.py. Skrypt odczytuje konfigurację ze zmiennych środowiskowych, co pozwala na zmianę modeli bez modyfikacji kodu.

#!/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

Podział różnic (diff) na poszczególne pliki umożliwia filtrowanie ścieżek. Numerowanie każdej linii pozwala na poprawne przypisanie komentarzy z recenzji. GitHub akceptuje komentarz wewnątrz pliku tylko wtedy, gdy dotyczy on linii obecnej w diffie, dlatego model musi wskazać rzeczywisty numer linii. Przekazanie numerów pozwala modelowi na skopiowanie poprawnej wartości zamiast jej generowania.

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)

Nagłówek hunka zawiera numerację. @@ -12,7 +12,9 @@ oznacza, że hunk nowego pliku zaczyna się od linii 12, więc licznik startuje od tej wartości i zwiększa się tylko dla linii dodanych oraz niezmienionych. Linie usunięte pozostają nienumerowane, ponieważ nie istnieją w nowym pliku. Warunek dla linii zaczynających się od backslasha pomija znacznik no-newline, który git zapisuje na końcu pliku; w przeciwnym razie znacznik ten przesuwałby każdy kolejny numer o jeden.

Oba wyjścia używają statusu 0, a nie 1. Przefiltrowany lub zbyt duży pull request powinien wyświetlać zielony znacznik. Czerwony znacznik, na który człowiek nie może zareagować, jest ignorowany, a po zignorowaniu jednego sprawdzenia, ignorowane są wszystkie kolejne.

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,
)

Trzy szczegóły w tym bloku mają kluczowe znaczenie. JSON jest wycinany pomiędzy pierwszym { a ostatnim }, ponieważ model czasami otacza odpowiedź blokiem kodu, a json.loads nie radzi sobie z takimi znacznikami. Z path usuwany jest początkowy b/, ponieważ ten prefiks pochodzi z nagłówka diffa, a GitHub wymaga ścieżki relatywnej względem repozytorium. Z kolei --input - wysyła całą recenzję jako jedno wywołanie API, dzięki czemu dziesięć uwag dociera jako jedno powiadomienie, a nie dziesięć osobnych.

Jeśli nie ma nic do zgłoszenia, skrypt nie publikuje żadnej wiadomości. Bot, który w każdym pull requeście pisze "no issues found", uczy użytkowników ignorowania komunikatów, co prowadzi do przeoczenia istotnych uwag.

Włączenie do przepływu pracy

Zapisz to jako .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 nie znajduje się w tym bloku env:, ponieważ Actions ustawia tę zmienną dla każdego zadania automatycznie. Grupa concurrency ma znaczenie dla rozliczeń: bez niej wypchnięcie trzech szybkich poprawek do gałęzi uruchamia trzy pełne przeglądy, za które trzeba zapłacić, natomiast z nią przetrwa tylko ostatni z nich.

Linia if: wykonuje dwa zadania. Pierwsza część pomija pull requesty z forków, które i tak zakończyłyby się niepowodzeniem z powodu braku klucza. Druga część zapewnia zespołowi wyłącznik: dodanie etykiety no-ai-review do pull requesta powoduje, że zadanie nie zostanie uruchomione.

Otwórz pull request i obserwuj działanie:

gh run list --workflow=pr-review.yml --limit 3
gh run view --log
gh pr view 42 --comments

Uruchomienie, które kończy się w kilka sekund z wpisem nothing above the severity threshold; posting no comment w dzienniku, oznacza poprawne działanie. W przypadku małego, czystego pull requesta jest to oczekiwany rezultat.

Jaki jest koszt automatycznego przeglądu pull requesta?

Różnica (diff) stanowi niemal całą treść wejściową, więc to jej rozmiar determinuje cenę. Poniżej przedstawiono pomiar dla diffa o długości 500 linii wraz z promptem systemowym, obliczony za pomocą endpointu do zliczania tokenów, a nie oszacowany.

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"
  }
]

Ten diff wygenerował 8,000 tokenów wejściowych w modelu Haiku 4.5 oraz 10,400 w Sonnet 5. Ten sam tekst, różne wyniki. Modele Claude od wersji 4.7 wzwyż używają nowszego tokenizatora, który generuje około 30% więcej tokenów dla tego samego wejścia, co Anthropic dokumentuje na swojej stronie z cennikiem. Należy uwzględnić ten fakt przy porównywaniu nowszego modelu ze starszym wyłącznie pod kątem ceny za milion tokenów.

Ceny katalogowe na sierpień 2026: Haiku 4.5 kosztuje 1 USD za milion tokenów wejściowych i 5 USD za milion wyjściowych. Sonnet 5 kosztuje 2 USD i 10 USD w ramach ceny promocyjnej obowiązującej do 31 sierpnia 2026, a następnie 3 USD i 15 USD. Opus 5 kosztuje 5 USD i 25 USD.

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"
  }
]

Daje to 1.4 centa za pull request w modelu Haiku 4.5 oraz 9.1 centa w Opus 5. Zespół scalający 200 pull requestów miesięcznie zapłaci około $2.80 w Haiku 4.5, $7.28 w Sonnet 5 lub $18.20 w Opus 5. Od 1 września 2026 należy pomnożyć wartości dla Sonnet 5 przez 1.5.

Dwie kwestie mogą podnieść rzeczywisty rachunek powyżej tego szacunku. synchronize uruchamia przeglądy przy każdym pushu, więc aktywna gałąź z ośmioma pushami kosztuje tyle, co osiem przeglądów, a zasada współbieżności pomaga tylko wtedy, gdy pushe następują po sobie w krótkim odstępie czasu. Powyższe liczby zakładają również, że filtry ścieżek działają poprawnie: jeden niefiltrowany plik blokady (lock file) może samodzielnie podwoić ilość danych wejściowych.

Prompt caching nie przynosi tutaj korzyści. Buforowany prefiks musi być identyczny bajt po bajcie między wywołaniami, a diff jest za każdym razem inny. Prompt systemowy jest jedyną stałą częścią, a jego długość jest znacznie poniżej minimalnego progu dla cache'owania. Ogólne zasady opisano w kiedy prompt caching się opłaca, a wybór między trzema powyższymi modelami w który model Claude wybrać do konkretnego zadania.

Zmierz własne diffy przed uruchomieniem

Dodaj tę linię po zbudowaniu payload, a następnie uruchom skrypt ręcznie dla kilku pull requestów z zeszłego miesiąca:

print(client.messages.count_tokens(
    model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)

Endpoint do zliczania nie uruchamia modelu, więc nie zużywa tokenów wejściowych ani wyjściowych i korzysta z tokenizatora przypisanego do wskazanego modelu. Uruchom go dla dziesięciu rzeczywistych pull requestów z własnego repozytorium i wyciągnij medianę zamiast średniej, aby jedna ogromna migracja nie zniekształciła szacunków.

Dlaczego boty recenzujące są wyciszane i jak tego unikać

Dwa zachowania niszczą zaufanie do tych botów, a oba można naprawić za pomocą powyższego kodu.

Recenzowanie wszystkiego naraz. Bot, który zostawia czterdzieści komentarzy, nie doczeka się przeczytania żadnego z nich. Próg istotności i limit dziesięciu komentarzy nie wynikają z uprzejmości, lecz z konieczności zachowania widoczności rzeczywistych usterek. Sortowanie według istotności przed ucięciem listy sprawia, że limit usuwa najmniej ważne znaleziska, a nie losowe dziesięć.

Komentowanie z pewnością siebie spraw, których bot nie może zweryfikować. To zachowanie sprawia, że inżynierowie trwale wyłączają takie narzędzia. Model, któremu pokazano 200 linii z 40 000 linii kodu, nadal napisze: „to psuje unieważnianie pamięci podręcznej w redis_client.py” w odniesieniu do pliku, którego nigdy nie widział. Prompt systemowy ogranicza to zachowanie w prosty sposób: zgłaszaj tylko usterki widoczne w pokazanych liniach i pomijaj wszystko, co do czego nie masz pewności. Bezpośrednie wskazanie błędu działa lepiej niż ogólne żądanie dokładności, a poinformowanie modelu, że brak wyniku jest stanem normalnym, powstrzymuje go przed wymyślaniem treści dla zmian obejmujących dwie linie.

Publikuj recenzję jako COMMENT, nigdy jako REQUEST_CHANGES. Opinia modelu nie powinna mieć możliwości zablokowania scalenia (merge). W momencie, gdy zyska taką możliwość, osoba pracująca pod presją czasu usunie cały workflow, zamiast z nim dyskutować.

Tryby awarii i towarzyszące im komunikaty

Błąd HTTP 422 podczas wysyłania recenzji. gh wypisuje gh: Unprocessable Entity (HTTP 422), a treść odpowiedzi wskazuje konkretne pole: Pull request review thread line must be part of the diff. GitHub nie może przypiąć komentarza. Typowe przyczyny to numer linii wymyślony przez model, path zawierający nadal prefiks b/ lub komentarz w usuniętej linii, co wymaga ustawienia side na LEFT zamiast RIGHT. Przed wysłaniem należy wyświetlić JSON recenzji i ręcznie zweryfikować jeden komentarz względem diffa.

invalid x-api-key z API modelu. Krok kończy się niepowodzeniem przy pierwszym wywołaniu messages.create. Przyczyną jest brak ustawienia sekretu ANTHROPIC_API_KEY w repozytorium lub fakt, że pull request pochodzi z forka, przez co Actions nie przekazało żadnych sekretów. Mechanizm zabezpieczający przed forkami w linii if: powinien pominąć to wywołanie, więc należy sprawdzić tę linię w pierwszej kolejności.

gh: Resource not accessible by integration (HTTP 403). Token zadania nie ma uprawnień do zapisu w pull requestach. Należy dodać pull-requests: write do bloku permissions:. Jeśli uprawnienie jest już nadane, należy sprawdzić w Settings, następnie Actions i General, czy polityka organizacji nie ogranicza uprawnień tokenów workflow.

json.decoder.JSONDecodeError. Model nie zwrócił parsowalnego formatu JSON. Częstą przyczyną jest odpowiedź, która osiągnęła limit tokenów i została ucięta w połowie obiektu. Wiersz logu wypisuje stop_reason w takiej sytuacji: wartość max_tokens oznacza konieczność zwiększenia max_tokens lub zmniejszenia MAX_COMMENTS.

Workflow nie uruchamia się. gh run list nie wykazuje żadnej aktywności dla pull requesta. Należy sprawdzić, czy paths-ignore nie odfiltrowało wszystkich zmienionych plików, następnie zweryfikować zabezpieczenia przed forkami i etykietami, a na końcu sprawdzić stan runnera za pomocą sudo systemctl status 'actions.runner.*' na VPS. Offline runner powoduje, że zadanie pozostaje w kolejce bez żadnego komunikatu o błędzie w pull requeście.

Każda recenzja wraca pusta. Należy ustawić MIN_SEVERITY na low dla jednego uruchomienia. Jeśli wyniki się pojawią, próg działa poprawnie. Jeśli nadal nic nie widać, należy wypisać payload i potwierdzić, czy filtry nie usunęły całego diffa.

Uruchamianie obok innych agentów

Reviewer jest niewielki, więc kuszące jest umieszczenie go na serwerze, na którym działa już wszystko inne. Należy jednak odizolować go, jeśli bezpieczeństwo repozytorium jest istotne. Proces ten przechowuje token umożliwiający komentowanie kodu oraz klucz pozwalający na wydawanie środków, a self-hosted runner z założenia jest środowiskiem, w którym wykonywany jest kod workflow. Dedykowane konto bez uprawnień bez dostępu do sudo, na hoście nieobsługującym innych zadań, stanowi standardowe minimum. Jeśli uruchamiane są również interaktywne agenty pobierające kod, wzorcem zapewniającym bezpieczeństwo jest jednorazowa maszyna wirtualna dla każdego agenta, a uruchomienie agenta programistycznego na VPS opisuje ogólną konfigurację. Jeśli API Anthropic jest nowością, pierwsza aplikacja Claude API na VPS będzie lepszym punktem wyjścia niż niniejszy przewodnik.

FAQ

Czy agent AI do przeglądu PR wymaga uprawnień do zapisu w repozytorium?

Nie. Wymaga jedynie pull-requests: write do opublikowania recenzji oraz contents: read do pobrania diffa. To pełna lista uprawnień, które definiuje się w bloku permissions: w pliku workflow, co ogranicza zakres działań przypisanych do GITHUB_TOKEN dla danego zadania. Dzięki tym dwóm uprawnieniom agent może komentować pull request, ale nie może wypchnąć commita ani scalić brancha. Publikuj recenzje za pomocą event: COMMENT zamiast REQUEST_CHANGES, aby agent nie mógł również zablokować scalenia.

Dlaczego mój komentarz do recenzji kończy się błędem HTTP 422?

GitHub akceptuje komentarz wewnątrz wiersza tylko wtedy, gdy dotyczy on linii będącej częścią diffa pull requesta; w przeciwnym razie zwraca Pull request review thread line must be part of the diff. Sprawdź, czy path jest ścieżką relatywną względem repozytorium, bez prefiksu b/ z nagłówka diffa, oraz czy numer linii znajduje się wewnątrz bloku (hunk) tego pliku. side musi mieć wartość RIGHT dla linii dodanej lub niezmienionej oraz LEFT dla linii usuniętej. Poprzedzenie każdej linii diffa jej numerem z nowego pliku przed wysłaniem do modelu zapobiega generowaniu przez model nieistniejących numerów linii.

Czy mogę uruchomić to w publicznym repozytorium z pull requestami z forków?

Nie przy tej architekturze. GitHub nie przekazuje sekretów do workflow wyzwalanego z forka, więc klucz modelu jest niedostępny i uruchomienie kończy się niepowodzeniem. GitHub zaznacza również, że self-hosted runners „prawie nigdy nie powinny być używane w publicznych repozytoriach”, ponieważ każdy może otworzyć pull request, który spowoduje wykonanie kodu na Twojej maszynie. W przypadku projektu publicznego należy albo ograniczyć recenzenta do branchy wypchniętych bezpośrednio do repozytorium, co zapewnia zabezpieczenie if:, albo przenieść krok recenzji na GitHub-hosted runner i zaakceptować fakt, że diff opuszcza Twoją infrastrukturę.

Którego modelu użyć do przeglądu pull requesta?

Zacznij od Haiku 4.5. Analiza ograniczonego diffa pod kątem ustalonej listy typów defektów nie jest trudnym zadaniem logicznym, a najtańszy model pozwala utrzymać miesięczne koszty na poziomie, który nie budzi zastrzeżeń. Przejdź na Sonnet 5, jeśli zauważysz, że model pomija rzeczywiste błędy w Twoim języku lub frameworku – mierz to, zamiast zakładać z góry. Opus 5 jest najdroższym z tej trójki w przeliczeniu na pull request, co łatwiej uzasadnić w przypadku brancha wydawniczego niż przy każdym pushu do każdego brancha z funkcjonalnością.