titan-iac/ci/Jenkinsfile.titan-iac

176 lines
5.2 KiB
Plaintext

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
}
}
}