Módulo 1: Claude Code en GitHub Actions

Tu Primer Workflow YAML con Claude Code

Tu Primer Workflow YAML con Claude Code

Descripción

Esta es la cápsula donde Claude Code deja de ser una herramienta local y empieza a ser un agente automático. Vas a crear, paso a paso, tu primer workflow de GitHub Actions que ejecuta Claude Code en cada pull request — sin que tengas que abrir nada, sin que dependa de tu memoria.

Al terminar la cápsula, vas a poder escribir un workflow YAML desde cero que se dispara en eventos de PR, ejecuta el SDK headless de Claude Code, y produce un output verificable. No es teoría — es un YAML ejecutable que copias, haces push, y ves correr en tu repositorio.


Anatomía de un Workflow de GitHub Actions

Antes de escribir código, una imagen mental: un workflow de GitHub Actions tiene tres niveles jerárquicos.

WORKFLOW (un archivo .yml en .github/workflows/)
├── name: nombre humano del workflow
├── on: cuándo se dispara (triggers)
└── jobs:
    ├── JOB-1
    │   ├── runs-on: en qué máquina corre (ubuntu, macos, windows)
    │   ├── steps:
    │   │   ├── STEP-1 (ej. checkout del código)
    │   │   ├── STEP-2 (ej. setup de Python)
    │   │   ├── STEP-3 (ej. ejecutar Claude Code)
    │   │   └── STEP-N
    │   └── env / outputs / etc.
    ├── JOB-2 (puede correr en paralelo o esperar a JOB-1)
    └── ...

Workflow es el archivo entero. Job es un conjunto de pasos que corren en una misma máquina. Step es la unidad atómica — una acción o un comando shell.


El Workflow Mínimo Funcional

Empezamos con un workflow que hace una sola cosa: cuando se abre un PR, ejecuta Claude Code para analizar el diff y publica el resultado en los logs.

Crea el archivo .github/workflows/claude-code-analysis.yml:

name: Claude Code Analysis

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  analyze:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install Claude Code SDK
        run: pip install anthropic

      - name: Get PR diff
        id: diff
        run: |
          git diff origin/${{ github.base_ref }}...HEAD > pr_diff.txt
          echo "diff_size=$(wc -l < pr_diff.txt)" >> $GITHUB_OUTPUT

      - name: Run Claude Code analysis
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: python .github/scripts/analyze_pr.py

Y crea el script .github/scripts/analyze_pr.py:

"""Análisis básico de un PR con Claude Code SDK."""
import os
from pathlib import Path
from anthropic import Anthropic

client = Anthropic()  # lee ANTHROPIC_API_KEY del environment

diff_text = Path("pr_diff.txt").read_text()

if not diff_text.strip():
    print("No hay cambios para analizar.")
    exit(0)

response = client.messages.create(
    model="claude-haiku-4-5",
    max_tokens=2000,
    messages=[
        {
            "role": "user",
            "content": (
                "Analiza este diff de un pull request. "
                "Identifica:\n"
                "1. Qué cambia funcionalmente.\n"
                "2. Posibles problemas (bugs, edge cases, seguridad).\n"
                "3. Sugerencias concretas de mejora.\n\n"
                f"Diff:\n```\n{diff_text}\n```"
            ),
        }
    ],
)

print("=== Análisis de Claude Code ===")
print(response.content[0].text)
print(f"\nTokens usados: {response.usage.input_tokens} in / {response.usage.output_tokens} out")

Comando de ejecución: push estos dos archivos a una rama, abre un PR contra main, y espera unos segundos. El workflow se dispara automáticamente. En la pestaña "Actions" del repositorio vas a ver el run; en sus logs aparece el análisis de Claude Code.

Output esperado (ejemplo):

=== Análisis de Claude Code ===
Este PR modifica `src/payment_service.py` agregando una nueva función
`apply_discount(amount, percentage)`.

Posibles problemas:
1. La función no valida que `percentage` esté entre 0 y 100. Si pasas
   `percentage=150`, devuelve un monto negativo.
2. La operación es punto flotante; con dinero, considera usar Decimal
   para evitar errores de redondeo.

Sugerencias:
- Agregar validación de `percentage` con un raise apropiado.
- Cambiar a `Decimal` para los cálculos monetarios.

Tokens usados: 850 in / 245 out

Diseccionando el YAML pieza por pieza

Cada parte del workflow tiene un propósito específico. Vamos línea por línea.

Triggers: cuándo se dispara

on:
  pull_request:
    types: [opened, synchronize]
  • pull_request activa el workflow en eventos de PR
  • types: [opened, synchronize] lo limita a dos eventos:
    • opened — se abrió el PR
    • synchronize — se hizo push a la rama del PR (cambios nuevos)

Sin types, el workflow correría también en closed, reviewed, labeled, etc. — generando ejecuciones que no necesitas. Restringir es ahorrar costos.

Job y máquina

jobs:
  analyze:
    runs-on: ubuntu-latest
  • analyze es el nombre interno del job (usado para referencias)
  • runs-on: ubuntu-latest es la máquina virtual donde corre. Para Claude Code en CI, Ubuntu es la opción más rápida y económica.

Steps esenciales

Step 1 — Checkout del código

- name: Checkout code
  uses: actions/checkout@v4
  with:
    fetch-depth: 0

actions/checkout@v4 es la action oficial que clona el repo en la máquina del runner. Crítico: fetch-depth: 0 clona el historial completo. Sin esto, el git diff del paso siguiente no puede comparar contra main porque el runner solo tiene un commit.

Step 2 — Setup de Python

- name: Setup Python
  uses: actions/setup-python@v5
  with:
    python-version: '3.11'

Instala Python en el runner. Versión 3.11 es estable y soportada por el SDK de Anthropic.

Step 3 — Instalar el SDK

- name: Install Claude Code SDK
  run: pip install anthropic

Instala el paquete oficial. Para producción, vas a querer pinear la versión: pip install anthropic==0.39.0 (o la última estable).

Step 4 — Extraer el diff

- name: Get PR diff
  id: diff
  run: |
    git diff origin/${{ github.base_ref }}...HEAD > pr_diff.txt
    echo "diff_size=$(wc -l < pr_diff.txt)" >> $GITHUB_OUTPUT
  • github.base_ref es la rama destino del PR (típicamente main)
  • origin/main...HEAD es el rango del diff
  • El output se guarda en pr_diff.txt para que el step siguiente lo use
  • echo ... >> $GITHUB_OUTPUT exporta la variable diff_size para uso opcional en steps posteriores

Step 5 — Ejecutar Claude Code

- name: Run Claude Code analysis
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
  run: python .github/scripts/analyze_pr.py
  • ${{ secrets.ANTHROPIC_API_KEY }} referencia un secret configurado en el repo (la cápsula 03 lo desarrolla)
  • env lo pasa como variable de environment al script Python
  • El script lee el diff y produce el análisis

Trabajando el Workflow Localmente Antes del Push

Una buena práctica: probar el script Python local antes de subirlo al CI.

# En tu máquina, en la raíz del repo
export ANTHROPIC_API_KEY="sk-ant-..."

# Genera un diff de prueba
git diff main...HEAD > pr_diff.txt

# Corre el script localmente
python .github/scripts/analyze_pr.py

Si el script funciona local, también va a funcionar en GitHub Actions (el environment es predecible). Esta práctica te ahorra el ciclo "push → ver fallar → corregir → push de nuevo".


Trampas Comunes en el Primer Workflow

Cinco errores que aparecen específicamente al escribir tu primer workflow de Claude Code.

Error 1: Olvidar fetch-depth: 0

Síntoma: El step "Get PR diff" falla con un mensaje sobre que el commit base no existe.

Por qué pasa: Por defecto, actions/checkout hace shallow clone (solo el último commit). Sin el historial completo, git diff origin/main...HEAD no puede resolver origin/main.

Cómo corregir: Siempre incluir fetch-depth: 0 cuando necesitas comparar contra otra rama.

Error 2: Sintaxis YAML inválida

Síntoma: GitHub muestra "invalid workflow file" antes de ejecutar.

Por qué pasa: YAML es sensible a indentación. Mezclar tabs y espacios, indentar mal un step, o poner dos puntos sin espacio rompe el parser.

Cómo corregir: Usa un editor con linting de YAML (VS Code con la extensión YAML lo hace nativamente). Antes de pushear, valida el archivo con yamllint o el linter integrado de GitHub Actions.

Error 3: Hardcodear la API key "para probar"

Síntoma: El workflow funciona, pero en el historial de git queda ANTHROPIC_API_KEY: sk-ant-real....

Por qué pasa: Es la tentación clásica al hacer debug. "La pongo aquí para que funcione, después la muevo a Secrets". Pero cualquier commit con la key queda público para siempre (incluso si después la rotas).

Cómo corregir: Configura GitHub Secrets desde el primer commit (cápsula 03). Si por accidente comiteas una key, revócala inmediatamente desde la consola de Anthropic y rota.

Error 4: Triggers demasiado amplios

Síntoma: El workflow corre en cada push de cada rama, generando docenas de runs por día y costos no esperados.

Por qué pasa: Configurar on: push sin filtros, o on: [push, pull_request] sin restringir tipos.

Cómo corregir: Empezar con triggers específicos (pull_request: types: [opened, synchronize]) y agregar más solo si hay necesidad real. La cápsula 05 desarrolla estrategias de selectividad.

Error 5: El script Python falla y el workflow muestra "success"

Síntoma: El workflow se marca como exitoso aunque el script falló silenciosamente.

Por qué pasa: El script puede capturar la excepción y no propagar el error. Si pr_diff.txt está vacío o el SDK falla, el script termina con exit code 0 aunque no produjo análisis.

Cómo corregir: Validar exit codes en el script. Usar exit(1) en errores. En el step del workflow, evitar continue-on-error: true excepto cuando es intencional.


Diagnóstico: Verifica Tu Primer Workflow

Pregunta 1: ¿Tu archivo YAML está en `.github/workflows/`?

GitHub solo detecta workflows en esa carpeta. Si lo pusiste en otro lado, no se dispara.

Pregunta 2: ¿Configuraste el secret `ANTHROPIC_API_KEY` en el repo?

Settings → Secrets and variables → Actions → New repository secret. Si no está configurado, el workflow corre pero el script falla con "API key missing". La cápsula 03 desarrolla esto.

Pregunta 3: ¿Probaste el script Python localmente antes de pushear?

Si funciona local, casi seguro funciona en CI. Si nunca lo probaste local, vas a iterar varios push-fail-push antes de que funcione.

Pregunta 4: ¿El workflow corrió cuando abriste el PR?

Si no aparece en "Actions", revisa: (1) que el archivo esté en la rama target del PR, (2) que la sintaxis YAML sea válida, (3) que los triggers on: incluyan pull_request.

Pregunta 5: ¿El log del workflow muestra el análisis de Claude Code?

Si dice "No hay cambios para analizar", el diff salió vacío (probablemente fetch-depth mal). Si muestra error de API, revisa el secret. Si muestra el análisis: el primer workflow funciona.


Ejercicios

Ejercicio 1: Configurar el primer workflow (Fácil)

Crea los dos archivos (workflow + script Python) en un repositorio de prueba. Configura el secret ANTHROPIC_API_KEY. Abre un PR con un cambio simple. Verifica que el análisis aparece en los logs.

Ver checklist de verificación
  • .github/workflows/claude-code-analysis.yml creado en la rama target
  • .github/scripts/analyze_pr.py creado
  • Secret ANTHROPIC_API_KEY configurado en Settings
  • PR abierto contra la rama que tiene los archivos
  • Workflow run aparece en pestaña "Actions"
  • Logs muestran el análisis con texto del modelo

Ejercicio 2: Cambiar el trigger (Medio)

Modifica el workflow para que corra también cuando se agrega la label needs-review al PR (no solo en opened/synchronize). Verifica que funciona agregando la label manualmente.

Ver solución
on:
  pull_request:
    types: [opened, synchronize, labeled]

jobs:
  analyze:
    runs-on: ubuntu-latest
    if: github.event.action != 'labeled' || github.event.label.name == 'needs-review'
    steps:
      # ... resto igual

El if filtra: si la action es labeled, solo continúa cuando la label es needs-review. Para opened y synchronize, el workflow corre siempre.

Ejercicio 3: Manejo robusto de errores (Medio)

Modifica el script Python para que:

  1. Falle con exit code 1 si el diff está vacío (en lugar de exit 0).
  2. Capture errores de la API y los muestre claramente antes de terminar con exit 1.
Ver solución
import os, sys
from pathlib import Path
from anthropic import Anthropic, APIError

try:
    diff_text = Path("pr_diff.txt").read_text()
except FileNotFoundError:
    print("ERROR: pr_diff.txt no encontrado.", file=sys.stderr)
    sys.exit(1)

if not diff_text.strip():
    print("ERROR: el diff está vacío. Revisa fetch-depth.", file=sys.stderr)
    sys.exit(1)

try:
    client = Anthropic()
    response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=2000,
        messages=[{"role": "user", "content": f"Analiza este diff:\n{diff_text}"}],
    )
    print(response.content[0].text)
except APIError as e:
    print(f"ERROR de API: {e}", file=sys.stderr)
    sys.exit(1)

Resumen

  • Workflow YAML vive en .github/workflows/ y se dispara según los on: configurados
  • Triggers específicos (no on: push sin filtro) ahorran costos
  • fetch-depth: 0 es necesario para comparar contra otra rama
  • El SDK headless se invoca desde un script Python externo, no inline en el YAML
  • Probar local antes de pushear ahorra ciclos de iteración
  • Validar exit codes evita que el workflow muestre "success" cuando el script falló silenciosamente

Próxima cápsula: 03 — Secrets management con GitHub Secrets. La parte que no negociamos: cómo manejar tu API key sin exponerla, en qué nivel configurar el secret (repo / org / environment), y qué hacer si por accidente la commiteas.


Recursos Adicionales

  1. GitHub Actions Workflow Syntax — Referencia oficial completa
  2. actions/checkout — Action oficial de checkout
  3. actions/setup-python — Action oficial de setup Python
  4. Anthropic Python SDK — SDK oficial
  5. GitHub Actions Pricing — Costos de minutos de runner
  6. yamllint — Linter de YAML para validar antes de pushear