#!/usr/bin/env python3 """Quality gate checker for CleverAgents CI pipeline. Aggregates results from multiple quality tools and produces a summary report. Returns non-zero exit code if any critical gate fails. Usage: python scripts/check-quality-gates.py [--coverage-min 85] [--complexity-max 10] Gates checked: 1. Coverage >= threshold (default 85%) 2. No type errors (pyright clean) 3. No high-severity security issues (bandit) 4. No dead code (vulture clean) 5. No extreme complexity (radon grade F) """ import argparse import json import subprocess import sys from pathlib import Path def check_coverage(min_coverage: int) -> tuple[bool, str]: """Check test coverage meets minimum threshold.""" result = subprocess.run( ["coverage", "report", "--format=total"], capture_output=True, text=True, check=False, ) if result.returncode != 0: return False, f"Coverage command failed: {result.stderr.strip()}" try: total = float(result.stdout.strip()) except ValueError: return False, f"Could not parse coverage output: {result.stdout.strip()}" passed = total >= min_coverage msg = f"Coverage: {total:.1f}% (minimum: {min_coverage}%)" return passed, msg def check_typecheck() -> tuple[bool, str]: """Check pyright type checking passes.""" result = subprocess.run( ["pyright", "--outputjson"], capture_output=True, text=True, check=False, ) try: data = json.loads(result.stdout) error_count = data.get("summary", {}).get("errorCount", -1) if error_count == 0: return True, "Type checking: 0 errors" return False, f"Type checking: {error_count} errors" except (json.JSONDecodeError, KeyError): # Fallback to exit code if result.returncode == 0: return True, "Type checking: passed" return False, f"Type checking: failed (exit code {result.returncode})" def check_security() -> tuple[bool, str]: """Check bandit finds no high-severity issues.""" result = subprocess.run( [ "bandit", "-c", "pyproject.toml", "-r", "src/cleveragents", "--severity-level", "high", "--format", "json", ], capture_output=True, text=True, check=False, ) try: data = json.loads(result.stdout) high_issues = len(data.get("results", [])) if high_issues == 0: return True, "Security: 0 high-severity issues" return False, f"Security: {high_issues} high-severity issues" except json.JSONDecodeError: if result.returncode == 0: return True, "Security: passed" return False, f"Security: failed (exit code {result.returncode})" def check_dead_code() -> tuple[bool, str]: """Check vulture finds no dead code.""" result = subprocess.run( [ "vulture", "src/cleveragents", "vulture_whitelist.py", "--min-confidence", "80", "--exclude", "src/cleveragents/discovery", ], capture_output=True, text=True, check=False, ) if result.returncode == 0: return True, "Dead code: none detected" lines = result.stdout.strip().split("\n") return False, f"Dead code: {len(lines)} issue(s) found" def check_complexity(max_grade: str = "E") -> tuple[bool, str]: """Check no functions exceed maximum complexity grade.""" result = subprocess.run( [ "radon", "cc", "src/cleveragents", "--min", max_grade, "--show-complexity", "--json", ], capture_output=True, text=True, check=False, ) try: data = json.loads(result.stdout) total_blocks = sum(len(blocks) for blocks in data.values()) if total_blocks == 0: return True, f"Complexity: no blocks with grade >= {max_grade}" return False, f"Complexity: {total_blocks} block(s) with grade >= {max_grade}" except json.JSONDecodeError: return True, "Complexity: passed (no extreme complexity)" def main() -> int: """Run all quality gates and report results.""" parser = argparse.ArgumentParser(description="Check quality gates") parser.add_argument( "--coverage-min", type=int, default=85, help="Minimum coverage percentage" ) parser.add_argument( "--complexity-max", type=str, default="F", help="Maximum allowed complexity grade (F = fail only on worst)", ) args = parser.parse_args() # Ensure build directory exists Path("build").mkdir(exist_ok=True) gates: list[tuple[str, tuple[bool, str]]] = [ ("Coverage", check_coverage(args.coverage_min)), ("Type Check", check_typecheck()), ("Security", check_security()), ("Dead Code", check_dead_code()), ("Complexity", check_complexity(args.complexity_max)), ] print("=" * 60) print("QUALITY GATE REPORT") print("=" * 60) all_passed = True for name, (passed, message) in gates: icon = "[+]" if passed else "[X]" print(f" {icon} {name}: {message}") if not passed: all_passed = False print("=" * 60) if all_passed: print("All quality gates PASSED") else: print("Some quality gates FAILED") print("=" * 60) return 0 if all_passed else 1 if __name__ == "__main__": sys.exit(main())