SSD Nodes Learn 🎉 VPS dès $4.99/mois
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-13

Auto-héberger un agent IA de revue de PR sur un VPS

Déployez un reviewer IA de pull requests sur votre VPS avec un runner auto-hébergé, des prompts limités au diff, des filtres et des commentaires inline.

Ce que fait un agent de revue des pull requests auto-hébergé

Un agent de revue des pull requests auto-hébergé est un petit programme installé sur un serveur que vous contrôlez. Il lit le diff d’une pull request (PR) et envoie uniquement les lignes modifiées à un modèle. La réponse est publiée sous forme de commentaires de revue inline. L’agent ne checkout jamais votre branche et ne lit jamais un fichier que la pull request n’a pas modifié. Il ne détient que deux identifiants : une clé d’API (application programming interface) du modèle et un token qui peut ajouter des commentaires, sans aucun autre droit.

Un modèle sait lire un diff. Cette partie est résolue. Le point important est de savoir où le diff est envoyé et qui détient la clé. Avec un bot de revue hébergé, chaque diff de chaque dépôt privé quitte votre réseau, arrive dans les journaux d’un tiers et est conservé selon sa politique de rétention. Sur un VPS (virtual private server) que vous contrôlez, le diff va de GitHub vers votre serveur, puis vers l’API du modèle. Vous pouvez lire les quarante lignes de code qui déterminent ce qui est envoyé.

Ce dont vous avez besoin avant de commencer

  • Un VPS sous Ubuntu 24.04 avec un runner GitHub Actions auto-hébergé déjà enregistré auprès du dépôt. Ajoutez-lui le label supplémentaire pr-review lors de son enregistrement, car le workflow ci-dessous sélectionne ce label.
  • Une clé API Anthropic obtenue depuis la Claude Console.
  • Un dépôt dans lequel vous contrôlez les personnes autorisées à ouvrir une pull request. Un dépôt privé est le cas le plus simple. La section ci-dessous consacrée aux forks couvre le cas public, mais la réponse y est moins satisfaisante.

Installer le reviewer sur le VPS

Le service runner s’exécute avec le compte non privilégié que vous avez créé lors de l’exécution de ./svc.sh install. Installez le reviewer avec ce même compte afin que le job puisse l’exécuter sans sudo. Remplacez runner ci-dessous par le nom de votre compte.

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 affiche gh version 2.45.0 sur Ubuntu 24.04 en août 2026. Toutes les versions à partir de 2.20 prennent en charge l’option --input utilisée ci-dessous. Un message Command 'gh' not found signifie que le composant universe n’est pas activé. Exécutez sudo add-apt-repository universe, puis réessayez.

Où se trouvent la clé et le token

Deux secrets, deux durées de vie différentes. Aucun des deux ne doit se trouver dans le dépôt.

ANTHROPIC_API_KEY est un secret du dépôt. Vous le définissez sous Settings, puis Secrets and variables, puis Actions. GitHub le chiffre et l’injecte dans l’environnement de l’étape au moment de l’exécution. Ce n’est jamais un fichier sur le disque et il n’apparaît jamais dans l’historique Git.

GITHUB_TOKEN fonctionne différemment. Actions génère un nouveau token pour chaque job et le détruit à la fin du job. Les actions autorisées pour ce token sont définies par le bloc permissions: du workflow. C’est donc ici que s’applique réellement le principe du moindre privilège :

permissions:
  contents: read
  pull-requests: write

Ce token peut publier une review. Il ne peut pas pousser un commit, fusionner une branche, modifier un fichier de workflow ni accéder à un autre dépôt. Un agent qui peut commenter est un reviewer. Un agent qui peut pousser est un committer, et personne n’a accepté cela. Protégez la clé du modèle avec le même soin, car elle dépense de l’argent sur votre compte. Vous trouverez davantage d’informations sur ce type de problème dans garder les secrets hors de portée d’un agent IA.

Actions remplace la chaîne exacte du secret par *** dans les journaux des jobs. La correspondance porte uniquement sur la chaîne exacte. Une clé encodée en base64, répartie sur deux lignes ou affichée caractère par caractère apparaît donc en clair. N’ajoutez pas d’étape de debug qui affiche l’environnement.

Pourquoi une pull request issue d’un fork ne voit jamais votre clé API

La règle de GitHub est simple : à l’exception de GITHUB_TOKEN, les secrets ne sont pas transmis au runner lorsqu’un workflow est déclenché depuis un dépôt forké. Ainsi, une exécution pull_request depuis un fork lance votre script sans ANTHROPIC_API_KEY, et le premier appel d’API échoue avec invalid x-api-key.

La solution tentante consiste à remplacer le déclencheur par pull_request_target, qui s’exécute dans le contexte du dépôt de base et dispose bien des secrets. Ne faites pas cela ici. Les recommandations de sécurité de GitHub indiquent que ces workflows « sont privilégiés : ils partagent le même cache que la branche principale avec les autres déclencheurs de workflows privilégiés, et peuvent disposer d’un accès en écriture au dépôt ainsi que d’un accès aux secrets référencés », et que le résultat « peut être exploité pour prendre le contrôle d’un dépôt ».

Ces mêmes recommandations sont catégoriques au sujet du runner : « Les self-hosted runners ne devraient presque jamais être utilisés pour des dépôts publics sur GitHub, car n’importe quel utilisateur peut ouvrir des pull requests vers le dépôt et compromettre l’environnement. »

Cela impose deux choix de conception. Le job comporte une condition afin de ne s’exécuter que sur les branches poussées vers votre propre dépôt. Et le workflow ne contient aucune étape actions/checkout. L’agent n’a jamais la branche sur le disque : une pull request hostile n’est donc qu’un texte envoyé à un modèle. Elle ne peut pas exécuter de script de build sur votre VPS, car rien sur votre VPS ne l’exécute. Un texte n’est toutefois pas nécessairement inoffensif : un diff écrit par un inconnu est une entrée non fiable transmise à un modèle, avec la même limite de confiance que lorsque vous donnez à un agent accès à une recherche web, et la seule mesure de confinement ici est que cet agent ne peut que publier un commentaire.

Récupérez le diff, pas le dépôt

Une seule requête récupère l’intégralité du diff en texte brut.

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"

Le type de média Accept: application/vnd.github.diff transforme la réponse, qui décrit normalement la pull request dans un objet JSON, en diff unifié. gh api affiche ensuite ce corps sans le modifier. La première ligne affichée doit commencer par diff --git a/. Un gh: Not Found (HTTP 404) signifie que le token ne peut pas accéder au dépôt. Avec un personal token à granularité fine, cela signifie presque toujours que l’autorisation Pull requests n’a pas été activée.

Filtrez avant de consommer un token

Cette section fait la différence entre un bot que les utilisateurs lisent et un bot qu’ils mettent en sourdine. Chaque filtre ci-dessous s’exécute avant que le modèle ne voie le moindre octet.

  • Filtres de chemin. Excluez les fichiers de verrouillage, les répertoires de dépendances intégrées, les bundles minifiés et le code généré. Un commentaire du modèle sur package-lock.json n’apporte rien, et ces fichiers représentent souvent la majorité des octets d’un diff.
  • Une limite de taille. Au-delà de cette limite, ignorez la revue et quittez avec un code de succès. Pour un refactoring de 4,000 lignes, affichez une ligne indiquant honnêtement qu’il était trop volumineux pour être analysé automatiquement, plutôt que soixante suppositions.
  • Un seuil de gravité et une limite de commentaires. Signalez les problèmes de gravité élevée et moyenne, dans la limite de dix, en commençant par les plus graves. Personne ne lit le onzième commentaire.

Le script

Enregistrez-le sous /opt/pr-review/review.py. Il lit sa configuration depuis l’environnement. Le workflow peut donc changer de modèle sans modification du code.

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

Le découpage du diff par fichier permet d’appliquer le filtrage par chemin. La numérotation de chaque ligne permet de placer les commentaires de revue au bon endroit. GitHub n’accepte un commentaire inline que sur une ligne présente dans le diff. Le modèle doit donc citer un numéro de ligne réel. Lui fournir ces numéros lui permet d’en recopier un au lieu d’en inventer un.

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)

L’en-tête du hunk contient la numérotation. @@ -12,7 +12,9 @@ indique que le hunk du nouveau fichier commence à la ligne 12. Le compteur commence donc à cette ligne et avance uniquement pour les lignes ajoutées et inchangées. Les lignes supprimées restent sans numéro, car elles n’existent pas dans le nouveau fichier. La condition qui ignore les lignes commençant par une barre oblique inverse exclut le marqueur de fin de fichier sans saut de ligne écrit par git. Sans cette condition, tous les numéros suivants seraient décalés d’une unité.

Les deux sorties utilisent le code de retour 0, et non 1. Une pull request filtrée ou trop volumineuse doit afficher une vérification réussie. Une vérification en échec sur laquelle un humain ne peut pas agir est ignorée. Dès qu’une vérification est ignorée, elles le sont toutes.

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

Trois détails de ce bloc sont essentiels. Le JSON est extrait entre le premier { et le dernier }, car un modèle peut parfois entourer sa réponse d’un code fence, et json.loads ne gère pas ce fence. Le path est privé de son b/ initial, car ce préfixe provient de l’en-tête du diff et GitHub attend un chemin relatif au dépôt. Enfin, --input - envoie toute la revue en un seul appel d’API. Dix résultats arrivent ainsi dans une seule notification, au lieu de dix notifications.

Lorsqu’il n’y a rien à signaler, le script n’envoie rien. Un bot qui écrit « aucun problème trouvé » sur chaque pull request apprend aux utilisateurs à ne plus le lire. Ils risquent alors de ne pas voir celui qui était important.

Intégrez-le au workflow

Enregistrez ceci sous .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 n’apparaît pas dans ce bloc env:, car Actions le définit déjà pour chaque job. Le groupe concurrency est important pour la facturation : sans lui, pousser trois corrections rapides sur une branche lance trois revues complètes, et vous payez les trois. Avec lui, seule la dernière est conservée.

La ligne if: remplit deux fonctions. La première moitié ignore les pull requests provenant de forks, qui échoueraient de toute façon sans clé. La seconde fournit à votre équipe un mécanisme d’arrêt : ajoutez le label no-ai-review à une pull request pour empêcher l’exécution du job.

Ouvrez une pull request et observez le résultat :

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

Un run qui se termine en quelques secondes avec nothing above the severity threshold; posting no comment dans le log fonctionne correctement. Pour une pull request courte et propre, c’est le résultat attendu.

Quel est le coût d’une revue automatisée de pull request ?

Le diff constitue presque toute l’entrée. Sa taille détermine donc le prix. Voici un diff mesuré de 500 lignes, avec le system prompt, compté avec l’endpoint de comptage des tokens plutôt qu’estimé.

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

Ce diff représente 8,000 tokens d’entrée avec Haiku 4.5 et 10,400 avec Sonnet 5. Le texte est identique, mais le comptage diffère. Les modèles Claude à partir de 4.7 utilisent un tokenizer plus récent, qui produit environ 30 % de tokens supplémentaires pour la même entrée. Anthropic le documente sur sa page tarifaire. Tenez-en compte lorsque vous comparez le prix par million de tokens d’un modèle récent avec celui d’un modèle plus ancien.

Tarifs catalogue en août 2026 : Haiku 4.5 coûte 1 $ par million de tokens d’entrée et 5 $ par million de tokens de sortie. Sonnet 5 coûte 2 $ et 10 $ avec le tarif de lancement valable jusqu’au 31 août 2026, puis 3 $ et 15 $. Opus 5 coûte 5 $ et 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"
  }
]

Cela représente 1.4 centime par pull request avec Haiku 4.5 et 9.1 centimes avec Opus 5. Une équipe qui fusionne 200 pull requests par mois paie environ $2.80 avec Haiku 4.5, $7.28 avec Sonnet 5 ou $18.20 avec Opus 5. À partir du 1 septembre 2026, multipliez la ligne Sonnet 5 par 1.5.

Deux éléments augmentent la facture réelle par rapport à cette estimation. Le déclencheur synchronize lance une revue à chaque push. Une branche active avec huit pushes coûte donc huit revues. La règle de concurrence n’est utile que lorsque les pushes arrivent à peu près au même moment. Ces chiffres supposent également que les filtres de chemins fonctionnent. Un seul lock file non filtré peut à lui seul doubler la taille de l’entrée.

La mise en cache du prompt n’est pas utile ici. Le préfixe mis en cache doit être identique octet par octet entre les appels, alors que le diff change à chaque fois. Le system prompt est la seule partie stable, mais sa taille est très inférieure à la longueur minimale requise pour la mise en cache. Pour la règle générale, consultez dans quels cas la mise en cache du prompt est rentable, et pour choisir entre les trois modèles ci-dessus, consultez quel modèle Claude utiliser selon la tâche.

Mesurez vos propres diffs avant de l’activer

Ajoutez cette ligne après la construction de payload, puis exécutez le script manuellement sur quelques pull requests du mois dernier :

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

L’endpoint de comptage n’exécute pas le modèle. Il ne consomme donc aucun token d’entrée ou de sortie et utilise le tokenizer correspondant au modèle que vous indiquez. Exécutez-le sur dix pull requests réelles de votre propre dépôt et utilisez la médiane plutôt que la moyenne, afin qu’une migration très volumineuse ne fausse pas l’estimation.

Pourquoi les bots de review sont désactivés, et comment l’éviter

Deux comportements détruisent la confiance envers ces bots. Le code ci-dessus corrige les deux.

Tout reviewer en une seule fois. Un bot qui laisse quarante commentaires n’en voit aucun lu. Le seuil de sévérité et la limite de dix commentaires ne relèvent pas de la politesse. Ils permettent de garder les vrais problèmes visibles. Trier par sévérité avant de tronquer garantit que la limite élimine les problèmes les moins importants, et non dix problèmes pris au hasard.

Commenter avec certitude un élément qu’il ne peut pas vérifier. C’est ce qui pousse les ingénieurs à désactiver définitivement le bot. Un modèle auquel on présente 200 lignes d’une codebase de 40,000 lignes écrira quand même « ce changement casse l’invalidation du cache dans redis_client.py » à propos d’un fichier qu’il n’a jamais vu. Le system prompt est explicite : signaler uniquement les défauts visibles dans les lignes présentées et omettre tout ce qui n’est pas certain. Nommer directement le mode d’échec fonctionne mieux qu’une demande générale de précision. Indiquer au modèle qu’un résultat vide est normal l’empêche d’inventer un commentaire sur une modification de deux lignes.

Publiez la review en tant que COMMENT, jamais en tant que REQUEST_CHANGES. L’avis d’un modèle ne doit pas pouvoir bloquer un merge. Dès que c’est possible, une personne pressée supprimera tout le workflow plutôt que de devoir composer avec lui.

Modes d’échec et chaînes affichées

HTTP 422 lors de l’envoi de la review. gh affiche gh: Unprocessable Entity (HTTP 422) et le corps de la réponse indique le champ concerné : Pull request review thread line must be part of the diff. GitHub ne peut pas positionner ce commentaire. Les causes habituelles sont un numéro de ligne inventé par le modèle, un path qui contient encore le préfixe b/, ou un commentaire portant sur une ligne supprimée. Dans ce dernier cas, il faut définir side sur LEFT au lieu de RIGHT. Affichez le JSON de la review avant l’envoi et vérifiez manuellement un commentaire par rapport au diff.

invalid x-api-key depuis l’API du modèle. L’étape échoue lors du premier appel messages.create. Soit le secret ANTHROPIC_API_KEY n’est pas défini dans le dépôt, soit la pull request provient d’un fork et Actions n’a transmis aucun secret. La protection contre les forks dans la ligne if: aurait dû ignorer cette exécution. Vérifiez donc d’abord cette ligne.

gh: Resource not accessible by integration (HTTP 403). Le token du job ne peut pas écrire dans les pull requests. Ajoutez pull-requests: write au bloc permissions:. S’il est déjà présent, ouvrez Settings, puis Actions, puis General. Une policy de l’organisation peut limiter ce que chaque token de workflow est autorisé à demander.

json.decoder.JSONDecodeError. Le modèle n’a pas renvoyé de JSON analysable. La cause la plus fréquente est une réponse qui a atteint la limite de tokens et s’est arrêtée au milieu d’un objet. La ligne de log affiche stop_reason à cette fin : une valeur de max_tokens signifie qu’il faut augmenter max_tokens ou réduire MAX_COMMENTS.

Le workflow ne s’exécute jamais. gh run list n’affiche rien pour la pull request. Vérifiez que paths-ignore n’a pas exclu tous les fichiers modifiés, puis contrôlez la protection contre les forks et celle concernant le label. Vérifiez enfin que le runner est actif avec sudo systemctl status 'actions.runner.*' sur le VPS. Un runner hors ligne laisse le job en file d’attente, sans message d’erreur dans la pull request.

Toutes les reviews sont vides. Définissez MIN_SEVERITY sur low pour une exécution. Si des résultats apparaissent, le seuil fonctionne comme prévu. Si rien n’apparaît, affichez payload et vérifiez que les filtres n’ont pas supprimé l’intégralité du diff.

L’exécuter avec vos autres agents

Le reviewer est léger. Il peut donc être tentant de l’installer sur la machine qui exécute déjà tous les autres services. Séparez-le si le dépôt est sensible. Ce processus détient un token capable de commenter votre code et une clé qui peut dépenser votre argent. De plus, un runner auto-hébergé est par conception un environnement où le code des workflows s’exécute. Un compte dédié sans privilèges et sans droits sudo, sur un hôte qui n’exécute rien d’autre, constitue la base. Si vous exécutez aussi des agents interactifs qui récupèrent du code, une VM éphémère par agent est le modèle le plus fiable. Exécuter un agent de codage sur un VPS présente la configuration générale. Si l’API Anthropic est nouvelle pour vous, une première application Claude API sur un VPS constitue un meilleur point de départ que cette configuration.

FAQ

Un agent d’IA chargé de la review des PR a-t-il besoin d’un accès en écriture à mon dépôt ?

Non. Il lui faut pull-requests: write pour publier une review et contents: read pour récupérer le diff. C’est tout, et vous définissez ces permissions dans le bloc permissions: du workflow, qui limite ce que peut faire le GITHUB_TOKEN de chaque job. Avec ces deux lignes, l’agent peut commenter une pull request, mais ne peut ni pousser un commit ni fusionner une branche. Publiez les reviews avec event: COMMENT plutôt qu’avec REQUEST_CHANGES afin qu’il ne puisse pas non plus bloquer une fusion.

Pourquoi mon commentaire de review échoue-t-il avec HTTP 422 ?

GitHub n’accepte un commentaire de review inline que sur une ligne présente dans le diff de la pull request. Sinon, il renvoie Pull request review thread line must be part of the diff. Vérifiez que path est relatif au dépôt et ne contient pas le préfixe b/ provenant de l’en-tête du diff, et que le numéro de ligne se trouve dans un hunk de ce fichier. side doit être égal à RIGHT pour une ligne ajoutée ou inchangée, et à LEFT pour une ligne supprimée. Ajouter à chaque ligne du diff son numéro de ligne dans le nouveau fichier avant de transmettre le diff au modèle permet d’éviter que le modèle n’invente des numéros.

Puis-je exécuter cette configuration sur un dépôt public avec des pull requests provenant de forks ?

Pas avec cette conception. GitHub ne transmet pas les secrets à un workflow déclenché depuis un fork. La clé du modèle est donc absente et l’exécution échoue. GitHub précise également que les self-hosted runners « ne devraient presque jamais être utilisés pour des dépôts publics », car n’importe qui peut ouvrir une pull request qui déclenche l’exécution de code sur votre machine. Pour un projet public, limitez la review aux branches poussées vers le dépôt lui-même, ce que fait la condition if:, ou déplacez l’étape de review vers un runner GitHub-hosted et acceptez que le diff quitte votre propre infrastructure.

Quel modèle dois-je utiliser pour la review des pull requests ?

Commencez avec Haiku 4.5. Lire un diff limité et le comparer à une liste fixe de types de défauts ne constitue pas un problème de raisonnement difficile. Le modèle le moins cher maintient donc la facture mensuelle à un niveau raisonnable. Passez à Sonnet 5 si vous constatez qu’il ne détecte pas de vrais bugs dans votre langage ou votre framework, et mesurez ce résultat au lieu de le supposer. Opus 5 est de loin le plus cher des trois par pull request. Il est donc plus facile de justifier son coût sur une release branch que sur chaque push vers chaque feature branch.