Git Hooks para Despliegue Automático: Automatización CI/CD con Git

Introducción

Los Git hooks son scripts poderosos que se ejecutan automáticamente en puntos específicos del flujo de trabajo de Git, habilitando pruebas automatizadas, verificaciones de calidad de código y pipelines de despliegue. Al aprovechar los Git hooks, puedes implementar prácticas de integración y despliegue continuo directamente dentro de tu sistema de control de versiones, asegurando la calidad del código y automatizando tareas repetitivas.

Esta guía completa explora implementaciones prácticas de Git hooks para despliegue automático, validación pre-commit y automatización CI/CD.

Entendiendo Git Hooks

¿Qué son los Git Hooks?

Los Git hooks son scripts personalizados que Git ejecuta antes o después de eventos como commit, push, merge y checkout. Te permiten automatizar flujos de trabajo, hacer cumplir políticas y disparar despliegues automáticamente.

Tipos de Git Hooks

Hooks del Lado del Cliente:

  • pre-commit: Se ejecuta antes de confirmar cambios
  • prepare-commit-msg: Prepara la plantilla del mensaje de commit
  • commit-msg: Valida mensajes de commit
  • post-commit: Se ejecuta después de que se completa el commit
  • pre-rebase: Se ejecuta antes de hacer rebase
  • post-checkout: Se ejecuta después de hacer checkout de una rama
  • post-merge: Se ejecuta después de hacer merge
  • pre-push: Se ejecuta antes de hacer push al remoto

Hooks del Lado del Servidor:

  • pre-receive: Se ejecuta antes de aceptar commits enviados
  • update: Se ejecuta para cada rama que se está actualizando
  • post-receive: Se ejecuta después de recibir y desempaquetar objetos
  • post-update: Se ejecuta después de actualizar referencias remotas

Ubicación de Hooks

# Project hooks
.git/hooks/

# Global hooks (Git 2.9+)
git config --global core.hooksPath ~/.git-hooks

# List available hooks
ls -la .git/hooks/

Pre-Commit Hooks

1. Linting y Formateo de Código

#!/bin/bash
# .git/hooks/pre-commit
# Ensure code quality before commits

set -e

echo "Running pre-commit checks..."

# Check if there are files to commit
if git diff --cached --name-only | grep -qE '\\.(js|jsx|ts|tsx)$'; then
    echo "Running ESLint..."
    npm run lint

    echo "Running Prettier..."
    npm run format:check
fi

# Python files
if git diff --cached --name-only | grep -qE '\\.py$'; then
    echo "Running Black..."
    black --check .

    echo "Running Flake8..."
    flake8 .

    echo "Running mypy..."
    mypy .
fi

# Shell scripts
if git diff --cached --name-only | grep -qE '\\.sh$'; then
    echo "Running shellcheck..."
    git diff --cached --name-only | grep '\\.sh$' | xargs shellcheck
fi

echo "✓ Pre-commit checks passed"

2. Escaneo de Seguridad

#!/bin/bash
# .git/hooks/pre-commit
# Scan for secrets and security issues

set -e

# Check for AWS keys
if git diff --cached | grep -qiE 'AKIA[0-9A-Z]{16}'; then
    echo "❌ AWS Access Key detected in commit"
    exit 1
fi

# Check for private keys
if git diff --cached | grep -qiE '-----BEGIN (RSA|DSA|EC) PRIVATE KEY-----'; then
    echo "❌ Private key detected in commit"
    exit 1
fi

# Check for common secrets
if git diff --cached | grep -qiE 'password\\s*=|api_key\\s*=|secret\\s*='; then
    echo "⚠️  Possible hardcoded credentials detected"
    echo "Please review your changes carefully"
fi

# Run gitleaks if available
if command -v gitleaks &> /dev/null; then
    echo "Running gitleaks..."
    gitleaks protect --staged
fi

echo "✓ Security checks passed"

3. Ejecución de Pruebas

#!/bin/bash
# .git/hooks/pre-commit
# Run tests before allowing commit

set -e

echo "Running unit tests..."

# Node.js projects
if [ -f "package.json" ]; then
    npm test
fi

# Python projects
if [ -f "pytest.ini" ] || [ -f "setup.py" ]; then
    pytest
fi

# Go projects
if [ -f "go.mod" ]; then
    go test ./...
fi

echo "✓ All tests passed"

Validación de Mensajes de Commit

4. Forzar Formato de Mensaje de Commit

#!/bin/bash
# .git/hooks/commit-msg
# Enforce conventional commit message format

commit_msg_file=$1
commit_msg=$(cat "$commit_msg_file")

# Conventional commits pattern
pattern='^(feat|fix|docs|style|refactor|test|chore)(\\(.+\\))?: .{1,100}$'

if ! echo "$commit_msg" | grep -qE "$pattern"; then
    cat <<EOF
❌ Invalid commit message format

Commit messages must follow the conventional commits format:
  <type>(<scope>): <subject>

Examples:
  feat(api): add user authentication endpoint
  fix(ui): resolve button alignment issue
  docs: update installation instructions

Types: feat, fix, docs, style, refactor, test, chore
EOF
    exit 1
fi

echo "✓ Commit message format is valid"

Pre-Push Hooks

5. Validación Pre-Push

#!/bin/bash
# .git/hooks/pre-push
# Validate before pushing to remote

set -e

echo "Running pre-push checks..."

# Get current branch
current_branch=$(git branch --show-current)

# Prevent pushing to main/master directly
if [[ "$current_branch" == "main" || "$current_branch" == "master" ]]; then
    echo "❌ Direct push to $current_branch is not allowed"
    echo "Please create a feature branch and submit a pull request"
    exit 1
fi

# Run full test suite
echo "Running full test suite..."
npm test || pytest || go test ./...

# Check for merge conflicts markers
if git grep -qE '<<<<<<< HEAD|>>>>>>> |=======' -- ':!.git'; then
    echo "❌ Merge conflict markers detected"
    exit 1
fi

echo "✓ Pre-push checks passed"

Hooks del Lado del Servidor para Despliegue

6. Post-Receive Hook para Despliegue Automático

#!/bin/bash
# hooks/post-receive
# Automatic deployment after receiving push

set -e

DEPLOY_DIR="/var/www/production"
REPO_DIR="/var/repos/myapp.git"
BRANCH="main"

while read oldrev newrev refname; do
    branch=$(git rev-parse --symbolic --abbrev-ref $refname)

    if [ "$branch" = "$BRANCH" ]; then
        echo "Deploying $branch to production..."

        # Create backup
        BACKUP_DIR="/var/backups/deployments"
        mkdir -p "$BACKUP_DIR"
        TIMESTAMP=$(date +%Y%m%d_%H%M%S)

        if [ -d "$DEPLOY_DIR" ]; then
            tar -czf "$BACKUP_DIR/backup-${TIMESTAMP}.tar.gz" -C "$DEPLOY_DIR" .
            echo "✓ Backup created: backup-${TIMESTAMP}.tar.gz"
        fi

        # Checkout code
        mkdir -p "$DEPLOY_DIR"
        GIT_WORK_TREE="$DEPLOY_DIR" git checkout -f "$BRANCH"

        # Install dependencies
        cd "$DEPLOY_DIR"

        if [ -f "package.json" ]; then
            echo "Installing npm dependencies..."
            npm install --production
        fi

        if [ -f "requirements.txt" ]; then
            echo "Installing Python dependencies..."
            pip install -r requirements.txt
        fi

        # Run database migrations
        if [ -f "manage.py" ]; then
            python manage.py migrate --noinput
        fi

        # Build assets
        if [ -f "package.json" ] && grep -q "\\"build\\"" package.json; then
            npm run build
        fi

        # Restart application
        echo "Restarting application..."
        systemctl restart myapp

        # Verify deployment
        sleep 5
        if systemctl is-active --quiet myapp; then
            echo "✓ Deployment successful"

            # Send notification
            curl -X POST "https://hooks.slack.com/services/YOUR/WEBHOOK" \
                -H 'Content-Type: application/json' \
                -d "{\\\"text\\\":\\\"✓ Deployment successful: ${newrev:0:7} to production\\\"}"
        else
            echo "❌ Deployment failed - service not running"

            # Rollback
            echo "Rolling back to previous version..."
            latest_backup=$(ls -t "$BACKUP_DIR" | head -1)
            tar -xzf "$BACKUP_DIR/$latest_backup" -C "$DEPLOY_DIR"
            systemctl restart myapp

            # Send failure notification
            curl -X POST "https://hooks.slack.com/services/YOUR/WEBHOOK" \
                -H 'Content-Type: application/json' \
                -d "{\\\"text\\\":\\\"❌ Deployment failed: rolling back\\\"}"

            exit 1
        fi
    fi
done

echo "Post-receive hook completed"

7. Zero-Downtime Deployment Hook

#!/bin/bash
# hooks/post-receive
# Zero-downtime deployment with health checks

set -e

DEPLOY_DIR="/var/www/production"
RELEASE_DIR="/var/www/releases"
CURRENT_LINK="/var/www/current"
BRANCH="main"

while read oldrev newrev refname; do
    branch=$(git rev-parse --symbolic --abbrev-ref $refname)

    if [ "$branch" = "$BRANCH" ]; then
        TIMESTAMP=$(date +%Y%m%d_%H%M%S)
        RELEASE_PATH="$RELEASE_DIR/$TIMESTAMP"

        echo "Creating release: $TIMESTAMP"
        mkdir -p "$RELEASE_PATH"

        # Checkout code to new release directory
        GIT_WORK_TREE="$RELEASE_PATH" git checkout -f "$BRANCH"

        cd "$RELEASE_PATH"

        # Install dependencies
        npm install --production

        # Build application
        npm run build

        # Link shared directories (uploads, logs, etc.)
        ln -s "$DEPLOY_DIR/shared/uploads" "$RELEASE_PATH/public/uploads"
        ln -s "$DEPLOY_DIR/shared/logs" "$RELEASE_PATH/logs"

        # Update current symlink
        ln -sfn "$RELEASE_PATH" "$CURRENT_LINK"

        # Reload application (graceful restart)
        systemctl reload myapp

        # Health check
        echo "Performing health check..."
        sleep 5

        if curl -f http://localhost:3000/health > /dev/null 2>&1; then
            echo "✓ Health check passed"

            # Cleanup old releases (keep last 5)
            cd "$RELEASE_DIR"
            ls -t | tail -n +6 | xargs -r rm -rf

            echo "✓ Deployment completed successfully"
        else
            echo "❌ Health check failed - rolling back"

            # Rollback to previous release
            PREVIOUS_RELEASE=$(ls -t "$RELEASE_DIR" | sed -n '2p')
            ln -sfn "$RELEASE_DIR/$PREVIOUS_RELEASE" "$CURRENT_LINK"
            systemctl reload myapp

            # Cleanup failed release
            rm -rf "$RELEASE_PATH"

            exit 1
        fi
    fi
done

Gestión de Git Hooks

Instalación de Hooks

#!/bin/bash
# install-hooks.sh
# Install Git hooks for all developers

HOOKS_DIR=".githooks"
GIT_HOOKS_DIR=".git/hooks"

# Copy hooks to .git/hooks
for hook in "$HOOKS_DIR"/*; do
    hook_name=$(basename "$hook")
    cp "$hook" "$GIT_HOOKS_DIR/$hook_name"
    chmod +x "$GIT_HOOKS_DIR/$hook_name"
    echo "Installed: $hook_name"
done

# Configure Git to use custom hooks directory
git config core.hooksPath "$HOOKS_DIR"

echo "✓ Git hooks installed successfully"

Compartir Hooks con el Equipo

# Store hooks in repository
mkdir -p .githooks

# Move hooks to .githooks directory
mv .git/hooks/pre-commit .githooks/
mv .git/hooks/commit-msg .githooks/

# Configure git to use .githooks directory
git config core.hooksPath .githooks

# Add to repository
git add .githooks
git commit -m "chore: add git hooks for team"

# Team members run:
git config core.hooksPath .githooks
chmod +x .githooks/*

Patrones Avanzados de Despliegue

8. Blue-Green Deployment Hook

#!/bin/bash
# hooks/post-receive
# Blue-green deployment pattern

set -e

BLUE_DIR="/var/www/blue"
GREEN_DIR="/var/www/green"
CURRENT_LINK="/var/www/current"
BRANCH="main"

# Determine which environment is currently active
if [ -L "$CURRENT_LINK" ]; then
    CURRENT=$(readlink "$CURRENT_LINK")
    if [ "$CURRENT" = "$BLUE_DIR" ]; then
        TARGET_DIR="$GREEN_DIR"
        TARGET_PORT=3001
        CURRENT_PORT=3000
    else
        TARGET_DIR="$BLUE_DIR"
        TARGET_PORT=3000
        CURRENT_PORT=3001
    fi
else
    TARGET_DIR="$BLUE_DIR"
    TARGET_PORT=3000
fi

echo "Deploying to $(basename $TARGET_DIR) environment..."

# Deploy to target environment
GIT_WORK_TREE="$TARGET_DIR" git checkout -f "$BRANCH"
cd "$TARGET_DIR"

# Install and build
npm install --production
npm run build

# Start application on target port
PORT=$TARGET_PORT npm start &
PID=$!

# Wait for application to start
sleep 10

# Health check
if curl -f "http://localhost:$TARGET_PORT/health" > /dev/null 2>&1; then
    echo "✓ New version is healthy"

    # Switch traffic
    ln -sfn "$TARGET_DIR" "$CURRENT_LINK"

    # Update load balancer or reverse proxy
    # Update Nginx upstream
    sed -i "s/localhost:$CURRENT_PORT/localhost:$TARGET_PORT/" /etc/nginx/sites-enabled/myapp
    nginx -s reload

    # Stop old version
    OLD_PID=$(lsof -ti:$CURRENT_PORT)
    if [ -n "$OLD_PID" ]; then
        kill $OLD_PID
    fi

    echo "✓ Traffic switched to new version"
else
    echo "❌ Health check failed"
    kill $PID
    exit 1
fi

Mejores Prácticas

Plantilla de Hook

#!/bin/bash
# Git Hook Template

set -euo pipefail

# Colors for output
RED='\\033[0;31m'
GREEN='\\033[0;32m'
YELLOW='\\033[1;33m'
NC='\\033[0m'

log_info() {
    echo -e "${GREEN}[INFO]${NC} $*"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $*" >&2
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $*"
}

# Main logic here

exit 0

Probar Hooks

# Test pre-commit hook
git commit --allow-empty -m "test" --no-verify  # Skip hooks
git commit --allow-empty -m "test"              # Run hooks

# Test post-receive hook
# Push to test repository with hook installed
git push test-repo main

Depurar Hooks

#!/bin/bash
# Add to beginning of hook

# Enable debug mode
set -x

# Log to file
exec 2>> /tmp/git-hook-debug.log

Conclusión

Los Git hooks proporcionan capacidades poderosas de automatización para hacer cumplir la calidad del código, ejecutar pruebas y desplegar aplicaciones automáticamente. Al implementar estos hooks, puedes crear pipelines CI/CD robustos directamente dentro de tu flujo de trabajo de Git.

Puntos clave:

  • Usa hooks del lado del cliente para calidad de código y pruebas
  • Implementa hooks del lado del servidor para despliegue automático
  • Siempre incluye verificaciones de salud y mecanismos de rollback
  • Comparte hooks con tu equipo vía repositorio
  • Prueba los hooks exhaustivamente antes de uso en producción
  • Implementa manejo de errores y registro adecuados
  • Documenta el comportamiento de los hooks para los miembros del equipo

Comienza con hooks simples y gradualmente agrega automatización más sofisticada a medida que tu equipo se sienta cómodo con el flujo de trabajo.