Autoalojar un agente de revisión de PR en un VPS
Ejecuta un revisor de PR en tu VPS con runner propio, prompts limitados al diff, filtros de rutas y tamaño, comentarios en línea y coste por PR.
Qué hace un agente de revisión de PR autoalojado
Un agente de revisión de PR autoalojado es un programa pequeño que se ejecuta en un servidor propio. Lee el diff de una pull request (PR) y envía al modelo sólo las líneas modificadas. La respuesta se publica como comentarios insertados en la revisión. Nunca hace checkout de la rama y nunca lee un archivo que la pull request no haya modificado. Las únicas credenciales que almacena son una clave de una API de modelo y un token que sólo permite añadir comentarios. Es una definición limitada de agente, más cercana a un prompt con filtros delante que a algo autónomo. Si quiere conocer el contexto completo antes de crear este agente, la ruta gradual desde los conceptos hasta un bucle que usted mismo escribe explica los fundamentos.
Un modelo puede leer un diff. Esa parte ya está resuelta. Lo importante es adónde va el diff y quién conserva la clave. Con un bot de revisión alojado, cada diff de cada repositorio privado sale de su red, llega a los registros de un tercero y queda sujeto a su política de retención. En un VPS (servidor privado virtual) propio, el diff va de GitHub a su servidor y de ahí a la API del modelo. Además, puede leer las cuarenta líneas de código que determinan qué se envía.
Qué necesita antes de empezar
- Un VPS con Ubuntu 24.04 y un runner autohospedado de GitHub Actions ya registrado en el repositorio. Asígnele la etiqueta adicional
pr-reviewal registrarlo, porque el workflow que aparece a continuación selecciona los runners con esa etiqueta. - Una clave de API de Anthropic obtenida desde Claude Console.
- Un repositorio en el que pueda controlar quién abre un pull request. Un repositorio privado es la opción más sencilla. La sección sobre forks cubre el caso público, pero la respuesta es menos favorable.
Instalar el revisor en el VPS
El servicio runner se ejecuta con la cuenta sin privilegios que creó al ejecutar ./svc.sh install. Instale el revisor con esa misma cuenta para que el trabajo pueda ejecutarlo sin sudo. Sustituya runner por el nombre de su cuenta.
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 muestra gh version 2.45.0 en Ubuntu 24.04 en agosto de 2026. Cualquier versión a partir de 2.20 incluye la opción --input que se usa a continuación. Un mensaje Command 'gh' not found indica que el componente universe no está habilitado. Ejecute sudo add-apt-repository universe y vuelva a intentarlo.
Dónde se almacenan la clave y el token
Son dos secretos con ciclos de vida distintos. Ninguno debe estar en el repositorio.
ANTHROPIC_API_KEY es un secreto del repositorio. Se configura en Settings, luego en Secrets and variables y después en Actions. GitHub lo cifra y lo inyecta en el entorno del paso durante la ejecución. Nunca es un archivo en disco ni aparece en el historial de git.
GITHUB_TOKEN funciona de otra manera. Actions genera un token nuevo para cada trabajo y lo destruye cuando termina el trabajo. Las acciones que puede realizar ese token se definen en el bloque permissions: del workflow. Ahí es donde se aplica realmente el principio de mínimo privilegio:
permissions:
contents: read
pull-requests: writeEse token puede publicar una revisión. No puede hacer push de un commit, fusionar una rama, editar un archivo de workflow ni acceder a otro repositorio. Un agente que puede comentar actúa como revisor. Un agente que puede hacer push actúa como committer, y nadie autorizó eso. Proteja la clave del modelo con el mismo cuidado, porque consume dinero de su cuenta. Encontrará más información sobre este tipo de problema en mantener los secretos fuera del alcance de un agente de IA.
Actions sustituye la cadena exacta del secreto por *** en los registros de los trabajos. Sólo busca la cadena exacta. Por tanto, una clave codificada en base64, dividida en dos líneas o mostrada carácter a carácter aparece sin ocultar. No añada un paso de depuración que vuelque el entorno.
Por qué un pull request de un fork nunca recibe tu clave de API
La regla de GitHub es sencilla: con la excepción de GITHUB_TOKEN, los secretos no se pasan al runner cuando un workflow se activa desde un repositorio bifurcado. Por tanto, una ejecución de pull_request desde un fork inicia el script sin ningún ANTHROPIC_API_KEY, y la primera llamada a la API falla con invalid x-api-key.
La solución tentadora es cambiar el activador a pull_request_target, que se ejecuta en el contexto del repositorio base y sí recibe los secretos. No lo haga aquí. La propia documentación de seguridad de GitHub indica que esos workflows «tienen privilegios, lo que significa que comparten la misma caché de la rama principal con otros activadores de workflows con privilegios, y pueden tener acceso de escritura al repositorio y acceso a los secretos referenciados», y que el resultado «se puede explotar para tomar el control de un repositorio».
La misma documentación es clara sobre el runner: «Los self-hosted runners casi nunca deben usarse para repositorios públicos en GitHub, porque cualquier usuario puede abrir pull requests contra el repositorio y comprometer el entorno».
Esto determina dos decisiones de diseño. El job incluye una condición para ejecutarse sólo en ramas que se hayan enviado a tu propio repositorio. Además, el workflow no tiene ningún paso actions/checkout. El agente nunca tiene la rama en el disco, por lo que un pull request malicioso sólo es texto que se envía a un modelo. No puede ejecutar un script de compilación en tu VPS, porque nada en tu VPS llega a ejecutarlo. Sin embargo, el texto no es necesariamente inocuo: un diff escrito por un desconocido es una entrada no confiable que llega a un modelo, el mismo límite de confianza que aparece cuando das a un agente acceso a búsquedas web, y aquí lo único que lo contiene es que este agente sólo puede publicar un comentario.
Obtenga el diff, no el repositorio
Una solicitud obtiene todo el diff como texto sin formato.
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"El tipo de contenido Accept: application/vnd.github.diff convierte la respuesta, que normalmente es un objeto JSON con la descripción de la pull request, en el diff unificado. gh api muestra ese cuerpo sin cambios. La primera línea que verá debe comenzar por diff --git a/. Un gh: Not Found (HTTP 404) indica que el token no puede ver el repositorio. En un token personal con permisos detallados, casi siempre significa que no se habilitó el permiso Pull requests.
Filtre antes de gastar un token
Esta sección marca la diferencia entre un bot que la gente lee y uno al que silencia. Cada filtro siguiente se ejecuta antes de que el modelo vea un solo byte.
- Filtros de rutas. Excluya archivos de bloqueo, directorios de dependencias incluidas, archivos minificados y código generado. Un comentario del modelo sobre
package-lock.jsonsólo añade ruido, y esos archivos suelen representar la mayor parte de los bytes de un diff. - Un límite de tamaño. Si se supera el límite, omita la revisión y termine correctamente. Una refactorización de 4,000 líneas recibe una única línea clara que indica que era demasiado grande para revisarla automáticamente, en lugar de sesenta suposiciones.
- Un umbral de gravedad y un límite de comentarios. Informe de los hallazgos de gravedad alta y media, hasta un máximo de diez, ordenados primero por gravedad. Nadie lee el comentario once.
El script
Guarde esto como /opt/pr-review/review.py. Lee su configuración desde el entorno, por lo que el flujo de trabajo puede cambiar de modelo sin modificar el código.
#!/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,
).stdoutSeparar el diff por archivo es lo que permite filtrar por ruta. Numerar cada línea es lo que permite ubicar los comentarios de la revisión. GitHub sólo acepta un comentario en línea sobre una línea que forme parte del diff, por lo que el modelo debe indicar un número de línea real. Proporcionarle los números permite que copie uno en lugar de inventarlo.
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)La cabecera del bloque contiene la numeración. @@ -12,7 +12,9 @@ indica que el bloque del archivo nuevo comienza en la línea 12, por lo que el contador empieza allí y avanza sólo en las líneas añadidas y sin cambios. Las líneas eliminadas pasan sin numerar porque no existen en el archivo nuevo. La comprobación de las líneas que comienzan con una barra inversa omite el marcador de ausencia de salto de línea que git escribe al final de un archivo. De lo contrario, desplazaría en uno todos los números posteriores.
Ambas salidas usan el estado 0, no el 1. Una solicitud de extracción filtrada o demasiado grande debe mostrar una comprobación correcta. Una comprobación fallida sobre la que una persona no puede actuar se ignora, y cuando se ignora una comprobación, se ignoran todas.
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,
)Hay tres detalles fundamentales en ese bloque. El JSON se extrae entre el primer { y el último } porque a veces un modelo incluye su respuesta dentro de un bloque de código, y json.loads no puede procesar ese bloque. A path se le quita el b/ inicial, porque ese prefijo procede de la cabecera del diff y GitHub necesita una ruta relativa al repositorio. Y --input - envía toda la revisión en una sola llamada a la API, por lo que diez hallazgos llegan como una notificación en lugar de diez.
Cuando no hay nada que informar, el script no publica nada. Un bot que escribe «no se encontraron problemas» en cada solicitud de extracción enseña a las personas a pasarla por alto. Después también pasan por alto la que sí era importante.
Conéctelo al flujo de trabajo
Guarde esto como .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 no está en ese bloque env: porque Actions lo establece para todos los trabajos. El grupo concurrency es importante para la facturación: sin él, si envía tres correcciones rápidas a una rama, se ejecutan tres revisiones completas y paga las tres; con él, sólo se mantiene la última.
La línea if: cumple dos funciones. La primera mitad omite las solicitudes de incorporación de cambios de forks, que fallarían de todos modos porque no tienen una clave. La segunda proporciona a su equipo un interruptor de desactivación: añada la etiqueta no-ai-review a una solicitud de incorporación de cambios y el trabajo no se ejecutará.
Abra una solicitud de incorporación de cambios y observe qué ocurre:
gh run list --workflow=pr-review.yml --limit 3
gh run view --log
gh pr view 42 --commentsUna ejecución que termina en unos segundos y muestra nothing above the severity threshold; posting no comment en el registro funciona correctamente. En una solicitud de incorporación de cambios pequeña y limpia, ese es el resultado esperado.
¿Cuánto cuesta una revisión automatizada de un pull request?
El diff constituye casi toda la entrada, por lo que su tamaño determina el precio. A continuación se muestra un diff medido de 500 líneas junto con el system prompt. El recuento se obtuvo mediante el endpoint de recuento de tokens, no mediante una estimación.
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"
}
]Ese diff produjo 8,000 tokens de entrada en Haiku 4.5 y 10,400 en Sonnet 5. Es el mismo texto, pero el recuento es distinto. Los modelos Claude a partir de 4.7 usan un tokenizer más reciente que produce aproximadamente un 30% más de tokens para la misma entrada, según la documentación de Anthropic en su página de precios. Téngalo en cuenta al comparar el precio por millón de tokens de un modelo nuevo con el de uno antiguo.
Precios de lista a agosto de 2026: Haiku 4.5 cuesta $1 por millón de tokens de entrada y $5 por millón de tokens de salida. Sonnet 5 cuesta $2 y $10 con el precio introductorio vigente hasta el 31 de agosto de 2026; después costará $3 y $15. Opus 5 cuesta $5 y $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"
}
]Eso equivale a 1.4 centavos por pull request en Haiku 4.5 y 9.1 centavos en Opus 5. Un equipo que fusiona 200 pull requests al mes paga aproximadamente $2.80 con Haiku 4.5, $7.28 con Sonnet 5 o $18.20 con Opus 5. A partir del 1 de septiembre de 2026, multiplique por 1.5 la fila de Sonnet 5.
Dos factores pueden hacer que la factura real supere esa estimación. El disparador synchronize revisa cada push, por lo que una rama activa con ocho pushes genera ocho revisiones. La regla de concurrencia sólo ayuda cuando los pushes llegan con poca diferencia de tiempo. Además, las cifras suponen que los filtros de ruta funcionan: un solo archivo de bloqueo sin filtrar puede duplicar por sí solo la entrada.
El prompt caching no ayuda en este caso. El prefijo almacenado en caché debe ser idéntico byte a byte entre las llamadas, y el diff cambia cada vez. El system prompt es la única parte estable, pero su tamaño está muy por debajo de la longitud mínima que se puede almacenar en caché. Para consultar la regla general, vea cuándo se amortiza el prompt caching. Para elegir entre los tres modelos anteriores, consulte qué modelo Claude usar para cada tarea.
Mida sus propios diffs antes de activarlo
Añada esta línea después de construir payload y ejecute manualmente el script con varios pull requests del mes pasado:
print(client.messages.count_tokens(
model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)El endpoint de recuento no ejecuta el modelo. Por tanto, no consume tokens de entrada ni de salida y usa el tokenizer correspondiente al modelo que indique. Ejecútelo con diez pull requests reales de su propio repositorio y use la mediana en lugar de la media, para que una migración enorme no distorsione la estimación.
Por qué los bots de revisión se silencian y cómo evitarlo
Hay dos comportamientos que destruyen la confianza en estos bots. Ambos tienen una solución en el código anterior.
Revisarlo todo de una vez. Un bot que deja cuarenta comentarios consigue que nadie lea ninguno. El umbral de gravedad y el límite de diez comentarios no son una cuestión de cortesía. Mantienen visibles los hallazgos reales. Ordenar por gravedad antes de truncar hace que el límite elimine los hallazgos menos importantes en lugar de eliminar diez al azar.
Comentar con seguridad sobre algo que no puede comprobar. Esto es lo que hace que los ingenieros lo desactiven para siempre. Un modelo que recibe 200 líneas de una base de código de 40,000 líneas todavía escribirá «esto rompe la invalidación de la caché en redis_client.py» sobre un archivo que nunca ha visto. El prompt del sistema lo impide con un lenguaje claro: informe sólo de los defectos visibles en las líneas mostradas y omita todo aquello de lo que no esté seguro. Nombrar directamente el fallo funciona mejor que pedir precisión en términos generales. Indicar al modelo que un resultado vacío es normal evita que invente algo que decir sobre un cambio de dos líneas. Los defectos que sólo aparecen al analizar todo el repositorio requieren otro trabajo. Alojar open-kritt para el análisis de seguridad es una forma de cubrir ese ámbito sin ampliar lo que este revisor puede ver.
Publique la revisión como COMMENT, nunca como REQUEST_CHANGES. La opinión de un modelo no debe poder bloquear una fusión. En cuanto pueda hacerlo, alguien con una fecha límite eliminará todo el flujo de trabajo en lugar de discutir con él. Si quiere que el criterio de un agente tenga un peso real, haga que produzca algo que pueda comprobar en lugar de algo que tenga que creer. Esa es la idea de hacer que un agente entregue un informe de evidencias que pueda volver a ejecutar por su cuenta.
Modos de fallo y las cadenas que verá
HTTP 422 al publicar la revisión. gh muestra gh: Unprocessable Entity (HTTP 422) y el cuerpo de la respuesta indica el campo: Pull request review thread line must be part of the diff. GitHub no puede anclar ese comentario. Las causas habituales son un número de línea inventado por el modelo, un path que todavía conserva el prefijo b/ o un comentario sobre una línea eliminada, que requiere establecer side en LEFT en lugar de RIGHT. Muestre el JSON de la revisión antes de publicarlo y compruebe manualmente un comentario comparándolo con el diff.
invalid x-api-key de la API del modelo. El paso falla en la primera llamada messages.create. Puede que el secreto ANTHROPIC_API_KEY no esté configurado en el repositorio o que la pull request proceda de un fork, por lo que Actions no haya pasado ningún secreto. La protección contra forks de la línea if: debería haber omitido el paso, así que compruebe primero esa línea.
gh: Resource not accessible by integration (HTTP 403). El token del trabajo no puede escribir en las pull requests. Añada pull-requests: write al bloque permissions:. Si ya está incluido, vaya a Settings, después a Actions y luego a General, donde una política de la organización puede limitar lo que cualquier token de workflow puede solicitar.
json.decoder.JSONDecodeError. El modelo no devolvió JSON válido para el análisis. La causa habitual es que la respuesta alcanzó el límite de tokens y se detuvo a mitad de un objeto. La línea de registro muestra stop_reason precisamente para esto: un valor de max_tokens significa que debe aumentar max_tokens o reducir MAX_COMMENTS.
El workflow nunca se ejecuta. gh run list no muestra nada para la pull request. Compruebe que paths-ignore no haya filtrado todos los archivos modificados. Después, compruebe la protección contra forks y la protección por etiquetas. Por último, compruebe que el runner esté activo con sudo systemctl status 'actions.runner.*' en el VPS. Un runner desconectado deja el trabajo en cola sin mostrar ningún mensaje de error en la pull request.
Todas las revisiones vuelven vacías. Establezca MIN_SEVERITY en low durante una ejecución. Si aparecen hallazgos, el umbral funciona correctamente. Si no aparece nada, muestre payload y confirme que los filtros no hayan eliminado todo el diff.
Ejecutarlo junto con los demás agentes
El revisor es pequeño, por lo que puede resultar tentador instalarlo en el servidor que ya ejecuta todo lo demás. Manténgalo separado si el repositorio es importante. Este proceso contiene un token que puede comentar en el código y una clave que puede gastar su dinero. Además, un runner autoalojado está diseñado para ejecutar código de workflows. Una cuenta dedicada sin privilegios y sin permisos de sudo, en un host que no ejecute nada más, es la configuración mínima recomendada. Si también ejecuta agentes interactivos que clonan código, una VM desechable por agente es el patrón más sólido. Ejecutar un agente de programación en un VPS cubre la configuración general. Si la API de Anthropic es nueva para usted, una primera aplicación de Claude API en un VPS es un punto de partida más sencillo que esto.
FAQ
¿Un agente de revisión de PR basado en IA necesita acceso de escritura a mi repositorio?
No. Necesita pull-requests: write para publicar una revisión y contents: read para obtener el diff. Esa es toda la lista, y se configura en el bloque permissions: del workflow, que limita lo que puede hacer el GITHUB_TOKEN de cada trabajo. Con esas dos líneas, el agente puede comentar en un pull request, pero no puede enviar un commit ni fusionar una rama. Publique las revisiones con event: COMMENT en lugar de REQUEST_CHANGES para que tampoco pueda bloquear una fusión.
¿Por qué falla mi comentario de revisión con HTTP 422?
GitHub sólo acepta un comentario de revisión insertado en una línea que forme parte del diff del pull request. Devuelve Pull request review thread line must be part of the diff cuando la línea no forma parte del diff. Compruebe que path sea relativo al repositorio y no incluya el prefijo b/ de la cabecera del diff. Compruebe también que el número de línea aparezca dentro de un hunk de ese archivo. side debe ser RIGHT para una línea añadida o sin cambios, y LEFT para una línea eliminada. Anteponer a cada línea del diff su número de línea en el archivo nuevo antes de enviarla al modelo evita que el modelo invente números desde el principio.
¿Puedo ejecutar esto en un repositorio público con pull requests de forks?
No con este diseño. GitHub no pasa secretos a un workflow activado desde un fork, por lo que falta la clave del modelo y la ejecución falla. GitHub también indica que los self-hosted runners "should almost never be used for public repositories", porque cualquiera puede abrir un pull request que provoque la ejecución de código en su máquina. En un proyecto público, restrinja el revisor a las ramas enviadas al propio repositorio, que es lo que hace la guarda if:, o traslade el paso de revisión a un runner alojado por GitHub y acepte que el diff salga de su propia infraestructura.
¿Qué modelo debo usar para revisar pull requests?
Empiece con Haiku 4.5. Leer un diff acotado y compararlo con una lista fija de tipos de defectos no es un problema complejo de razonamiento. Además, el modelo más barato mantiene la factura mensual en una cifra que nadie discute. Cambie a Sonnet 5 si observa que no detecta errores reales en su lenguaje o framework, y mídalo en lugar de darlo por supuesto. Opus 5 es, con diferencia, el más caro de los tres por pull request. Es más fácil justificarlo en una rama de lanzamiento que en cada push a todas las ramas de funcionalidades.