From 44471741089c3fa660258b22f0e0b99e515d808c Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Fri, 10 Apr 2026 05:18:52 -0300 Subject: [PATCH] ci: add backend quality gate and pushgateway status --- Jenkinsfile | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1e615bc..904741a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -44,6 +44,13 @@ spec: mountPath: /root/.docker - name: harbor-config mountPath: /docker-config + - name: tester + image: python:3.12-slim + command: ["cat"] + tty: true + volumeMounts: + - name: workspace-volume + mountPath: /home/jenkins/agent volumes: - name: workspace-volume emptyDir: {} @@ -67,6 +74,8 @@ spec: BACK_IMAGE = "${REGISTRY}/bstein-dev-home-backend" VERSION_TAG = 'dev' SEMVER = 'dev' + SUITE_NAME = 'bstein-home' + PUSHGATEWAY_URL = 'http://platform-quality-gateway.monitoring.svc.cluster.local:9091' } options { disableConcurrentBuilds() @@ -88,7 +97,7 @@ spec: sh ''' set -euo pipefail for attempt in 1 2 3 4 5; do - if apk add --no-cache bash git jq; then + if apk add --no-cache bash git jq curl; then break fi if [ "$attempt" -eq 5 ]; then @@ -142,13 +151,26 @@ spec: sleep 2 done docker buildx create --name bstein-builder --driver docker-container \ - --driver-opt image=registry.bstein.dev/bstein/buildkit:buildx-stable-1-arm64 \ + --driver-opt image=moby/buildkit:buildx-stable-1 \ --bootstrap --use || docker buildx use bstein-builder ''' } } } + stage('Backend unit tests') { + steps { + container('tester') { + sh ''' + set -euo pipefail + mkdir -p build + python -m pip install --no-cache-dir -r backend/requirements.txt pytest pytest-mock + python -m pytest backend/tests -q --junitxml=build/junit-backend.xml + ''' + } + } + } + stage('Build & push frontend') { steps { container('builder') { @@ -187,11 +209,102 @@ spec: } post { + success { + container('tester') { + sh ''' + set -euo pipefail + export QUALITY_STATUS=ok + python - <<'PY' +import os +import re +import urllib.request + +suite = os.environ.get("SUITE_NAME", "bstein-home") +status = os.environ.get("QUALITY_STATUS", "failed") +gateway = os.environ.get("PUSHGATEWAY_URL", "http://platform-quality-gateway.monitoring.svc.cluster.local:9091").rstrip("/") +text = urllib.request.urlopen(f"{gateway}/metrics", timeout=10).read().decode("utf-8", errors="replace") + +def counter(name: str) -> float: + pattern = re.compile( + rf'^platform_quality_gate_runs_total\\{{[^}}]*job="platform-quality-ci"[^}}]*suite="{re.escape(suite)}"[^}}]*status="{name}"[^}}]*\\}}\\s+([0-9]+(?:\\.[0-9]+)?)$', + re.M, + ) + match = pattern.search(text) + return float(match.group(1)) if match else 0.0 + +ok = counter("ok") +failed = counter("failed") +if status == "ok": + ok += 1 +else: + failed += 1 +payload = ( + "# TYPE platform_quality_gate_runs_total counter\\n" + f'platform_quality_gate_runs_total{{suite="{suite}",status="ok"}} {int(ok)}\\n' + f'platform_quality_gate_runs_total{{suite="{suite}",status="failed"}} {int(failed)}\\n' +) +req = urllib.request.Request( + f"{gateway}/metrics/job/platform-quality-ci/suite/{suite}", + data=payload.encode("utf-8"), + method="POST", + headers={"Content-Type": "text/plain"}, +) +urllib.request.urlopen(req, timeout=10).read() +PY + ''' + } + } + failure { + container('tester') { + sh ''' + set -euo pipefail + export QUALITY_STATUS=failed + python - <<'PY' +import os +import re +import urllib.request + +suite = os.environ.get("SUITE_NAME", "bstein-home") +status = os.environ.get("QUALITY_STATUS", "failed") +gateway = os.environ.get("PUSHGATEWAY_URL", "http://platform-quality-gateway.monitoring.svc.cluster.local:9091").rstrip("/") +text = urllib.request.urlopen(f"{gateway}/metrics", timeout=10).read().decode("utf-8", errors="replace") + +def counter(name: str) -> float: + pattern = re.compile( + rf'^platform_quality_gate_runs_total\\{{[^}}]*job="platform-quality-ci"[^}}]*suite="{re.escape(suite)}"[^}}]*status="{name}"[^}}]*\\}}\\s+([0-9]+(?:\\.[0-9]+)?)$', + re.M, + ) + match = pattern.search(text) + return float(match.group(1)) if match else 0.0 + +ok = counter("ok") +failed = counter("failed") +if status == "ok": + ok += 1 +else: + failed += 1 +payload = ( + "# TYPE platform_quality_gate_runs_total counter\\n" + f'platform_quality_gate_runs_total{{suite="{suite}",status="ok"}} {int(ok)}\\n' + f'platform_quality_gate_runs_total{{suite="{suite}",status="failed"}} {int(failed)}\\n' +) +req = urllib.request.Request( + f"{gateway}/metrics/job/platform-quality-ci/suite/{suite}", + data=payload.encode("utf-8"), + method="POST", + headers={"Content-Type": "text/plain"}, +) +urllib.request.urlopen(req, timeout=10).read() +PY + ''' + } + } always { script { def props = fileExists('build.env') ? readProperties(file: 'build.env') : [:] echo "Build complete for ${props['SEMVER'] ?: env.VERSION_TAG}" } + archiveArtifacts artifacts: 'build/junit-backend.xml', allowEmptyArchive: true } } }