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 changesprepare-commit-msg: Prepares commit message templatecommit-msg: Validates commit messagespost-commit: Runs after commit completespre-rebase: Runs before rebasingpost-checkout: Runs after checking out a branchpost-merge: Runs after mergingpre-push: Runs before pushing to remote
Server-Side Hooks:
pre-receive: Runs before accepting pushed commitsupdate: Runs for each branch being updatedpost-receive: Runs after receiving and unpacking objectspost-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.


