CULTIVA IA

Patrones Bash Defensivos — Deploy Script

Produccion macOS · Linux · CI/CD v1.0.0

deploy-web.sh · CULTIVA IA

Script de despliegue de la web Astro a Vercel — generado aplicando los 10 patrones defensivos de Bash.

CULTIVA IA
Web Astro → Vercel
macOS / GitHub Actions
PATRON 1
Strict Mode
set -Eeuo pipefail
PATRON 2
Error Trapping
trap ERR + EXIT para cleanup automatico
PATRON 5
Logging estructurado
Timestamps ISO + niveles INFO/WARN/ERROR
PATRON 3
Variable Safety
Todas las variables entre comillas dobles
PATRON 10
Dry-run
--dry-run para simular sin aplicar cambios
PATRON 8
Idempotencia
Seguro de re-ejecutar; checks de estado previo
🔒

1 · Strict Mode & cabecera del script

Fundacional
Por que importa Sin strict mode, Bash ignora errores silenciosamente. Con set -Eeuo pipefail, cualquier comando fallido detiene el script inmediatamente, evitando que un deploy roto llegue a produccion.
deploy-web.sh bash
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════
# deploy-web.sh — Despliegue web Astro a Vercel · CULTIVA IA
# Uso:  ./deploy-web.sh [--dry-run] [--env production|preview]
# Deps: node >=20, npm, vercel, git, curl
# ═══════════════════════════════════════════════════════════════
set -Eeuo pipefail   # E=heredar ERR en funciones | e=exit on error
                     # u=error en var indefinida  | pipefail=pipes

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
SCRIPT_NAME="$(basename -- "${BASH_SOURCE[0]}")"
🪤

2 · Error Trapping & Cleanup automatico

Patron 2 + 3
deploy-web.sh — traps bash
# ── Archivos temporales ──────────────────────────────────────
TMPDIR=$(mktemp -d) || { log_error "No se pudo crear tmpdir"; exit 1; }
BUILD_LOG="$TMPDIR/build.log"
DEPLOY_LOG="$TMPDIR/deploy.log"

# ── Cleanup al salir (EXIT = siempre; ERR = solo en error) ───
cleanup() {
    local exit_code="$?"
    log_info "Limpiando archivos temporales..."
    rm -rf -- "$TMPDIR"
    if [[ $exit_code -ne 0 ]]; then
        log_error "Script terminado con error (exit $exit_code)"
        notify_slack "❌ Deploy FALLIDO en $GIT_BRANCH (exit $exit_code)" "danger"
    fi
}

on_error() {
    log_error "Error inesperado en linea $BASH_LINENO → comando: $BASH_COMMAND"
}

trap cleanup EXIT
trap on_error ERR
📋

5 · Logging estructurado

Patron 5
deploy-web.sh — logging bash
log_info()  { printf '[%s] \033[32mINFO\033[0m  %s\n'  "$(date +'%Y-%m-%d %H:%M:%S')" "$*" >&2; }
log_warn()  { printf '[%s] \033[33mWARN\033[0m  %s\n'  "$(date +'%Y-%m-%d %H:%M:%S')" "$*" >&2; }
log_error() { printf '[%s] \033[31mERROR\033[0m %s\n' "$(date +'%Y-%m-%d %H:%M:%S')" "$*" >&2; }
log_debug() {
    [[ "${DEBUG:-0}" == "1" ]] && \
    printf '[%s] \033[36mDEBUG\033[0m %s\n' "$(date +'%Y-%m-%d %H:%M:%S')" "$*" >&2 || true
}

# Salida de ejemplo en terminal:
# [2026-06-16 09:34:12] INFO  Iniciando deploy de CULTIVA IA web
# [2026-06-16 09:34:13] INFO  Build completado (47s)
# [2026-06-16 09:34:15] WARN  Variable SLACK_WEBHOOK_URL no configurada
# [2026-06-16 09:34:18] ERROR Vercel token invalido
📦

Dep · Validacion de dependencias

command -v (seguro)
⚠️
Usa command -v, nunca which which no es POSIX y puede fallar en algunas shells. command -v es el estandar correcto y funciona en macOS, Linux y CI.
deploy-web.sh — check_dependencies bash
check_dependencies() {
    local -a required=("node" "npm" "vercel" "git" "curl")
    local -a missing=()

    for cmd in "${required[@]}"; do
        if ! command -v "$cmd" >/dev/null 2>&1; then
            missing+=("$cmd")
        fi
    done

    if [[ ${#missing[@]} -gt 0 ]]; then
        log_error "Dependencias faltantes: ${missing[*]}"
        log_error "Instala con: npm i -g vercel"
        return 1
    fi

    # Verificar versiones minimas
    local node_ver
    node_ver=$(node --version | tr -d 'v' | cut -d. -f1)
    if [[ $node_ver -lt 20 ]]; then
        log_error "Se requiere Node >= 20, encontrado: $node_ver"
        return 1
    fi

    log_info "Dependencias OK (node v$node_ver)"
}

# Requerir variables de entorno criticas
: "${VERCEL_TOKEN:?La variable VERCEL_TOKEN es obligatoria}"
⚙️

4 · Parsing de argumentos robusto

Patron 4
deploy-web.sh — parse_args bash
# Valores por defecto
DRY_RUN=false
DEPLOY_ENV="preview"
SKIP_TESTS=false
VERBOSE=false

usage() {
    cat <<EOF
Uso: $SCRIPT_NAME [OPCIONES]

Opciones:
  -d, --dry-run          Simular sin ejecutar cambios reales
  -e, --env ENV          Entorno destino: production|preview  [default: preview]
      --skip-tests       Omitir suite de tests
  -v, --verbose          Salida DEBUG detallada
  -h, --help             Mostrar esta ayuda
EOF
    exit "${1:-0}"
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        -d|--dry-run)    DRY_RUN=true;         shift   ;;
        -e|--env)        DEPLOY_ENV="$2";      shift 2 ;;
        --skip-tests)    SKIP_TESTS=true;      shift   ;;
        -v|--verbose)    VERBOSE=true; DEBUG=1; shift   ;;
        -h|--help)       usage 0                          ;;
        --)              shift; break                    ;;
        *)               log_error "Opcion desconocida: $1"; usage 1 ;;
    esac
done

# Validar enum
if [[ ! "$DEPLOY_ENV" =~ ^(production|preview)$ ]]; then
    log_error "--env debe ser 'production' o 'preview', recibido: '$DEPLOY_ENV'"
    exit 1
fi
🔍

10 · Dry-run mode

Patron 10
deploy-web.sh — run_cmd + dry-run bash
run_cmd() {
    if [[ "$DRY_RUN" == "true" ]]; then
        log_info "[DRY-RUN] Simularia: $*"
        return 0
    fi
    log_debug "Ejecutando: $*"
    "$@"
}

# Uso en el script principal:
build_project() {
    log_info "Iniciando build de Astro..."
    run_cmd npm run build 2> "$BUILD_LOG"
    log_info "Build completado — artefactos en ./dist"
}

deploy_to_vercel() {
    local vercel_flags=""
    [[ "$DEPLOY_ENV" == "production" ]] && vercel_flags="--prod"

    log_info "Desplegando a Vercel ($DEPLOY_ENV)..."
    run_cmd vercel deploy $vercel_flags --token "$VERCEL_TOKEN" --yes \
        2> "$DEPLOY_LOG"
}

# Con --dry-run la salida seria:
# [2026-06-16 09:41:00] INFO  [DRY-RUN] Simularia: npm run build
# [2026-06-16 09:41:00] INFO  [DRY-RUN] Simularia: vercel deploy --token *** --yes
🚀

Script completo — flujo principal

Todos los patrones integrados
deploy-web.sh — main() bash
main() {
    log_info "═══ Deploy CULTIVA IA Web ═══ (env: $DEPLOY_ENV)"
    [[ "$DRY_RUN" == "true" ]] && log_warn "Modo DRY-RUN activo — no se haran cambios reales"

    # 1. Verificar dependencias
    check_dependencies

    # 2. Validar repo limpio (no commitear archivos sin staging)
    if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
        log_warn "El repo tiene cambios sin commitear"
        [[ "$DEPLOY_ENV" == "production" ]] && {
            log_error "No se puede desplegar a produccion con cambios sin commitear"
            exit 1
        }
    fi

    # 3. Capturar rama actual (idempotente: info de contexto)
    GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
    GIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
    log_info "Rama: $GIT_BRANCH · commit: $GIT_SHA"

    # 4. Instalar dependencias (idempotente)
    log_info "Instalando dependencias npm..."
    run_cmd npm ci --prefer-offline

    # 5. Tests (opcional)
    if [[ "$SKIP_TESTS" == "false" ]] && [[ -f package.json ]] && \
       grep -q '"test"' package.json; then
        log_info "Ejecutando tests..."
        run_cmd npm test
    else
        log_warn "Tests omitidos"
    fi

    # 6. Build (si falla, el trap ERR detiene todo — no se despliega)
    build_project

    # 7. Deploy
    DEPLOY_URL=$(deploy_to_vercel)
    log_info "Deploy OK → $DEPLOY_URL"

    # 8. Notificar
    notify_slack "✅ Deploy OK · $GIT_BRANCH@$GIT_SHA$DEPLOY_ENV · $DEPLOY_URL" "good"
    log_info "═══ Deploy completado ═══"
}

main "$@"
📌

Cheatsheet — Referencia rapida

Patron Snippet Aplica cuando Nivel
Strict mode set -Eeuo pipefail Siempre, linea 1 tras shebang esencial
Cleanup trap trap 'rm -rf "$TMP"' EXIT Cuando usas archivos temporales esencial
Quoting cp "$src" "$dst" Toda expansion de variable esencial
Require var : "${VAR:?mensaje}" Variables de entorno criticas esencial
Check cmd command -v node Antes de usar herramientas externas esencial
Dry-run run_cmd vercel deploy Cualquier operacion destructiva recomendado
Logging printf '[%s] INFO %s\n' "$(date)" "$*" Toda salida informativa recomendado
Idempotencia [[ -d "$dir" ]] || mkdir -p "$dir" Scripts que crean recursos recomendado

Anti-patrones — Lo que NO hacer

diff — anti-patron vs patron correcto
- #!/bin/bash                    # sin strict mode
+ #!/usr/bin/env bash
+ set -Eeuo pipefail
  
- cp $source $dest               # sin comillas → falla con espacios
+ cp "$source" "$dest"
  
- which node                     # no POSIX, puede no estar en PATH
+ command -v node
  
- TMPFILE=/tmp/mi-script-temp    # colision entre instancias paralelas
+ TMPFILE=$(mktemp)
  
- result=`comando`              # backticks: no anidables, poco legibles
+ result=$(comando)
  
- if [ "$var" == "valor" ]       # [ ] no soporta ==, usar =
+ if [[ "$var" == "valor" ]]     # [[ ]] es Bash-nativo y mas seguro
  
- echo "Error: algo salio mal"   # echo a stdout, se mezcla con output util
+ log_error "algo salio mal"     # stderr con timestamp y nivel

Checklist de revision para scripts de produccion

  • Strict mode habilitado al inicio
  • Todas las variables entre comillas dobles
  • trap EXIT limpia recursos temporales
  • trap ERR registra linea del error
  • command -v verifica dependencias
  • mktemp para archivos temporales
  • Logging con timestamps a stderr
  • --dry-run soportado para debugging
  • Validacion de inputs antes de operar
  • Idempotencia verificada en re-ejecuciones