pipeline { agent { kubernetes { defaultContainer 'tester' yaml """ apiVersion: v1 kind: Pod spec: nodeSelector: kubernetes.io/arch: arm64 node-role.kubernetes.io/worker: "true" containers: - name: dind image: docker:27-dind securityContext: privileged: true env: - name: DOCKER_TLS_CERTDIR value: "" args: - "--mtu=1400" - "--host=unix:///var/run/docker.sock" - "--host=tcp://0.0.0.0:2375" volumeMounts: - name: dind-storage mountPath: /var/lib/docker - name: workspace-volume mountPath: /home/jenkins/agent - name: builder image: docker:27 command: - cat tty: true env: - name: DOCKER_HOST value: tcp://localhost:2375 - name: DOCKER_TLS_CERTDIR value: "" volumeMounts: - name: workspace-volume mountPath: /home/jenkins/agent - name: docker-config-writable 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: docker-config-writable emptyDir: {} - name: dind-storage persistentVolumeClaim: claimName: jenkins-dind-cache - name: harbor-config secret: secretName: harbor-robot-pipeline items: - key: .dockerconfigjson path: config.json - name: workspace-volume emptyDir: {} """ } } environment { PIP_DISABLE_PIP_VERSION_CHECK = '1' PYTHONUNBUFFERED = '1' SUITE_NAME = 'atlasbot' PUSHGATEWAY_URL = 'http://platform-quality-gateway.monitoring.svc.cluster.local:9091' } stages { stage('Checkout') { steps { checkout scm } } stage('Prep toolchain') { steps { container('builder') { sh ''' set -euo pipefail mkdir -p /root/.docker cp /docker-config/config.json /root/.docker/config.json ''' } } } stage('Compute version') { steps { container('builder') { script { def base = sh(returnStdout: true, script: 'cat VERSION_BASE 2>/dev/null || true').trim() if (!base) { base = "0.1.0" } def buildNum = env.BUILD_NUMBER?.trim() if (!buildNum) { buildNum = "0" } def semver = "${base}-${buildNum}" sh "echo SEMVER=${semver} > build.env" } } } } stage('Buildx setup') { steps { container('builder') { sh ''' set -euo pipefail seq 1 10 | while read _; do docker info && break || sleep 2 done BUILDER_NAME="atlasbot-${BUILD_NUMBER}" docker buildx rm "${BUILDER_NAME}" >/dev/null 2>&1 || true docker buildx create --name "${BUILDER_NAME}" --driver docker-container --bootstrap --use ''' } } } stage('Unit tests') { steps { container('builder') { sh ''' set -euo pipefail mkdir -p build docker buildx build --platform linux/arm64 --target test --load -t atlasbot-test . docker run --rm -v "$PWD/build:/out" atlasbot-test \ python -m ruff check atlasbot --select C90,PLR docker run --rm -v "$PWD/build:/out" atlasbot-test \ python -m slipcover --json --out /out/coverage.json --source atlasbot --fail-under 90 \ -m pytest -q --junitxml /out/junit.xml ''' } } } stage('Publish test metrics') { steps { container('builder') { sh ''' set -euo pipefail docker run --rm -v "$PWD/build:/out" \ -e JUNIT_PATH=/out/junit.xml \ -e COVERAGE_PATH=/out/coverage.json \ atlasbot-test python scripts/publish_test_metrics.py ''' } } } stage('Build & push image') { steps { container('builder') { sh ''' set -euo pipefail VERSION_TAG=$(cut -d= -f2 build.env) docker buildx build --platform linux/arm64 \ --target runtime \ --tag registry.bstein.dev/bstein/atlasbot:${VERSION_TAG} \ --tag registry.bstein.dev/bstein/atlasbot:latest \ --push . ''' } } } } 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", "atlasbot") 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", "atlasbot") 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 { if (fileExists('build.env')) { def env = readProperties file: 'build.env' echo "Build complete for ${env.SEMVER}" } } archiveArtifacts artifacts: 'build/*', allowEmptyArchive: true } } }