2026-01-20 18:11:13 -03:00
|
|
|
// Mirror of ci/Jenkinsfile.titan-iac for multibranch discovery.
|
|
|
|
|
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'
|
2026-04-19 14:18:41 -03:00
|
|
|
SUITE_NAME = 'titan_iac'
|
2026-04-10 16:38:55 -03:00
|
|
|
PUSHGATEWAY_URL = 'http://platform-quality-gateway.monitoring.svc.cluster.local:9091'
|
2026-04-19 14:18:41 -03:00
|
|
|
QUALITY_GATE_SONARQUBE_ENFORCE = '1'
|
|
|
|
|
QUALITY_GATE_SONARQUBE_REPORT = 'build/sonarqube-quality-gate.json'
|
|
|
|
|
QUALITY_GATE_IRONBANK_ENFORCE = '1'
|
|
|
|
|
QUALITY_GATE_IRONBANK_REQUIRED = '0'
|
|
|
|
|
QUALITY_GATE_IRONBANK_REPORT = 'build/ironbank-compliance.json'
|
2026-01-20 18:11:13 -03:00
|
|
|
}
|
|
|
|
|
stages {
|
|
|
|
|
stage('Checkout') {
|
|
|
|
|
steps {
|
|
|
|
|
checkout scm
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
stage('Install deps') {
|
|
|
|
|
steps {
|
|
|
|
|
sh 'pip install --no-cache-dir -r ci/requirements.txt'
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-19 14:18:41 -03:00
|
|
|
stage('Collect SonarQube evidence') {
|
|
|
|
|
steps {
|
|
|
|
|
sh '''
|
|
|
|
|
set -eu
|
|
|
|
|
mkdir -p build
|
|
|
|
|
python3 - <<'PY'
|
|
|
|
|
import base64
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import urllib.parse
|
|
|
|
|
import urllib.request
|
|
|
|
|
|
|
|
|
|
host = os.getenv('SONARQUBE_HOST_URL', '').strip().rstrip('/')
|
|
|
|
|
project_key = os.getenv('SONARQUBE_PROJECT_KEY', '').strip()
|
|
|
|
|
token = os.getenv('SONARQUBE_TOKEN', '').strip()
|
|
|
|
|
report_path = os.getenv('QUALITY_GATE_SONARQUBE_REPORT', 'build/sonarqube-quality-gate.json')
|
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
"status": "ERROR",
|
|
|
|
|
"note": "missing SONARQUBE_HOST_URL and/or SONARQUBE_PROJECT_KEY",
|
|
|
|
|
}
|
|
|
|
|
if host and project_key:
|
|
|
|
|
query = urllib.parse.urlencode({"projectKey": project_key})
|
|
|
|
|
request = urllib.request.Request(
|
|
|
|
|
f"{host}/api/qualitygates/project_status?{query}",
|
|
|
|
|
method="GET",
|
|
|
|
|
)
|
|
|
|
|
if token:
|
|
|
|
|
encoded = base64.b64encode(f"{token}:".encode("utf-8")).decode("utf-8")
|
|
|
|
|
request.add_header("Authorization", f"Basic {encoded}")
|
|
|
|
|
try:
|
|
|
|
|
with urllib.request.urlopen(request, timeout=12) as response:
|
|
|
|
|
payload = json.loads(response.read().decode("utf-8"))
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
payload = {"status": "ERROR", "error": str(exc)}
|
|
|
|
|
|
|
|
|
|
with open(report_path, "w", encoding="utf-8") as handle:
|
|
|
|
|
json.dump(payload, handle, indent=2, sort_keys=True)
|
|
|
|
|
handle.write("\\n")
|
|
|
|
|
PY
|
|
|
|
|
'''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
stage('Collect IronBank evidence') {
|
|
|
|
|
steps {
|
|
|
|
|
sh '''
|
|
|
|
|
set -eu
|
|
|
|
|
mkdir -p build
|
|
|
|
|
python3 - <<'PY'
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
report_path = Path(os.getenv('QUALITY_GATE_IRONBANK_REPORT', 'build/ironbank-compliance.json'))
|
|
|
|
|
if report_path.exists():
|
|
|
|
|
raise SystemExit(0)
|
|
|
|
|
|
|
|
|
|
status = os.getenv('IRONBANK_COMPLIANCE_STATUS', '').strip()
|
|
|
|
|
compliant = os.getenv('IRONBANK_COMPLIANT', '').strip().lower()
|
|
|
|
|
payload = {
|
|
|
|
|
"status": status or "unknown",
|
|
|
|
|
"compliant": compliant in {"1", "true", "yes", "on"} if compliant else None,
|
|
|
|
|
}
|
|
|
|
|
payload = {k: v for k, v in payload.items() if v is not None}
|
|
|
|
|
if "status" not in payload:
|
|
|
|
|
payload["status"] = "unknown"
|
|
|
|
|
payload["note"] = (
|
|
|
|
|
"Set IRONBANK_COMPLIANCE_STATUS/IRONBANK_COMPLIANT "
|
|
|
|
|
"or write build/ironbank-compliance.json in image-building repos."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
report_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\\n", encoding="utf-8")
|
|
|
|
|
PY
|
|
|
|
|
'''
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 17:06:53 -03:00
|
|
|
stage('Run quality gate') {
|
2026-01-20 18:11:13 -03:00
|
|
|
steps {
|
2026-04-10 16:38:55 -03:00
|
|
|
sh '''
|
|
|
|
|
set -eu
|
|
|
|
|
mkdir -p build
|
|
|
|
|
set +e
|
2026-04-10 17:06:53 -03:00
|
|
|
python3 -m testing.quality_gate --profile jenkins --build-dir build
|
|
|
|
|
quality_gate_rc=$?
|
2026-04-10 16:38:55 -03:00
|
|
|
set -e
|
2026-04-10 17:06:53 -03:00
|
|
|
printf '%s\n' "${quality_gate_rc}" > build/quality-gate.rc
|
2026-04-10 16:38:55 -03:00
|
|
|
'''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
stage('Publish test metrics') {
|
|
|
|
|
steps {
|
|
|
|
|
sh '''
|
|
|
|
|
set -eu
|
2026-04-10 17:06:53 -03:00
|
|
|
export JUNIT_GLOB='build/junit-*.xml'
|
|
|
|
|
export QUALITY_GATE_EXIT_CODE_PATH='build/quality-gate.rc'
|
|
|
|
|
export QUALITY_GATE_SUMMARY_PATH='build/quality-gate-summary.json'
|
2026-04-10 16:38:55 -03:00
|
|
|
python3 ci/scripts/publish_test_metrics.py
|
|
|
|
|
'''
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 17:06:53 -03:00
|
|
|
stage('Enforce quality gate') {
|
2026-04-10 16:38:55 -03:00
|
|
|
steps {
|
|
|
|
|
sh '''
|
|
|
|
|
set -eu
|
2026-04-10 17:06:53 -03:00
|
|
|
test "$(cat build/quality-gate.rc 2>/dev/null || echo 1)" -eq 0
|
2026-04-10 16:38:55 -03:00
|
|
|
'''
|
2026-01-20 18:11:13 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
stage('Resolve Flux branch') {
|
|
|
|
|
steps {
|
|
|
|
|
script {
|
|
|
|
|
env.FLUX_BRANCH = sh(
|
|
|
|
|
returnStdout: true,
|
2026-04-19 14:33:26 -03:00
|
|
|
script: "awk '/branch:/{print \\$2; exit}' clusters/atlas/flux-system/gotk-sync.yaml"
|
2026-01-20 18:11:13 -03:00
|
|
|
).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 {
|
2026-04-19 14:18:41 -03:00
|
|
|
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}
|
|
|
|
|
'''
|
2026-01-20 18:11:13 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 16:38:55 -03:00
|
|
|
post {
|
|
|
|
|
always {
|
|
|
|
|
script {
|
2026-04-10 17:06:53 -03:00
|
|
|
if (fileExists('build/junit-unit.xml') || fileExists('build/junit-glue.xml')) {
|
2026-04-10 16:38:55 -03:00
|
|
|
try {
|
2026-04-10 17:06:53 -03:00
|
|
|
junit allowEmptyResults: true, testResults: 'build/junit-*.xml'
|
2026-04-10 16:38:55 -03:00
|
|
|
} catch (Throwable err) {
|
|
|
|
|
echo "junit step unavailable: ${err.class.simpleName}"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
archiveArtifacts artifacts: 'build/**', allowEmptyArchive: true, fingerprint: true
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-20 18:11:13 -03:00
|
|
|
}
|