From 420c6a90ad33cbea3f21a0616eb9beda5d55ac36 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Sun, 19 Apr 2026 14:12:58 -0300 Subject: [PATCH] ci: collect sonar and supply-chain evidence for gate metrics --- Jenkinsfile | 246 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 160 insertions(+), 86 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index ada559b..bec5193 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -70,6 +70,8 @@ spec: environment { SUITE_NAME = 'soteria' 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 { disableConcurrentBuilds() @@ -91,9 +93,6 @@ spec: } } stage('Prep toolchain') { - when { - expression { return params.PUBLISH_IMAGES } - } steps { container('builder') { sh ''' @@ -108,97 +107,170 @@ spec: stage('Run quality gate') { steps { container('tester') { - retry(2) { - sh ''' - set -eu - apt-get update >/dev/null - apt-get install -y --no-install-recommends jq python3 ripgrep >/dev/null - mkdir -p build - set +e - bash scripts/check.sh - gate_rc=$? - set -e - if [ ! -f build/go-test.json ]; then - : > build/go-test.json - fi - tests_total="$(jq -s '[.[] | select(.Test != null and (.Action=="pass" or .Action=="fail" or .Action=="skip"))] | length' build/go-test.json 2>/dev/null || echo 0)" - tests_failed="$(jq -s '[.[] | select(.Test != null and .Action=="fail")] | length' build/go-test.json 2>/dev/null || echo 0)" - tests_skipped="$(jq -s '[.[] | select(.Test != null and .Action=="skip")] | length' build/go-test.json 2>/dev/null || echo 0)" - tests_errors="$(jq -s '[.[] | select(.Test == null and .Action=="fail")] | length' build/go-test.json 2>/dev/null || echo 0)" - tests_passed=$((tests_total - tests_failed - tests_skipped)) - if [ "${tests_passed}" -lt 0 ]; then - tests_passed=0 - fi - coverage_percent="$(jq -r '.coverage_percent // 0' build/quality-summary.json 2>/dev/null || echo 0)" - over_500="$(jq -r '.source_lines_over_500 // 0' build/quality-summary.json 2>/dev/null || echo 0)" - cat > build/test-summary.json </dev/null + apt-get install -y --no-install-recommends jq python3 >/dev/null + mkdir -p build + python3 - <<'PY' +import base64 +import json +import os +import urllib.parse +import urllib.request +from pathlib import Path + +host = os.getenv('SONARQUBE_HOST_URL', '').strip().rstrip('/') +project_key = os.getenv('SONARQUBE_PROJECT_KEY', '').strip() +token = os.getenv('SONARQUBE_TOKEN', '').strip() +sonar_report = 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)} +Path(sonar_report).write_text(json.dumps(payload, indent=2, sort_keys=True) + "\\n", encoding="utf-8") + +ironbank_report = Path(os.getenv('QUALITY_GATE_IRONBANK_REPORT', 'build/ironbank-compliance.json')) +if not ironbank_report.exists(): + status = os.getenv('IRONBANK_COMPLIANCE_STATUS', '').strip() + compliant = os.getenv('IRONBANK_COMPLIANT', '').strip().lower() + ironbank_payload = { + "status": status or "unknown", + "compliant": compliant in {"1", "true", "yes", "on"} if compliant else None, + } + ironbank_payload = {k: v for k, v in ironbank_payload.items() if v is not None} + if "status" not in ironbank_payload: + ironbank_payload["status"] = "unknown" + ironbank_payload["note"] = ( + "Set IRONBANK_COMPLIANCE_STATUS/IRONBANK_COMPLIANT " + "or write build/ironbank-compliance.json in image-building repos." + ) + ironbank_report.parent.mkdir(parents=True, exist_ok=True) + ironbank_report.write_text(json.dumps(ironbank_payload, indent=2, sort_keys=True) + "\\n", encoding="utf-8") +PY + set +e + bash scripts/check.sh + gate_rc=$? + set -e + if [ ! -f build/go-test.json ]; then + : > build/go-test.json + fi + tests_total="$(jq -s '[.[] | select(.Test != null and (.Action=="pass" or .Action=="fail" or .Action=="skip"))] | length' build/go-test.json 2>/dev/null || echo 0)" + tests_failed="$(jq -s '[.[] | select(.Test != null and .Action=="fail")] | length' build/go-test.json 2>/dev/null || echo 0)" + tests_skipped="$(jq -s '[.[] | select(.Test != null and .Action=="skip")] | length' build/go-test.json 2>/dev/null || echo 0)" + tests_errors="$(jq -s '[.[] | select(.Test == null and .Action=="fail")] | length' build/go-test.json 2>/dev/null || echo 0)" + tests_passed=$((tests_total - tests_failed - tests_skipped)) + if [ "${tests_passed}" -lt 0 ]; then + tests_passed=0 + fi + coverage_percent="$(jq -r '.coverage_percent // 0' build/quality-summary.json 2>/dev/null || echo 0)" + over_500="$(jq -r '.source_lines_over_500 // 0' build/quality-summary.json 2>/dev/null || echo 0)" + cat > build/test-summary.json < build/test.exitcode - ''' - } + printf '%s\n' "${gate_rc}" > build/test.exitcode + ''' } } } stage('Publish test metrics') { steps { container('tester') { - retry(2) { - sh ''' - set -eu - apt-get update >/dev/null - apt-get install -y --no-install-recommends curl jq >/dev/null - suite="${SUITE_NAME}" - gateway="${PUSHGATEWAY_URL}" - test_rc="$(cat build/test.exitcode 2>/dev/null || echo 1)" - status="ok" - if [ "${test_rc}" -ne 0 ]; then - status="failed" - fi - fetch_counter() { - status_name="$1" - line="$(curl -fsS "${gateway}/metrics" 2>/dev/null | awk -v suite="${suite}" -v status="${status_name}" ' - /platform_quality_gate_runs_total/ { - if (index($0, "job=\\"platform-quality-ci\\"") && index($0, "suite=\\"" suite "\\"") && index($0, "status=\\"" status "\\"")) { - print $2 - exit - } + sh ''' + set -eu + apt-get update >/dev/null + apt-get install -y --no-install-recommends curl jq >/dev/null + suite="${SUITE_NAME}" + gateway="${PUSHGATEWAY_URL}" + test_rc="$(cat build/test.exitcode 2>/dev/null || echo 1)" + status="ok" + if [ "${test_rc}" -ne 0 ]; then + status="failed" + fi + fetch_counter() { + status_name="$1" + line="$(curl -fsS "${gateway}/metrics" 2>/dev/null | awk -v suite="${suite}" -v status="${status_name}" ' + /platform_quality_gate_runs_total/ { + if (index($0, "job=\\"platform-quality-ci\\"") && index($0, "suite=\\"" suite "\\"") && index($0, "status=\\"" status "\\"")) { + print $2 + exit } - ' || true)" - [ -n "${line}" ] && printf '%s\n' "${line}" || printf '0\n' - } - ok_count="$(fetch_counter ok)" - failed_count="$(fetch_counter failed)" - if [ "${status}" = "ok" ]; then - ok_count=$((ok_count + 1)) + } + ' || true)" + [ -n "${line}" ] && printf '%s\n' "${line}" || printf '0\n' + } + ok_count="$(fetch_counter ok)" + failed_count="$(fetch_counter failed)" + if [ "${status}" = "ok" ]; then + ok_count=$((ok_count + 1)) + else + failed_count=$((failed_count + 1)) + fi + tests_passed="$(jq -r '.passed // 0' build/test-summary.json 2>/dev/null || echo 0)" + tests_failed="$(jq -r '.failed // 0' build/test-summary.json 2>/dev/null || echo 0)" + tests_errors="$(jq -r '.errors // 0' build/test-summary.json 2>/dev/null || echo 0)" + tests_skipped="$(jq -r '.skipped // 0' build/test-summary.json 2>/dev/null || echo 0)" + coverage_percent="$(jq -r '.coverage_percent // 0' build/test-summary.json 2>/dev/null || echo 0)" + over_500="$(jq -r '.source_lines_over_500 // 0' build/test-summary.json 2>/dev/null || echo 0)" + tests_check="failed" + if [ "${test_rc}" -eq 0 ] && [ "${tests_failed}" -eq 0 ] && [ "${tests_errors}" -eq 0 ] && [ "${tests_passed}" -gt 0 ]; then + tests_check="ok" + fi + coverage_check="$(awk -v value="${coverage_percent}" 'BEGIN { if ((value + 0) >= 95) { print "ok" } else { print "failed" } }')" + loc_check="failed" + if [ "${over_500}" -eq 0 ]; then + loc_check="ok" + fi + docs_naming_check="not_applicable" + gate_glue_check="ok" + sonarqube_check="not_applicable" + if [ -f build/sonarqube-quality-gate.json ]; then + sonar_status="$(jq -r '.status // .projectStatus.status // .qualityGate.status // empty' build/sonarqube-quality-gate.json 2>/dev/null | tr '[:upper:]' '[:lower:]')" + if [ -n "${sonar_status}" ]; then + case "${sonar_status}" in + ok|pass|passed|success) sonarqube_check="ok" ;; + *) sonarqube_check="failed" ;; + esac else - failed_count=$((failed_count + 1)) + sonarqube_check="failed" fi - tests_passed="$(jq -r '.passed // 0' build/test-summary.json 2>/dev/null || echo 0)" - tests_failed="$(jq -r '.failed // 0' build/test-summary.json 2>/dev/null || echo 0)" - tests_errors="$(jq -r '.errors // 0' build/test-summary.json 2>/dev/null || echo 0)" - tests_skipped="$(jq -r '.skipped // 0' build/test-summary.json 2>/dev/null || echo 0)" - coverage_percent="$(jq -r '.coverage_percent // 0' build/test-summary.json 2>/dev/null || echo 0)" - over_500="$(jq -r '.source_lines_over_500 // 0' build/test-summary.json 2>/dev/null || echo 0)" - tests_check="failed" - if [ "${test_rc}" -eq 0 ] && [ "${tests_failed}" -eq 0 ] && [ "${tests_errors}" -eq 0 ] && [ "${tests_passed}" -gt 0 ]; then - tests_check="ok" + fi + supply_chain_check="not_applicable" + if [ -f build/ironbank-compliance.json ]; then + compliant="$(jq -r '.compliant // empty' build/ironbank-compliance.json 2>/dev/null)" + if [ "${compliant}" = "true" ]; then + supply_chain_check="ok" + elif [ "${compliant}" = "false" ]; then + supply_chain_check="failed" + else + ironbank_status="$(jq -r '.status // .result // .compliance // empty' build/ironbank-compliance.json 2>/dev/null | tr '[:upper:]' '[:lower:]')" + case "${ironbank_status}" in + ok|pass|passed|success|compliant) supply_chain_check="ok" ;; + "") supply_chain_check="failed" ;; + *) supply_chain_check="failed" ;; + esac fi - coverage_check="$(awk -v value="${coverage_percent}" 'BEGIN { if ((value + 0) >= 95) { print "ok" } else { print "failed" } }')" - loc_check="failed" - if [ "${over_500}" -eq 0 ]; then - loc_check="ok" - fi - if ! cat </dev/null; then - echo "warning: metrics push failed for suite=${suite}" >&2 + fi + if ! cat </dev/null; then + echo "warning: metrics push failed for suite=${suite}" >&2 + fi # TYPE platform_quality_gate_runs_total counter platform_quality_gate_runs_total{suite="${suite}",status="ok"} ${ok_count} platform_quality_gate_runs_total{suite="${suite}",status="failed"} ${failed_count} @@ -217,10 +289,12 @@ platform_quality_gate_source_lines_over_500_total{suite="${suite}"} ${over_500} soteria_quality_gate_checks_total{suite="${suite}",check="tests",result="${tests_check}"} 1 soteria_quality_gate_checks_total{suite="${suite}",check="coverage",result="${coverage_check}"} 1 soteria_quality_gate_checks_total{suite="${suite}",check="loc",result="${loc_check}"} 1 +soteria_quality_gate_checks_total{suite="${suite}",check="docs_naming",result="${docs_naming_check}"} 1 +soteria_quality_gate_checks_total{suite="${suite}",check="gate_glue",result="${gate_glue_check}"} 1 +soteria_quality_gate_checks_total{suite="${suite}",check="sonarqube",result="${sonarqube_check}"} 1 +soteria_quality_gate_checks_total{suite="${suite}",check="supply_chain",result="${supply_chain_check}"} 1 METRICS - fi - ''' - } + ''' } } }