Git Hooks for Automatic Deployment: CI/CD Automation with Git

Introduction

Git hooks are powerful scripts that automatically execute at specific points in the Git workflow, enabling automated testing, code quality checks, and deployment pipelines. By leveraging Git hooks, you can implement continuous integration and deployment practices directly within your version control system, ensuring code quality and automating repetitive tasks.

This comprehensive guide explores practical Git hook implementations for automatic deployment, pre-commit validation, and CI/CD automation.

Understanding Git Hooks

What are Git Hooks?

Git hooks are custom scripts that Git executes before or after events such as commit, push, merge, and checkout. They enable you to automate workflows, enforce policies, and trigger deployments automatically.

Types of Git Hooks

Client-Side Hooks:

  • pre-commit: Runs before committing changes
  • prepare-commit-msg: Prepares commit message template
  • commit-msg: Validates commit messages
  • post-commit: Runs after commit completes
  • pre-rebase: Runs before rebasing
  • post-checkout: Runs after checking out a branch
  • post-merge: Runs after merging
  • pre-push: Runs before pushing to remote

Server-Side Hooks:

  • pre-receive: Runs before accepting pushed commits
  • update: Runs for each branch being updated
  • post-receive: Runs after receiving and unpacking objects
  • post-update: Runs after updating remote refs

Hook Location

# 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. Code Linting and Formatting

#!/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. Security Scanning

#!/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. Test Execution

#!/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"

Commit Message Validation

4. Enforce Commit Message Format

#!/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. Pre-Push Validation

#!/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"

Server-Side Hooks for Deployment

6. Post-Receive Hook for Automatic Deployment

#!/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

Git Hook Management

Installing 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"

Sharing Hooks with Team

# 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/*

Advanced Deployment Patterns

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

Best Practices

Hook Template

#!/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

Testing 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

Debugging Hooks

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

# Enable debug mode
set -x

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

Conclusion

Git hooks provide powerful automation capabilities for enforcing code quality, running tests, and deploying applications automatically. By implementing these hooks, you can create robust CI/CD pipelines directly within your Git workflow.

Key takeaways:

  • Use client-side hooks for code quality and testing
  • Implement server-side hooks for automatic deployment
  • Always include health checks and rollback mechanisms
  • Share hooks with your team via repository
  • Test hooks thoroughly before production use
  • Implement proper error handling and logging
  • Document hook behavior for team members

Start with simple hooks and gradually add more sophisticated automation as your team becomes comfortable with the workflow.