Módulo 1: Claude Code en GitHub Actions

Parsear Output y Generar Artefactos

Parsear Output y Generar Artefactos

Descripción

Tu workflow ya ejecuta Claude Code de forma automática y segura (cápsulas 02-03). Pero el resultado del análisis sigue viviendo en los logs de GitHub Actions — y los developers no leen logs. Si el hallazgo no aparece en la página del PR, no existe operacionalmente.

Esta cápsula resuelve el problema: cómo convertir el output crudo de Claude Code en algo visible y accionable dentro del flujo de trabajo del equipo. Vas a aprender tres formatos de salida (PR comments, annotations, artifacts) y cómo elegir cuál usar según el caso.

Al terminar, vas a poder publicar el resultado del análisis como comentario en el PR, generar annotations que aparecen como check warnings, y guardar reportes detallados como artifacts descargables. El workflow deja de ser "una cosa que corre en el background" para ser "una cosa que el equipo ve y usa".


Los Tres Formatos de Output

1. PR COMMENT (comentario general en el PR)
   → Visibilidad: alta (todos lo ven al abrir el PR)
   → Mejor para: resúmenes y hallazgos generales
   → Limitación: 65,536 caracteres máximo
   → API: POST /repos/{owner}/{repo}/issues/{pr}/comments

2. ANNOTATIONS (warnings/errors en líneas específicas)
   → Visibilidad: media (aparecen en la pestaña "Files changed"
     y como check warning)
   → Mejor para: issues localizados (esta línea tiene problema)
   → Limitación: máximo 50 por workflow run
   → Output: ::warning file=X,line=N::mensaje

3. ARTIFACTS (archivos descargables)
   → Visibilidad: baja (hay que ir a la pestaña Actions y descargar)
   → Mejor para: reportes detallados, datos para análisis posterior
   → Limitación: 500 MB por artifact, 10 GB por workflow run
   → Action: actions/upload-artifact

Regla general: combina los tres. Resumen ejecutivo como PR comment, issues específicos como annotations, reporte completo como artifact.


Formato 1: PR Comments

El PR comment es el formato más visible. Cuando alguien abre la página del PR, ve los comentarios automáticamente. Es el lugar correcto para el resumen del análisis.

Implementación con la API de GitHub

Modifica el script Python para que, después de generar el análisis, lo publique como comentario:

"""Análisis de PR + publicación como comentario."""
import os
import sys
from pathlib import Path
from anthropic import Anthropic
import requests

# 1. Generar análisis
client = Anthropic()
diff_text = Path("pr_diff.txt").read_text()

response = client.messages.create(
    model="claude-haiku-4-5",
    max_tokens=2000,
    messages=[
        {
            "role": "user",
            "content": (
                "Analiza este diff y genera un resumen markdown con:\n"
                "## Resumen\n[Qué cambia]\n"
                "## Hallazgos\n[Posibles issues]\n"
                "## Sugerencias\n[Mejoras concretas]\n\n"
                f"Diff:\n```\n{diff_text}\n```"
            ),
        }
    ],
)

analysis = response.content[0].text

# 2. Publicar como PR comment
github_token = os.environ["GITHUB_TOKEN"]
repo = os.environ["GITHUB_REPOSITORY"]  # "owner/repo"
pr_number = os.environ["PR_NUMBER"]

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

body = f"## 🤖 Análisis de Claude Code\n\n{analysis}\n\n---\n*Generado automáticamente. Tokens: {response.usage.input_tokens} in / {response.usage.output_tokens} out*"

result = requests.post(url, headers=headers, json={"body": body})

if result.status_code != 201:
    print(f"ERROR al publicar comment: {result.status_code} {result.text}", file=sys.stderr)
    sys.exit(1)

print(f"Comment publicado: {result.json()['html_url']}")

YAML actualizado

- name: Run analysis and post comment
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    GITHUB_REPOSITORY: ${{ github.repository }}
    PR_NUMBER: ${{ github.event.pull_request.number }}
  run: |
    pip install requests
    python .github/scripts/analyze_pr.py

Nota clave: secrets.GITHUB_TOKEN es un secret automático que GitHub genera para cada workflow run. No lo configuras tú — está disponible siempre. Tiene permisos limitados al repo del workflow.

Permisos del GITHUB_TOKEN

Por defecto, GITHUB_TOKEN tiene permisos de lectura. Para escribir comments, tienes que ampliar:

permissions:
  pull-requests: write
  contents: read

jobs:
  analyze:
    runs-on: ubuntu-latest
    # ... resto

pull-requests: write permite crear comments. contents: read es necesario para checkout. Restringe siempre a los mínimos necesarios — no uses write-all.


Formato 2: Annotations

Las annotations aparecen como warnings o errors visibles en la pestaña "Files changed" del PR, en la línea específica del archivo. Son ideales para señalar issues localizados.

Sintaxis

GitHub Actions parsea ciertos comandos en stdout y los convierte en annotations:

::warning file={path},line={line}::{message}
::error file={path},line={line}::{message}
::notice file={path},line={line}::{message}

Ejemplo: Generar annotations desde el análisis

Modifica el script para que, además del comment, emita annotations para cada issue específico:

import re
import json

# ... (código previo de análisis)

# Pedimos a Claude que devuelva issues estructurados
response = client.messages.create(
    model="claude-haiku-4-5",
    max_tokens=2000,
    messages=[
        {
            "role": "user",
            "content": (
                "Analiza este diff. Devuelve un JSON con issues encontrados.\n"
                'Formato: {"issues": [{"file": "...", "line": N, "severity": "warning|error", "message": "..."}]}\n'
                "Devuelve solo el JSON, sin texto adicional.\n\n"
                f"Diff:\n```\n{diff_text}\n```"
            ),
        }
    ],
)

# Parsear el JSON de la respuesta
text = response.content[0].text.strip()
# Limpiar code fences si los puso
text = re.sub(r"^```json\n?", "", text)
text = re.sub(r"\n?```$", "", text)

try:
    issues_data = json.loads(text)
except json.JSONDecodeError:
    print(f"WARNING: respuesta no es JSON válido:\n{text}", file=sys.stderr)
    issues_data = {"issues": []}

# Emitir annotations
for issue in issues_data.get("issues", []):
    severity = issue.get("severity", "warning")
    file_path = issue.get("file", "")
    line = issue.get("line", 1)
    message = issue.get("message", "").replace("\n", " ")
    print(f"::{severity} file={file_path},line={line}::{message}")

Cómo se ve en el PR:

En la pestaña "Files changed", al lado del archivo afectado, aparece un banner amarillo (warning) o rojo (error) con el mensaje. El check del workflow se marca con un warning visible.

Limitaciones de annotations

  • Máximo 10 annotations por command y 50 por workflow run total. Si tienes más, las extras se ignoran silenciosamente.
  • No soportan markdown. Solo texto plano.
  • No persisten entre runs. Cada run genera sus propias annotations; no se acumulan.

Para issues por encima del límite de 50, combina: las 10 más críticas como annotations, el resto en el PR comment.


Formato 3: Artifacts

Los artifacts son archivos arbitrarios que se guardan asociados al workflow run. Útiles para:

  • Reportes detallados (HTML, PDF, JSON grande)
  • Logs estructurados para análisis posterior
  • Outputs intermedios que el equipo puede descargar y revisar

Implementación

Genera el reporte como archivo y súbelo:

# En el script Python:
import json

report = {
    "pr_number": int(os.environ["PR_NUMBER"]),
    "analysis": analysis,
    "issues": issues_data.get("issues", []),
    "tokens_used": {
        "input": response.usage.input_tokens,
        "output": response.usage.output_tokens,
    },
}

Path("analysis_report.json").write_text(json.dumps(report, indent=2))
# En el workflow:
- name: Run analysis
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    GITHUB_REPOSITORY: ${{ github.repository }}
    PR_NUMBER: ${{ github.event.pull_request.number }}
  run: python .github/scripts/analyze_pr.py

- name: Upload analysis report
  uses: actions/upload-artifact@v4
  with:
    name: analysis-report-pr-${{ github.event.pull_request.number }}
    path: analysis_report.json
    retention-days: 30

Resultado: en la pestaña "Actions" del workflow run, aparece una sección "Artifacts" con un archivo descargable. El equipo puede bajarlo, abrirlo, hacer análisis estadístico, etc.

Cuándo usar artifacts

  • ✅ Reportes que no caben en un PR comment (>65K caracteres)
  • ✅ Datos estructurados para análisis offline (JSON, CSV)
  • ✅ Outputs visuales (HTML reports, gráficos)
  • ❌ NO para hallazgos que el equipo debería ver inmediatamente (esos van como comment)

Combinar los Tres Formatos

El patrón profesional combina los tres según severidad y volumen:

ANÁLISIS GENERADO POR CLAUDE CODE
        │
        ├─→ Resumen ejecutivo (3-5 párrafos)
        │   → PR Comment (todos lo ven)
        │
        ├─→ Issues localizados (línea X tiene problema Y)
        │   → Annotations (10 más críticos, en files changed)
        │
        └─→ Reporte completo (todos los detalles, datos crudos)
            → Artifact JSON descargable

Ejemplo de script integrado

# Resumen → PR comment
post_pr_comment(summary_markdown)

# Top 10 issues → annotations
for issue in sorted(issues, key=lambda i: i["severity"])[:10]:
    print(f"::{issue['severity']} file={issue['file']},line={issue['line']}::{issue['message']}")

# Reporte completo → artifact
Path("analysis_report.json").write_text(json.dumps({
    "summary": summary_markdown,
    "issues": issues,
    "metadata": {...}
}, indent=2))

El equipo:

  • Abre el PR → ve el comment con resumen
  • Va a "Files changed" → ve los warnings inline
  • Si necesita el detalle → descarga el artifact

Cada developer encuentra lo que necesita al nivel de profundidad que requiere.


Trampas Comunes

Error 1: Comments duplicados en cada push al PR

Síntoma: Cada synchronize (push al PR) genera un comment nuevo. Después de 5 pushes, el PR tiene 5 comentarios del bot.

Por qué pasa: El script siempre crea un comment nuevo, no reutiliza el anterior.

Cómo corregir: Antes de crear, buscar si ya existe un comment del bot y actualizarlo:

# Buscar comment existente
list_url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
existing = requests.get(list_url, headers=headers).json()

bot_comments = [c for c in existing if "🤖 Análisis de Claude Code" in c["body"]]

if bot_comments:
    # Actualizar el existente
    update_url = f"https://api.github.com/repos/{repo}/issues/comments/{bot_comments[0]['id']}"
    requests.patch(update_url, headers=headers, json={"body": body})
else:
    # Crear nuevo
    requests.post(list_url, headers=headers, json={"body": body})

Error 2: Annotations sin path o line incorrectos

Síntoma: Las annotations aparecen pero no se asocian a ninguna línea visible en "Files changed".

Por qué pasa: El path debe ser relativo al root del repo (no absoluto). El line debe ser un número del archivo modificado en el PR (no de un archivo no tocado).

Cómo corregir: Validar paths antes de emitir. Si el modelo sugiere un archivo que no está en el diff, ignorar esa annotation o convertirla en parte del comment general.

Error 3: Artifacts sin retention-days

Síntoma: Los artifacts ocupan espacio y se acumulan. La quota de la org se agota.

Por qué pasa: Por defecto, los artifacts retienen 90 días. Para análisis de PR, eso es excesivo.

Cómo corregir: Configurar retention-days: 30 (o menos) explícitamente. Para artifacts efímeros (debug temporal), usar retention-days: 1.

Error 4: PR comment con HTML/markdown roto

Síntoma: El comment muestra el código del bloque de markdown en lugar de renderizarlo.

Por qué pasa: El modelo a veces devuelve markdown con triple backtick incorrecto, o caracteres especiales que GitHub interpreta literalmente.

Cómo corregir: Validar el markdown antes de publicar. Si el comment es muy largo, considerar partirlo en varios o dejar solo el resumen y poner detalles en artifact.

Error 5: Permisos faltantes del GITHUB_TOKEN

Síntoma: El script falla con 403 Forbidden al intentar crear el comment.

Por qué pasa: El YAML no declara permissions: pull-requests: write. GitHub usa permisos default restrictivos.

Cómo corregir: Agregar el bloque permissions: explícito al workflow o al job. Mínimo necesario: pull-requests: write, contents: read.


Diagnóstico

Pregunta 1: Si tu PR tiene 5 actualizaciones, ¿cuántos comments del bot debe haber al final?

Respuesta correcta: 1. Si tienes 5, te falta deduplicación (Error 1). Si tienes 0, el script no se está ejecutando o falla silenciosamente.

Pregunta 2: ¿Sabes por qué el GITHUB_TOKEN no requiere configurar como secret manualmente?

GitHub lo genera automáticamente para cada workflow run. Está disponible vía ${{ secrets.GITHUB_TOKEN }} siempre. Sus permisos se controlan con el bloque permissions: del workflow.

Pregunta 3: Si Claude Code identifica 30 issues, ¿cómo los presentas?

10 más críticos como annotations (límite del workflow), resumen + lista en PR comment, JSON completo como artifact. Combinar formatos según volumen.

Pregunta 4: ¿Por qué retention-days importa?

Sin configurarlo, artifacts persisten 90 días. Acumular runs de PRs cerrados llena la quota de storage de la org. Para análisis efímero, 30 o menos días es razonable.

Pregunta 5: Si el bot publica un comment pero el formato markdown se ve mal, ¿qué pruebas primero?

Imprimir el body antes de enviarlo y revisar el markdown a mano. A veces el modelo cierra mal un code fence o usa caracteres que GitHub no renderiza igual que un editor local.


Ejercicios

Ejercicio 1: Implementar PR comment (Fácil)

Toma el script de la cápsula 02 y agregale la publicación como PR comment usando la API. Verifica que aparece en tu PR de prueba.

Ejercicio 2: Deduplicación de comments (Medio)

Modifica el script para que actualice el comment existente en lugar de crear uno nuevo cada vez. Usa un marcador único en el body (ej. <!-- claude-code-bot -->) para identificar el comment del bot.

Ver solución
MARKER = "<!-- claude-code-bot -->"
body_with_marker = f"{MARKER}\n\n## 🤖 Análisis\n\n{analysis}"

# Buscar
existing = requests.get(list_url, headers=headers).json()
bot_comments = [c for c in existing if MARKER in c["body"]]

if bot_comments:
    update_url = f"https://api.github.com/repos/{repo}/issues/comments/{bot_comments[0]['id']}"
    requests.patch(update_url, headers=headers, json={"body": body_with_marker})
else:
    requests.post(list_url, headers=headers, json={"body": body_with_marker})

El marcador HTML comment es invisible para el usuario pero detectable por el script.

Ejercicio 3: Combinar los tres formatos (Difícil)

Implementa un script que combine los tres formatos: PR comment con resumen, annotations para top 10 issues, artifact con reporte JSON completo. Verifica los tres en un PR de prueba.


Resumen

  • Tres formatos de output: PR comments (alta visibilidad), annotations (issues localizados), artifacts (datos detallados)
  • PR comments son el lugar más visible — usa para resumen ejecutivo
  • Annotations son ideales para issues en líneas específicas — máximo 50 por run
  • Artifacts son para reportes detallados que no caben en comments
  • Combinar los tres es el patrón profesional
  • Deduplicar comments evita spam en PRs con muchas actualizaciones
  • Permisos del GITHUB_TOKEN deben declararse explícitos: pull-requests: write

Próxima cápsula: 05 — Costos y rate limiting. Tu workflow ya es funcional, seguro, y produce output útil. Falta la última pieza: que sea económicamente sustentable. Aprendes a calcular costos por run, ejecutar selectivamente, y cuándo skipear ejecuciones innecesarias.


Recursos Adicionales

  1. GitHub REST API: Comments — Documentación de la API de comments
  2. GitHub Actions: Workflow Commands — Sintaxis de annotations
  3. actions/upload-artifact — Action oficial de artifacts
  4. GitHub Actions: Permissions — Configurar permisos del GITHUB_TOKEN
  5. Octokit — Cliente oficial de GitHub API en JS/TS
  6. GitHub API rate limits — Límites a considerar