Módulo 4: Deployment Automation

Flujo Staging → Producción con Approval Gates

Flujo Staging → Producción con Approval Gates

Descripción

Las dos cápsulas previas cubrieron pre-deploy: changelog (cápsula 02) y readiness validation (cápsula 03). Esta cápsula cubre el flujo de promoción de código desde staging hasta producción con approval gates apropiados. Es donde el pipeline pasa de "código probado" a "código sirviendo tráfico real".

La pregunta central: ¿qué partes del deployment son seguras de automatizar y cuáles requieren intervención humana? La respuesta no es uniforme — depende del riesgo de cada step. Vas a aprender a diseñar el flujo, dónde colocar approval gates, cómo automatizar staging completamente, y por qué producción casi siempre debe tener un click humano.

Al terminar, vas a tener un workflow que deploya automáticamente a staging tras merge, valida el ambiente staging, y espera aprobación humana antes de promocionar a producción.


El Modelo Mental

┌──────────────────────────────────────────────────────┐
│  AUTOMATIZACIÓN COMPLETA (sin humanos)                │
│  ✅ Build, lint, tests, security scan                 │
│  ✅ Deploy a staging                                  │
│  ✅ Smoke tests post-deploy en staging                │
│  ✅ Métricas de health check de staging               │
└──────────────────────────────────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────────┐
│  GATE HUMANO                                          │
│  🔒 Click "Approve" en GitHub                         │
│  Reviewer humano confirma:                            │
│    - Vio el changelog                                 │
│    - Confirmó que staging está saludable              │
│    - Asume responsabilidad por el deploy              │
└──────────────────────────────────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────────┐
│  AUTOMATIZACIÓN COMPLETA (post-approval)              │
│  ✅ Deploy a producción                               │
│  ✅ Smoke tests post-deploy en producción             │
│  ✅ Notificación al equipo                            │
│  ✅ Monitoring activo (con rollback automático        │
│     que cubre cápsula del Módulo 5)                   │
└──────────────────────────────────────────────────────┘

El humano es la única gate. Todo lo demás es automático. La gate humana es rápida (un click) pero explícita — alguien asume responsabilidad por el deploy.


Por Qué Producción Necesita Humano

SIN GATE HUMANO:
→ Bug pasa todos los tests automatizados
→ Pasa staging (porque staging tiene menos tráfico/data)
→ Llega a producción
→ Empieza a fallar con tráfico real
→ Rollback automático corrige (si está configurado)
→ Pero los usuarios ya vieron errores
→ Damage limited but not zero

CON GATE HUMANO:
→ Mismo path hasta staging
→ Antes de producción, humano revisa:
   - ¿Cambios riesgosos?
   - ¿Métricas de staging saludables?
   - ¿Es buen momento para deployar? (no fin de semana, no
     antes de un evento crítico, etc.)
→ Si todo OK: click → producción
→ Si algo no convence: pausar, investigar, decidir
→ Damage previo a deploy

El humano agrega lo que la automatización no puede dar: contexto de timing, juicio sobre riesgo, autoridad para asumir responsabilidad.


Configurar Environments en GitHub

GitHub Environments es la primitiva clave para approval gates.

Setup paso a paso

Settings → Environments → New environment → "production"

Configuración importante:

  1. Required reviewers — agregar 1+ personas
  2. Wait timer — opcional (ej. 5 minutos para "cooling off")
  3. Deployment branches — restringir a main solamente
  4. Environment secrets — secrets exclusivos de prod
production
├── Required reviewers: [tech_lead, sre_lead]
├── Wait timer: 0 minutes
├── Deployment branches: main
├── Variables:
│   └── DEPLOYMENT_TARGET: prod-cluster
└── Secrets:
    ├── PROD_DATABASE_URL: xxx
    └── PROD_STRIPE_KEY: xxx

Setup paralelo: staging

staging
├── Required reviewers: (none — automático)
├── Deployment branches: main
├── Variables:
│   └── DEPLOYMENT_TARGET: staging-cluster
└── Secrets:
    ├── STAGING_DATABASE_URL: xxx
    └── STAGING_STRIPE_KEY: xxx (test key)

Diferencia clave: staging sin required reviewers (deploy automático), production con reviewers (gate humano).


El Workflow Completo

# .github/workflows/staged-deployment.yml
name: Staged Deployment

on:
  push:
    branches: [main]

permissions:
  contents: read
  deployments: write

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: false  # NO cancelar deploys en progreso

jobs:
  # ============================================
  # STAGE 1: Validación pre-deploy
  # ============================================
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: '3.11' }
      - run: pip install anthropic requests
      - name: Readiness validation
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
        run: python scripts/validate_readiness.py
      - uses: actions/upload-artifact@v4
        with:
          name: readiness-report
          path: readiness_report.json

  # ============================================
  # STAGE 2: Deploy a staging (automático)
  # ============================================
  deploy-staging:
    needs: validate
    runs-on: ubuntu-latest
    environment: staging  # ← env staging, sin required reviewers
    outputs:
      deploy_url: ${{ steps.deploy.outputs.url }}
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to staging
        id: deploy
        run: |
          # Reemplaza este script con tu mecanismo real de deploy
          # (kubectl, terraform, deploy script propio, etc.)
          ./scripts/deploy.sh staging
          echo "url=https://staging.example.com" >> $GITHUB_OUTPUT
      
      - name: Smoke tests on staging
        run: ./scripts/smoke_tests.sh https://staging.example.com
      
      - name: Validate staging metrics
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: python scripts/validate_staging.py

  # ============================================
  # STAGE 3: Deploy a producción (con approval gate)
  # ============================================
  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production  # ← env production con required reviewers
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to production
        run: ./scripts/deploy.sh production
      
      - name: Smoke tests on production
        run: ./scripts/smoke_tests.sh https://app.example.com
      
      - name: Notify team
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        run: python scripts/post_deploy_notification.py

El flujo:

  1. Validación corre primero (readiness check)
  2. Deploy a staging es automático tras validación exitosa
  3. Smoke tests + validación de métricas en staging
  4. Deploy a producción espera approval humano (porque environment requiere reviewers)
  5. Después del approval, deploy automático + smoke tests + notificación

El "click" del approval

Cuando el job deploy-production espera aprobación, GitHub muestra:

⏸️  Waiting for approval to deploy
   Environment: production
   Reviewers needed: 1 of 2
   [Approve and deploy]  [Reject]

Cualquier reviewer configurado puede aprobar. Click → deploy continúa.


Validación Automática de Staging

Antes de que el humano apruebe producción, el sistema debería darle información clara sobre el estado de staging. Acá Claude Code ayuda:

"""scripts/validate_staging.py

Análisis post-deploy de staging para informar la decisión de promoción.
"""
import json
import os
import subprocess
import sys
import requests
from anthropic import Anthropic


def get_staging_metrics() -> dict:
    """Obtener métricas de health de staging."""
    # Adapt to tu observability stack
    metrics = {
        "error_rate": fetch_metric("error_rate", env="staging"),
        "latency_p95": fetch_metric("latency_p95", env="staging"),
        "throughput": fetch_metric("throughput", env="staging"),
        "deployment_success": True,  # del step previo
    }
    return metrics


def fetch_metric(name: str, env: str) -> float | None:
    """Wrapper de tu sistema de monitoring."""
    # Ejemplo: Prometheus, Datadog, CloudWatch
    # Implementación específica del stack
    return None


def smoke_test_results() -> dict:
    """Leer resultados de smoke tests."""
    try:
        with open("smoke_results.json") as f:
            return json.load(f)
    except FileNotFoundError:
        return {"passed": 0, "failed": 0, "skipped": 0}


def analyze_with_claude(metrics: dict, smoke: dict, changelog: str) -> str:
    """Análisis: ¿es seguro promocionar a producción?"""
    
    prompt = f"""Analiza si es seguro promocionar este deploy de staging a producción.

CHANGELOG (qué se está deployando):
{changelog}

MÉTRICAS DE STAGING (post-deploy):
{json.dumps(metrics, indent=2)}

SMOKE TESTS:
{json.dumps(smoke, indent=2)}

Evalúa:
1. ¿Las métricas de staging son saludables?
2. ¿Los smoke tests pasaron?
3. ¿Hay señales en el changelog que requieran extra cuidado en prod?
4. ¿Recomiendas promocionar a producción ahora?

Devuelve un JSON:
{{
  "recommendation": "promote|hold|investigate",
  "summary": "1-2 párrafos con tu razonamiento",
  "concerns": ["concern 1", "concern 2"] o [],
  "checklist_for_human": ["verificar X antes de aprobar", "..."]
}}
"""
    
    client = Anthropic()
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=2000,
        messages=[{"role": "user", "content": prompt}],
    )
    
    text = response.content[0].text.strip()
    if text.startswith("```"):
        text = "\n".join(text.split("\n")[1:-1])
    
    return json.loads(text)


def post_summary_to_pr(analysis: dict, repo: str, token: str):
    """Postear el análisis al PR para que el reviewer lo vea antes de aprobar."""
    # Encontrar el PR asociado al merge commit
    sha = os.environ["GITHUB_SHA"]
    url = f"https://api.github.com/repos/{repo}/commits/{sha}/pulls"
    headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"}
    r = requests.get(url, headers=headers)
    
    if not r.ok or not r.json():
        return
    
    pr_number = r.json()[0]["number"]
    
    body = format_summary_markdown(analysis)
    comment_url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
    requests.post(comment_url, headers=headers, json={"body": body})


def format_summary_markdown(analysis: dict) -> str:
    rec_emoji = {"promote": "✅", "hold": "⏸️", "investigate": "🚨"}
    
    body = f"""# 🤖 Pre-Production Promotion Analysis

## Recomendación
{rec_emoji.get(analysis['recommendation'], '💬')} **{analysis['recommendation'].upper()}**

## Resumen
{analysis['summary']}

"""
    
    if analysis.get("concerns"):
        body += "## Concerns\n"
        for c in analysis["concerns"]:
            body += f"- ⚠️ {c}\n"
        body += "\n"
    
    if analysis.get("checklist_for_human"):
        body += "## Antes de aprobar producción, verifica:\n"
        for c in analysis["checklist_for_human"]:
            body += f"- [ ] {c}\n"
    
    return body


def main() -> int:
    metrics = get_staging_metrics()
    smoke = smoke_test_results()
    
    # Leer changelog del último release
    changelog_files = list(Path(".").glob("CHANGELOG_v*.md"))
    changelog = changelog_files[0].read_text() if changelog_files else "No changelog found"
    
    analysis = analyze_with_claude(metrics, smoke, changelog)
    
    # Postear al PR (informativo, no bloqueante)
    repo = os.environ.get("GITHUB_REPOSITORY")
    token = os.environ.get("GITHUB_TOKEN")
    if repo and token:
        post_summary_to_pr(analysis, repo, token)
    
    # Imprimir en logs
    print(json.dumps(analysis, indent=2))
    
    # Si la recomendación es investigate, fallar el job (no promocionar)
    if analysis["recommendation"] == "investigate":
        print("Recomendación: investigar. Pipeline pausado.")
        return 1
    
    return 0


if __name__ == "__main__":
    from pathlib import Path
    sys.exit(main())

Resultado: cuando el humano va a aprobar el deploy a producción, ya tiene en el PR un comment con análisis del estado de staging y un checklist específico de qué verificar.


Patrón Avanzado: Wait Timer

Para cambios de bajo riesgo, puedes agregar un "cooling off period" en lugar de approval explícito:

production environment
├── Required reviewers: 0
├── Wait timer: 30 minutes
├── Deployment branches: main

Resultado: el deploy a producción espera 30 minutos automáticamente. Si en ese tiempo no hay alerts ni cancelación manual, continúa. Si alguien detecta algo en staging, puede cancelar.

Cuándo usar:

  • Equipos chicos sin SRE 24/7
  • Deploys frecuentes de bajo riesgo
  • Compañías donde la velocidad de iteración importa más que el approval explícito

Cuándo NO usar:

  • Producción con tráfico crítico
  • Deploys de cambios riesgosos (DB migrations, breaking changes)
  • Cumplimiento regulatorio que requiere approval explícito

Patrón: Canary Deploy

Más sofisticado: en lugar de promover 100% del tráfico a producción de una vez, promover gradualmente:

deploy-canary:
  needs: deploy-staging
  environment: production-canary  # ← 5% del tráfico
  steps:
    - run: ./scripts/deploy.sh canary

deploy-full:
  needs: deploy-canary
  environment: production-full    # ← 100% del tráfico
  # required reviewers acá
  steps:
    - run: ./scripts/deploy.sh full

El canary recibe 5% del tráfico durante un tiempo. Si las métricas son buenas, se promueve a 100%. Si no, rollback automático afecta solo al 5%.

Trade-off: más complejidad de infraestructura vs menos blast radius en incidents.


Trampas Comunes

Error 1: Approval gate sin contexto

Síntoma: El reviewer recibe la notificación de "approve production deploy" sin info. Aprueba a ciegas.

Por qué pasa: El approval gate no incluye el análisis previo.

Cómo corregir: Postear al PR el análisis de staging antes del approval (script validate_staging.py arriba). El reviewer tiene el contexto en su feed.

Error 2: Wait timer sin alertas activas

Síntoma: Configuraste 30 min de wait timer, pero el equipo no ve métricas en ese tiempo. El deploy sale a prod con un bug.

Por qué pasa: Wait timer asume que alguien está monitoreando. Si nadie mira, no aporta.

Cómo corregir: Configurar alerts apropiadas en staging que notifiquen al equipo. Wait timer + alerts activas = combo.

Error 3: Mismo set de secrets para staging y prod

Síntoma: Un bug en staging usa una API key de producción y afecta sistemas reales.

Por qué pasa: Por conveniencia, el mismo STRIPE_KEY se usa en ambos.

Cómo corregir: Siempre secrets separados por environment. Stripe tiene test keys vs live keys — úsalas. Mismo principio para todo lo demás.

Error 4: Auto-rollback no configurado

Síntoma: Deploy a producción falla, equipo improvisando rollback bajo presión.

Por qué pasa: Auto-rollback es trabajo separado que dejaron "para después".

Cómo corregir: Configurar auto-rollback antes de habilitar deploys automáticos a producción. Es lo que cubre la cápsula del Módulo 5.

Error 5: cancel-in-progress: true en deploys

Síntoma: Mergeas dos PRs seguidos. El segundo cancela el deploy del primero a mitad. Estado inconsistente.

Por qué pasa: cancel-in-progress: true está bien para CI checks, mal para deploys.

Cómo corregir: Para deploys, siempre cancel-in-progress: false. Los deploys deben completar serialmente.


Diagnóstico

Pregunta 1: ¿Tu deploy a producción tiene approval gate explícito?

Si no, dependes 100% de automatización. Para sistemas críticos, el humano agrega valor.

Pregunta 2: ¿El reviewer recibe contexto antes de aprobar?

Si solo ve "approve deploy", aprueba a ciegas. Análisis post-staging + checklist le da el contexto.

Pregunta 3: ¿Tus secrets están separados entre staging y producción?

Si son los mismos, un bug en staging puede tocar producción real.

Pregunta 4: ¿Tu workflow tiene `concurrency: cancel-in-progress: false` para deploys?

Si está en true, deploys consecutivos se cancelan a mitad. Mal patrón.

Pregunta 5: ¿Tienes rollback automático configurado, o el rollback es manual?

Manual = bajo presión, propenso a errores. Automático con triggers apropiados es lo robusto. Cápsula del Módulo 5 lo desarrolla.


Ejercicios

Ejercicio 1: Configurar environments (Fácil)

Crea los environments staging y production en GitHub Settings:

  • staging: sin reviewers, deployment branches main
  • production: con reviewers (al menos tú mismo en testing), deployment branches main

Configura secrets de prueba distintos en cada uno.

Ejercicio 2: Pipeline de 3 stages (Medio)

Implementa el workflow con los 3 jobs (validate, deploy-staging, deploy-production). Verifica que:

  1. validate corre primero
  2. deploy-staging corre tras validate exitoso, sin approval
  3. deploy-production espera approval antes de correr

Usa scripts mock (echo "Deploying to ...") para testear el flujo sin infraestructura real.

Ejercicio 3: Análisis pre-promoción con Claude (Difícil)

Implementa validate_staging.py que:

  1. Obtiene métricas mock de staging
  2. Pasa el changelog + métricas a Claude
  3. Genera recomendación con concerns y checklist
  4. Postea el análisis como comment en el PR
  5. Falla el job si la recomendación es "investigate"

Resumen

  • El humano es la única gate entre staging y producción para cambios críticos
  • GitHub Environments es la primitiva: required reviewers, wait timers, environment secrets
  • Análisis automático pre-approval da contexto al humano (recomendación, concerns, checklist)
  • Secrets separados por environment evita cross-contamination
  • cancel-in-progress: false para deploys (a diferencia de CI checks)
  • Wait timer es alternativa para low-risk deploys, pero solo con monitoring activo
  • Canary deploys reducen blast radius en cambios riesgosos

Próxima cápsula: 05 — Proyecto: Workflow de deployment completo. Última cápsula técnica del módulo 4. Combinas changelog + readiness + staged deployment en un workflow end-to-end que vas a poder adaptar a tu proyecto.


Recursos Adicionales

  1. GitHub Environments — Setup completo
  2. GitHub: Required reviewers — Configuración de gates
  3. Canary Deployments — Martin Fowler sobre el patrón
  4. Blue-Green Deployments — Alternativa al canary
  5. 12-Factor App: Backing services — Por qué staging y prod deben ser idénticos
  6. SRE Workbook: Release Engineering — Capítulo del libro de Google