Módulo 6: Proyecto — Pipeline CI/CD Completo con Claude Code
Implementación: Stages 1-3 (PR Review)
Implementación: Stages 1-3 (PR Review)
Descripción
Esta cápsula es donde empiezas a construir el pipeline. Tomas el diseño de la cápsula 02 y implementas la Fase 1: los 3 stages de PR review que corren en paralelo en cada pull request — code review automático, security scan, y tests + linting.
Esta es la fase más visible del pipeline para el equipo: cada developer la va a ver en cada PR. Vale la pena hacerla bien — reusable, rápida, con feedback útil. Vas a integrar las técnicas de los Módulos 1, 2 y 5 en un único workflow coherente, evitando duplicación y aprovechando paralelismo.
Al terminar, vas a tener la primera fase del pipeline funcionando end-to-end en un repositorio de prueba — un cimiento sólido para las fases 2-4.
La Estructura del Workflow
# .github/workflows/pr-review.yml
name: PR Review (Phase 1)
on:
pull_request:
types: [opened, synchronize, ready_for_review]
paths:
- 'src/**'
- 'tests/**'
- 'package.json'
- 'requirements.txt'
permissions:
contents: read
pull-requests: write
security-events: write
concurrency:
group: pr-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
# Job común: extraer diff (output usado por los demás)
extract-diff:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
outputs:
diff_size: ${{ steps.extract.outputs.diff_size }}
should_review: ${{ steps.extract.outputs.should_review }}
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Extract filtered diff
id: extract
run: |
git diff origin/${{ github.base_ref }}...HEAD \
--diff-filter=ACMR \
-- '*.py' '*.ts' '*.tsx' '*.js' '*.jsx' '*.go' '*.rb' \
> pr_diff.txt
SIZE=$(wc -l < pr_diff.txt)
echo "diff_size=$SIZE" >> $GITHUB_OUTPUT
if [ "$SIZE" -eq 0 ]; then
echo "should_review=false" >> $GITHUB_OUTPUT
elif [ "$SIZE" -gt 2000 ]; then
echo "should_review=large" >> $GITHUB_OUTPUT
else
echo "should_review=true" >> $GITHUB_OUTPUT
fi
- uses: actions/upload-artifact@v4
with:
name: pr-diff
path: pr_diff.txt
retention-days: 1
# ========================================
# Stages paralelos
# ========================================
code-review:
needs: extract-diff
if: needs.extract-diff.outputs.should_review == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11', cache: 'pip' }
- run: pip install anthropic requests
- uses: actions/download-artifact@v4
with: { name: pr-diff }
- name: Run code 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 }}
CLAUDE_MODEL: 'claude-haiku-4-5'
run: python scripts/code_review.py
security-scan:
needs: extract-diff
if: needs.extract-diff.outputs.should_review == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11', cache: 'pip' }
- run: pip install anthropic requests
- uses: actions/download-artifact@v4
with: { name: pr-diff }
- name: Run security scan
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
CLAUDE_MODEL: 'claude-sonnet-5'
run: python scripts/security_scan.py
tests-and-linting:
needs: extract-diff
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11', cache: 'pip' }
- run: pip install -r requirements.txt
- name: Run linting
run: |
pip install ruff
ruff check .
- name: Run tests
run: pytest --cov=src --cov-report=xml
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
# Job de rollup que reporta el estado consolidado
pr-review-summary:
needs: [code-review, security-scan, tests-and-linting]
if: always() # corre incluso si jobs anteriores fallan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11' }
- run: pip install requests
- name: Post consolidated summary
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
CODE_REVIEW_STATUS: ${{ needs.code-review.result }}
SECURITY_SCAN_STATUS: ${{ needs.security-scan.result }}
TESTS_STATUS: ${{ needs.tests-and-linting.result }}
run: python scripts/post_consolidated_summary.py
Lo importante de la estructura:
extract-diffcorre primero (rápido, output usado por todos)- Los 3 stages corren en paralelo (independientes entre sí)
pr-review-summaryconsolida al final, incluso si algunos fallanconcurrencycancela runs viejos del mismo PR (cápsula 05 M1)
El Job de Extracción del Diff
"""scripts/extract_pr_files.py — usado por extract-diff job"""
# Si el bash inline no es suficiente, puedes moverlo a un script Python:
import json
import os
import subprocess
import sys
RELEVANT_EXTENSIONS = {".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".rb"}
def main() -> int:
base_ref = os.environ["GITHUB_BASE_REF"]
# Asegurarse de tener el remote actualizado
subprocess.run(["git", "fetch", "origin", base_ref], check=True)
# Generar diff filtrado
result = subprocess.run(
["git", "diff", f"origin/{base_ref}...HEAD",
"--diff-filter=ACMR",
"--", "*.py", "*.ts", "*.tsx", "*.js", "*.jsx", "*.go", "*.rb"],
capture_output=True, text=True, check=True,
)
diff = result.stdout
# Guardar
with open("pr_diff.txt", "w") as f:
f.write(diff)
# Stats
diff_lines = len(diff.splitlines())
# Decidir si vale la pena revisar
if diff_lines == 0:
should_review = "false"
elif diff_lines > 2000:
should_review = "large" # señal para chunking más agresivo
else:
should_review = "true"
# Output para el siguiente step
with open(os.environ.get("GITHUB_OUTPUT", "/dev/stdout"), "a") as f:
f.write(f"diff_size={diff_lines}\n")
f.write(f"should_review={should_review}\n")
print(f"Diff size: {diff_lines} lines")
print(f"Should review: {should_review}")
return 0
if __name__ == "__main__":
sys.exit(main())
Code Review Job (Reusing Module 2)
El script ya lo construiste en el Módulo 2 cápsula 03. Acá lo reusas como está:
# scripts/code_review.py
# Script del Módulo 2 cápsula 03 — sin cambios.
# Toma pr_diff.txt como input, postea inline comments al PR.
Punto clave: no reescribir el código de Módulo 2. Reusarlo. Si en el M2 cápsula 03 implementaste correctamente, este job es plug-and-play.
Security Scan Job (Reusing Module 5)
Similar al code review, el script viene del Módulo 5 cápsula 02:
# scripts/security_scan.py
# Script del Módulo 5 cápsula 02 — sin cambios.
# Detecta vulnerabilidades, postea findings al PR.
Diferencias clave entre code review y security scan:
CODE REVIEW (M2):
- Modelo: Haiku (más económico)
- Operación: suggest-only por default
- Output: PR comment + inline comments
- Failure behavior: warn (no bloquea)
SECURITY SCAN (M5):
- Modelo: Sonnet (más calidad)
- Operación: bloquea en critical findings
- Output: PR comment estructurado
- Failure behavior: block on critical
Cada uno con su prompt, modelo, y política — pero en el mismo workflow paralelo.
Tests and Linting Job
tests-and-linting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- run: pip install -r requirements.txt
- name: Linting (ruff)
run: |
pip install ruff
ruff check . --output-format=github
- name: Type checking (mypy, opcional)
continue-on-error: true # warning, no block
run: |
pip install mypy
mypy src/
- name: Unit tests
run: pytest tests/ --cov=src --cov-report=xml --cov-fail-under=70
- name: Upload coverage
if: always()
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
Notas:
ruff check . --output-format=githubproduce annotations nativas (warnings inline en el PR)mypyconcontinue-on-error: truepara warning sin block--cov-fail-under=70requiere coverage mínimo de 70%
El Summary Consolidado
El job final del workflow es rollup de status — toma los resultados de los 3 jobs paralelos y postea un resumen claro al PR:
"""scripts/post_consolidated_summary.py
Postea un comment unificado al PR resumiendo los 3 jobs.
"""
import os
import sys
import requests
MARKER = "<!-- pr-review-phase-1-summary -->"
def status_emoji(status: str) -> str:
return {
"success": "✅",
"failure": "❌",
"cancelled": "🚫",
"skipped": "⏭️",
}.get(status, "❓")
def status_label(status: str) -> str:
return {
"success": "PASS",
"failure": "FAIL",
"cancelled": "CANCELLED",
"skipped": "SKIPPED",
}.get(status, "UNKNOWN")
def build_summary() -> str:
code_review = os.environ.get("CODE_REVIEW_STATUS", "skipped")
security = os.environ.get("SECURITY_SCAN_STATUS", "skipped")
tests = os.environ.get("TESTS_STATUS", "skipped")
overall_pass = all(s == "success" for s in [code_review, security, tests])
return f"""{MARKER}
# 🤖 PR Review Summary (Phase 1)
| Check | Status |
|-------|--------|
| Code Review | {status_emoji(code_review)} {status_label(code_review)} |
| Security Scan | {status_emoji(security)} {status_label(security)} |
| Tests + Linting | {status_emoji(tests)} {status_label(tests)} |
{'✅ **Todos los checks pasaron — PR listo para review humano.**' if overall_pass
else '⚠️ **Hay checks que requieren atención antes del merge.**'}
---
### Detalles
- **Code review:** ver comments inline arriba (con marker `claude-code-bot`)
- **Security scan:** ver comment con marker `security-scan-bot`
- **Tests + linting:** ver detalle en el [workflow run]({get_run_url()})
*Resumen generado automáticamente.*
"""
def get_run_url() -> str:
repo = os.environ.get("GITHUB_REPOSITORY", "")
run_id = os.environ.get("GITHUB_RUN_ID", "")
return f"https://github.com/{repo}/actions/runs/{run_id}"
def find_existing_summary(comments: list) -> dict | None:
for c in comments:
if MARKER in c.get("body", ""):
return c
return None
def main() -> int:
repo = os.environ["GITHUB_REPOSITORY"]
pr_number = os.environ["PR_NUMBER"]
token = os.environ["GITHUB_TOKEN"]
body = build_summary()
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
}
list_url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
existing = requests.get(list_url, headers=headers).json()
summary_comment = find_existing_summary(existing)
if summary_comment:
# Update
update_url = f"https://api.github.com/repos/{repo}/issues/comments/{summary_comment['id']}"
r = requests.patch(update_url, headers=headers, json={"body": body})
else:
# Create
r = requests.post(list_url, headers=headers, json={"body": body})
if r.status_code in [200, 201]:
print("Consolidated summary posted")
return 0
print(f"ERROR: {r.status_code} {r.text}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
Resultado en el PR: un único comment al inicio del PR con el resumen de los 3 jobs. Cada job puede tener su propio comment detallado, pero el summary es la vista panorámica.
Optimizaciones Importantes
1. Cache de pip
Cada job instala dependencias. Sin cache, son ~30s por job × 3 = ~90s desperdiciados.
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip' # ← cache automático
Reduce a ~5s por job.
2. Concurrency: cancelar runs viejos
concurrency:
group: pr-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
Si el developer hace 3 pushes seguidos al PR, solo el último completa los 3 jobs. Los anteriores se cancelan.
3. Filtrado por path
El workflow solo dispara si el PR toca código fuente (no docs, configs):
on:
pull_request:
paths:
- 'src/**'
- 'tests/**'
- 'package.json'
- 'requirements.txt'
Cambios solo a docs no necesitan code review automático.
4. Skipear PRs en draft
extract-diff:
if: github.event.pull_request.draft == false
Ahorra tokens en trabajo en progreso.
5. Skipear PRs sin diff relevante
El job extract-diff ya output should_review. Los demás jobs lo respetan:
code-review:
if: needs.extract-diff.outputs.should_review == 'true'
Si el PR solo cambia archivos no relevantes, code review skipea.
Trampas Comunes
1. Stages dependiendo de un mismo job sin necesidad
Síntoma: Code review espera a security scan aunque son independientes.
Por qué pasa: Configurar needs: por costumbre.
Cómo corregir: needs: solo cuando hay dependencia real (output de un stage usado por otro). Code review y security scan ambos dependen de extract-diff, pero no entre ellos. Pueden correr en paralelo.
2. Diff extraído 3 veces (uno por stage)
Síntoma: Cada stage hace su propio git diff. Repetir lo mismo 3 veces.
Por qué pasa: Cada job es independiente y nadie centralizó la extracción.
Cómo corregir: Job extract-diff que corre primero y publica como artifact. Los demás stages lo descargan.
3. Comments duplicados o conflictivos
Síntoma: PR con 5 comments del bot — uno por job y dos del summary.
Por qué pasa: Cada stage postea su comment, sumary también, sin coordinación.
Cómo corregir: Markers únicos por tipo de comment (<!-- code-review-bot -->, <!-- security-scan-bot -->, <!-- pr-review-summary -->). Cada stage actualiza su comment en lugar de crear nuevo.
4. Workflow tarda 10+ minutos
Síntoma: Phase 1 que debería ser ~5 min se demora 12.
Por qué pasa: Sin paralelización, sin cache, sin filtrado.
Cómo corregir: Aplicar las 5 optimizaciones de arriba. Phase 1 debería ser ~5 min con paralelización + cache.
5. if: always() en summary que oculta failures
Síntoma: Summary muestra "todo OK" pero un job falló.
Por qué pasa: El summary tiene if: always() para correr incluso con fails, pero el body no refleja correctamente los status.
Cómo corregir: El summary recibe ${{ needs.X.result }} y construye el comment basado en esos status. La función build_summary() arriba lo hace correctamente.
Diagnóstico
Pregunta 1: ¿Tu workflow tiene los 3 stages corriendo en paralelo?
Si están secuenciales, el pipeline tarda 3x más. Paralelización con needs: extract-diff pero independientes entre sí.
Pregunta 2: ¿Cada stage tiene su propio comment con marker único?
Markers permiten actualizar en lugar de duplicar. Sin markers, después de 5 pushes hay 15 comments del bot.
Pregunta 3: ¿Tu workflow respeta paths filter y skipea drafts?
Sin estos filtros, gastas tokens en cosas que no necesitan review (docs, work-in-progress).
Pregunta 4: ¿Tienes job de summary consolidado?
Sin summary, el equipo tiene que mirar 3 lugares. Con summary, todo en un comment.
Pregunta 5: ¿Configuraste `concurrency` para cancelar runs viejos?
Sin esto, 5 pushes seguidos = 5 runs paralelos = 5x los costos.
Ejercicios
Ejercicio 1: Workflow básico (Medio)
Implementa el workflow con:
- Job
extract-diff - 3 jobs paralelos (code-review, security-scan, tests)
- Job
summarycon consolidación
Probarlo en un PR de prueba. Verifica:
- Los 3 jobs corren en paralelo
- El summary refleja correctamente los status
- Tarda menos que la suma de los 3 individualmente
Ejercicio 2: Optimizaciones (Fácil)
Agrega las 5 optimizaciones:
- Cache de pip
- Concurrency con cancel
- Paths filter
- Skip drafts
- Skip PRs sin diff relevante
Medir el tiempo antes y después.
Ejercicio 3: Markers únicos por stage (Difícil)
Modifica los scripts (code review, security scan, summary) para que cada uno tenga su propio marker y actualice comments existentes en lugar de crear nuevos.
Probarlo: 3 pushes al mismo PR debería resultar en exactamente 3 comments del bot al final, no 9.
Resumen
- Phase 1 paralela: code review + security scan + tests + linting corren simultáneamente
- Job común
extract-diffevita duplicar trabajo en cada stage - Job de summary consolidado da vista panorámica
- Markers únicos (
<!-- code-review-bot -->, etc.) evitan duplicación de comments - 5 optimizaciones clave: cache, concurrency, paths filter, skip drafts, skip si no hay diff
- Reuso de scripts de Módulos 2 y 5 — no reescribir
- Phase 1 target: ~5 min desde push hasta summary visible
Próxima cápsula: 04 — Implementación: Stages 4-6 (Deployment). Tienes Phase 1 funcionando. Ahora implementas Phase 2-3: changelog, readiness, deploy a staging, approval gate, deploy a producción. La parte que mueve código del repo al ambiente de usuarios.
Recursos Adicionales
- GitHub Actions: jobs in parallel — Sintaxis de paralelización
- GitHub Actions: caching — Cache strategies
- actions/upload-artifact — Pasar archivos entre jobs
- ruff — Linter ultra-rápido
- Codecov — Coverage tracking
- GitHub Actions: workflow status — Acceder a status de jobs anteriores