# app/modules/dev_tools/tasks/code_quality.py """ Celery tasks for code quality scans. Wraps the existing execute_code_quality_scan function for Celery execution. This is the canonical location - task is re-exported from the legacy location for backward compatibility. """ import json import logging import subprocess from datetime import UTC, datetime from app.core.celery_config import celery_app from app.modules.messaging.services.admin_notification_service import admin_notification_service from app.modules.task_base import ModuleTask from app.modules.dev_tools.models import ArchitectureScan, ArchitectureViolation logger = logging.getLogger(__name__) # Validator type constants VALIDATOR_ARCHITECTURE = "architecture" VALIDATOR_SECURITY = "security" VALIDATOR_PERFORMANCE = "performance" VALID_VALIDATOR_TYPES = [VALIDATOR_ARCHITECTURE, VALIDATOR_SECURITY, VALIDATOR_PERFORMANCE] # Map validator types to their scripts VALIDATOR_SCRIPTS = { VALIDATOR_ARCHITECTURE: "scripts/validate/validate_architecture.py", VALIDATOR_SECURITY: "scripts/validate/validate_security.py", VALIDATOR_PERFORMANCE: "scripts/validate/validate_performance.py", } # Human-readable names VALIDATOR_NAMES = { VALIDATOR_ARCHITECTURE: "Architecture", VALIDATOR_SECURITY: "Security", VALIDATOR_PERFORMANCE: "Performance", } def _get_git_commit_hash() -> str | None: """Get current git commit hash.""" try: result = subprocess.run( ["git", "rev-parse", "HEAD"], capture_output=True, text=True, timeout=5, ) if result.returncode == 0: return result.stdout.strip()[:40] except Exception: pass return None @celery_app.task( bind=True, base=ModuleTask, name="app.modules.dev_tools.tasks.code_quality.execute_code_quality_scan", max_retries=1, time_limit=700, # 11+ minutes hard limit soft_time_limit=600, # 10 minutes soft limit ) def execute_code_quality_scan(self, scan_id: int): """ Celery task to execute a code quality scan. This task: 1. Gets the scan record from DB 2. Updates status to 'running' 3. Runs the validator script 4. Parses JSON output and creates violation records 5. Updates scan with results and status 'completed' or 'failed' Args: scan_id: ID of the ArchitectureScan record Returns: dict: Scan results summary """ with self.get_db() as db: # Get the scan record scan = db.query(ArchitectureScan).filter(ArchitectureScan.id == scan_id).first() if not scan: logger.error(f"Code quality scan {scan_id} not found") return {"error": f"Scan {scan_id} not found"} # Store Celery task ID scan.celery_task_id = self.request.id validator_type = scan.validator_type if validator_type not in VALID_VALIDATOR_TYPES: scan.status = "failed" scan.error_message = f"Invalid validator type: {validator_type}" db.commit() return {"error": f"Invalid validator type: {validator_type}"} script_path = VALIDATOR_SCRIPTS[validator_type] validator_name = VALIDATOR_NAMES[validator_type] try: # Update status to running scan.status = "running" scan.started_at = datetime.now(UTC) scan.progress_message = f"Running {validator_name} validator..." scan.git_commit_hash = _get_git_commit_hash() db.commit() logger.info(f"Starting {validator_name} scan (scan_id={scan_id})") # Run validator with JSON output start_time = datetime.now(UTC) try: result = subprocess.run( ["python", script_path, "--json"], capture_output=True, text=True, timeout=600, # 10 minute timeout ) except subprocess.TimeoutExpired: logger.error(f"{validator_name} scan {scan_id} timed out after 10 minutes") scan.status = "failed" scan.error_message = "Scan timed out after 10 minutes" scan.completed_at = datetime.now(UTC) db.commit() return {"error": "Scan timed out"} duration = (datetime.now(UTC) - start_time).total_seconds() # Update progress scan.progress_message = "Parsing results..." db.commit() # Parse JSON output try: lines = result.stdout.strip().split("\n") json_start = -1 for i, line in enumerate(lines): if line.strip().startswith("{"): json_start = i break if json_start == -1: raise ValueError("No JSON output found in validator output") json_output = "\n".join(lines[json_start:]) data = json.loads(json_output) except (json.JSONDecodeError, ValueError) as e: logger.error(f"Failed to parse {validator_name} validator output: {e}") scan.status = "failed" scan.error_message = f"Failed to parse validator output: {e}" scan.completed_at = datetime.now(UTC) scan.duration_seconds = duration db.commit() return {"error": str(e)} # Update progress scan.progress_message = "Storing violations..." db.commit() # Create violation records violations_data = data.get("violations", []) logger.info(f"Creating {len(violations_data)} {validator_name} violation records") for v in violations_data: violation = ArchitectureViolation( scan_id=scan.id, validator_type=validator_type, rule_id=v.get("rule_id", "UNKNOWN"), rule_name=v.get("rule_name", "Unknown Rule"), severity=v.get("severity", "warning"), file_path=v.get("file_path", ""), line_number=v.get("line_number", 0), message=v.get("message", ""), context=v.get("context", ""), suggestion=v.get("suggestion", ""), status="open", ) db.add(violation) # Update scan with results scan.total_files = data.get("files_checked", 0) scan.total_violations = data.get("total_violations", len(violations_data)) scan.errors = data.get("errors", 0) scan.warnings = data.get("warnings", 0) scan.duration_seconds = duration scan.completed_at = datetime.now(UTC) scan.progress_message = None # Set final status based on results if scan.errors > 0: scan.status = "completed_with_warnings" else: scan.status = "completed" db.commit() logger.info( f"{validator_name} scan {scan_id} completed: " f"files={scan.total_files}, violations={scan.total_violations}, " f"errors={scan.errors}, warnings={scan.warnings}, " f"duration={duration:.1f}s" ) return { "scan_id": scan_id, "validator_type": validator_type, "status": scan.status, "total_files": scan.total_files, "total_violations": scan.total_violations, "errors": scan.errors, "warnings": scan.warnings, "duration_seconds": duration, } except Exception as e: logger.error(f"Code quality scan {scan_id} failed: {e}", exc_info=True) scan.status = "failed" scan.error_message = str(e)[:500] scan.completed_at = datetime.now(UTC) scan.progress_message = None # Create admin notification for scan failure admin_notification_service.create_notification( db=db, title="Code Quality Scan Failed", message=f"{VALIDATOR_NAMES.get(scan.validator_type, 'Unknown')} scan failed: {str(e)[:200]}", notification_type="error", category="code_quality", action_url="/admin/code-quality", ) db.commit() raise # Re-raise for Celery