Módulo 6: Proyecto — Pipeline CI/CD Completo con Claude Code

Implementación: Stages 4-6 (Deployment)

Implementación: Stages 4-6 (Deployment)

Descripción

Esta cápsula construye la mitad post-merge del pipeline: desde merge a main hasta producción servida a usuarios. Implementas changelog automático, readiness validation, deploy a staging con validación, approval gate humano, y deploy a producción con notificación al equipo.

Es la fase de mayor riesgo (los cambios afectan infraestructura real) y por lo tanto la que más disciplina requiere. Cada step tiene un check antes de avanzar al siguiente. La gate humana antes de producción es la única intervención manual del flujo automático.

Al terminar, vas a tener Phases 2-3 funcionando: el código mergeado a main llega automáticamente a staging, y con un click humano, a producción.


La Estructura del Workflow

# .github/workflows/staged-deployment.yml
name: Staged Deployment (Phases 2-3)

on:
  push:
    branches: [main]

permissions:
  contents: write       # para tags
  pull-requests: write  # para comments
  deployments: write    # para registrar deployments
  issues: write         # para crear changelog issue si aplica

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

env:
  PYTHON_VERSION: '3.11'

jobs:
  # ============================================
  # PHASE 2: Pre-Deployment
  # ============================================
  
  generate-changelog:
    runs-on: ubuntu-latest
    outputs:
      changelog_file: ${{ steps.gen.outputs.file }}
    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: Generate changelog
        id: gen
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
          NEW_TAG: ${{ github.sha }}
        run: |
          python scripts/generate_changelog.py
          FILE=$(ls CHANGELOG_*.md | head -1)
          echo "file=$FILE" >> $GITHUB_OUTPUT
      
      - uses: actions/upload-artifact@v4
        with:
          name: changelog
          path: CHANGELOG_*.md
          retention-days: 30

  readiness-validation:
    runs-on: ubuntu-latest
    outputs:
      status: ${{ steps.validate.outputs.status }}
    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: Run readiness validation
        id: validate
        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
          if [ $? -eq 0 ]; then
            echo "status=ready" >> $GITHUB_OUTPUT
          else
            echo "status=blocked" >> $GITHUB_OUTPUT
            exit 1
          fi
      
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: readiness-report
          path: readiness_report.json

  # ============================================
  # PHASE 3: Deployment
  # ============================================

  deploy-staging:
    needs: [generate-changelog, readiness-validation]
    runs-on: ubuntu-latest
    environment: staging
    outputs:
      previous_release: ${{ steps.capture.outputs.previous }}
      new_release: ${{ steps.deploy.outputs.new }}
      deploy_url: ${{ steps.deploy.outputs.url }}
    steps:
      - uses: actions/checkout@v4
      
      - name: Capture previous release
        id: capture
        run: |
          PREV=$(./scripts/get_current_release.sh staging)
          echo "previous=$PREV" >> $GITHUB_OUTPUT
      
      - name: Deploy to staging
        id: deploy
        env:
          DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
        run: |
          NEW=$(./scripts/deploy.sh staging)
          echo "new=$NEW" >> $GITHUB_OUTPUT
          echo "url=https://staging.example.com" >> $GITHUB_OUTPUT
      
      - name: Smoke tests on staging
        run: ./scripts/smoke_tests.sh https://staging.example.com

  validate-staging:
    needs: deploy-staging
    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: changelog }
      
      - name: Validate staging with Claude
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
          METRICS_API_URL: ${{ secrets.METRICS_API_URL }}
          METRICS_API_TOKEN: ${{ secrets.METRICS_API_TOKEN }}
        run: python scripts/validate_staging.py

  deploy-production:
    needs: validate-staging
    runs-on: ubuntu-latest
    environment: production  # ← required reviewers
    outputs:
      previous_release: ${{ steps.capture.outputs.previous }}
      new_release: ${{ steps.deploy.outputs.new }}
    steps:
      - uses: actions/checkout@v4
      
      - name: Capture previous release
        id: capture
        run: |
          PREV=$(./scripts/get_current_release.sh production)
          echo "previous=$PREV" >> $GITHUB_OUTPUT
      
      - name: Deploy to production
        id: deploy
        env:
          DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
        run: |
          NEW=$(./scripts/deploy.sh production)
          echo "new=$NEW" >> $GITHUB_OUTPUT
      
      - 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

Lo importante:

  • concurrency: cancel-in-progress: false — deploys nunca se cancelan a mitad
  • Phase 2 paralela (changelog + readiness en simultáneo)
  • Phase 3 secuencial: staging → validate → approval → production
  • environment: production agrega la gate humana
  • Capture de previous_release antes de deploy (para rollback en Phase 4)

Scripts Reusados

La mayoría de scripts vienen de módulos previos:

generate_changelog.py     ← Módulo 4 cápsula 02
validate_readiness.py     ← Módulo 4 cápsula 03
validate_staging.py       ← Módulo 4 cápsula 04
post_deploy_notification.py ← Módulo 4 cápsula 05
deploy.sh                 ← Módulo 4 cápsula 05 (mock)
smoke_tests.sh            ← Módulo 4 cápsula 05 (mock)
get_current_release.sh    ← Módulo 5 cápsula 04

Punto importante: no reescribir nada. Reusar lo que ya construiste. Si en módulos previos los scripts están bien, este pipeline es ensamblaje, no construcción nueva.


El Script Faltante: get_current_release.sh

Este script es nuevo en este módulo. Adapta a tu plataforma:

#!/bin/bash
# scripts/get_current_release.sh
# Retorna el release actualmente deployado en un environment

set -euo pipefail

ENV="${1:?'Usage: get_current_release.sh <env>'}"

# Adapta según tu plataforma:

# Kubernetes
# CURRENT=$(kubectl get deployment app -n $ENV -o jsonpath='{.spec.template.metadata.labels.version}')

# AWS ECS
# CURRENT=$(aws ecs describe-services --cluster $ENV --services app \
#   --query 'services[0].taskDefinition' --output text | rev | cut -d':' -f1 | rev)

# Heroku
# CURRENT=$(heroku releases --app "app-$ENV" --json | jq -r '.[0].version')

# Docker Compose / simple
# CURRENT=$(docker inspect app-$ENV --format '{{ index .Config.Labels "version" }}')

# Mock para demo (NO usar en producción real)
CURRENT="v$(date +%s)"

echo "$CURRENT"

Reemplaza la implementación mock con la lógica real de tu plataforma. Es el único lugar específico de tu infrastructure que tienes que adaptar.


Configurar los Environments en GitHub

Para que el pipeline funcione, configura los environments antes de pushear el workflow:

Environment: staging

Settings → Environments → New environment → "staging"

Configuración:
- Required reviewers: (vacío — automático)
- Wait timer: 0 minutes
- Deployment branches: main only
- Variables:
  - STAGING_URL: https://staging.example.com
- Secrets:
  - STAGING_DATABASE_URL: [valor]
  - STAGING_API_TOKENS: [valores]

Environment: production

Settings → Environments → New environment → "production"

Configuración:
- Required reviewers: [tech_lead, sre_lead] ← CRÍTICO
- Wait timer: 0 minutes (o 5 si quieres cooling-off)
- Deployment branches: main only
- Variables:
  - PROD_URL: https://app.example.com
- Secrets:
  - PROD_DATABASE_URL: [valor diferente al de staging]
  - PROD_API_TOKENS: [valores reales de prod]

Sin estas configuraciones, el workflow falla o (peor) deploya a producción sin gate humana.


Branch Protection para main

Settings → Branches → Branch protection rules → main

Configuración:
☑ Require a pull request before merging
   ☑ Require approvals: 1
   ☑ Dismiss stale pull request approvals when new commits are pushed
   ☑ Require review from Code Owners (opcional)

☑ Require status checks to pass before merging
   ☑ Require branches to be up to date before merging
   Required status checks:
     ☑ tests-and-linting
     ☑ security-scan
     ☑ pr-review-summary

☑ Require conversation resolution before merging

☑ Restrict who can push to matching branches
   - Solo bots de CI o emergency-team

Sin branch protection, alguien con write access puede pushear directo a main saltando todo el pipeline. Branch protection es la línea de defensa básica.


Flujo Completo: Lo Que Pasa al Mergear

T+0:    Developer mergea PR → push a main
T+0:    Workflow staged-deployment se dispara

T+0-3min:  Phase 2 paralela:
           - generate-changelog (~1 min)
           - readiness-validation (~2 min)

T+3min:    Si ambos pasaron, comienza Phase 3:
T+3-7min:  deploy-staging
T+7-9min:  smoke-tests-staging
T+9-10min: validate-staging (Claude analiza métricas + changelog)

T+10min:   GitHub muestra "Waiting for approval to deploy: production"
           Notificación a reviewers

T+10min - variable:  Approval wait
                     - Reviewer ve el PR con análisis de staging
                     - Click "Approve"
                     - O click "Reject" + comment

T+approval+0:  Comienza deploy-production
T+approval+4min: Deploy completo
T+approval+6min: Smoke tests en producción
T+approval+7min: Notification al equipo

TIEMPO TOTAL HAPPY PATH: ~17 min + approval wait

El Análisis Pre-Promoción

Cuando el reviewer humano va a aprobar, ya tiene el comment del job validate-staging en el PR. Eso le da contexto:

Lo que ve el reviewer ANTES de aprobar:

1. PR original con su descripción
2. Comments de Phase 1 (code review, security)
3. Métricas y status del workflow run
4. Comment del job validate-staging:
   - Recomendación de Claude (promote/hold/investigate)
   - Concerns identificados
   - Checklist específico antes de aprobar
5. Botón "Approve and deploy"

El reviewer no aprueba a ciegas. Tiene contexto rich. La cápsula 04 del Módulo 4 detalla validate_staging.py.


Trampas Comunes

1. cancel-in-progress: true para deploys

Síntoma: Haces merge de 2 PRs seguidos. El segundo deploy cancela el primero a mitad. Estado inconsistente en producción.

Por qué pasa: Configuración heredada de Phase 1 (donde sí queremos cancelar).

Cómo corregir: cancel-in-progress: false para deploys siempre.

2. Olvidar capturar previous_release

Síntoma: Phase 4 (cápsula 05) necesita revertir, pero no sabes a qué versión.

Por qué pasa: El step de captura no está antes del deploy.

Cómo corregir: Step explícito Capture previous release ANTES del deploy. Output del job propagado para uso posterior.

3. Mismo secret en staging y producción

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

Por qué pasa: Por simplicidad usaron el mismo secret.

Cómo corregir: Secrets diferentes por environment. Stripe test keys en staging, live keys en producción. Mismo principio para todo.

4. Approval gate sin notificar reviewers

Síntoma: El job production espera 2 horas porque ningún reviewer vio la notificación.

Por qué pasa: Reviewers no configurados para recibir notifs de GitHub.

Cómo corregir: En Settings → Notifications → enable "Workflow runs". Agregar también notification a Slack si el equipo prefiere.

5. No validar que el deploy efectivamente sucedió

Síntoma: El step deploy.sh retorna 0 pero el deploy en realidad falló.

Por qué pasa: El script no valida con health check post-deploy.

Cómo corregir: smoke_tests.sh después del deploy, antes de continuar. Si los smoke tests fallan, el job falla y el approval no se dispara.


Diagnóstico

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

Si no, deploys consecutivos pueden cancelarse a mitad — peligroso.

Pregunta 2: ¿Capturas previous_release antes del deploy?

Sin esto, no puedes revertir programáticamente desde Phase 4.

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: ¿Configuraste required reviewers en environment "production"?

Sin esto, no hay gate humana — el deploy continúa automáticamente.

Pregunta 5: ¿Branch protection rules previenen pushes directos a main?

Sin esto, alguien puede saltar todo el pipeline pusheando directo. Pérdida de control.


Ejercicios

Ejercicio 1: Configurar environments y branch protection (Fácil)

Antes de implementar el workflow:

  1. Crear environments staging y production con la configuración mostrada
  2. Configurar branch protection para main
  3. Verificar que solo se puede mergear vía PR

Ejercicio 2: Implementar Phase 2 + 3 (Medio)

Implementa los 5 jobs (generate-changelog, readiness, deploy-staging, validate-staging, deploy-production). Verifica:

  1. Phase 2 corre paralelo (changelog + readiness)
  2. Phase 3 secuencial
  3. El approval gate se activa antes de production

Probarlo end-to-end mergeando un PR de prueba.

Ejercicio 3: Implementar get_current_release.sh real (Difícil)

Reemplaza el mock con la lógica real de tu plataforma:

  • Si usas Kubernetes: kubectl + labels
  • Si usas Heroku: heroku releases API
  • Si usas otro: la API/CLI correspondiente

Verifica que después de un deploy, get_current_release.sh production retorna el nuevo release.


Resumen

  • Phase 2 paralela: changelog + readiness en simultáneo
  • Phase 3 secuencial: staging → validate → approval → production
  • cancel-in-progress: false para deploys (nunca cancelar)
  • Capture previous release ANTES del deploy — necesario para rollback (Phase 4)
  • Environments configurados: staging sin reviewers, production con required reviewers
  • Secrets separados por environment
  • Branch protection en main previene saltar el pipeline
  • Reuso de scripts de Módulos 4 y 5 — no reescribir

Próxima cápsula: 05 — Failure paths y observabilidad. Las phases 2-3 funcionan en happy path. Pero tienes que cubrir los casos donde algo falla: deploy a staging falla, métricas degradan en producción, approval rechazado. La cápsula 05 cubre todos los failure modes con el sistema de monitoring + auto-rollback de Módulo 5.


Recursos Adicionales

  1. GitHub Environments — Configuración completa
  2. GitHub Branch Protection — Reglas de protección
  3. GitHub Actions: concurrency — cancel-in-progress
  4. GitHub Actions: needs — Dependencias entre jobs
  5. The Twelve-Factor App: Backing services — Por qué staging y prod deben diferir
  6. Argo Rollouts — Para deployments avanzados (canary, blue-green)