quality: add Ariadne LOC ratchet and platform metrics
This commit is contained in:
parent
81a6dc629f
commit
36f76a98a2
1
Jenkinsfile
vendored
1
Jenkinsfile
vendored
@ -103,6 +103,7 @@ set -euo pipefail
|
||||
mkdir -p build
|
||||
python -m pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt
|
||||
python -m ruff check ariadne scripts --select PLR
|
||||
python scripts/check_file_sizes.py --roots ariadne scripts testing --max-lines 500 --waivers ci/loc_hygiene_waivers.tsv
|
||||
python -m slipcover \
|
||||
--json \
|
||||
--out "${COVERAGE_JSON}" \
|
||||
|
||||
13
ci/loc_hygiene_waivers.tsv
Normal file
13
ci/loc_hygiene_waivers.tsv
Normal file
@ -0,0 +1,13 @@
|
||||
# relative_path<TAB>why_it_is_allowlisted_for_now
|
||||
ariadne/app.py core application router/orchestration; scheduled split in ratchet/cluster-state-split
|
||||
ariadne/manager/provisioning.py provisioning workflow hub pending modular extraction
|
||||
ariadne/services/firefly.py firefly service handlers pending split by endpoint domain
|
||||
ariadne/services/jenkins_workspace_cleanup.py workspace cleanup service pending refactor into strategy modules
|
||||
ariadne/services/nextcloud.py nextcloud integration surface pending staged decomposition
|
||||
ariadne/services/vault.py vault integration flow pending dedicated auth/storage modules
|
||||
ariadne/services/wger.py wger integration flow pending endpoint-layer split
|
||||
ariadne/settings.py configuration map pending domain-specific config modules
|
||||
testing/test_app.py broad integration assertions pending test-suite decomposition
|
||||
testing/test_keycloak_admin.py keycloak contract tests pending helper extraction
|
||||
testing/test_provisioning.py provisioning matrix tests pending split by workflow phase
|
||||
testing/test_services.py service integration matrix pending split by service domain
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
86
scripts/check_file_sizes.py
Normal file
86
scripts/check_file_sizes.py
Normal file
@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Enforce a ratcheted source file line-budget contract.
|
||||
|
||||
The check fails when:
|
||||
- a file exceeds the configured line budget and is not allowlisted; or
|
||||
- an allowlist entry is stale (file removed or now within budget).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _iter_source_files(roots: list[str], exts: set[str]) -> list[Path]:
|
||||
files: list[Path] = []
|
||||
for root_text in roots:
|
||||
root = Path(root_text)
|
||||
if not root.exists():
|
||||
continue
|
||||
for path in root.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if path.suffix not in exts:
|
||||
continue
|
||||
if "__pycache__" in path.parts or ".venv" in path.parts:
|
||||
continue
|
||||
files.append(path.resolve())
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def _load_waivers(path: Path) -> dict[str, str]:
|
||||
waivers: dict[str, str] = {}
|
||||
if not path.exists():
|
||||
return waivers
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
rel_path = parts[0].strip()
|
||||
reason = parts[1].strip() if len(parts) > 1 else ""
|
||||
if rel_path:
|
||||
waivers[rel_path] = reason
|
||||
return waivers
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--roots", nargs="+", default=["ariadne", "scripts", "testing"])
|
||||
parser.add_argument("--max-lines", type=int, default=500)
|
||||
parser.add_argument("--waivers", default="ci/loc_hygiene_waivers.tsv")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = Path.cwd().resolve()
|
||||
waivers = _load_waivers(repo_root / args.waivers)
|
||||
source_files = _iter_source_files(args.roots, {".py", ".sh"})
|
||||
|
||||
violations: dict[str, int] = {}
|
||||
for path in source_files:
|
||||
rel = path.relative_to(repo_root).as_posix()
|
||||
lines = len(path.read_text(encoding="utf-8", errors="ignore").splitlines())
|
||||
if lines > args.max_lines:
|
||||
violations[rel] = lines
|
||||
|
||||
unexpected = sorted(rel for rel in violations if rel not in waivers)
|
||||
stale = sorted(rel for rel in waivers if rel not in violations)
|
||||
if not unexpected and not stale:
|
||||
print(
|
||||
f"[hygiene] source line budget check passed (limit={args.max_lines}, over_limit={len(violations)}, waivers={len(waivers)})"
|
||||
)
|
||||
return 0
|
||||
|
||||
if unexpected:
|
||||
print("[hygiene] files over budget missing from waiver list:")
|
||||
for rel in unexpected:
|
||||
print(f"- {rel}: {violations[rel]} lines (limit {args.max_lines})")
|
||||
if stale:
|
||||
print("[hygiene] stale waiver entries (remove from waiver list):")
|
||||
for rel in stale:
|
||||
print(f"- {rel}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -5,12 +5,15 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
HTTP_BAD_REQUEST = 400
|
||||
MIN_METRIC_FIELDS = 2
|
||||
SOURCE_SCAN_ROOTS = ("ariadne", "scripts", "testing")
|
||||
SOURCE_EXTENSIONS = {".py", ".sh"}
|
||||
|
||||
|
||||
def _escape_label(value: str) -> str:
|
||||
@ -100,7 +103,25 @@ def _fetch_existing_counter(pushgateway_url: str, metric: str, labels: dict[str,
|
||||
return 0.0
|
||||
|
||||
|
||||
def _count_source_files_over_limit(repo_root: Path, max_lines: int = 500) -> int:
|
||||
count = 0
|
||||
for rel_root in SOURCE_SCAN_ROOTS:
|
||||
base = repo_root / rel_root
|
||||
if not base.exists():
|
||||
continue
|
||||
for path in base.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if path.suffix not in SOURCE_EXTENSIONS:
|
||||
continue
|
||||
lines = len(path.read_text(encoding="utf-8", errors="ignore").splitlines())
|
||||
if lines > max_lines:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
coverage_path = os.getenv("COVERAGE_JSON", "build/coverage.json")
|
||||
junit_path = os.getenv("JUNIT_XML", "build/junit.xml")
|
||||
pushgateway_url = os.getenv(
|
||||
@ -117,6 +138,7 @@ def main() -> int:
|
||||
raise RuntimeError(f"missing junit file {junit_path}")
|
||||
|
||||
coverage = _load_coverage(coverage_path)
|
||||
source_lines_over_500 = _count_source_files_over_limit(repo_root, max_lines=500)
|
||||
totals = _load_junit(junit_path)
|
||||
passed = max(totals["tests"] - totals["failures"] - totals["errors"] - totals["skipped"], 0)
|
||||
|
||||
@ -157,6 +179,10 @@ def main() -> int:
|
||||
f'ariadne_quality_gate_tests_total{{suite="{suite}",result="skipped"}} {totals["skipped"]}',
|
||||
"# TYPE ariadne_quality_gate_coverage_percent gauge",
|
||||
f'ariadne_quality_gate_coverage_percent{{suite="{suite}"}} {coverage:.3f}',
|
||||
"# TYPE platform_quality_gate_workspace_line_coverage_percent gauge",
|
||||
f'platform_quality_gate_workspace_line_coverage_percent{{suite="{suite}"}} {coverage:.3f}',
|
||||
"# TYPE platform_quality_gate_source_lines_over_500_total gauge",
|
||||
f'platform_quality_gate_source_lines_over_500_total{{suite="{suite}"}} {source_lines_over_500}',
|
||||
"# TYPE ariadne_quality_gate_build_info gauge",
|
||||
f"ariadne_quality_gate_build_info{_label_str(labels)} 1",
|
||||
]
|
||||
@ -174,6 +200,7 @@ def main() -> int:
|
||||
"tests_errors": totals["errors"],
|
||||
"tests_skipped": totals["skipped"],
|
||||
"coverage_percent": round(coverage, 3),
|
||||
"source_lines_over_500": source_lines_over_500,
|
||||
"ok_counter": ok_count,
|
||||
"failed_counter": failed_count,
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user