Files
hermes-agent/.githooks/pre-receive
Allegro 91e6540a23 feat: implement Syntax Guard as Gitea pre-receive hook
Add pre-receive hook to prevent merging code with Python syntax errors.

Features:
- Checks all Python files (.py) in each push using python -m py_compile
- Special protection for critical files:
  - run_agent.py
  - model_tools.py
  - hermes-agent/tools/nexus_architect.py
  - cli.py, batch_runner.py, hermes_state.py
- Clear error messages showing file and line number
- Rejects pushes containing syntax errors

Files added:
- .githooks/pre-receive (Bash implementation)
- .githooks/pre-receive.py (Python implementation)
- docs/GITEA_SYNTAX_GUARD.md (installation guide)
- .githooks/pre-commit (existing secret detection hook)

Closes #82
2026-04-05 06:12:37 +00:00

217 lines
6.0 KiB
Bash
Executable File

#!/bin/bash
#
# Pre-receive hook for Gitea - Python Syntax Guard
#
# This hook validates Python files for syntax errors before allowing pushes.
# It uses `python -m py_compile` to check files for syntax errors.
#
# Installation in Gitea:
# 1. Go to Repository Settings → Git Hooks
# 2. Edit the "pre-receive" hook
# 3. Copy the contents of this file
# 4. Save and enable
#
# Or for system-wide Gitea hooks, place in:
# /path/to/gitea-repositories/<repo>.git/hooks/pre-receive
#
# Features:
# - Checks all Python files (.py) in the push
# - Focuses on critical files: run_agent.py, model_tools.py, nexus_architect.py
# - Provides detailed error messages with line numbers
# - Rejects pushes containing syntax errors
#
set -euo pipefail
# Colors for output (may not work in all Gitea environments)
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Exit codes
EXIT_SUCCESS=0
EXIT_SYNTAX_ERROR=1
EXIT_INTERNAL_ERROR=2
# Temporary directory for file extraction
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT
# Counters
ERRORS_FOUND=0
FILES_CHECKED=0
CRITICAL_FILES_CHECKED=0
# Critical files that must always be checked
CRITICAL_FILES=(
"run_agent.py"
"model_tools.py"
"hermes-agent/tools/nexus_architect.py"
"cli.py"
"batch_runner.py"
"hermes_state.py"
)
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Extract file content from git object
get_file_content() {
local ref="$1"
git show "$ref" 2>/dev/null || echo ""
}
# Check if file is a Python file
is_python_file() {
local filename="$1"
[[ "$filename" == *.py ]]
}
# Check if file is in the critical list
is_critical_file() {
local filename="$1"
for critical in "${CRITICAL_FILES[@]}"; do
if [[ "$filename" == *"$critical" ]]; then
return 0
fi
done
return 1
}
# Check Python file for syntax errors
check_syntax() {
local filename="$1"
local content="$2"
local ref="$3"
# Write content to temp file
local temp_file="$TEMP_DIR/$(basename "$filename")"
echo "$content" > "$temp_file"
# Run py_compile
local output
if ! output=$(python3 -m py_compile "$temp_file" 2>&1); then
echo "SYNTAX_ERROR"
echo "$output"
return 1
fi
echo "OK"
return 0
}
# ============================================================================
# MAIN PROCESSING
# ============================================================================
echo "========================================"
echo " Python Syntax Guard - Pre-receive"
echo "========================================"
echo ""
# Read refs from stdin (provided by Git)
# Format: <oldrev> <newrev> <refname>
while read -r oldrev newrev refname; do
# Skip if this is a branch deletion (newrev is all zeros)
if [[ "$newrev" == "0000000000000000000000000000000000000000" ]]; then
log_info "Branch deletion detected, skipping syntax check"
continue
fi
# If this is a new branch (oldrev is all zeros), check all files
if [[ "$oldrev" == "0000000000000000000000000000000000000000" ]]; then
# List all files in the new commit
files=$(git ls-tree --name-only -r "$newrev" 2>/dev/null || echo "")
else
# Get list of changed files between old and new
files=$(git diff --name-only "$oldrev" "$newrev" 2>/dev/null || echo "")
fi
# Process each file
while IFS= read -r file; do
[ -z "$file" ] && continue
# Only check Python files
if ! is_python_file "$file"; then
continue
fi
FILES_CHECKED=$((FILES_CHECKED + 1))
# Check if critical file
local is_critical=false
if is_critical_file "$file"; then
is_critical=true
CRITICAL_FILES_CHECKED=$((CRITICAL_FILES_CHECKED + 1))
fi
# Get file content at the new revision
content=$(git show "$newrev:$file" 2>/dev/null || echo "")
if [ -z "$content" ]; then
# File might have been deleted
continue
fi
# Check syntax
result=$(check_syntax "$file" "$content" "$newrev")
status=$?
if [ $status -ne 0 ]; then
ERRORS_FOUND=$((ERRORS_FOUND + 1))
log_error "Syntax error in: $file"
if [ "$is_critical" = true ]; then
echo " ^^^ CRITICAL FILE - This file is essential for system operation"
fi
# Display the py_compile error
echo ""
echo "$result" | grep -v "^SYNTAX_ERROR$" | sed 's/^/ /'
echo ""
else
if [ "$is_critical" = true ]; then
log_info "✓ Critical file OK: $file"
fi
fi
done <<< "$files"
done
echo ""
echo "========================================"
echo " SUMMARY"
echo "========================================"
echo "Files checked: $FILES_CHECKED"
echo "Critical files checked: $CRITICAL_FILES_CHECKED"
echo "Errors found: $ERRORS_FOUND"
echo ""
# Exit with appropriate code
if [ $ERRORS_FOUND -gt 0 ]; then
log_error "╔════════════════════════════════════════════════════════════╗"
log_error "║ PUSH REJECTED: Syntax errors detected! ║"
log_error "║ ║"
log_error "║ Please fix the syntax errors above before pushing again. ║"
log_error "╚════════════════════════════════════════════════════════════╝"
echo ""
exit $EXIT_SYNTAX_ERROR
fi
log_info "✓ All Python files passed syntax check"
exit $EXIT_SUCCESS