pipeline { agent { kubernetes { defaultContainer 'python' yaml """ apiVersion: v1 kind: Pod metadata: labels: atlas.bstein.dev/workload: hermes-voice-image-builder spec: serviceAccountName: hermes-image-builder automountServiceAccountToken: false enableServiceLinks: false restartPolicy: Never securityContext: fsGroup: 1000 fsGroupChangePolicy: OnRootMismatch nodeSelector: kubernetes.io/arch: arm64 tolerations: # Jetson kubelet/network maintenance can briefly outlast Kubernetes' five # minute default. Keep the disposable build workspace intact long enough # for the node to recover instead of restarting a large image expansion. - key: node.kubernetes.io/not-ready operator: Exists effect: NoExecute tolerationSeconds: 600 - key: node.kubernetes.io/unreachable operator: Exists effect: NoExecute tolerationSeconds: 600 affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In # Voice images expand the large Jetson base and two Whisper # models. Keep that transient I/O on the roomy idle # accelerator, away from Longhorn and the live speech node. values: [titan-20] imagePullSecrets: - name: harbor-bstein-robot containers: - name: jnlp image: jenkins/inbound-agent@sha256:8eda4fe2a66bcf6a5e43436d9918fc14c306204dc8fcd75f4e15e0e6e5dc759a securityContext: allowPrivilegeEscalation: false capabilities: {drop: ["ALL"]} runAsNonRoot: true runAsUser: 1000 seccompProfile: {type: RuntimeDefault} resources: requests: {cpu: 25m, memory: 256Mi} limits: {cpu: 500m, memory: 512Mi} - name: python image: registry.bstein.dev/bstein/python@sha256:269541d3387baae008df4608ead893dba2b5cdaad1a5a380731a88992d34b808 command: ["sleep"] args: ["99d"] tty: true securityContext: allowPrivilegeEscalation: false capabilities: {drop: ["ALL"]} runAsNonRoot: true runAsUser: 1000 seccompProfile: {type: RuntimeDefault} resources: requests: {cpu: 25m, memory: 64Mi} limits: {cpu: 500m, memory: 512Mi} - name: kaniko image: gcr.io/kaniko-project/executor@sha256:c3109d5926a997b100c4343944e06c6b30a6804b2f9abe0994d3de6ef92b028e command: ["/busybox/sh", "-c"] args: ["/busybox/sleep 99d"] tty: true securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] # The pinned Jetson Whisper base contains gst-ptp-helper with a # security.capability xattr. Kaniko needs SETFCAP only while # unpacking that reviewed base; the pod remains non-privileged. add: ["CHOWN", "FOWNER", "DAC_OVERRIDE", "SETGID", "SETUID", "SETFCAP"] runAsUser: 0 seccompProfile: {type: RuntimeDefault} resources: # The STT image snapshots two checksum-pinned Whisper models. A 1 GiB # request let the builder overpack titan-20 and a 4 GiB cgroup killed # Kaniko while it emitted the large model layer. requests: {cpu: 250m, memory: 2Gi, ephemeral-storage: 10Gi} limits: {cpu: "2", memory: 6Gi, ephemeral-storage: 20Gi} """ } } parameters { booleanParam(name: 'PUBLISH_IMAGE', defaultValue: false, description: 'Publish the reviewed voice image to Harbor.') choice(name: 'IMAGE_COMPONENT', choices: ['stt', 'tts'], description: 'Private voice component to build.') string(name: 'EXPECTED_SOURCE_REVISION', defaultValue: '', description: 'Full reviewed commit contained by main.') string(name: 'CONFIRM_PUBLISH', defaultValue: '', description: 'Exact component-specific confirmation.') } options { disableConcurrentBuilds() buildDiscarder(logRotator(daysToKeepStr: '30', numToKeepStr: '100', artifactDaysToKeepStr: '30', artifactNumToKeepStr: '100')) skipDefaultCheckout(true) timeout(time: 150, unit: 'MINUTES') } stages { stage('Checkout reviewed source') { steps { checkout scm } } stage('Enforce release boundary') { steps { container('jnlp') { sh ''' set -eu mkdir -p build test "${PUBLISH_IMAGE}" = "true" case "${IMAGE_COMPONENT}" in stt) expected_confirmation='PUBLISH HERMES STT' ;; tts) expected_confirmation='PUBLISH HERMES TTS' ;; *) echo 'IMAGE_COMPONENT must be stt or tts' >&2; exit 2 ;; esac test "${CONFIRM_PUBLISH}" = "${expected_confirmation}" case "${EXPECTED_SOURCE_REVISION}" in *[!0-9a-f]*|'') echo 'EXPECTED_SOURCE_REVISION must be a lowercase full commit' >&2; exit 2 ;; esac test "${#EXPECTED_SOURCE_REVISION}" -eq 40 main_revision="$(git rev-parse HEAD)" test "${main_revision}" = "$(git rev-parse origin/main)" git merge-base --is-ancestor "${EXPECTED_SOURCE_REVISION}" "${main_revision}" git checkout --detach "${EXPECTED_SOURCE_REVISION}" actual_revision="$(git rev-parse HEAD)" test "${actual_revision}" = "${EXPECTED_SOURCE_REVISION}" test -z "$(git status --porcelain)" case "${BUILD_NUMBER}" in ''|0*|*[!0-9]*) exit 2 ;; esac image="registry.bstein.dev/bstein/hermes-jetson-${IMAGE_COMPONENT}" printf '%s\n' "${image}:git-${actual_revision}-build-${BUILD_NUMBER}" > build/hermes-voice.destination printf '%s\n' "${actual_revision}" > build/hermes-voice.source-revision printf '%s\n' "${IMAGE_COMPONENT}" > build/hermes-voice.component test -f "dockerfiles/Dockerfile.hermes-jetson-${IMAGE_COMPONENT}" ''' } } } stage('Validate reviewed voice source') { steps { container('python') { sh ''' set -eu python3 -m pip install --disable-pip-version-check --no-cache-dir \ --target=/tmp/hermes-voice-test-deps pytest==8.3.4 PyYAML==6.0.2 python3 -m py_compile \ dockerfiles/hermes-jetson-stt-server.py \ dockerfiles/hermes-jetson-tts-server.py \ dockerfiles/hermes_jetson_tts_cues.py PYTHONPATH=/tmp/hermes-voice-test-deps python3 -m pytest -q \ testing/tests/test_hermes_stt_streaming.py \ testing/tests/test_hermes_stt_rolling_model.py \ testing/tests/test_hermes_tts_language_routing.py \ testing/tests/test_hermes_voice_language_routing.py \ testing/tests/test_hermes_oci_promote.py \ testing/tests/test_hermes_image_automation.py \ testing/tests/test_hermes_voice_release.py ''' } } } stage('Build and publish without a daemon') { steps { container('kaniko') { withCredentials([usernamePassword(credentialsId: 'harbor-robot', usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASSWORD')]) { sh '''#!/busybox/sh set -eu set +x component="$(cat build/hermes-voice.component)" destination="$(cat build/hermes-voice.destination)" source_revision="$(cat build/hermes-voice.source-revision)" config_path=/kaniko/.docker/config.json umask 077 auth="$(printf '%s:%s' "${HARBOR_USER}" "${HARBOR_PASSWORD}" | /busybox/base64 | /busybox/tr -d '\n')" /busybox/mkdir -p /kaniko/.docker /busybox/printf '{"auths":{"registry.bstein.dev":{"auth":"%s"}}}\n' "${auth}" > "${config_path}" unset HARBOR_USER HARBOR_PASSWORD auth trap '/busybox/rm -f "${config_path}"' EXIT HUP INT TERM umask 022 /kaniko/executor \ --registry-mirror=harbor-core.harbor.svc.cluster.local \ --insecure-registry=harbor-core.harbor.svc.cluster.local \ --compressed-caching=false \ --snapshot-mode=redo \ --context="dir://${WORKSPACE}" \ --dockerfile="${WORKSPACE}/dockerfiles/Dockerfile.hermes-jetson-${component}" \ --destination="${destination}" \ --digest-file="${WORKSPACE}/build/hermes-voice.digest" \ --image-name-tag-with-digest-file="${WORKSPACE}/build/hermes-voice.image" \ --label="org.opencontainers.image.revision=${source_revision}" \ --label="org.opencontainers.image.source=https://scm.bstein.dev/titan/atlas-iac" \ --label="org.opencontainers.image.title=hermes-jetson-${component}" \ --cleanup --push-retry=3 /busybox/chmod 644 build/hermes-voice.digest build/hermes-voice.image ''' } } } } stage('Verify, archive, and publish Flux release') { steps { withCredentials([usernamePassword(credentialsId: 'harbor-robot', usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASSWORD')]) { sh ''' set -eu set +x destination="$(cat build/hermes-voice.destination)" source_revision="$(cat build/hermes-voice.source-revision)" component="$(cat build/hermes-voice.component)" digest="$(cat build/hermes-voice.digest)" image="$(cat build/hermes-voice.image)" test "${image}" = "${destination}@${digest}" python3 ci/scripts/hermes_oci_promote.py \ --destination "${destination}" \ --digest-file build/hermes-voice.digest \ --source-revision "${source_revision}" \ --build-number "${BUILD_NUMBER}" \ > build/hermes-voice.promotion.json python3 -c 'import json; data=json.load(open("build/hermes-voice.promotion.json")); assert data["result"] in {"published", "already-present"}' ''' archiveArtifacts( artifacts: 'build/hermes-voice.component,build/hermes-voice.destination,build/hermes-voice.digest,build/hermes-voice.image,build/hermes-voice.source-revision,build/hermes-voice.promotion.json', allowEmptyArchive: false, fingerprint: true ) } } } } }