#!/bin/bash # Pre-push hook: full verification including security checks # Skip with: git push --no-verify # # This hook is contextual - it only runs checks for file types in the commits being pushed. set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # No Color # Get the commits being pushed CURRENT_BRANCH=$(git symbolic-ref --short HEAD) UPSTREAM=$(git rev-parse --abbrev-ref @{u} 2>/dev/null || echo "") if [ -z "$UPSTREAM" ]; then # No upstream, compare against main/master BASE_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main") CHANGED_FILES=$(git diff --name-only "origin/${BASE_BRANCH}...HEAD" 2>/dev/null || git diff --name-only HEAD~10..HEAD 2>/dev/null || echo "") else # Compare against upstream CHANGED_FILES=$(git diff --name-only @{u}..HEAD 2>/dev/null || echo "") fi # Detect which file types are in the commits being pushed HAS_GO_FILES=$(echo "$CHANGED_FILES" | grep -E '\.go$' || true) HAS_MD_FILES=$(echo "$CHANGED_FILES" | grep -E '\.md$' || true) HAS_TS_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx)$' || true) # Exit early if no relevant files in the push if [ -z "$HAS_GO_FILES" ] && [ -z "$HAS_MD_FILES" ] && [ -z "$HAS_TS_FILES" ]; then echo -e "${GREEN}✓${NC} No Go, Markdown, or TypeScript files in push, skipping checks" exit 0 fi # Header echo "" echo -e "${BLUE}╔═══════════════════════════════════════════════════╗${NC}" echo -e "${BLUE}║${NC} ${BOLD}PRE-PUSH VERIFICATION${NC} ${BLUE}║${NC}" echo -e "${BLUE}╚═══════════════════════════════════════════════════╝${NC}" echo "" # Show what's being pushed echo -e "${YELLOW}Branch:${NC} ${CURRENT_BRANCH}" echo -e "${YELLOW}Commits:${NC}" if [ -n "$UPSTREAM" ]; then git log --oneline @{u}.. 2>/dev/null | head -5 | sed 's/^/ /' || echo " (new branch)" else echo " (new branch)" fi echo "" # Show what's being checked if [ -n "$HAS_GO_FILES" ]; then GO_COUNT=$(echo "$HAS_GO_FILES" | wc -l | tr -d ' ') echo -e "${YELLOW}Go files changed:${NC} ${GO_COUNT} file(s)" fi if [ -n "$HAS_MD_FILES" ]; then MD_COUNT=$(echo "$HAS_MD_FILES" | wc -l | tr -d ' ') echo -e "${YELLOW}Markdown files changed:${NC} ${MD_COUNT} file(s)" fi if [ -n "$HAS_TS_FILES" ]; then TS_COUNT=$(echo "$HAS_TS_FILES" | wc -l | tr -d ' ') echo -e "${YELLOW}TypeScript files changed:${NC} ${TS_COUNT} file(s)" fi echo "" # Track if any check fails FAILED=0 START_TIME=$(date +%s) # Count total checks to run TOTAL_CHECKS=0 if [ -n "$HAS_GO_FILES" ]; then TOTAL_CHECKS=$((TOTAL_CHECKS + 6)) # fmt-check, vet, lint-go, gosec, vulncheck, test-race fi if [ -n "$HAS_MD_FILES" ]; then TOTAL_CHECKS=$((TOTAL_CHECKS + 1)) # markdownlint fi if [ -n "$HAS_TS_FILES" ]; then TOTAL_CHECKS=$((TOTAL_CHECKS + 2)) # eslint + tsc fi CURRENT_CHECK=0 # Helper function to run a check with timing run_check() { local name=$1 local command=$2 local start=$(date +%s) CURRENT_CHECK=$((CURRENT_CHECK + 1)) echo -e "${CYAN}[${CURRENT_CHECK}/${TOTAL_CHECKS}]${NC} ${YELLOW}▶${NC} ${name}..." if eval "$command" > /tmp/hook-output.txt 2>&1; then local duration=$(($(date +%s) - start)) echo -e " ${GREEN}✓${NC} ${name} passed ${CYAN}(${duration}s)${NC}" return 0 else echo -e " ${RED}✗${NC} ${name} failed" echo "" cat /tmp/hook-output.txt echo "" return 1 fi } # Go checks (only if .go files are in the push) if [ -n "$HAS_GO_FILES" ]; then # 1. Format check run_check "Checking Go code formatting" "make fmt-check" || FAILED=1 # 2. Static analysis (go vet) run_check "Running static analysis (go vet)" "make vet" || FAILED=1 # 3. Go linting run_check "Running golangci-lint" "make lint-go" || FAILED=1 # 4. Security scan (gosec) echo -e "${CYAN}[$((CURRENT_CHECK + 1))/${TOTAL_CHECKS}]${NC} ${YELLOW}▶${NC} Running security scan (gosec)..." CURRENT_CHECK=$((CURRENT_CHECK + 1)) START=$(date +%s) if make gosec > /tmp/hook-output.txt 2>&1; then DURATION=$(($(date +%s) - START)) ISSUES=$(grep -oE "Issues : [0-9]+" /tmp/hook-output.txt | grep -oE "[0-9]+" || echo "0") NOSEC=$(grep -oE "Nosec : [0-9]+" /tmp/hook-output.txt | grep -oE "[0-9]+" || echo "0") echo -e " ${GREEN}✓${NC} Security scan passed - ${ISSUES} issues, ${NOSEC} suppressed ${CYAN}(${DURATION}s)${NC}" else echo -e " ${RED}✗${NC} Security scan failed" echo "" cat /tmp/hook-output.txt echo "" FAILED=1 fi # 5. Vulnerability check (govulncheck) echo -e "${CYAN}[$((CURRENT_CHECK + 1))/${TOTAL_CHECKS}]${NC} ${YELLOW}▶${NC} Checking for vulnerabilities (govulncheck)..." CURRENT_CHECK=$((CURRENT_CHECK + 1)) START=$(date +%s) if make vulncheck > /tmp/hook-output.txt 2>&1; then DURATION=$(($(date +%s) - START)) if grep -q "No vulnerabilities found" /tmp/hook-output.txt; then echo -e " ${GREEN}✓${NC} No vulnerabilities found ${CYAN}(${DURATION}s)${NC}" else echo -e " ${GREEN}✓${NC} Vulnerability check completed ${CYAN}(${DURATION}s)${NC}" fi else echo -e " ${RED}✗${NC} Vulnerability check failed" echo "" cat /tmp/hook-output.txt echo "" FAILED=1 fi # 6. Tests with race detector and coverage echo -e "${CYAN}[$((CURRENT_CHECK + 1))/${TOTAL_CHECKS}]${NC} ${YELLOW}▶${NC} Running tests with race detector..." CURRENT_CHECK=$((CURRENT_CHECK + 1)) START=$(date +%s) if make test-race > /tmp/hook-output.txt 2>&1; then DURATION=$(($(date +%s) - START)) PASSED=$(grep -c "^ok" /tmp/hook-output.txt || echo "0") echo -e " ${GREEN}✓${NC} All tests passed (${PASSED} packages) ${CYAN}(${DURATION}s)${NC}" else echo -e " ${RED}✗${NC} Tests failed" cat /tmp/hook-output.txt FAILED=1 fi fi # Markdown checks (only if .md files are in the push) if [ -n "$HAS_MD_FILES" ]; then run_check "Running markdownlint" "make lint-md" || FAILED=1 fi # TypeScript checks (only if .ts/.tsx files are in the push) if [ -n "$HAS_TS_FILES" ]; then run_check "Running ESLint (TypeScript)" "(cd web && pnpm lint)" || FAILED=1 run_check "TypeScript type check (tsc --noEmit)" "make tsc" || FAILED=1 fi # Summary TOTAL_TIME=$(($(date +%s) - START_TIME)) echo "" echo -e "${BLUE}╔═══════════════════════════════════════════════════╗${NC}" if [ $FAILED -eq 0 ]; then echo -e "${BLUE}║${NC} ${GREEN}${BOLD}✓ ALL CHECKS PASSED!${NC} ${BLUE}║${NC}" printf "${BLUE}║${NC} ${CYAN}Total time: %-37s${NC}${BLUE}║${NC}\n" "${TOTAL_TIME}s" echo -e "${BLUE}╚═══════════════════════════════════════════════════╝${NC}" echo "" echo -e "${GREEN}Ready to push!${NC}" echo "" exit 0 else echo -e "${BLUE}║${NC} ${RED}${BOLD}✗ CHECKS FAILED${NC} ${BLUE}║${NC}" printf "${BLUE}║${NC} ${CYAN}Total time: %-37s${NC}${BLUE}║${NC}\n" "${TOTAL_TIME}s" echo -e "${BLUE}╚═══════════════════════════════════════════════════╝${NC}" echo "" echo -e "${RED}Fix the issues above before pushing.${NC}" echo -e "${YELLOW}Or skip with: ${NC}git push --no-verify" echo "" exit 1 fi