Módulo 2: Code Review Automático en PRs

Inline Comments via GitHub API

Inline Comments via GitHub API

Descripción

Esta es la cápsula que transforma a Claude Code de "una herramienta que comenta en general" a un reviewer real: un agente que deja comentarios en la línea exacta donde detecta un problema. Los inline comments son la diferencia entre feedback que se acciona inmediatamente y feedback que se ignora.

Vas a aprender la diferencia técnica entre comments generales y inline comments, cómo crear los segundos vía la GitHub API, qué SHA de commit usar, cómo manejar errores comunes (líneas fuera de rango, archivos no en el diff), y cómo combinar inline comments con un summary general en un único review.

Al terminar, vas a tener un bot que produce comentarios inline calidad-review-humano: específicos, en la línea correcta, con sugerencias accionables.


La Diferencia: Comment General vs Inline Comment

COMMENT GENERAL (al final del PR):
┌─────────────────────────────────────────────┐
│ 🤖 Análisis de Claude Code                 │
│                                             │
│ Encontré 3 problemas:                      │
│ - Falta validación en payment.py:42        │
│ - SQL injection en auth.py:87              │
│ - Naming inconsistente en orders.py:120    │
└─────────────────────────────────────────────┘
PROBLEMA: el developer tiene que SALTAR entre
el comment y cada archivo. Visibilidad baja.

VERSUS

INLINE COMMENTS (en cada línea):
payment.py
┌──────────────────────────────────────┐
│ Línea 42:                             │
│   if amount < 0:                      │
│       process()                       │
│                                       │
│   💬 Claude Code:                     │
│   Falta validar que amount sea numérico │
│   antes de comparar. Si recibe string,│
│   esto compara strings (resultado no  │
│   determinístico).                    │
└──────────────────────────────────────┘

Visibilidad y accionabilidad son superiores con inline comments. El developer ve el problema en contexto del código, no a tres clicks de distancia.


La API de Review Comments

GitHub tiene dos APIs distintas para "comentarios" en PRs:

APIEndpointUso
Issue comments/repos/{repo}/issues/{number}/commentsComments generales (al final del PR)
PR review comments/repos/{repo}/pulls/{number}/commentsInline comments (línea-archivo específico)

Suenan similares pero son distintas. La cápsula 04 del módulo 1 usó la primera (issue comments para summary general). Esta cápsula usa la segunda.

Crear un inline comment: estructura mínima

import requests
import os

token = os.environ["GITHUB_TOKEN"]
repo = os.environ["GITHUB_REPOSITORY"]
pr_number = os.environ["PR_NUMBER"]
commit_sha = os.environ["PR_HEAD_SHA"]

url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/comments"
headers = {
    "Authorization": f"Bearer {token}",
    "Accept": "application/vnd.github+json",
    "X-GitHub-Api-Version": "2022-11-28",
}

payload = {
    "body": "Falta validar que `amount` sea numérico.",
    "commit_id": commit_sha,
    "path": "payment.py",
    "line": 42,
    "side": "RIGHT",
}

response = requests.post(url, headers=headers, json=payload)

if response.status_code == 201:
    print(f"Inline comment creado: {response.json()['html_url']}")
else:
    print(f"Error: {response.status_code} {response.text}")

Campos clave:

CampoSignificadoNotas
bodyTexto del comentario (markdown)Hasta 65,536 caracteres
commit_idSHA del commit a comentarTípicamente el HEAD del PR
pathArchivo, relativo al root del repoDebe estar en el diff del PR
lineLínea (1-indexed) en el archivoDebe estar en el diff
sideRIGHT (versión nueva) o LEFT (versión vieja)Casi siempre RIGHT

Pasar el commit SHA al workflow

En el YAML, expone el SHA correcto:

- name: Run review
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    GITHUB_REPOSITORY: ${{ github.repository }}
    PR_NUMBER: ${{ github.event.pull_request.number }}
    PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
  run: python .github/scripts/review_pr.py

pull_request.head.sha es el SHA del último commit del PR — el que quieres comentar.


El Patrón Recomendado: Crear un Review Completo

En lugar de crear comments uno por uno, puedes crear un review con múltiples comentarios en una sola llamada. Es más eficiente y atómico.

Endpoint

POST /repos/{repo}/pulls/{number}/reviews

Estructura del payload

review_payload = {
    "commit_id": commit_sha,
    "body": "## Resumen del review\n\nEncontré 3 issues. Detalles inline.",
    "event": "COMMENT",  # o "REQUEST_CHANGES" o "APPROVE"
    "comments": [
        {
            "path": "payment.py",
            "line": 42,
            "side": "RIGHT",
            "body": "Falta validar que `amount` sea numérico.",
        },
        {
            "path": "auth.py",
            "line": 87,
            "side": "RIGHT",
            "body": "SQL injection: usar parameterized query.",
        },
        {
            "path": "orders.py",
            "line": 120,
            "side": "RIGHT",
            "body": "El naming `createOrder` no sigue snake_case.",
        },
    ],
}

url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/reviews"
response = requests.post(url, headers=headers, json=review_payload)

El campo event

ValorComportamiento
COMMENTSolo comenta, no aprueba ni rechaza
APPROVEAprueba el PR (equivale a un review aprobatorio)
REQUEST_CHANGESSolicita cambios (bloquea merge si está configurado)

Para suggest-only mode (cápsula 04), siempre usas COMMENT. Aprobar o rechazar automáticamente requiere mucha más confianza en el modelo y la cápsula 04 desarrolla cuándo es apropiado.


Comments Multi-Línea

A veces el problema involucra varias líneas (no solo una). GitHub soporta comments multi-línea:

{
    "path": "payment.py",
    "start_line": 40,    # primera línea del rango
    "line": 45,          # última línea del rango
    "start_side": "RIGHT",
    "side": "RIGHT",
    "body": "Esta función completa tiene problemas: ...",
}

Casos de uso:

  • Una función entera con problemas estructurales
  • Un bloque de código duplicado
  • Una secuencia de líneas con lógica defectuosa

Generar los Inline Comments desde Claude

Pedirle a Claude Code que devuelva los comments en formato estructurado JSON simplifica la integración:

"""Review con inline comments estructurados."""
import json
import re
import os
from anthropic import Anthropic

client = Anthropic()

# Leer el diff filtrado (cápsula 02)
with open("filtered_diff.txt") as f:
    diff_text = f.read()

prompt = f"""Haz code review de este diff de pull request.
Devuelve un JSON con esta estructura exacta:

{{
  "summary": "Resumen del review (2-3 párrafos en markdown)",
  "comments": [
    {{
      "path": "ruta/al/archivo",
      "line": 42,
      "severity": "critical|warning|suggestion",
      "body": "Texto del comentario en markdown"
    }}
  ]
}}

Reglas:
- Solo incluye comentarios accionables (no "se ve bien")
- `path` debe ser exactamente como aparece en el diff
- `line` debe ser un número de línea presente en el diff
- Usa severity "critical" solo para bugs reales o vulnerabilidades
- Devuelve SOLO el JSON, sin texto adicional

Diff:

{diff_text}

"""

response = client.messages.create(
    model="claude-haiku-4-5",
    max_tokens=4000,
    messages=[{"role": "user", "content": prompt}],
)

# Parsear el JSON
text = response.content[0].text.strip()
text = re.sub(r"^```json\n?", "", text)
text = re.sub(r"\n?```$", "", text)

try:
    review_data = json.loads(text)
except json.JSONDecodeError as e:
    print(f"ERROR: respuesta no es JSON válido: {e}")
    print(f"Texto recibido:\n{text}")
    sys.exit(1)

print(f"Review generado: {len(review_data['comments'])} comments")

Ahora review_data["comments"] es directamente el formato que la API espera.


Validar Antes de Publicar

Crítico: validar que los comments referencien líneas que existen en el diff. Si Claude sugiere comentar la línea 200 de un archivo cuyo diff solo toca líneas 40-60, la API responde 422 (Unprocessable Entity).

def validate_comment(comment, files_in_diff):
    """Validar que un comment es publicable."""
    path = comment["path"]
    line = comment["line"]
    
    # ¿El archivo está en el diff?
    file_data = next((f for f in files_in_diff if f["filename"] == path), None)
    if not file_data:
        return False, f"Archivo {path} no está en el diff"
    
    # ¿La línea está en alguna sección del patch?
    patch = file_data.get("patch", "")
    if not patch:
        return False, f"Archivo {path} no tiene patch (binario o muy grande)"
    
    # Extraer rangos del patch (formato: @@ -X,Y +A,B @@)
    line_in_patch = False
    for hunk in re.finditer(r"@@\s+-\d+,\d+\s+\+(\d+),(\d+)\s+@@", patch):
        start = int(hunk.group(1))
        count = int(hunk.group(2))
        if start <= line < start + count:
            line_in_patch = True
            break
    
    if not line_in_patch:
        return False, f"Línea {line} de {path} no está en el diff"
    
    return True, "OK"

# Filtrar comments válidos
valid_comments = []
for c in review_data["comments"]:
    is_valid, reason = validate_comment(c, files_in_diff)
    if is_valid:
        valid_comments.append({
            "path": c["path"],
            "line": c["line"],
            "side": "RIGHT",
            "body": format_severity(c["severity"]) + c["body"],
        })
    else:
        print(f"SKIP: {reason}")

Resultado: comentarios inválidos se filtran antes de la API, evitando errores 422.


Formatear con Severity

Para que los comments comuniquen importancia visual rápido, prepende emoji o tag según severity:

def format_severity(severity: str) -> str:
    """Prefijo visual por severidad."""
    return {
        "critical": "🚨 **CRITICAL** — ",
        "warning": "⚠️ **WARNING** — ",
        "suggestion": "💡 **Suggestion** — ",
    }.get(severity, "")

# Uso:
body = format_severity("critical") + "SQL injection: usar parameterized query"
# Resultado: "🚨 **CRITICAL** — SQL injection: usar parameterized query"

Trampas Comunes

Error 1: Línea fuera del diff

Síntoma: API responde 422 "line must be part of the diff".

Por qué pasa: El modelo sugiere comentar una línea que no fue modificada en el PR (aunque exista en el archivo).

Cómo corregir: Validar contra el patch antes de publicar (sección "Validar Antes de Publicar").

Error 2: Path con ./ al inicio

Síntoma: API responde 422 "path must be relative".

Por qué pasa: Algunos modelos devuelven paths como ./src/payment.py. La API espera src/payment.py.

Cómo corregir: Limpiar paths antes de enviar:

path = comment["path"].lstrip("./")

Error 3: SHA del commit incorrecto

Síntoma: API responde 422 "commit_id is not the head of the pull request".

Por qué pasa: Pasaste github.sha (SHA del workflow run) en lugar de github.event.pull_request.head.sha.

Cómo corregir: En PRs, siempre pull_request.head.sha. github.sha apunta a un commit merge artificial que GitHub crea para los workflows, no al HEAD del PR.

Error 4: Permisos insuficientes

Síntoma: API responde 403 "Resource not accessible by integration".

Por qué pasa: El YAML no declara permissions: pull-requests: write.

Cómo corregir: Agregar al workflow:

permissions:
  pull-requests: write
  contents: read

Error 5: Crear comments uno por uno en lugar de un review

Síntoma: Varios comments se publican gradualmente; si el script falla a mitad, queda un review parcial.

Por qué pasa: Llamar al endpoint de comments en loop en lugar del endpoint de reviews.

Cómo corregir: Usar POST /pulls/{number}/reviews con todos los comments en una sola llamada. Atomicidad: o se publica todo o nada.


Diagnóstico

Pregunta 1: ¿Sabes la diferencia entre el endpoint de issue comments y el de PR review comments?

Issue comments → comments generales del PR (al final). Review comments → inline en línea-archivo. Distintos endpoints, distinto payload.

Pregunta 2: ¿Validas que la línea del comment esté efectivamente en el diff?

Si no, vas a tener errores 422 frecuentes. La validación contra el patch evita 90% de esos errores.

Pregunta 3: ¿Estás usando `github.event.pull_request.head.sha` o `github.sha`?

Para inline comments en PRs, debe ser el primero. El segundo apunta a un merge commit artificial.

Pregunta 4: ¿Tu workflow declara `permissions: pull-requests: write`?

Sin esto, la API responde 403. Default permissions de GITHUB_TOKEN son read-only.

Pregunta 5: ¿Publicas todos los comments en una sola llamada (review) o uno por uno?

Uno por uno = no atómico, frágil. Una sola llamada (review) = atómica, eficiente.


Ejercicios

Ejercicio 1: Crear un inline comment manualmente (Fácil)

Usando curl o Postman, crea un inline comment en un PR de prueba directamente con la API. Verifica que aparece en la línea correcta.

curl -X POST \
  -H "Authorization: Bearer $GH_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  https://api.github.com/repos/OWNER/REPO/pulls/PR_NUMBER/comments \
  -d '{
    "body": "Test comment",
    "commit_id": "COMMIT_SHA",
    "path": "ARCHIVO_DEL_PR",
    "line": NÚMERO_DE_LÍNEA,
    "side": "RIGHT"
  }'

Ejercicio 2: Crear un review con múltiples comments (Medio)

Modifica el script de la cápsula 02 para que genere un review con 2-3 inline comments en líneas reales del diff.

Ver solución
# (asumiendo que ya tienes review_data del modelo)

review_payload = {
    "commit_id": commit_sha,
    "body": review_data["summary"],
    "event": "COMMENT",
    "comments": valid_comments,  # del paso de validación
}

url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/reviews"
r = requests.post(url, headers=headers, json=review_payload)
print(f"Review: {r.status_code} — {r.json().get('html_url', r.text)}")

Ejercicio 3: Validación robusta (Difícil)

Implementa la función validate_comment que:

  1. Verifica que el archivo está en el diff
  2. Verifica que la línea está en algún hunk del patch
  3. Limpia paths con ./ o trailing slashes
  4. Maneja archivos sin patch (binarios)

Usa la implementación de la sección "Validar Antes de Publicar" como base y extiéndela.


Resumen

  • Inline comments > comments generales — visibilidad y accionabilidad
  • Dos APIs distintas: issue comments (general) vs review comments (inline)
  • Crear un review con múltiples comments en una llamada es más atómico que loops
  • Validar línea en el diff antes de publicar evita errores 422
  • github.event.pull_request.head.sha es el SHA correcto para PRs
  • permissions: pull-requests: write es prerequisito
  • Severity con emoji prefix comunica importancia visual rápido

Próxima cápsula: 04 — Review summary y suggest-only mode. Tienes inline comments funcionales. Ahora aprendes a complementarlos con un summary del review y a decidir cuándo el bot bloquea merges vs cuándo solo sugiere.


Recursos Adicionales

  1. GitHub PR Reviews API — Endpoint de reviews
  2. GitHub PR Review Comments API — Endpoint de comments individuales
  3. Octokit — Cliente oficial JS/TS
  4. PyGithub — Cliente Python
  5. GitHub Actions: GITHUB_TOKEN permissions — Permisos default
  6. Anthropic structured output — Forzar JSON output del modelo