atlas-iac/ci/Jenkinsfile.hermes-voice-image
2026-08-23 18:53:11 -03:00

223 lines
9.6 KiB
Plaintext

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:
requests: {cpu: 250m, memory: 1Gi, ephemeral-storage: 10Gi}
limits: {cpu: "2", memory: 4Gi, 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
actual_revision="$(git rev-parse HEAD)"
test "${actual_revision}" = "$(git rev-parse origin/main)"
git merge-base --is-ancestor "${EXPECTED_SOURCE_REVISION}" "${actual_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
PYTHONPATH=/tmp/hermes-voice-test-deps python3 -m pytest -q \
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 \
--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/atlas/titan-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
)
}
}
}
}
}