Files
cleveragents-core/scripts/check-quality-gates.py
T
brent.edwards 7ef5ebb695 feat: Add Q0: Pre-commit hooks setup.
This should automatically check for problems on build.
2026-02-10 16:04:17 +00:00

193 lines
5.6 KiB
Python

#!/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())