Migrate dev_tools module to self-contained structure: - routes/api/ - API endpoints - models/architecture_scan.py - Architecture scan models - models/test_run.py - Test run models - schemas/ - Pydantic schemas - services/ - Business logic services - tasks/ - Celery background tasks - exceptions.py - Module exceptions Updated definition.py with self-contained paths. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
239 lines
8.4 KiB
Python
239 lines
8.4 KiB
Python
# 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.services.admin_notification_service import admin_notification_service
|
|
from app.tasks.celery_tasks.base import DatabaseTask
|
|
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_architecture.py",
|
|
VALIDATOR_SECURITY: "scripts/validate_security.py",
|
|
VALIDATOR_PERFORMANCE: "scripts/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=DatabaseTask,
|
|
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
|