Módulo 4: Deployment Automation

Deployment Readiness Validation

Deployment Readiness Validation

Descripción

"Tests pasan" no significa "está listo para producción". Esta cápsula te enseña a construir un checklist automatizado que Claude Code verifica antes de cada deploy, cubriendo lo que los tests no detectan: migraciones de DB pendientes, env vars faltantes, dependencias deprecated, breaking changes no documentados, y runtime mismatches entre staging y producción.

La pregunta que responde el módulo: ¿el sistema está realmente listo para deployar? No solo "el código compila", sino "todas las piezas necesarias están en su lugar".

Al terminar, vas a tener un workflow que ejecuta una validación de readiness pre-deploy, reporta cualquier issue, y bloquea el deployment si algo crítico falta.


Por Qué Tests Verdes No Alcanzan

TESTS VERDES VERIFICAN:
✅ El código compila/se interpreta sin errores
✅ Las funciones individuales funcionan como esperas
✅ Las integraciones internas funcionan
✅ Los happy paths cubiertos pasan

TESTS VERDES NO VERIFICAN:
❌ Si la migración de DB está aplicada en el ambiente target
❌ Si las env vars necesarias están configuradas en producción
❌ Si las dependencias del nuevo código están instaladas en el container
❌ Si breaking changes están documentados y comunicados
❌ Si las features nuevas están detrás de feature flags
❌ Si los recursos de infraestructura (queues, buckets, etc.) existen

Readiness validation llena ese gap. Es la diferencia entre "el código funciona en mi máquina" y "el sistema está listo para servir tráfico real".


El Checklist de Readiness Estándar

1. CÓDIGO
   □ Tests pasan (todos los suites)
   □ Linting pasa
   □ Type checking pasa
   □ Sin TODO marcados como "blocker"

2. BASE DE DATOS
   □ Si hay migrations nuevas → están en el orden correcto
   □ Migrations son idempotentes (IF NOT EXISTS)
   □ Down migrations existen para rollback
   □ Cambios destructivos están coordinados (no romper production con DROP)

3. CONFIGURACIÓN
   □ Env vars nuevas requeridas están documentadas
   □ Env vars sensibles están en el secret manager (no .env)
   □ Cambios en config tienen default safe

4. DEPENDENCIAS
   □ Nuevas dependencias en requirements.txt / package.json
   □ Sin dependencies con CVEs conocidos críticos
   □ Versiones pineadas (no `latest`)

5. BREAKING CHANGES
   □ Si hay breaking change → documentado en changelog
   □ Si hay deprecation → tiene migration path
   □ Si afecta clientes externos → comunicado al equipo

6. INFRAESTRUCTURA
   □ Recursos nuevos (queues, buckets, etc.) existen en el ambiente target
   □ Permisos IAM/RBAC actualizados si necesario
   □ Capacidad/quotas suficientes para el cambio esperado

7. OBSERVABILIDAD
   □ Métricas nuevas configuradas
   □ Alerts apropiadas si feature crítico
   □ Logging adecuado para debugging post-deploy

8. ROLLBACK
   □ Plan de rollback documentado
   □ Tag/release del estado anterior accesible
   □ Tiempo target de rollback definido (RTO)

El Workflow de Validación

# .github/workflows/deployment-readiness.yml
name: Deployment Readiness Check

on:
  push:
    branches: [main]
  workflow_dispatch:  # también disparable manualmente

permissions:
  contents: read
  pull-requests: write

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'
      
      - run: pip install anthropic requests
      
      - name: Validate readiness
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
          TARGET_ENV: production
        run: python scripts/validate_readiness.py
      
      - name: Upload readiness report
        if: always()  # subir incluso si falla
        uses: actions/upload-artifact@v4
        with:
          name: readiness-report
          path: readiness_report.json

El Script de Validación

"""scripts/validate_readiness.py

Valida que el sistema está listo para deployar.
Combina checks programáticos con análisis con Claude Code.
"""
import json
import os
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
from anthropic import Anthropic


@dataclass
class CheckResult:
    name: str
    category: str
    status: str  # "pass", "warning", "fail"
    message: str
    details: list[str] = field(default_factory=list)


def check_tests_passing() -> CheckResult:
    """Asume que CI ya corrió tests. Verifica que el último commit
    tenga status check positivo."""
    # Para simplicidad: verificar que existe un test directory
    # En producción real, consultarías la API de checks
    has_tests = (
        Path("tests").exists() or
        Path("test").exists() or
        Path("__tests__").exists()
    )
    
    if has_tests:
        return CheckResult(
            name="Tests existen",
            category="código",
            status="pass",
            message="Directorio de tests detectado",
        )
    return CheckResult(
        name="Tests existen",
        category="código",
        status="fail",
        message="No hay directorio de tests",
    )


def check_migrations() -> CheckResult:
    """Verificar migrations nuevas y su consistencia."""
    migrations_dir = next(
        (d for d in [Path("migrations"), Path("alembic/versions"), Path("db/migrate")]
         if d.exists()),
        None,
    )
    
    if not migrations_dir:
        return CheckResult(
            name="Migrations",
            category="base de datos",
            status="pass",
            message="No hay directorio de migrations (proyecto sin DB)",
        )
    
    # Ver migrations agregadas en el último commit
    result = subprocess.run(
        ["git", "diff", "--name-only", "HEAD~1", "HEAD", "--",
         str(migrations_dir)],
        capture_output=True, text=True,
    )
    new_migrations = [f for f in result.stdout.strip().split("\n") if f]
    
    if not new_migrations:
        return CheckResult(
            name="Migrations",
            category="base de datos",
            status="pass",
            message="Sin migrations nuevas",
        )
    
    return CheckResult(
        name="Migrations",
        category="base de datos",
        status="warning",
        message=f"{len(new_migrations)} migrations nuevas — verificar antes del deploy",
        details=new_migrations,
    )


def check_env_vars() -> CheckResult:
    """Detectar nuevas env vars requeridas en el código."""
    # Simplificado: buscar referencias a os.environ en código nuevo
    result = subprocess.run(
        ["git", "diff", "HEAD~1", "HEAD", "--",
         "*.py", "*.ts", "*.js"],
        capture_output=True, text=True,
    )
    diff = result.stdout
    
    # Patrón básico: detectar env vars referenciadas
    import re
    env_var_pattern = re.compile(r'os\.environ\["([A-Z_]+)"\]|process\.env\.([A-Z_]+)')
    new_vars = set()
    
    for line in diff.split("\n"):
        if line.startswith("+") and not line.startswith("+++"):
            for match in env_var_pattern.finditer(line):
                var = match.group(1) or match.group(2)
                if var:
                    new_vars.add(var)
    
    if not new_vars:
        return CheckResult(
            name="Env vars",
            category="configuración",
            status="pass",
            message="Sin nuevas env vars detectadas",
        )
    
    return CheckResult(
        name="Env vars",
        category="configuración",
        status="warning",
        message=f"Detectadas {len(new_vars)} env vars referenciadas en código nuevo",
        details=sorted(new_vars),
    )


def check_dependencies() -> CheckResult:
    """Verificar cambios en archivos de dependencies."""
    dep_files = ["requirements.txt", "package.json", "pyproject.toml", "Gemfile"]
    
    result = subprocess.run(
        ["git", "diff", "HEAD~1", "HEAD", "--"] + dep_files,
        capture_output=True, text=True,
    )
    
    if not result.stdout.strip():
        return CheckResult(
            name="Dependencias",
            category="dependencias",
            status="pass",
            message="Sin cambios en dependencies",
        )
    
    return CheckResult(
        name="Dependencias",
        category="dependencias",
        status="warning",
        message="Cambios en dependencias detectados — verificar audit",
        details=result.stdout.split("\n")[:20],
    )


def analyze_with_claude(diff: str, conventions: str) -> list[CheckResult]:
    """Análisis adicional con Claude Code: breaking changes,
    cambios riesgosos, mejoras al checklist."""
    prompt = f"""Analiza este diff de release para detectar issues
de readiness para deployment. CONTEXTO DEL PROYECTO:

{conventions}

DIFF DEL RELEASE:

{diff[:8000]}


Identifica:
1. Breaking changes (funciones/endpoints removidos, schemas cambiados)
2. Cambios riesgosos (lógica de negocio crítica modificada)
3. Documentación faltante (cambios sin update de docs)
4. Posibles problemas de configuración (refs a env vars, paths)

Devuelve JSON:
{{
  "issues": [
    {{
      "name": "nombre corto",
      "category": "código|base de datos|configuración|dependencias|breaking changes|infraestructura|observabilidad|rollback",
      "status": "pass|warning|fail",
      "message": "descripción accionable",
      "details": ["punto 1", "punto 2"]
    }}
  ]
}}
"""
    
    client = Anthropic()
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=3000,
        messages=[{"role": "user", "content": prompt}],
    )
    
    text = response.content[0].text.strip()
    if text.startswith("```"):
        text = "\n".join(text.split("\n")[1:-1])
    
    try:
        data = json.loads(text)
        return [CheckResult(**i) for i in data.get("issues", [])]
    except (json.JSONDecodeError, TypeError) as e:
        print(f"WARNING: análisis con Claude falló: {e}", file=sys.stderr)
        return []


def aggregate_results(checks: list[CheckResult]) -> dict:
    """Combinar resultados en reporte final."""
    by_status = {"pass": 0, "warning": 0, "fail": 0}
    for c in checks:
        by_status[c.status] = by_status.get(c.status, 0) + 1
    
    overall = "ready"
    if by_status["fail"] > 0:
        overall = "blocked"
    elif by_status["warning"] > 0:
        overall = "ready_with_warnings"
    
    return {
        "overall_status": overall,
        "summary": by_status,
        "checks": [
            {
                "name": c.name,
                "category": c.category,
                "status": c.status,
                "message": c.message,
                "details": c.details,
            }
            for c in checks
        ],
    }


def main() -> int:
    print("=== Deployment Readiness Validation ===\n")
    
    # Checks programáticos rápidos
    checks = [
        check_tests_passing(),
        check_migrations(),
        check_env_vars(),
        check_dependencies(),
    ]
    
    # Diff completo para análisis con Claude
    diff_result = subprocess.run(
        ["git", "diff", "HEAD~1", "HEAD"],
        capture_output=True, text=True,
    )
    
    conventions = (
        Path("CLAUDE.md").read_text() if Path("CLAUDE.md").exists()
        else "Sin convenciones documentadas."
    )
    
    # Análisis con Claude
    print("Analizando con Claude Code...")
    claude_checks = analyze_with_claude(diff_result.stdout, conventions)
    checks.extend(claude_checks)
    
    # Aggregar
    report = aggregate_results(checks)
    
    # Imprimir resultados legibles
    print(f"\nOverall: {report['overall_status'].upper()}")
    print(f"Pass: {report['summary']['pass']}, Warning: {report['summary']['warning']}, Fail: {report['summary']['fail']}\n")
    
    for c in checks:
        emoji = {"pass": "✅", "warning": "⚠️ ", "fail": "❌"}[c.status]
        print(f"{emoji} [{c.category}] {c.name}: {c.message}")
        for d in c.details[:3]:
            print(f"    - {d}")
    
    # Guardar reporte como artifact
    Path("readiness_report.json").write_text(json.dumps(report, indent=2))
    
    # Exit code: bloquear si hay fails
    if report["overall_status"] == "blocked":
        print("\n❌ DEPLOYMENT BLOCKED — issues críticos detectados")
        return 1
    
    if report["overall_status"] == "ready_with_warnings":
        print("\n⚠️  DEPLOYMENT READY con warnings — revisar antes")
    else:
        print("\n✅ DEPLOYMENT READY")
    
    return 0


if __name__ == "__main__":
    sys.exit(main())

Validación Cross-Environment

Un check importante: verificar que el ambiente target está sincronizado con lo que el deploy va a esperar. Por ejemplo, si el código nuevo necesita STRIPE_API_KEY, verificar que esa env var esté configurada en producción.

def check_env_vars_in_target(target_env: str, github_token: str, repo: str) -> CheckResult:
    """Verificar env vars configuradas en el ambiente target."""
    import requests
    
    # Listar variables del environment via GitHub API
    url = f"https://api.github.com/repos/{repo}/environments/{target_env}/variables"
    headers = {"Authorization": f"Bearer {github_token}", "Accept": "application/vnd.github+json"}
    r = requests.get(url, headers=headers)
    
    if r.status_code != 200:
        return CheckResult(
            name="Env vars en target",
            category="configuración",
            status="warning",
            message=f"No pude leer variables de {target_env}",
        )
    
    configured_vars = {v["name"] for v in r.json().get("variables", [])}
    
    # Detectar vars requeridas del código (similar al check anterior)
    required = detect_required_env_vars()
    missing = required - configured_vars
    
    if missing:
        return CheckResult(
            name="Env vars en target",
            category="configuración",
            status="fail",
            message=f"Faltan env vars en {target_env}: {sorted(missing)}",
            details=sorted(missing),
        )
    
    return CheckResult(
        name="Env vars en target",
        category="configuración",
        status="pass",
        message=f"Todas las env vars requeridas están en {target_env}",
    )

Checks Específicos por Tipo de Proyecto

Diferentes proyectos tienen diferentes prioridades de readiness:

API REST

  • Backwards compatibility de endpoints públicos
  • Versionado de API (header, path, query)
  • Rate limits configurados

Web App con DB

  • Migraciones pendientes
  • Índices necesarios para nuevos queries
  • Backups recientes confirmados

Microservicio

  • Service contracts (OpenAPI, gRPC) compatibles
  • Service discovery actualizado
  • Circuit breakers configurados

CLI tool

  • Versión semver bumpada
  • Backwards compatibility de comandos
  • Updated docs en man pages / README

Adapta los checks específicos a tu tipo de proyecto.


Trampas Comunes

Error 1: Treat warnings como passes

Síntoma: El reporte muestra warnings pero el deploy continúa sin que nadie los revise.

Por qué pasa: El workflow no diferencia visualmente entre pass y warning, o no tiene approval gate después.

Cómo corregir: En modo strict, warnings deben requerir confirmación humana antes del deploy. Solo passes permiten auto-deploy.

Error 2: Check de migrations sin verificar idempotencia

Síntoma: Detectas que hay migrations nuevas pero no validas si son idempotentes. Una migration con CREATE TABLE (sin IF NOT EXISTS) falla en re-runs.

Por qué pasa: El check es superficial — solo cuenta archivos, no analiza contenido.

Cómo corregir: Pasar el contenido de las migrations a Claude para análisis de idempotencia y rollback.

Error 3: Confiar 100% en automatización

Síntoma: Todo es ready según el script, pero falta algo que el humano sabía pero no estaba codificado.

Por qué pasa: El conocimiento "tribal" del equipo no está en el checklist.

Cómo corregir: El checklist es un baseline, no reemplaza el approval humano. Para deploys a producción, mantener gate humano (cápsula 04 del módulo).

Error 4: Reporte sin acción

Síntoma: Genera un reporte pero nadie lo lee. Issues pasan al deploy.

Por qué pasa: El reporte queda como artifact sin notificación al equipo.

Cómo corregir: Postear summary del reporte como comment en el commit/PR. Bloquear el deploy si hay fails.

Error 5: Check de env vars solo del código nuevo

Síntoma: Detectas que el código nuevo necesita NEW_VAR, pero no detectas que el código viejo ya necesitaba OLD_VAR que nunca se configuró.

Por qué pasa: Estás scaneando solo el diff, no el codebase entero.

Cómo corregir: Para readiness, scanear todo el código. Para review de PR, scanear solo el diff. Distintos contextos.


Diagnóstico

Pregunta 1: ¿Tu equipo tiene un checklist de readiness o se basa en "tests pasan"?

Solo tests = riesgo. Checklist explícito = más seguro. Si no tienes uno escrito, el primer paso es escribirlo.

Pregunta 2: ¿Validas migraciones y env vars antes de deploy?

Migraciones y env vars son las causas #1 de incidents post-deploy. Automatizar esos checks es alto leverage.

Pregunta 3: ¿Qué pasa cuando el script reporta warnings?

Si "siguen igual sin revisar", los warnings son ruido. Si "requieren approval humano", funcionan como debe.

Pregunta 4: ¿El reporte de readiness se lee o queda como artifact ignorado?

Si nadie lo lee, no aporta. Postear summary al PR/commit + bloquear si hay fails es el patrón correcto.

Pregunta 5: ¿Tu check considera el ambiente target (prod, staging) o solo el código?

Solo código = check parcial. Cross-environment (env vars, recursos en target) = check completo.


Ejercicios

Ejercicio 1: Checklist mínimo programático (Fácil)

Implementa los 4 checks programáticos del script (tests, migrations, env vars, dependencies). Verifica que produce output legible.

Ejercicio 2: Análisis con Claude (Medio)

Agrega el análisis con Claude Code para detectar breaking changes y cambios riesgosos. Asegúrate que el JSON parsing es robusto a code fences.

Ejercicio 3: Cross-environment validation (Difícil)

Implementa el check de env vars contra GitHub Environments. Conecta con la API, lista variables configuradas en production, y compáralas con las requeridas por el código.

Ver enfoque

Necesitas:

  1. PAT con scope de leer environments (o GITHUB_TOKEN con permission environments:read)
  2. API call a /repos/{repo}/environments/{env}/variables
  3. Diff entre vars requeridas (parseadas del código) y vars configuradas
  4. Reportar las que faltan como fail

Resumen

  • Tests verdes ≠ ready to deploy — readiness es más amplio
  • 8 categorías de checks: código, DB, config, dependencias, breaking changes, infraestructura, observabilidad, rollback
  • Combinar checks programáticos (rápidos, baratos) con Claude Code (analítico, profundo)
  • Cross-environment validation detecta gaps entre código y ambiente target
  • Diferenciar pass / warning / fail y actuar según severidad
  • Reporte que se lee > reporte que queda como artifact
  • Approval humano sigue siendo necesario para producción

Próxima cápsula: 04 — Flujo staging → producción con approval gates. Tienes validación de readiness funcionando. Ahora aprendes a integrar esto con el flujo completo de deployment: deploy a staging automático, validación de staging, approval humano, deploy a producción.


Recursos Adicionales

  1. 12-Factor App: Config — Principios de configuración
  2. The Twelve-Factor App: Build, release, run — Modelo de releases
  3. GitHub Environments API — Para validar configuración de targets
  4. Database Migration Best Practices — Martin Fowler sobre evolución de DB
  5. Site Reliability Engineering: Release Engineering — Capítulo de Google
  6. Anthropic on Code Review use cases — Patterns aplicables a readiness