pipeline { agent { kubernetes { defaultContainer 'python' yaml """ apiVersion: v1 kind: Pod spec: nodeSelector: hardware: rpi5 kubernetes.io/arch: arm64 node-role.kubernetes.io/worker: "true" containers: - name: python image: python:3.12-slim command: - cat tty: true """ } } environment { PIP_DISABLE_PIP_VERSION_CHECK = '1' PYTHONUNBUFFERED = '1' SUITE_NAME = 'titan-iac' PUSHGATEWAY_URL = 'http://platform-quality-gateway.monitoring.svc.cluster.local:9091' } stages { stage('Checkout') { steps { checkout scm } } stage('Install deps') { steps { sh 'pip install --no-cache-dir -r ci/requirements.txt' } } stage('Glue tests') { steps { sh ''' set -eu mkdir -p build pytest -q ci/tests/glue --junitxml=build/junit-glue.xml ''' } } stage('Resolve Flux branch') { steps { script { env.FLUX_BRANCH = sh( returnStdout: true, script: "awk '/branch:/{print $2; exit}' clusters/atlas/flux-system/gotk-sync.yaml" ).trim() if (!env.FLUX_BRANCH) { error('Flux branch not found in gotk-sync.yaml') } echo "Flux branch: ${env.FLUX_BRANCH}" } } } stage('Promote') { when { expression { def branch = env.BRANCH_NAME ?: (env.GIT_BRANCH ?: '').replaceFirst('origin/', '') return env.FLUX_BRANCH && branch == env.FLUX_BRANCH } } steps { withCredentials([usernamePassword(credentialsId: 'gitea-pat', usernameVariable: 'GIT_USER', passwordVariable: 'GIT_TOKEN')]) { sh ''' set +x git config user.email "jenkins@bstein.dev" git config user.name "jenkins" git remote set-url origin https://${GIT_USER}:${GIT_TOKEN}@scm.bstein.dev/bstein/titan-iac.git git push origin HEAD:${FLUX_BRANCH} ''' } } } } post { always { script { env.QUALITY_STATUS = currentBuild.currentResult == 'SUCCESS' ? 'ok' : 'failed' } sh ''' set -eu python - <<'PY' import os import urllib.request import xml.etree.ElementTree as ET from pathlib import Path suite = os.getenv("SUITE_NAME", "titan-iac") status = os.getenv("QUALITY_STATUS", "failed") gateway = os.getenv("PUSHGATEWAY_URL", "http://platform-quality-gateway.monitoring.svc.cluster.local:9091").rstrip("/") junit_path = Path("build/junit-glue.xml") totals = {"tests": 0, "failures": 0, "errors": 0, "skipped": 0} if junit_path.exists(): root = ET.parse(junit_path).getroot() suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite")) if root.tag == "testsuites" else [] for node in suites: for key in totals: raw = node.attrib.get(key) or "0" try: totals[key] += int(float(raw)) except ValueError: pass passed = max(totals["tests"] - totals["failures"] - totals["errors"] - totals["skipped"], 0) def read_metrics() -> str: try: with urllib.request.urlopen(f"{gateway}/metrics", timeout=10) as resp: return resp.read().decode("utf-8", errors="replace") except Exception: return "" def read_counter(text: str, counter_status: str) -> float: for line in text.splitlines(): if not line.startswith("platform_quality_gate_runs_total{"): continue if 'job="platform-quality-ci"' not in line: continue if f'suite="{suite}"' not in line: continue if f'status="{counter_status}"' not in line: continue parts = line.split() if len(parts) < 2: continue try: return float(parts[1]) except ValueError: return 0.0 return 0.0 metrics = read_metrics() ok_count = read_counter(metrics, "ok") failed_count = read_counter(metrics, "failed") if status == "ok": ok_count += 1 else: failed_count += 1 payload = "\n".join( [ "# TYPE platform_quality_gate_runs_total counter", f'platform_quality_gate_runs_total{{suite="{suite}",status="ok"}} {ok_count:.0f}', f'platform_quality_gate_runs_total{{suite="{suite}",status="failed"}} {failed_count:.0f}', "# TYPE titan_iac_quality_gate_tests_total gauge", f'titan_iac_quality_gate_tests_total{{suite="{suite}",result="passed"}} {passed}', f'titan_iac_quality_gate_tests_total{{suite="{suite}",result="failed"}} {totals["failures"]}', f'titan_iac_quality_gate_tests_total{{suite="{suite}",result="error"}} {totals["errors"]}', f'titan_iac_quality_gate_tests_total{{suite="{suite}",result="skipped"}} {totals["skipped"]}', ] ) + "\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"}, ) with urllib.request.urlopen(req, timeout=10) as resp: if resp.status >= 400: raise RuntimeError(f"push failed: {resp.status}") PY ''' archiveArtifacts artifacts: 'build/junit-glue.xml', allowEmptyArchive: true } } }