// Build -> registry -> deploy, with nothing written back into this repository: Jenkins builds the image with // rootless BuildKit (no privileged docker-in-docker) and pushes it to Harbor as : and :latest, using the // project's push-only robot account. Argo CD Image Updater (ImageUpdater hello-world in namespace argocd) sees the // new : tag in Harbor and points the Argo CD application at it. No commit back means no webhook loop, so // no [skip ci] commits and no extra builds. pipeline { agent { kubernetes { label 'buildkit-agent' yaml """ apiVersion: v1 kind: Pod spec: serviceAccountName: jenkins containers: - name: buildkit image: moby/buildkit:v0.33.1-rootless command: ['sleep'] args: ['99d'] env: # Rootless BuildKit inside an unprivileged pod: no process sandbox of its own (the pod is the sandbox). - name: BUILDKITD_FLAGS value: --oci-worker-no-process-sandbox # buildctl and buildkitd are Go: SSL_CERT_DIR adds the internal CA (/certs) to both trust stores, beside the # image's own bundle -- the push talks TLS to Harbor's registry and to its token endpoint. - name: SSL_CERT_DIR value: /etc/ssl/certs:/certs - name: HTTP_PROXY value: http://10.0.10.10:3128 - name: HTTPS_PROXY value: http://10.0.10.10:3128 - name: NO_PROXY value: localhost,127.0.0.1,10.0.0.0/8,.cluster.local,.svc,.demo-06-10-2026.app.ariki.ai - name: http_proxy value: http://10.0.10.10:3128 - name: https_proxy value: http://10.0.10.10:3128 - name: no_proxy value: localhost,127.0.0.1,10.0.0.0/8,.cluster.local,.svc,.demo-06-10-2026.app.ariki.ai securityContext: runAsUser: 1000 runAsGroup: 1000 seccompProfile: type: Unconfined appArmorProfile: type: Unconfined volumeMounts: # buildkitd.toml: trust the internal CA for harbor.demo-06-10-2026.app.ariki.ai (ConfigMap hello-world-buildkitd) - name: buildkitd-config mountPath: /home/user/.config/buildkit - name: internal-root-ca mountPath: /certs readOnly: true # Harbor push credentials: the push-only robot (ExternalSecret hello-world-harbor-push) - name: harbor-push mountPath: /home/user/.docker readOnly: true - name: buildkit-state mountPath: /home/user/.local/share/buildkit volumes: - name: buildkitd-config configMap: name: hello-world-buildkitd - name: internal-root-ca secret: secretName: internal-root-ca - name: harbor-push secret: secretName: hello-world-harbor-push items: - key: .dockerconfigjson path: config.json - name: buildkit-state emptyDir: {} """ } } triggers { GenericTrigger( genericVariables: [ [key: 'ref', value: '$.ref'], [key: 'commit_message', value: '$.head_commit.message'] ], token: 'gitea-webhook' ) } environment { HARBOR_HOST = "harbor.demo-06-10-2026.app.ariki.ai" HARBOR_PROJECT = "demo-06-10-2026" IMAGE_NAME = "${HARBOR_HOST}/${HARBOR_PROJECT}/hello-world" } stages { stage('Build and push image') { steps { script { // The commit, not BUILD_NUMBER: unique even if the job is recreated, and it names the source // the image was built from. The ImageUpdater only accepts tags of this shape. env.IMAGE_TAG = env.GIT_COMMIT.take(12) currentBuild.description = env.IMAGE_TAG } container('buildkit') { // buildkitd.toml gives the daemon (blob push) the internal CA; the registry token is fetched by // buildctl itself, which needs it too (--registry-auth-tlscontext), or the push fails with // "failed to fetch oauth token ... x509: certificate signed by unknown authority". sh ''' buildctl-daemonless.sh build \ --frontend dockerfile.v0 \ --local context=. \ --local dockerfile=. \ --registry-auth-tlscontext "host=${HARBOR_HOST},ca=/certs/ca.crt" \ --output "type=image,\\"name=${IMAGE_NAME}:${IMAGE_TAG},${IMAGE_NAME}:latest\\",push=true" ''' } } } } post { success { echo "Pushed ${IMAGE_NAME}:${IMAGE_TAG}; Argo CD Image Updater deploys it to https://hello-world.demo-06-10-2026.app.ariki.ai" } failure { echo "Pipeline failed at: ${env.STAGE_NAME}" } } }