Auto-héberger un agent IA de review de PR sur VPS
Installez un agent de review de PR sur votre VPS Ubuntu 24.04 : runner auto-hébergé, diff filtré, commentaires inline et coût réel par PR.
Ce que fait un agent d’analyse des PR auto-hébergé
Un agent de revue de PR auto-hébergé est un petit programme exécuté sur un serveur que vous administrez. 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. Il n’effectue jamais de checkout de votre branche et ne lit jamais un fichier que la pull request n’a pas modifié. Les seuls identifiants qu’il détient sont une clé d’API de modèle et un token autorisé à publier des commentaires, sans autre permission. Il s’agit d’une définition étroite d’un agent, plus proche d’un prompt précédé de filtres que d’un système autonome. Si vous voulez comprendre l’ensemble du fonctionnement avant de construire celui-ci, le parcours progressif, des concepts à une boucle que vous écrivez vous-même présente les bases nécessaires.
Un modèle sait lire un diff. Ce point est résolu. L’essentiel est de savoir où le diff est envoyé et qui détient la clé. Avec un bot de review hébergé, chaque diff de chaque dépôt privé quitte votre réseau, est enregistré dans les logs d’un tiers et reste soumis à sa politique de conservation. Sur un VPS 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 qu’il vous faut 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-reviewlors de son enregistrement, car le workflow ci-dessous utilise ce label pour le sélectionner. - Une clé d’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 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 sans privilèges que vous avez créé en exécutant ./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 --versiongh --version affiche gh version 2.45.0 sur Ubuntu 24.04 en août 2026. Toute version à partir de 2.20 prend 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 donc 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 de dépôt, défini 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. Il n’est jamais enregistré dans un fichier sur le disque ni dans l’historique Git.
GITHUB_TOKEN fonctionne autrement. Actions génère un token distinct pour chaque job et le détruit à la fin du job. Les opérations autorisées avec 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: writeCe 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 plus 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 un caractère à la fois apparaît donc en clair. N’ajoutez pas d’étape de debug qui affiche l’environnement.
Pourquoi une pull request provenant 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 provenant d’un fork lance votre script sans ANTHROPIC_API_KEY, et le premier appel d’API échoue avec invalid x-api-key.
La correction tentante consiste à remplacer le déclencheur par pull_request_target, qui s’exécute dans le contexte du dépôt de base et reçoit bien les secrets. Ne faites pas cela ici. Les consignes de sécurité de GitHub précisent que ces workflows « sont privilégiés, ce qui signifie qu’ils partagent le même cache que la branche principale avec les autres déclencheurs de workflows privilégiés, et qu’ils 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 ».
Les mêmes consignes sont très claires concernant le 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 pour 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 que du texte transmis à 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. Le texte n’est toutefois pas forcément 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 celle rencontrée lorsque vous donnez à un agent accès à une recherche web, et la seule mesure qui le contient ici est que cet agent ne peut rien faire d’autre que publier un commentaire.
Récupérer le diff, pas le dépôt
Une seule requête récupère l’intégralité du diff au format 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 MIME Accept: application/vnd.github.diff transforme la réponse, qui est normalement un objet JSON décrivant la pull request, en diff unifié. gh api affiche ensuite ce corps sans modification. La première ligne affichée doit commencer par diff --git a/. Un gh: Not Found (HTTP 404) signifie que le jeton ne peut pas accéder au dépôt. Avec un jeton personnel à granularité fine, cela signifie presque toujours que l’autorisation Pull requests n’a pas été activée.
Filtrez avant de dépenser un token
Cette section fait la différence entre un bot que les gens lisent et un bot qu’ils désactivent. 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 vendored, les bundles minifiés et le code généré. Un commentaire du modèle sur
package-lock.jsonest inutile, et ces fichiers représentent souvent la majeure partie des octets d’un diff. - Une limite de taille. Au-delà de cette limite, ignorez la revue et quittez avec un statut de réussite. Un refactoring de 4,000 lignes reçoit une seule phrase honnête indiquant qu’il est trop volumineux pour être revu 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 commentaire numéro onze.
Le script
Enregistrez-le sous /opt/pr-review/review.py. Il lit sa configuration dans l’environnement. Le workflow peut donc changer de modèle sans modifier le 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,
).stdoutLe découpage du diff par fichier permet le filtrage des chemins. La numérotation de chaque ligne permet de placer les commentaires de revue. GitHub n’accepte un commentaire inline que sur une ligne présente dans le diff. Le modèle doit donc citer un vrai numéro de ligne. 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 évite de compter le marqueur de fin de fichier sans nouvelle ligne écrit par git. Sans cette condition, tous les numéros suivants seraient décalés d’une unité.
Les deux sorties utilisent le statut 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 personne ne peut 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 indispensables. Le JSON est extrait entre la première occurrence de { et la dernière occurrence de }, car un modèle peut parfois entourer sa réponse d’un bloc de code. json.loads ne gère pas ce bloc. Le path est débarrassé d’un b/ initial, car ce préfixe vient 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 problèmes arrivent ainsi dans une seule notification au lieu de dix.
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 à ignorer son message. Ils risquent ensuite d’ignorer aussi celui qui signalait un problème réel.
Intégrez-le au workflow
Enregistrez ceci dans .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 ne figure 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, trois corrections rapides poussées sur une branche déclenchent 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 --commentsUne exécution qui se termine en quelques secondes avec nothing above the severity threshold; posting no comment dans le journal fonctionne correctement. C’est le résultat attendu pour une pull request courte et propre.
Quel est le coût d’une revue automatisée de pull request ?
Le diff constitue presque tout l’input, donc sa taille détermine le prix. Voici un diff mesuré de 500 lignes, avec le system prompt. Le comptage a été effectué avec l’endpoint de comptage des tokens, et non par estimation.
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ésentait 8,000 tokens d’input avec Haiku 4.5 et 10,400 avec Sonnet 5. Le texte était identique, mais le comptage différait. À partir de la version 4.7, les modèles Claude utilisent un tokenizer plus récent qui produit environ 30 % de tokens supplémentaires pour le même input. 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 publics en août 2026 : Haiku 4.5 coûte 1 $ par million de tokens d’input et 5 $ par million de tokens d’output. 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 $.
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 centimes 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 le montant de la ligne Sonnet 5 par 1.5.
Deux éléments peuvent augmenter la facture réelle par rapport à cette estimation. Le déclencheur synchronize lance une revue à chaque push. Une branche active qui reçoit huit push coûte donc huit revues. La règle de concurrence n’est utile que lorsque plusieurs push arrivent presque en même temps. Les chiffres supposent également que les filtres de chemins fonctionnent. Un seul lock file non filtré peut à lui seul doubler la quantité d’input.
Le prompt caching 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 il est bien plus court que la longueur minimale requise pour le cache. Pour la règle générale, consultez dans quels cas le prompt caching devient rentable, et pour choisir entre les trois modèles ci-dessus, 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’input ou d’output et utilise le tokenizer correspondant au modèle indiqué. Exécutez-le sur dix pull requests réelles de votre dépôt et utilisez la médiane plutôt que la moyenne. Une migration volumineuse ne faussera ainsi pas l’estimation.
Pourquoi les bots de revue sont désactivés et comment l’éviter
Deux comportements détruisent la confiance dans ces bots. Le code ci-dessus corrige les deux.
Tout examiner en une seule fois. Un bot qui laisse quarante commentaires n’en voit aucun lu. Le seuil de gravité et la limite de dix commentaires ne relèvent pas de la politesse. Ils permettent de garder les vrais problèmes visibles. Trier par gravité avant de tronquer la liste permet de supprimer les problèmes les moins importants plutôt qu’une sélection aléatoire de dix problèmes.
Commenter avec certitude sur 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 montre 200 lignes d’une base de code de 40,000 lignes écrira tout de même « cela casse l’invalidation du cache dans redis_client.py » à propos d’un fichier qu’il n’a jamais vu. Le prompt système est explicite : signaler uniquement les défauts visibles dans les lignes affichées et omettre tout ce qui n’est pas certain. Nommer directement le risque d’erreur fonctionne mieux que de demander une précision générale. Indiquer au modèle qu’un résultat vide est normal l’empêche d’inventer un problème à signaler pour une modification de deux lignes. Les défauts qui apparaissent uniquement à l’échelle du dépôt complet relèvent d’une autre tâche. Auto-héberger open-kritt pour l’analyse de sécurité est une façon de couvrir ce périmètre sans élargir ce que ce reviewer est autorisé à voir.
Publiez la revue en tant que COMMENT, jamais en tant que REQUEST_CHANGES. L’avis d’un modèle ne doit pas pouvoir bloquer une fusion. Dès que c’est possible, une personne soumise à une échéance supprimera tout le workflow plutôt que de discuter avec le bot. Lorsque vous voulez qu’un agent ait un réel poids dans la décision, demandez-lui de produire un élément que vous pouvez vérifier plutôt qu’un résultat auquel vous devez simplement croire. C’est le principe de demander à un agent de fournir un rapport de preuves que vous pouvez réexécuter vous-même.
Modes d’échec et messages affichés
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 ancrer 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 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 de 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 étape. Vérifiez donc cette ligne en premier.
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 y figure déjà, ouvrez Settings, puis Actions, puis General. Une policy de l’organisation peut limiter les permissions que n’importe quel token de workflow est autorisé à demander.
json.decoder.JSONDecodeError. Le modèle n’a pas renvoyé de JSON analysable. La cause courante 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 précisément pour ce cas : 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 filtré tous les fichiers modifiés, puis contrôlez la protection contre les forks et celle liée au label. Vérifiez enfin que le runner fonctionne 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 reviennent 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.
Exécuter ce service avec vos autres agents
Le reviewer est léger, et il peut être tentant de l’installer sur le serveur qui exécute déjà tous les autres services. Gardez-le séparé si le dépôt est important. Ce processus détient un token capable de commenter votre code et une clé qui peut dépenser votre argent. De plus, un self-hosted runner 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 le niveau minimal recommandé. Si vous exécutez également des agents interactifs qui récupèrent du code, une VM éphémère par agent est le modèle le plus robuste, et exécuter un agent de programmation sur un VPS présente la configuration générale. Si l’API Anthropic est nouvelle pour vous, une première application utilisant l’API Claude sur un VPS constitue un point de départ plus simple.
FAQ
Un agent d’IA chargé de relire les PR a-t-il besoin d’un accès en écriture à mon dépôt ?
Non. Il a besoin de pull-requests: write pour publier une review et de 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 le GITHUB_TOKEN associé à chaque job peut faire. Avec ces deux lignes, l’agent peut commenter une pull request, mais il 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 accepte un commentaire de review inline uniquement sur une ligne qui fait partie du 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 comporte 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. Préfixer chaque ligne du diff avec son numéro de ligne dans le nouveau fichier avant de l’envoyer au modèle évite que celui-ci invente des numéros.
Puis-je exécuter ce workflow sur un dépôt public avec des pull requests provenant de forks ?
Pas avec cette architecture. 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 indique également que les self-hosted runners « devraient presque ne 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 le reviewer aux branches poussées directement vers le dépôt, ce que fait la condition if:, ou déplacez l’étape de review vers un runner hébergé par GitHub et acceptez que le diff quitte votre infrastructure.
Quel modèle dois-je utiliser pour la review des pull requests ?
Commencez par Haiku 4.5. Lire un diff limité et le comparer à une liste fixe de types de défauts ne demande pas un raisonnement complexe. Le modèle le moins cher maintient 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. Son coût se justifie plus facilement sur une branche de release que sur chaque push vers chaque branche de fonctionnalité.