pipeline { agent { kubernetes { defaultContainer 'builder' 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: golang:1.25-bookworm command: - cat tty: true volumeMounts: - name: workspace-volume mountPath: /home/jenkins/agent volumes: - name: docker-config-writable emptyDir: {} - name: dind-storage emptyDir: {} - name: harbor-config secret: secretName: harbor-robot-pipeline items: - key: .dockerconfigjson path: config.json - name: workspace-volume emptyDir: {} """ } } environment { SUITE_NAME = 'soteria' PUSHGATEWAY_URL = 'http://platform-quality-gateway.monitoring.svc.cluster.local:9091' } options { disableConcurrentBuilds() } parameters { booleanParam( name: 'PUBLISH_IMAGES', defaultValue: false, description: 'Build and push the Soteria runtime image (enable for release runs).' ) } triggers { pollSCM('H/5 * * * *') } stages { stage('Checkout') { steps { checkout scm } } stage('Prep toolchain') { steps { container('builder') { sh ''' set -eu apk add --no-cache bash git jq curl mkdir -p /root/.docker cp /docker-config/config.json /root/.docker/config.json ''' } } } stage('Unit tests') { steps { container('tester') { sh ''' set -eu apt-get update >/dev/null apt-get install -y --no-install-recommends jq >/dev/null mkdir -p build set +e go test -json ./... > build/go-test.json test_rc=$? set -e tests_total="$(jq -s '[.[] | select(.Test != null and (.Action=="pass" or .Action=="fail" or .Action=="skip"))] | length' build/go-test.json)" tests_failed="$(jq -s '[.[] | select(.Test != null and .Action=="fail")] | length' build/go-test.json)" tests_skipped="$(jq -s '[.[] | select(.Test != null and .Action=="skip")] | length' build/go-test.json)" tests_errors="$(jq -s '[.[] | select(.Test == null and .Action=="fail")] | length' build/go-test.json)" tests_passed=$((tests_total - tests_failed - tests_skipped)) if [ "${tests_passed}" -lt 0 ]; then tests_passed=0 fi cat > build/test-summary.json < build/test.exitcode ''' } } } stage('Publish test metrics') { steps { container('tester') { 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)) 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)" cat </dev/null # 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} # TYPE soteria_quality_gate_tests_total gauge soteria_quality_gate_tests_total{suite="${suite}",result="passed"} ${tests_passed} soteria_quality_gate_tests_total{suite="${suite}",result="failed"} ${tests_failed} soteria_quality_gate_tests_total{suite="${suite}",result="error"} ${tests_errors} soteria_quality_gate_tests_total{suite="${suite}",result="skipped"} ${tests_skipped} METRICS ''' } } } stage('Enforce test result') { steps { container('tester') { sh ''' set -eu test_rc="$(cat build/test.exitcode 2>/dev/null || echo 1)" exit "${test_rc}" ''' } } } stage('Compute version') { when { expression { return params.PUBLISH_IMAGES } } steps { container('builder') { script { sh 'git config --global --add safe.directory /home/jenkins/agent/workspace/Soteria' def semver = sh(returnStdout: true, script: 'git describe --tags --exact-match || true').trim() if (!semver) { semver = sh(returnStdout: true, script: 'git rev-list --count HEAD').trim() semver = "0.1.0-${semver}" } sh "echo SEMVER=${semver} > build.env" } } } } stage('Buildx setup') { when { expression { return params.PUBLISH_IMAGES } } steps { container('builder') { sh ''' set -eu seq 1 10 | while read _; do docker info && break || sleep 2 done BUILDER_NAME="soteria-${BUILD_NUMBER}" docker buildx rm "${BUILDER_NAME}" >/dev/null 2>&1 || true docker buildx create --name "${BUILDER_NAME}" --driver docker-container --bootstrap --use ''' } } } stage('Build & push image') { when { expression { return params.PUBLISH_IMAGES } } steps { container('builder') { withCredentials([usernamePassword(credentialsId: 'harbor-robot', usernameVariable: 'HARBOR_USERNAME', passwordVariable: 'HARBOR_PASSWORD')]) { sh ''' set -eu VERSION_TAG=$(cut -d= -f2 build.env) printf '%s' "${HARBOR_PASSWORD}" | docker login registry.bstein.dev -u "${HARBOR_USERNAME}" --password-stdin docker buildx build --platform linux/arm64 \ --provenance=false \ --tag registry.bstein.dev/bstein/soteria:${VERSION_TAG} \ --tag registry.bstein.dev/bstein/soteria:latest \ --push . ''' } } } } } post { always { script { if (fileExists('build.env')) { def env = readProperties file: 'build.env' echo "Build complete for ${env.SEMVER}" } } archiveArtifacts artifacts: 'build/*', allowEmptyArchive: true } } }