Compare commits
No commits in common. "7b67ee288bc5c5500dbe091a1d582bc9de54553b" and "8a59825a9c75f973d03c026561ab9f1869e52021" have entirely different histories.
7b67ee288b
...
8a59825a9c
69
Jenkinsfile
vendored
69
Jenkinsfile
vendored
@ -35,8 +35,6 @@ spec:
|
|||||||
environment {
|
environment {
|
||||||
SUITE_NAME = 'ananke'
|
SUITE_NAME = 'ananke'
|
||||||
PUSHGATEWAY_URL = 'http://platform-quality-gateway.monitoring.svc.cluster.local:9091'
|
PUSHGATEWAY_URL = 'http://platform-quality-gateway.monitoring.svc.cluster.local:9091'
|
||||||
QUALITY_GATE_SONARQUBE_REPORT = 'build/sonarqube-quality-gate.json'
|
|
||||||
QUALITY_GATE_IRONBANK_REPORT = 'build/ironbank-compliance.json'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
options {
|
options {
|
||||||
@ -54,73 +52,6 @@ spec:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Collect SonarQube evidence') {
|
|
||||||
steps {
|
|
||||||
container('publisher') {
|
|
||||||
sh '''
|
|
||||||
set -eu
|
|
||||||
mkdir -p build
|
|
||||||
python3 - <<'PY'
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
host = os.getenv('SONARQUBE_HOST_URL', '').strip().rstrip('/')
|
|
||||||
project_key = os.getenv('SONARQUBE_PROJECT_KEY', '').strip()
|
|
||||||
token = os.getenv('SONARQUBE_TOKEN', '').strip()
|
|
||||||
report_path = os.getenv('QUALITY_GATE_SONARQUBE_REPORT', 'build/sonarqube-quality-gate.json')
|
|
||||||
payload = {"status": "ERROR", "note": "missing SONARQUBE_HOST_URL and/or SONARQUBE_PROJECT_KEY"}
|
|
||||||
if host and project_key:
|
|
||||||
query = urllib.parse.urlencode({"projectKey": project_key})
|
|
||||||
request = urllib.request.Request(f"{host}/api/qualitygates/project_status?{query}", method="GET")
|
|
||||||
if token:
|
|
||||||
encoded = base64.b64encode(f"{token}:".encode("utf-8")).decode("utf-8")
|
|
||||||
request.add_header("Authorization", f"Basic {encoded}")
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(request, timeout=12) as response:
|
|
||||||
payload = json.loads(response.read().decode("utf-8"))
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
payload = {"status": "ERROR", "error": str(exc)}
|
|
||||||
with open(report_path, "w", encoding="utf-8") as handle:
|
|
||||||
json.dump(payload, handle, indent=2, sort_keys=True)
|
|
||||||
handle.write("\\n")
|
|
||||||
PY
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Collect Supply Chain evidence') {
|
|
||||||
steps {
|
|
||||||
container('publisher') {
|
|
||||||
sh '''
|
|
||||||
set -eu
|
|
||||||
mkdir -p build
|
|
||||||
python3 - <<'PY'
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
report_path = Path(os.getenv('QUALITY_GATE_IRONBANK_REPORT', 'build/ironbank-compliance.json'))
|
|
||||||
if report_path.exists():
|
|
||||||
raise SystemExit(0)
|
|
||||||
status = os.getenv('IRONBANK_COMPLIANCE_STATUS', '').strip()
|
|
||||||
compliant = os.getenv('IRONBANK_COMPLIANT', '').strip().lower()
|
|
||||||
payload = {"status": status or "unknown", "compliant": compliant in {"1", "true", "yes", "on"} if compliant else None}
|
|
||||||
payload = {k: v for k, v in payload.items() if v is not None}
|
|
||||||
if "status" not in payload:
|
|
||||||
payload["status"] = "unknown"
|
|
||||||
payload["note"] = "Set IRONBANK_COMPLIANCE_STATUS/IRONBANK_COMPLIANT or write build/ironbank-compliance.json in image-building repos."
|
|
||||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
report_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\\n", encoding="utf-8")
|
|
||||||
PY
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Run quality gate') {
|
stage('Run quality gate') {
|
||||||
steps {
|
steps {
|
||||||
container('go-tester') {
|
container('go-tester') {
|
||||||
|
|||||||
@ -4,10 +4,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import urllib.error
|
import urllib.error
|
||||||
@ -17,7 +15,6 @@ 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_SCAN_ROOTS = ("cmd", "internal", "scripts", "testing")
|
||||||
SOURCE_EXTENSIONS = {".go", ".py", ".sh"}
|
SOURCE_EXTENSIONS = {".go", ".py", ".sh"}
|
||||||
QUALITY_SUCCESS_STATES = {"ok", "pass", "passed", "success", "compliant"}
|
|
||||||
|
|
||||||
|
|
||||||
def _escape_label(value: str) -> str:
|
def _escape_label(value: str) -> str:
|
||||||
@ -45,7 +42,7 @@ def _post_text(url: str, payload: str, timeout_seconds: float, attempts: int, re
|
|||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
url,
|
url,
|
||||||
data=payload.encode("utf-8"),
|
data=payload.encode("utf-8"),
|
||||||
method="PUT",
|
method="POST",
|
||||||
headers={"Content-Type": "text/plain"},
|
headers={"Content-Type": "text/plain"},
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@ -77,49 +74,14 @@ def _fetch_existing_counter(pushgateway_url: str, metric: str, labels: dict[str,
|
|||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
def _build_payload(
|
def _build_payload(suite: str, trigger: str, ok_count: int, failed_count: int) -> str:
|
||||||
suite: str,
|
|
||||||
trigger: str,
|
|
||||||
ok_count: int,
|
|
||||||
failed_count: int,
|
|
||||||
*,
|
|
||||||
tests_passed: int,
|
|
||||||
tests_failed: int,
|
|
||||||
tests_errors: int,
|
|
||||||
tests_skipped: int,
|
|
||||||
test_cases: list[tuple[str, str]],
|
|
||||||
coverage_percent: float,
|
|
||||||
source_lines_over_500: int,
|
|
||||||
checks: dict[str, str],
|
|
||||||
) -> str:
|
|
||||||
lines = [
|
lines = [
|
||||||
"# TYPE platform_quality_gate_runs_total counter",
|
"# TYPE platform_quality_gate_runs_total counter",
|
||||||
f'platform_quality_gate_runs_total{{suite="{suite}",status="ok"}} {ok_count}',
|
f'platform_quality_gate_runs_total{{suite="{suite}",status="ok"}} {ok_count}',
|
||||||
f'platform_quality_gate_runs_total{{suite="{suite}",status="failed"}} {failed_count}',
|
f'platform_quality_gate_runs_total{{suite="{suite}",status="failed"}} {failed_count}',
|
||||||
"# TYPE ananke_quality_gate_tests_total gauge",
|
|
||||||
f'ananke_quality_gate_tests_total{{suite="{suite}",result="passed"}} {tests_passed}',
|
|
||||||
f'ananke_quality_gate_tests_total{{suite="{suite}",result="failed"}} {tests_failed}',
|
|
||||||
f'ananke_quality_gate_tests_total{{suite="{suite}",result="error"}} {tests_errors}',
|
|
||||||
f'ananke_quality_gate_tests_total{{suite="{suite}",result="skipped"}} {tests_skipped}',
|
|
||||||
"# TYPE ananke_quality_gate_coverage_percent gauge",
|
|
||||||
f'ananke_quality_gate_coverage_percent{{suite="{suite}"}} {coverage_percent:.3f}',
|
|
||||||
"# TYPE platform_quality_gate_workspace_line_coverage_percent gauge",
|
|
||||||
f'platform_quality_gate_workspace_line_coverage_percent{{suite="{suite}"}} {coverage_percent:.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 platform_quality_gate_test_case_result gauge",
|
|
||||||
"# TYPE ananke_quality_gate_checks_total gauge",
|
|
||||||
"# TYPE ananke_quality_gate_publish_info gauge",
|
"# TYPE ananke_quality_gate_publish_info gauge",
|
||||||
f'ananke_quality_gate_publish_info{_label_str({"suite": suite, "trigger": trigger})} 1',
|
f'ananke_quality_gate_publish_info{_label_str({"suite": suite, "trigger": trigger})} 1',
|
||||||
]
|
]
|
||||||
lines.extend(
|
|
||||||
f'platform_quality_gate_test_case_result{{suite="{suite}",test="{_escape_label(test_name)}",status="{_escape_label(test_status)}"}} 1'
|
|
||||||
for test_name, test_status in test_cases
|
|
||||||
)
|
|
||||||
lines.extend(
|
|
||||||
f'ananke_quality_gate_checks_total{{suite="{suite}",check="{check_name}",result="{check_status}"}} 1'
|
|
||||||
for check_name, check_status in checks.items()
|
|
||||||
)
|
|
||||||
return "\n".join(lines) + "\n"
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
@ -153,90 +115,6 @@ def _count_source_files_over_limit(repo_root: Path, max_lines: int = 500) -> int
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
def _parse_go_test_counts(output_path: Path) -> dict[str, int]:
|
|
||||||
if not output_path.exists():
|
|
||||||
return {"passed": 0, "failed": 0, "errors": 0, "skipped": 0}
|
|
||||||
text = output_path.read_text(encoding="utf-8", errors="ignore")
|
|
||||||
return {
|
|
||||||
"passed": len(re.findall(r"^--- PASS:", text, flags=re.M)),
|
|
||||||
"failed": len(re.findall(r"^--- FAIL:", text, flags=re.M)),
|
|
||||||
"errors": 0,
|
|
||||||
"skipped": len(re.findall(r"^--- SKIP:", text, flags=re.M)),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_go_test_cases(output_path: Path) -> list[tuple[str, str]]:
|
|
||||||
if not output_path.exists():
|
|
||||||
return []
|
|
||||||
text = output_path.read_text(encoding="utf-8", errors="ignore")
|
|
||||||
cases: list[tuple[str, str]] = []
|
|
||||||
for match in re.finditer(r"^---\s+(PASS|FAIL|SKIP):\s+(\S+)", text, flags=re.M):
|
|
||||||
raw_status, test_name = match.groups()
|
|
||||||
status = {"PASS": "passed", "FAIL": "failed", "SKIP": "skipped"}.get(raw_status, "error")
|
|
||||||
cases.append((test_name.strip(), status))
|
|
||||||
return cases
|
|
||||||
|
|
||||||
|
|
||||||
def _read_exit_code(path: Path) -> int:
|
|
||||||
if not path.exists():
|
|
||||||
return 1
|
|
||||||
raw = path.read_text(encoding="utf-8").strip()
|
|
||||||
try:
|
|
||||||
return int(raw)
|
|
||||||
except ValueError:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
def _read_status(path: Path, default: str = "failed") -> str:
|
|
||||||
if not path.exists():
|
|
||||||
return default
|
|
||||||
raw = path.read_text(encoding="utf-8").strip().lower()
|
|
||||||
if raw in {"ok", "pass", "passed", "success"}:
|
|
||||||
return "ok"
|
|
||||||
if raw in {"failed", "fail", "error"}:
|
|
||||||
return "failed"
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
def _load_json(path: Path) -> dict | None:
|
|
||||||
if not path.exists():
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
return payload if isinstance(payload, dict) else None
|
|
||||||
|
|
||||||
|
|
||||||
def _sonarqube_check_status(build_dir: Path) -> str:
|
|
||||||
report = _load_json(Path(os.getenv("QUALITY_GATE_SONARQUBE_REPORT", str(build_dir / "sonarqube-quality-gate.json"))))
|
|
||||||
if not report:
|
|
||||||
return "not_applicable"
|
|
||||||
status_candidates = [
|
|
||||||
report.get("status"),
|
|
||||||
((report.get("projectStatus") or {}).get("status") if isinstance(report.get("projectStatus"), dict) else None),
|
|
||||||
((report.get("qualityGate") or {}).get("status") if isinstance(report.get("qualityGate"), dict) else None),
|
|
||||||
]
|
|
||||||
for value in status_candidates:
|
|
||||||
if isinstance(value, str):
|
|
||||||
return "ok" if value.strip().lower() in QUALITY_SUCCESS_STATES else "failed"
|
|
||||||
return "failed"
|
|
||||||
|
|
||||||
|
|
||||||
def _supply_chain_check_status(build_dir: Path) -> str:
|
|
||||||
report = _load_json(Path(os.getenv("QUALITY_GATE_IRONBANK_REPORT", str(build_dir / "ironbank-compliance.json"))))
|
|
||||||
if not report:
|
|
||||||
return "not_applicable"
|
|
||||||
compliant = report.get("compliant")
|
|
||||||
if isinstance(compliant, bool):
|
|
||||||
return "ok" if compliant else "failed"
|
|
||||||
status_candidates = [report.get("status"), report.get("result"), report.get("compliance")]
|
|
||||||
for value in status_candidates:
|
|
||||||
if isinstance(value, str):
|
|
||||||
return "ok" if value.strip().lower() in QUALITY_SUCCESS_STATES else "failed"
|
|
||||||
return "failed"
|
|
||||||
|
|
||||||
|
|
||||||
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(
|
||||||
@ -277,7 +155,6 @@ 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]
|
repo_root = Path(__file__).resolve().parents[1]
|
||||||
build_dir = repo_root / "build"
|
|
||||||
|
|
||||||
remote_ok = 0
|
remote_ok = 0
|
||||||
remote_failed = 0
|
remote_failed = 0
|
||||||
@ -306,34 +183,14 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
resolved_failed = max(args.local_failed, remote_failed)
|
resolved_failed = max(args.local_failed, remote_failed)
|
||||||
coverage_percent = _read_coverage_percent(args.coverage_percent_file)
|
coverage_percent = _read_coverage_percent(args.coverage_percent_file)
|
||||||
source_lines_over_500 = _count_source_files_over_limit(repo_root, max_lines=500)
|
source_lines_over_500 = _count_source_files_over_limit(repo_root, max_lines=500)
|
||||||
test_output = Path(os.getenv("ANANKE_QUALITY_OUTPUT_FILE", str(build_dir / "quality-gate.out")))
|
payload = _build_payload(args.suite, args.trigger, resolved_ok, resolved_failed).rstrip("\n")
|
||||||
tests = _parse_go_test_counts(test_output)
|
payload += (
|
||||||
test_cases = _parse_go_test_cases(test_output)
|
"\n# TYPE ananke_quality_gate_coverage_percent gauge\n"
|
||||||
gate_rc = _read_exit_code(Path(os.getenv("ANANKE_QUALITY_EXIT_CODE_PATH", str(build_dir / "quality-gate.rc"))))
|
f'ananke_quality_gate_coverage_percent{{suite="{args.suite}"}} {coverage_percent:.3f}\n'
|
||||||
docs_status = _read_status(Path(os.getenv("ANANKE_QUALITY_DOCS_STATUS_PATH", str(build_dir / "docs-naming.status"))))
|
"# TYPE platform_quality_gate_workspace_line_coverage_percent gauge\n"
|
||||||
gate_failed = gate_rc != 0
|
f'platform_quality_gate_workspace_line_coverage_percent{{suite="{args.suite}"}} {coverage_percent:.3f}\n'
|
||||||
checks = {
|
"# TYPE platform_quality_gate_source_lines_over_500_total gauge\n"
|
||||||
"tests": "failed" if gate_failed or tests["failed"] > 0 else "ok",
|
f'platform_quality_gate_source_lines_over_500_total{{suite="{args.suite}"}} {source_lines_over_500}\n'
|
||||||
"coverage": "ok" if coverage_percent >= 95.0 else "failed",
|
|
||||||
"loc": "ok" if source_lines_over_500 == 0 else "failed",
|
|
||||||
"docs_naming": docs_status,
|
|
||||||
"gate_glue": "ok",
|
|
||||||
"sonarqube": _sonarqube_check_status(build_dir),
|
|
||||||
"supply_chain": _supply_chain_check_status(build_dir),
|
|
||||||
}
|
|
||||||
payload = _build_payload(
|
|
||||||
args.suite,
|
|
||||||
args.trigger,
|
|
||||||
resolved_ok,
|
|
||||||
resolved_failed,
|
|
||||||
tests_passed=tests["passed"],
|
|
||||||
tests_failed=tests["failed"],
|
|
||||||
tests_errors=tests["errors"],
|
|
||||||
tests_skipped=tests["skipped"],
|
|
||||||
test_cases=test_cases,
|
|
||||||
coverage_percent=coverage_percent,
|
|
||||||
source_lines_over_500=source_lines_over_500,
|
|
||||||
checks=checks,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
|
|||||||
@ -19,25 +19,6 @@ QUALITY_LAST_SUCCESS=0
|
|||||||
QUALITY_LAST_RUN_TS=0
|
QUALITY_LAST_RUN_TS=0
|
||||||
QUALITY_SUCCESS_PERCENT="0.00"
|
QUALITY_SUCCESS_PERCENT="0.00"
|
||||||
|
|
||||||
run_with_retry() {
|
|
||||||
local attempts="$1"
|
|
||||||
shift
|
|
||||||
local try=1
|
|
||||||
local delay=3
|
|
||||||
local rc=0
|
|
||||||
while true; do
|
|
||||||
"$@" && return 0
|
|
||||||
rc=$?
|
|
||||||
if [[ "${try}" -ge "${attempts}" ]]; then
|
|
||||||
return "${rc}"
|
|
||||||
fi
|
|
||||||
echo "[quality] retry ${try}/${attempts} after rc=${rc}: $*" >&2
|
|
||||||
sleep "${delay}"
|
|
||||||
delay=$((delay * 2))
|
|
||||||
try=$((try + 1))
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
read_quality_counter() {
|
read_quality_counter() {
|
||||||
local key="$1"
|
local key="$1"
|
||||||
if [[ ! -f "${QUALITY_STATE_FILE}" ]]; then
|
if [[ ! -f "${QUALITY_STATE_FILE}" ]]; then
|
||||||
@ -156,12 +137,9 @@ trap 'quality_gate_finalize $?' EXIT
|
|||||||
cd "${REPO_DIR}"
|
cd "${REPO_DIR}"
|
||||||
mkdir -p "${BUILD_DIR}"
|
mkdir -p "${BUILD_DIR}"
|
||||||
rm -f "${COVERAGE_PROFILE}" "${COVERAGE_PERCENT_FILE}"
|
rm -f "${COVERAGE_PROFILE}" "${COVERAGE_PERCENT_FILE}"
|
||||||
printf 'failed\n' > "${BUILD_DIR}/docs-naming.status"
|
|
||||||
|
|
||||||
echo "[quality] unit tests + workspace coverage profile"
|
echo "[quality] unit tests + workspace coverage profile"
|
||||||
export GOPROXY="${GOPROXY:-https://proxy.golang.org,direct}"
|
go test -coverprofile="${COVERAGE_PROFILE}" ./...
|
||||||
run_with_retry 4 go mod download
|
|
||||||
run_with_retry 3 go test -coverprofile="${COVERAGE_PROFILE}" ./...
|
|
||||||
coverage_percent="$(go tool cover -func="${COVERAGE_PROFILE}" | awk '/^total:/ {gsub("%","",$3); print $3}')"
|
coverage_percent="$(go tool cover -func="${COVERAGE_PROFILE}" | awk '/^total:/ {gsub("%","",$3); print $3}')"
|
||||||
if [[ -z "${coverage_percent}" ]]; then
|
if [[ -z "${coverage_percent}" ]]; then
|
||||||
coverage_percent="0"
|
coverage_percent="0"
|
||||||
@ -174,7 +152,6 @@ go test ./hygiene -run TestHygieneContracts/doc_contract -count=1
|
|||||||
|
|
||||||
echo "[quality] hygiene: naming contracts"
|
echo "[quality] hygiene: naming contracts"
|
||||||
go test ./hygiene -run TestHygieneContracts/naming_contract -count=1
|
go test ./hygiene -run TestHygieneContracts/naming_contract -count=1
|
||||||
printf 'ok\n' > "${BUILD_DIR}/docs-naming.status"
|
|
||||||
|
|
||||||
echo "[quality] hygiene: LOC limits"
|
echo "[quality] hygiene: LOC limits"
|
||||||
go test ./hygiene -run TestHygieneContracts/loc_limit -count=1
|
go test ./hygiene -run TestHygieneContracts/loc_limit -count=1
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user