quality: add platform hygiene metrics to ananke gate

This commit is contained in:
Brad Stein 2026-04-17 04:39:32 -03:00
parent c5282f4ca4
commit 8a59825a9c
4 changed files with 74 additions and 7 deletions

View File

@ -4,10 +4,11 @@ set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${REPO_DIR}" cd "${REPO_DIR}"
export PATH="$(go env GOPATH)/bin:${PATH}" export PATH="$(go env GOPATH)/bin:${PATH}"
STATICCHECK_VERSION="${ANANKE_STATICCHECK_VERSION:-2025.1.1}"
if ! command -v staticcheck >/dev/null 2>&1; then if ! command -v staticcheck >/dev/null 2>&1 || ! staticcheck -version 2>/dev/null | grep -q "${STATICCHECK_VERSION}"; then
echo "[lint] installing staticcheck" echo "[lint] installing staticcheck ${STATICCHECK_VERSION}"
go install honnef.co/go/tools/cmd/staticcheck@latest go install "honnef.co/go/tools/cmd/staticcheck@${STATICCHECK_VERSION}"
fi fi
echo "[lint] go vet" echo "[lint] go vet"

View File

@ -5,6 +5,7 @@ from __future__ import annotations
import argparse import argparse
import os import os
from pathlib import Path
import sys import sys
import time import time
import urllib.error import urllib.error
@ -12,6 +13,8 @@ import urllib.request
DEFAULT_PUSHGATEWAY_URL = "http://platform-quality-gateway.monitoring.svc.cluster.local:9091" DEFAULT_PUSHGATEWAY_URL = "http://platform-quality-gateway.monitoring.svc.cluster.local:9091"
SOURCE_SCAN_ROOTS = ("cmd", "internal", "scripts", "testing")
SOURCE_EXTENSIONS = {".go", ".py", ".sh"}
def _escape_label(value: str) -> str: def _escape_label(value: str) -> str:
@ -82,6 +85,36 @@ def _build_payload(suite: str, trigger: str, ok_count: int, failed_count: int) -
return "\n".join(lines) + "\n" return "\n".join(lines) + "\n"
def _read_coverage_percent(path: str) -> float:
if not path:
return 0.0
try:
raw = Path(path).read_text(encoding="utf-8").strip()
except OSError:
return 0.0
try:
return float(raw)
except ValueError:
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 parse_args(argv: list[str]) -> argparse.Namespace: def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument( parser.add_argument(
@ -96,6 +129,10 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
parser.add_argument("--trigger", default=os.getenv("ANANKE_QUALITY_PUSHGATEWAY_TRIGGER", "host")) parser.add_argument("--trigger", default=os.getenv("ANANKE_QUALITY_PUSHGATEWAY_TRIGGER", "host"))
parser.add_argument("--local-ok", type=int, required=True) parser.add_argument("--local-ok", type=int, required=True)
parser.add_argument("--local-failed", type=int, required=True) parser.add_argument("--local-failed", type=int, required=True)
parser.add_argument(
"--coverage-percent-file",
default=os.getenv("ANANKE_QUALITY_COVERAGE_PERCENT_FILE", "build/coverage-percent.txt"),
)
parser.add_argument( parser.add_argument(
"--timeout-seconds", "--timeout-seconds",
type=float, type=float,
@ -117,6 +154,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
def main(argv: list[str] | None = None) -> int: def main(argv: list[str] | None = None) -> int:
args = parse_args(argv or sys.argv[1:]) args = parse_args(argv or sys.argv[1:])
repo_root = Path(__file__).resolve().parents[1]
remote_ok = 0 remote_ok = 0
remote_failed = 0 remote_failed = 0
@ -143,7 +181,17 @@ def main(argv: list[str] | None = None) -> int:
resolved_ok = max(args.local_ok, remote_ok) resolved_ok = max(args.local_ok, remote_ok)
resolved_failed = max(args.local_failed, remote_failed) resolved_failed = max(args.local_failed, remote_failed)
payload = _build_payload(args.suite, args.trigger, resolved_ok, resolved_failed) coverage_percent = _read_coverage_percent(args.coverage_percent_file)
source_lines_over_500 = _count_source_files_over_limit(repo_root, max_lines=500)
payload = _build_payload(args.suite, args.trigger, resolved_ok, resolved_failed).rstrip("\n")
payload += (
"\n# TYPE ananke_quality_gate_coverage_percent gauge\n"
f'ananke_quality_gate_coverage_percent{{suite="{args.suite}"}} {coverage_percent:.3f}\n'
"# TYPE platform_quality_gate_workspace_line_coverage_percent gauge\n"
f'platform_quality_gate_workspace_line_coverage_percent{{suite="{args.suite}"}} {coverage_percent:.3f}\n'
"# TYPE platform_quality_gate_source_lines_over_500_total gauge\n"
f'platform_quality_gate_source_lines_over_500_total{{suite="{args.suite}"}} {source_lines_over_500}\n'
)
if args.dry_run: if args.dry_run:
sys.stdout.write(payload) sys.stdout.write(payload)
@ -152,7 +200,10 @@ def main(argv: list[str] | None = None) -> int:
push_url = f"{args.pushgateway_url.rstrip('/')}/metrics/job/{args.job_name}/suite/{args.suite}" push_url = f"{args.pushgateway_url.rstrip('/')}/metrics/job/{args.job_name}/suite/{args.suite}"
_post_text(push_url, payload, args.timeout_seconds, max(args.attempts, 1), max(args.retry_delay_seconds, 0.0)) _post_text(push_url, payload, args.timeout_seconds, max(args.attempts, 1), max(args.retry_delay_seconds, 0.0))
summary = f"[quality] published Pushgateway metrics suite={args.suite} job={args.job_name} ok={resolved_ok} failed={resolved_failed}" summary = (
f"[quality] published Pushgateway metrics suite={args.suite} job={args.job_name} ok={resolved_ok} "
f"failed={resolved_failed} coverage={coverage_percent:.3f} source_lines_over_500={source_lines_over_500}"
)
if remote_error: if remote_error:
summary += f" remote_read_error={remote_error}" summary += f" remote_read_error={remote_error}"
print(summary) print(summary)

View File

@ -91,6 +91,9 @@ class PublishQualityMetricsTest(unittest.TestCase):
self.assertIn('platform_quality_gate_runs_total{suite="ananke",status="ok"} 7', body) self.assertIn('platform_quality_gate_runs_total{suite="ananke",status="ok"} 7', body)
self.assertIn('platform_quality_gate_runs_total{suite="ananke",status="failed"} 2', body) self.assertIn('platform_quality_gate_runs_total{suite="ananke",status="failed"} 2', body)
self.assertIn('ananke_quality_gate_publish_info{suite="ananke",trigger="host"} 1', body) self.assertIn('ananke_quality_gate_publish_info{suite="ananke",trigger="host"} 1', body)
self.assertIn('ananke_quality_gate_coverage_percent{suite="ananke"}', body)
self.assertIn('platform_quality_gate_workspace_line_coverage_percent{suite="ananke"}', body)
self.assertIn('platform_quality_gate_source_lines_over_500_total{suite="ananke"}', body)
def test_publish_falls_back_to_local_counters_when_metrics_read_fails(self) -> None: def test_publish_falls_back_to_local_counters_when_metrics_read_fails(self) -> None:
_GatewayHandler.fail_metrics_read = True _GatewayHandler.fail_metrics_read = True
@ -115,6 +118,8 @@ class PublishQualityMetricsTest(unittest.TestCase):
_, body = _GatewayHandler.posts[0] _, body = _GatewayHandler.posts[0]
self.assertIn('platform_quality_gate_runs_total{suite="ananke",status="ok"} 11', body) self.assertIn('platform_quality_gate_runs_total{suite="ananke",status="ok"} 11', body)
self.assertIn('platform_quality_gate_runs_total{suite="ananke",status="failed"} 3', body) self.assertIn('platform_quality_gate_runs_total{suite="ananke",status="failed"} 3', body)
self.assertIn('platform_quality_gate_workspace_line_coverage_percent{suite="ananke"}', body)
self.assertIn('platform_quality_gate_source_lines_over_500_total{suite="ananke"}', body)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -2,6 +2,9 @@
set -euo pipefail set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD_DIR="${REPO_DIR}/build"
COVERAGE_PROFILE="${BUILD_DIR}/coverage.out"
COVERAGE_PERCENT_FILE="${BUILD_DIR}/coverage-percent.txt"
QUALITY_METRICS_ENABLED="${ANANKE_QUALITY_METRICS_ENABLED:-1}" QUALITY_METRICS_ENABLED="${ANANKE_QUALITY_METRICS_ENABLED:-1}"
QUALITY_METRICS_FILE="${ANANKE_QUALITY_METRICS_FILE:-/var/lib/ananke/quality-gate.prom}" QUALITY_METRICS_FILE="${ANANKE_QUALITY_METRICS_FILE:-/var/lib/ananke/quality-gate.prom}"
QUALITY_STATE_FILE="${ANANKE_QUALITY_STATE_FILE:-/var/lib/ananke/quality-gate.state}" QUALITY_STATE_FILE="${ANANKE_QUALITY_STATE_FILE:-/var/lib/ananke/quality-gate.state}"
@ -132,9 +135,16 @@ quality_gate_finalize() {
trap 'quality_gate_finalize $?' EXIT trap 'quality_gate_finalize $?' EXIT
cd "${REPO_DIR}" cd "${REPO_DIR}"
mkdir -p "${BUILD_DIR}"
rm -f "${COVERAGE_PROFILE}" "${COVERAGE_PERCENT_FILE}"
echo "[quality] unit tests" echo "[quality] unit tests + workspace coverage profile"
go test ./... go test -coverprofile="${COVERAGE_PROFILE}" ./...
coverage_percent="$(go tool cover -func="${COVERAGE_PROFILE}" | awk '/^total:/ {gsub("%","",$3); print $3}')"
if [[ -z "${coverage_percent}" ]]; then
coverage_percent="0"
fi
printf '%s\n' "${coverage_percent}" > "${COVERAGE_PERCENT_FILE}"
echo "[quality] hygiene: doc contracts" echo "[quality] hygiene: doc contracts"
cd testing cd testing