17 KiB
Git Hooks and Validation Scripts Guide
This guide provides comprehensive examples and guidelines for git hooks and validation scripts used in GitLab Stack projects.
Overview
Git hooks are scripts that run automatically at specific points in the git workflow. For GitLab Stack projects, we use hooks to ensure validation before commits.
Directory Structure
project-name/
├── .git/
│ └── hooks/
│ └── pre-commit # Installed hook
└── scripts/
├── pre-commit # Source hook script
├── validate-stack.sh # Full validation
└── setup-hooks.sh # Hook installer
Core Scripts
1. scripts/pre-commit
The pre-commit hook runs before each commit to validate the stack.
Location: scripts/pre-commit
Installed to: .git/hooks/pre-commit
When it runs: Before git commit
#!/usr/bin/env bash
#
# Pre-commit hook for GitLab Stack validation
# Prevents commits that violate stack patterns
#
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo "========================================="
echo "Pre-commit validation"
echo "========================================="
echo
ERRORS=0
# Check 1: Secrets in staged files
echo "1. Checking for secrets in staged files..."
if git diff --cached --name-only | grep -qE "secrets/.*[^.gitkeep]|\.env$"; then
echo -e "${RED}✗ ERROR: Attempting to commit secrets or .env file!${NC}"
echo " Secrets should NEVER be committed to git."
echo " Files detected:"
git diff --cached --name-only | grep -E "secrets/.*[^.gitkeep]|\.env$" | sed 's/^/ /'
((ERRORS++))
else
echo -e "${GREEN}✓ No secrets in staged files${NC}"
fi
echo
# Check 2: Root-owned files
echo "2. Checking for root-owned files..."
if find . -user root -not -path "./.git/*" 2>/dev/null | grep -q .; then
echo -e "${RED}✗ ERROR: Root-owned files detected!${NC}"
echo " All files should be owned by the user running Docker."
echo " Files detected:"
find . -user root -not -path "./.git/*" 2>/dev/null | sed 's/^/ /'
echo
echo " Fix with: sudo chown -R \$USER:\$USER ."
((ERRORS++))
else
echo -e "${GREEN}✓ No root-owned files${NC}"
fi
echo
# Check 3: Secrets in file content (basic check)
echo "3. Checking for hardcoded secrets in code..."
SUSPICIOUS_PATTERNS=(
"password\s*=\s*['\"][^'\"]+['\"]"
"api[_-]?key\s*=\s*['\"][^'\"]+['\"]"
"secret\s*=\s*['\"][^'\"]+['\"]"
"token\s*=\s*['\"][^'\"]+['\"]"
)
FOUND_SECRETS=false
for pattern in "${SUSPICIOUS_PATTERNS[@]}"; do
if git diff --cached | grep -iE "$pattern" | grep -v "\.example" | grep -q .; then
if [ "$FOUND_SECRETS" = false ]; then
echo -e "${YELLOW}⚠ WARNING: Possible hardcoded secrets detected:${NC}"
FOUND_SECRETS=true
fi
git diff --cached | grep -iE "$pattern" | grep -v "\.example" | sed 's/^/ /'
fi
done
if [ "$FOUND_SECRETS" = true ]; then
echo
echo " Review these carefully. Use environment variables or Docker secrets instead."
echo " If these are false positives, you can proceed."
else
echo -e "${GREEN}✓ No obvious hardcoded secrets${NC}"
fi
echo
# Check 4: Full stack validation (if available)
if [ -x "./scripts/validate-stack.sh" ]; then
echo "4. Running full stack validation..."
if ! ./scripts/validate-stack.sh; then
echo -e "${RED}✗ ERROR: Stack validation failed!${NC}"
echo " Fix all issues before committing."
echo " Or use 'git commit --no-verify' to skip (NOT recommended)."
((ERRORS++))
else
echo -e "${GREEN}✓ Stack validation passed${NC}"
fi
else
echo -e "${YELLOW}⚠ Skipping stack validation (./scripts/validate-stack.sh not found)${NC}"
fi
echo
# Final result
echo "========================================="
if [ $ERRORS -gt 0 ]; then
echo -e "${RED}Pre-commit validation FAILED with $ERRORS error(s)${NC}"
echo "========================================="
echo
echo "To skip this validation (NOT recommended):"
echo " git commit --no-verify"
exit 1
fi
echo -e "${GREEN}Pre-commit validation PASSED!${NC}"
echo "========================================="
exit 0
Usage:
# Automatic - runs on every commit
git commit -m "message"
# Skip validation (emergency only)
git commit --no-verify -m "message"
2. scripts/validate-stack.sh
Comprehensive validation script that runs all validators.
Location: scripts/validate-stack.sh
When to run: Before deployment, in CI/CD, or manually
#!/usr/bin/env bash
#
# Full GitLab Stack validation
# Runs all validators to ensure stack compliance
#
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo
echo -e "${BLUE}========================================"
echo "GitLab Stack Validation"
echo "========================================${NC}"
echo
ERRORS=0
WARNINGS=0
# Check if we're in a stack directory
if [ ! -f "docker-compose.yml" ]; then
echo -e "${RED}✗ ERROR: docker-compose.yml not found!${NC}"
echo " Are you in a stack directory?"
exit 1
fi
# Validation 1: Stack Validator
echo -e "${BLUE}[1/4] Running stack-validator...${NC}"
if command -v claude-code >/dev/null 2>&1; then
if claude-code run stack-validator 2>&1 | tee /tmp/stack-validator.log; then
echo -e "${GREEN}✓ Stack validation passed${NC}"
else
echo -e "${RED}✗ Stack validation failed${NC}"
echo " Review output above for details"
((ERRORS++))
fi
else
echo -e "${YELLOW}⚠ Claude Code not available, skipping stack-validator${NC}"
((WARNINGS++))
fi
echo
# Validation 2: Secrets Manager
echo -e "${BLUE}[2/4] Running secrets-manager validation...${NC}"
if command -v claude-code >/dev/null 2>&1; then
if claude-code run secrets-manager --validate 2>&1 | tee /tmp/secrets-manager.log; then
echo -e "${GREEN}✓ Secrets validation passed${NC}"
else
echo -e "${RED}✗ Secrets validation failed${NC}"
echo " Review output above for details"
((ERRORS++))
fi
else
echo -e "${YELLOW}⚠ Claude Code not available, running basic secrets check${NC}"
# Basic secrets check without Claude Code
if find secrets/ -type f ! -name ".gitkeep" 2>/dev/null | grep -q .; then
if grep -r "DOCKER_SECRET" docker-compose.yml >/dev/null 2>&1; then
echo -e "${GREEN}✓ Basic secrets check passed${NC}"
else
echo -e "${YELLOW}⚠ Secrets files found but not referenced in docker-compose.yml${NC}"
((WARNINGS++))
fi
else
echo -e "${GREEN}✓ No secrets configured${NC}"
fi
fi
echo
# Validation 3: Docker Validator
echo -e "${BLUE}[3/4] Running docker-validation...${NC}"
if command -v claude-code >/dev/null 2>&1; then
if claude-code run docker-validation 2>&1 | tee /tmp/docker-validation.log; then
echo -e "${GREEN}✓ Docker validation passed${NC}"
else
echo -e "${RED}✗ Docker validation failed${NC}"
echo " Review output above for details"
((ERRORS++))
fi
else
echo -e "${YELLOW}⚠ Claude Code not available, running basic Docker checks${NC}"
# Basic docker-compose syntax check
if docker compose config >/dev/null 2>&1; then
echo -e "${GREEN}✓ docker-compose.yml syntax valid${NC}"
else
echo -e "${RED}✗ docker-compose.yml syntax invalid${NC}"
((ERRORS++))
fi
fi
echo
# Validation 4: File Ownership
echo -e "${BLUE}[4/4] Checking file ownership...${NC}"
if find . -user root -not -path "./.git/*" 2>/dev/null | grep -q .; then
echo -e "${RED}✗ Root-owned files detected:${NC}"
find . -user root -not -path "./.git/*" 2>/dev/null | sed 's/^/ /'
echo
echo " Fix with: sudo chown -R \$USER:\$USER ."
((ERRORS++))
else
echo -e "${GREEN}✓ No root-owned files${NC}"
fi
echo
# Additional checks
echo -e "${BLUE}Additional checks:${NC}"
# Check .env vs .env.example sync
if [ -f ".env.example" ]; then
ENV_KEYS=$(grep -v '^#' .env.example 2>/dev/null | grep '=' | cut -d= -f1 | sort)
if [ -f ".env" ]; then
ACTUAL_KEYS=$(grep -v '^#' .env 2>/dev/null | grep '=' | cut -d= -f1 | sort)
if [ "$ENV_KEYS" != "$ACTUAL_KEYS" ]; then
echo -e "${YELLOW}⚠ .env and .env.example keys don't match${NC}"
((WARNINGS++))
else
echo -e "${GREEN}✓ .env synced with .env.example${NC}"
fi
else
echo -e "${YELLOW}⚠ .env file not found (expected from .env.example)${NC}"
((WARNINGS++))
fi
fi
# Check git setup
if [ -d ".git" ]; then
# Check default branch
DEFAULT_BRANCH=$(git config init.defaultBranch 2>/dev/null || echo "")
if [ "$DEFAULT_BRANCH" = "main" ]; then
echo -e "${GREEN}✓ Git default branch: main${NC}"
else
echo -e "${YELLOW}⚠ Git default branch not set to 'main'${NC}"
((WARNINGS++))
fi
# Check merge strategy
MERGE_FF=$(git config merge.ff 2>/dev/null || echo "")
if [ "$MERGE_FF" = "only" ]; then
echo -e "${GREEN}✓ Git merge strategy: ff-only${NC}"
else
echo -e "${YELLOW}⚠ Git merge strategy not set to 'ff-only'${NC}"
((WARNINGS++))
fi
else
echo -e "${YELLOW}⚠ Not a git repository${NC}"
((WARNINGS++))
fi
echo
# Final report
echo -e "${BLUE}========================================"
echo "Validation Summary"
echo "========================================${NC}"
if [ $ERRORS -gt 0 ]; then
echo -e "${RED}FAILED: $ERRORS error(s) found${NC}"
if [ $WARNINGS -gt 0 ]; then
echo -e "${YELLOW}$WARNINGS warning(s) found${NC}"
fi
echo "========================================${NC}"
exit 1
elif [ $WARNINGS -gt 0 ]; then
echo -e "${YELLOW}PASSED with $WARNINGS warning(s)${NC}"
echo "========================================${NC}"
exit 0
else
echo -e "${GREEN}ALL VALIDATIONS PASSED!${NC}"
echo "========================================${NC}"
exit 0
fi
Usage:
# Run full validation
./scripts/validate-stack.sh
# In CI/CD
./scripts/validate-stack.sh || exit 1
3. scripts/setup-hooks.sh
Script to install git hooks from scripts/ to .git/hooks/
Location: scripts/setup-hooks.sh
When to run: After cloning, during stack creation
#!/usr/bin/env bash
#
# Install git hooks from scripts/ to .git/hooks/
#
set -euo pipefail
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo "Installing git hooks..."
echo
# Check if .git exists
if [ ! -d ".git" ]; then
echo -e "${RED}✗ ERROR: Not a git repository!${NC}"
echo " Initialize git first: git init"
exit 1
fi
# Ensure hooks directory exists
mkdir -p .git/hooks
INSTALLED=0
FAILED=0
# Install pre-commit hook
if [ -f "scripts/pre-commit" ]; then
if cp scripts/pre-commit .git/hooks/pre-commit; then
chmod +x .git/hooks/pre-commit
echo -e "${GREEN}✓ Installed pre-commit hook${NC}"
((INSTALLED++))
else
echo -e "${RED}✗ Failed to install pre-commit hook${NC}"
((FAILED++))
fi
else
echo -e "${YELLOW}⚠ scripts/pre-commit not found, skipping${NC}"
fi
# Install pre-push hook (if exists)
if [ -f "scripts/pre-push" ]; then
if cp scripts/pre-push .git/hooks/pre-push; then
chmod +x .git/hooks/pre-push
echo -e "${GREEN}✓ Installed pre-push hook${NC}"
((INSTALLED++))
else
echo -e "${RED}✗ Failed to install pre-push hook${NC}"
((FAILED++))
fi
fi
echo
echo "========================================="
if [ $FAILED -gt 0 ]; then
echo -e "${RED}Installation completed with $FAILED error(s)${NC}"
echo "========================================="
exit 1
elif [ $INSTALLED -eq 0 ]; then
echo -e "${YELLOW}No hooks installed${NC}"
echo "========================================="
exit 0
else
echo -e "${GREEN}Successfully installed $INSTALLED hook(s)!${NC}"
echo "========================================="
echo
echo "Hooks will now run automatically:"
echo " - pre-commit: Before each commit"
echo
echo "To skip hooks (emergency only):"
echo " git commit --no-verify"
fi
Usage:
# Install hooks
./scripts/setup-hooks.sh
# Verify installation
ls -la .git/hooks/
Optional Hooks
scripts/pre-push (Optional)
Runs before git push to ensure remote-ready state.
#!/usr/bin/env bash
#
# Pre-push hook for GitLab Stack
# Runs before pushing to remote
#
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo "========================================="
echo "Pre-push validation"
echo "========================================="
echo
ERRORS=0
# Run full validation before push
echo "Running full stack validation..."
if [ -x "./scripts/validate-stack.sh" ]; then
if ! ./scripts/validate-stack.sh; then
echo -e "${RED}✗ Stack validation failed!${NC}"
echo " Fix issues before pushing."
((ERRORS++))
fi
else
echo -e "${YELLOW}⚠ ./scripts/validate-stack.sh not found${NC}"
fi
# Check for uncommitted changes
if ! git diff-index --quiet HEAD --; then
echo -e "${YELLOW}⚠ WARNING: You have uncommitted changes${NC}"
echo " Consider committing them before pushing."
fi
echo
if [ $ERRORS -gt 0 ]; then
echo -e "${RED}Pre-push validation FAILED${NC}"
echo "To skip: git push --no-verify"
exit 1
fi
echo -e "${GREEN}Pre-push validation PASSED${NC}"
exit 0
CI/CD Integration
GitLab CI (.gitlab-ci.yml)
stages:
- validate
- build
- deploy
validate:
stage: validate
image: docker:latest
services:
- docker:dind
before_script:
- apk add --no-cache bash findutils
script:
- chmod +x ./scripts/validate-stack.sh
- ./scripts/validate-stack.sh
only:
- merge_requests
- main
GitHub Actions (.github/workflows/validate.yml)
name: Stack Validation
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run stack validation
run: |
chmod +x ./scripts/validate-stack.sh
./scripts/validate-stack.sh
Best Practices
1. Always Make Scripts Executable
chmod +x scripts/*.sh
chmod +x scripts/pre-commit
2. Test Hooks Before Committing
# Test pre-commit manually
./scripts/pre-commit
# Test validation
./scripts/validate-stack.sh
3. Document Hook Behavior
Include in README.md:
## Git Hooks
This project uses git hooks for validation:
- **pre-commit**: Validates before each commit
- To skip: `git commit --no-verify` (emergency only)
4. Provide Skip Option
Always allow users to skip in emergencies:
git commit --no-verify -m "emergency fix"
5. Keep Hooks Fast
- Pre-commit should run in < 10 seconds
- Use quick checks when possible
- Defer expensive checks to CI/CD
6. Clear Error Messages
echo -e "${RED}✗ ERROR: Clear description${NC}"
echo " Explanation of what went wrong"
echo " How to fix it"
7. Exit Codes
# Success
exit 0
# Failure
exit 1
# Always use set -e to catch errors
set -euo pipefail
Troubleshooting
Hook Not Running
# Check if hook is installed
ls -la .git/hooks/pre-commit
# Check if executable
chmod +x .git/hooks/pre-commit
# Reinstall hooks
./scripts/setup-hooks.sh
Hook Fails Unexpectedly
# Run hook manually to see output
./scripts/pre-commit
# Check validation separately
./scripts/validate-stack.sh
# Debug with set -x
bash -x scripts/pre-commit
Skip Hook Temporarily
# Skip pre-commit
git commit --no-verify -m "message"
# Skip pre-push
git push --no-verify
Permission Denied
# Make script executable
chmod +x scripts/pre-commit
chmod +x scripts/validate-stack.sh
# Reinstall hooks
./scripts/setup-hooks.sh
Customization
Adding Custom Checks
Edit scripts/pre-commit:
# Add custom check
echo "5. Running custom validation..."
if ! ./scripts/my-custom-check.sh; then
echo -e "${RED}✗ Custom validation failed${NC}"
((ERRORS++))
else
echo -e "${GREEN}✓ Custom validation passed${NC}"
fi
echo
Adjusting Validation Strictness
Strict Mode (recommended for production):
# Fail on any error
set -euo pipefail
Lenient Mode (development only):
# Continue on errors, just report
set -uo pipefail
Environment-Specific Hooks
# Check environment
if [ "${ENV:-}" = "production" ]; then
# Strict validation for production
./scripts/validate-stack.sh
else
# Lenient for development
echo "Development environment, skipping some checks"
fi
Summary
Git hooks ensure:
- ✅ No secrets committed
- ✅ No root-owned files
- ✅ Full stack validation before commit
- ✅ Consistent code quality
- ✅ Automated validation in workflow
All scripts are:
- Executable (
chmod +x) - Well-documented
- Provide clear error messages
- Support emergency skip (
--no-verify) - Integrate with CI/CD