cecca72b8e
CI / benchmark-publish (push) Has started running
CI / benchmark-regression (push) Has been skipped
CI / e2e_tests (push) Failing after 14s
CI / helm (push) Failing after 8s
CI / unit_tests (push) Failing after 21s
CI / typecheck (push) Failing after 32s
CI / build (push) Failing after 9s
CI / quality (push) Failing after 24s
CI / integration_tests (push) Failing after 15s
CI / security (push) Failing after 24s
CI / lint (push) Failing after 34s
CI / coverage (push) Has been skipped
CI / docker (push) Has been skipped
CI / push-validation (push) Successful in 20s
CI / status-check (push) Failing after 6s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Failing after 1m11s
CI / coverage (pull_request) Successful in 14m34s
CI / push-validation (pull_request) Successful in 36s
CI / typecheck (pull_request) Successful in 1m14s
CI / integration_tests (pull_request) Successful in 3m42s
CI / e2e_tests (pull_request) Successful in 4m26s
CI / unit_tests (pull_request) Successful in 6m29s
CI / build (pull_request) Successful in 59s
CI / lint (pull_request) Successful in 53s
CI / quality (pull_request) Successful in 56s
CI / security (pull_request) Successful in 1m19s
CI / helm (pull_request) Successful in 53s
CI / docker (pull_request) Successful in 1m46s
CI / status-check (pull_request) Successful in 3s
193 lines
5.6 KiB
Python
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: float) -> 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())
|