pipeline { agent any environment { REGISTRY = 'registry.example.com/docker-images' } stages { stage('Detect changed images') { steps { script { def base = env.CHANGE_TARGET ? "origin/${env.CHANGE_TARGET}" : "HEAD~1" // returnStdout gives a String. Trim, split, and FORCE to a List. def diffOutput = sh( script: "git diff --name-only ${base} HEAD || true", returnStdout: true ).trim() // 'as List' avoids the non-serializable String[] array def changedFiles = diffOutput ? (diffOutput.split('\n') as List) : [] // Build a plain List of unique top-level dirs (Strings only) def candidates = [] for (String f : changedFiles) { def top = f.split('/')[0] if (top && !candidates.contains(top)) { candidates.add(top) } } // fileExists is a pipeline STEP, so it must stay in CPS land // (i.e. a normal for-loop, NOT inside @NonCPS or a findAll closure) def toBuild = [] for (String dir : candidates) { if (fileExists("${dir}/Dockerfile")) { toBuild.add(dir) } } // Store only a plain String in env env.IMAGES_TO_BUILD = toBuild.join(',') echo "Images to build: ${env.IMAGES_TO_BUILD ?: '(none)'}" } } } stage('Build & push') { when { expression { env.IMAGES_TO_BUILD } } steps { script { def shortSha = env.GIT_COMMIT.take(8) env.IMAGES_TO_BUILD.split(',').each { img -> def tag = "${REGISTRY}/${img}" sh """ docker build \ --label org.opencontainers.image.source=${env.GIT_URL} \ --label org.opencontainers.image.revision=${env.GIT_COMMIT} \ -t ${tag}:${shortSha} \ ${img}/ """ if (env.BRANCH_NAME == 'main') { sh """ docker tag ${tag}:${shortSha} ${tag}:latest docker push ${tag}:${shortSha} docker push ${tag}:latest """ } } } } } } }