/** * UAT: Checkout -> Maven -> (optional) push images to Harbor -> deploy jar + docker restart. * * Jenkins Job: Pipeline script from SCM * Script Path: docs/jenkins/uat/Jenkinsfile * * Harbor (153.68): when PUSH_TO_HARBOR=true, push e.g. * 39.105.153.68/alien_cloud/gateway:uat-latest * Before push: existing uat-latest is archived as uat-build- (Harbor API tag when * permitted; on 403 falls back to docker pull/tag/push). New image is pushed only as uat-latest. * Prod promote: SOURCE_TAG=uat-latest. */ /** Normalize GIT_BRANCH: uat-20260202 (not origin/uat-20260202 or refs/heads/...) */ def normalizeGitBranch(String raw) { def b = (raw ?: 'uat-20260202').trim() if (!b) { return 'uat-20260202' } while (b.startsWith('refs/heads/')) { b = b.substring('refs/heads/'.length()) } while (b.startsWith('origin/')) { b = b.substring('origin/'.length()) } return b ?: 'uat-20260202' } /** HARBOR_PUSH_SCOPE: all-java-services | -only */ def filterHarborPushScope(List allServices, String scope) { def s = (scope ?: 'all-java-services').trim() if (s == 'all-java-services') { return allServices } if (s.endsWith('-only')) { def repo = s.substring(0, s.length() - '-only'.length()) def picked = allServices.findAll { it.repo == repo } if (picked.isEmpty()) { error("Unknown HARBOR_PUSH_SCOPE: ${scope}") } return picked } error("Unknown HARBOR_PUSH_SCOPE: ${scope}") } /** Marker file: docker pull/tag/push archive needed when Harbor API tag is forbidden. */ def harborArchiveDockerFallbackMarker(String repo) { return "/tmp/harbor_archive_docker_fallback_${repo}" } /** Archive current uat-latest as uat-build-N via Harbor API (same digest, no layer re-upload). */ def archiveHarborLatestViaApi(def script, String reg, String proj, String repo, String latestTag, String buildTag) { def fallbackMarker = harborArchiveDockerFallbackMarker(repo) script.sh """ set -e REG='${reg}' PROJ='${proj}' REPO='${repo}' LATEST='${latestTag}' BUILD_TAG='${buildTag}' FALLBACK_MARKER='${fallbackMarker}' rm -f "\${FALLBACK_MARKER}" if ! command -v jq >/dev/null 2>&1; then echo ">>> WARN: jq missing, will docker-fallback archive for \${REPO}" touch "\${FALLBACK_MARKER}" exit 0 fi enc_repo=\$(printf '%s' "\${REPO}" | jq -sRr @uri) if ! curl -fsS -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\ "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts/\${LATEST}" >/dev/null 2>&1; then echo ">>> no prior \${LATEST} for \${REPO}, skip archive" exit 0 fi echo ">>> archive previous \${LATEST} -> \${BUILD_TAG} (\${REPO}, Harbor API)" archive_code=\$(curl -sS -o /tmp/harbor_archive_\${REPO}.json -w '%{http_code}' \\ -X POST -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\ "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts/\${LATEST}/tags" \\ -H 'Content-Type: application/json' \\ -d "{\\"name\\":\\"\${BUILD_TAG}\\"}") case "\${archive_code}" in 201|200) echo ">>> archived \${REPO}:\${BUILD_TAG} (same digest as prior \${LATEST})" ;; 409) echo ">>> WARN: \${REPO}:\${BUILD_TAG} already exists, continue" ;; 403) echo ">>> WARN: Harbor API archive forbidden (403) for \${REPO}, will docker-fallback" cat /tmp/harbor_archive_\${REPO}.json 2>/dev/null || true touch "\${FALLBACK_MARKER}" ;; *) echo ">>> ERROR: Harbor archive \${REPO} HTTP \${archive_code}" cat /tmp/harbor_archive_\${REPO}.json 2>/dev/null || true exit 1 ;; esac """ } def pushOneHarborImage(def script, Map svc, String reg, String proj, String latestTag, String buildTag, String baseImage, String dockerfile, String workspace) { def jarName = "${svc.module}-1.0.0.jar" def imageLatest = "${reg}/${proj}/${svc.repo}:${latestTag}" def imageBuild = "${reg}/${proj}/${svc.repo}:${buildTag}" def fallbackMarker = harborArchiveDockerFallbackMarker(svc.repo) def withLibFlag = svc.withLib ? 'true' : 'false' script.sh """ set -e test -f ${workspace}/${svc.module}/target/${jarName} cd ${workspace}/${svc.module} rm -rf .jenkins_docker_ctx && mkdir -p .jenkins_docker_ctx/lib cp -f target/${jarName} .jenkins_docker_ctx/${jarName} if [ "${withLibFlag}" = "true" ] && [ -d target/lib ]; then cp -rf target/lib/. .jenkins_docker_ctx/lib/ else touch .jenkins_docker_ctx/lib/.keep fi """ // Harbor API archive runs outside flock (HTTP only, safe to parallelize). archiveHarborLatestViaApi(script, reg, proj, svc.repo, latestTag, buildTag) // Parallel branches share one Docker daemon; serialize docker build/push to avoid containerd races. script.sh """ set -e DOCKER_LOCK=/tmp/jenkins-alien-cloud-docker.lock flock "\${DOCKER_LOCK}" sh -c ' set -e FALLBACK_MARKER="${fallbackMarker}" if [ -f "\${FALLBACK_MARKER}" ]; then echo ">>> docker-fallback archive ${svc.repo}:${buildTag} from ${latestTag}" if docker pull ${imageLatest}; then docker tag ${imageLatest} ${imageBuild} docker push ${imageBuild} docker rmi ${imageBuild} 2>/dev/null || true echo ">>> archived ${imageBuild} via docker" else echo ">>> WARN: docker pull ${imageLatest} failed, skip archive" fi rm -f "\${FALLBACK_MARKER}" fi cd ${workspace}/${svc.module}/.jenkins_docker_ctx build_ok=0 for attempt in 1 2 3; do if docker build -f ${workspace}/${dockerfile} \\ --build-arg BASE_IMAGE=${baseImage} \\ --build-arg JAR_FILE=${jarName} \\ --build-arg SERVER_PORT=${svc.port} \\ --build-arg WITH_LIB=${svc.withLib} \\ -t ${imageLatest} .; then build_ok=1 break fi echo ">>> WARN: docker build ${svc.repo} attempt \${attempt} failed, retrying..." sleep "\$((attempt * 5))" done if [ "\${build_ok}" -ne 1 ]; then echo ">>> ERROR: docker build ${svc.repo} failed after 3 attempts" exit 1 fi docker push ${imageLatest} echo ">>> pushed ${imageLatest} (archived prior latest as ${buildTag} if any)" docker rmi ${imageLatest} 2>/dev/null || true ' cd ${workspace}/${svc.module} rm -rf .jenkins_docker_ctx """ } def deployOneUatService(def script, String moduleName, String dirName, String workspace) { def sourceJar = "${workspace}/${moduleName}/target/${moduleName}-1.0.0.jar" def sourceLib = "${workspace}/${moduleName}/target/lib" def targetDir = "/app_deploy_uat/${dirName}" script.sh """ set -e echo ">>> Deploy module: ${moduleName}" if [ -f "${sourceJar}" ]; then mkdir -p "${targetDir}" if [ -d "${sourceLib}" ]; then rm -rf "${targetDir}/lib" cp -rf "${sourceLib}" "${targetDir}" fi cp -f "${sourceJar}" "${targetDir}/" if docker ps -a --format '{{.Names}}' | grep -wq "${dirName}"; then docker restart "${dirName}" echo ">>> [${dirName}] restarted" else echo ">>> [${dirName}] container missing, jar copied only" fi else echo ">>> [${dirName}] jar missing, skip" fi """ } /** Delete oldest uat-build-* tags in Harbor, keep newest KEEP. Never deletes uat-latest or current build tag. */ def pruneHarborUatTags(def script, String reg, String proj, List repoNames, int keepCount, String tagPrefix, String currentBuildTag, String latestTag) { if (repoNames == null || repoNames.isEmpty() || keepCount < 1) { return } def repos = repoNames.join(' ') // POSIX sh only (Jenkins sh step); no mapfile / process substitution script.sh """ set -e REG='${reg}' PROJ='${proj}' KEEP=${keepCount} PREFIX='${tagPrefix}' CURRENT='${currentBuildTag}' LATEST='${latestTag}' if ! command -v jq >/dev/null 2>&1; then echo '>>> Harbor prune skipped: jq not installed on Jenkins agent' exit 0 fi for repo in ${repos}; do enc_repo=\$(printf '%s' "\${repo}" | jq -sRr @uri) tags_file=\$(mktemp) curl -fsS -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\ "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts?page_size=100" \\ | jq -r '.[] | .tags[]? | .name' | grep "^\${PREFIX}" | sort -t- -k3 -n > "\${tags_file}" || true count=\$(wc -l < "\${tags_file}" | tr -d ' ') echo ">>> prune \${repo}: \${count} tag(s) matching \${PREFIX}*" if [ "\${count}" -le "\${KEEP}" ]; then rm -f "\${tags_file}" continue fi del_count=\$((count - KEEP)) deleted=0 while IFS= read -r t; do if [ -z "\${t}" ]; then continue fi if [ "\${t}" = "\${CURRENT}" ] || [ "\${t}" = "\${LATEST}" ]; then continue fi if [ "\${deleted}" -ge "\${del_count}" ]; then break fi echo ">>> DELETE Harbor tag \${repo}:\${t}" if ! curl -fsS -X DELETE -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\ "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts/\${t}/tags/\${t}"; then echo ">>> WARN: delete failed \${repo}:\${t} (check robot delete permission)" fi deleted=\$((deleted + 1)) done < "\${tags_file}" rm -f "\${tags_file}" done """ } pipeline { agent any options { buildDiscarder(logRotator(numToKeepStr: '5')) disableConcurrentBuilds() timestamps() timeout(time: 90, unit: 'MINUTES') } parameters { string( name: 'GIT_BRANCH', defaultValue: 'uat-20260202', trim: true, description: 'Git branch name only (e.g. uat-20260202). Do not prefix origin/' ) booleanParam(name: 'FORCE_UPDATE', defaultValue: false, description: 'mvn -U (routine builds leave unchecked for speed)') booleanParam(name: 'ALLOW_SNAPSHOTS', defaultValue: true, description: 'allow SNAPSHOT deps') booleanParam( name: 'PUSH_TO_HARBOR', defaultValue: true, description: 'After Maven: docker build + push to Harbor (tags uat-latest and uat-build-). Uncheck for jar-only UAT deploy.' ) choice( name: 'HARBOR_PUSH_SCOPE', choices: [ 'all-java-services', 'gateway-only', 'store-only', 'second-only', 'store-platform-only', 'lawyer-only', 'job-only', 'dining-only', ], description: 'Only when PUSH_TO_HARBOR=true; default=all seven; *-only=one service' ) string(name: 'HARBOR_REGISTRY', defaultValue: '39.105.153.68', trim: true) string(name: 'HARBOR_PROJECT', defaultValue: 'alien_cloud', trim: true) booleanParam( name: 'HARBOR_PRUNE_OLD_TAGS', defaultValue: true, description: 'After push: delete old uat-build-* tags in Harbor, keep last N per repo (never deletes uat-latest)' ) string(name: 'HARBOR_KEEP_TAG_COUNT', defaultValue: '10', trim: true, description: 'How many uat-build-* tags to keep per repository') booleanParam( name: 'HARBOR_PUSH_PARALLEL', defaultValue: true, description: 'Parallel per service (context prep + Harbor API archive); docker build/push serialized via flock' ) } environment { MAVEN_HOME = tool '3.6.3' PATH = "${MAVEN_HOME}/bin:${env.PATH}" GIT_URL = 'http://8.152.195.41:3000/alien/alien_cloud' GIT_CREDENTIALS = 'zhanghaomimapingzheng' HARBOR_CREDENTIALS = 'harbor-robot-alien' UAT_HARBOR_LATEST_TAG = 'uat-latest' UAT_HARBOR_BUILD_TAG = "uat-build-${env.BUILD_NUMBER}" DOCKERFILE_JAVA = 'docs/jenkins/produ/docker/Dockerfile.java-service' MAVEN_LOCAL_REPO = '/var/jenkins_home/.m2/repository' } stages { stage('Checkout') { steps { script { // Do not use env.GIT_BRANCH - Jenkins SCM plugin injects origin/ and overwrites it. def branch = normalizeGitBranch(params.GIT_BRANCH) if (!branch) { error('GIT_BRANCH is required') } env.UAT_GIT_BRANCH = branch if (params.GIT_BRANCH?.trim() != branch) { echo ">>> GIT_BRANCH normalized: '${params.GIT_BRANCH}' -> '${branch}'" } echo ">>> Checkout branch: ${branch}" sh """ set -e git fetch origin git checkout -B ${branch} origin/${branch} git log -1 --oneline """ } } } stage('Prepare Maven Settings') { steps { script { writeFile file: 'settings.xml', text: """ aliyun-central central Aliyun Central https://maven.aliyun.com/repository/central repo-mix aliyunmaven Aliyun Maven https://maven.aliyun.com/repository/public truedaily false central Maven Central https://repo.maven.apache.org/maven2 truedaily false spring-milestones Spring Milestones https://repo.spring.io/milestone truedaily false spring-snapshots Spring Snapshots https://repo.spring.io/snapshot false truedaily aliyunmaven https://maven.aliyun.com/repository/public true false central https://repo.maven.apache.org/maven2 true false spring-milestones https://repo.spring.io/milestone true false repo-mix """ } } } stage('Maven Build') { steps { script { def updateFlag = params.FORCE_UPDATE ? '-U' : '' retry(2) { sh """ set -e mvn -version unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY all_proxy no_proxy NO_PROXY || true export MAVEN_OPTS="-Xms512m -Xmx2048m -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true" mkdir -p ${MAVEN_LOCAL_REPO} mvn clean package -DskipTests -s settings.xml ${updateFlag} -e \\ -T 1C -Dmaven.artifact.threads=8 \\ -Dmaven.repo.local=${MAVEN_LOCAL_REPO} """ } } } } stage('Push images to Harbor') { when { expression { return params.PUSH_TO_HARBOR == true } } steps { script { def reg = params.HARBOR_REGISTRY.trim() def proj = params.HARBOR_PROJECT.trim() def latestTag = env.UAT_HARBOR_LATEST_TAG def buildTag = env.UAT_HARBOR_BUILD_TAG def baseImage = "${reg}/${proj}/base/openjdk8-ffmpeg:v1" def dockerfile = env.DOCKERFILE_JAVA def allHarborServices = [ [module: 'alien-gateway', repo: 'gateway', port: '8000', withLib: false], [module: 'alien-store', repo: 'store', port: '50014', withLib: true], [module: 'alien-second', repo: 'second', port: '50015', withLib: false], [module: 'alien-store-platform', repo: 'store-platform', port: '50016', withLib: false], [module: 'alien-lawyer', repo: 'lawyer', port: '50017', withLib: true], [module: 'alien-job', repo: 'job', port: '50108', withLib: false], [module: 'alien-dining', repo: 'dining', port: '50019', withLib: false], ] def harborServices = filterHarborPushScope(allHarborServices, params.HARBOR_PUSH_SCOPE) echo ">>> HARBOR_PUSH_SCOPE=${params.HARBOR_PUSH_SCOPE} repos=${harborServices*.repo.join(',')}" withCredentials([usernamePassword( credentialsId: env.HARBOR_CREDENTIALS, usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASS', )]) { sh """ set -e echo "\${HARBOR_PASS}" | docker login ${reg} -u "\${HARBOR_USER}" --password-stdin echo ">>> docker disk before Harbor push:" df -h /var/lib/docker 2>/dev/null || df -h / || true docker image prune -f 2>/dev/null || true """ if (params.HARBOR_PUSH_PARALLEL) { def pushBranches = [:] harborServices.each { svc -> def s = svc pushBranches[s.repo] = { pushOneHarborImage( this, s, reg, proj, latestTag, buildTag, baseImage, dockerfile, env.WORKSPACE, ) } } parallel pushBranches } else { harborServices.each { svc -> pushOneHarborImage( this, svc, reg, proj, latestTag, buildTag, baseImage, dockerfile, env.WORKSPACE, ) } } sh """ echo ">>> docker disk after Harbor push:" df -h /var/lib/docker 2>/dev/null || df -h / || true docker image prune -f 2>/dev/null || true """ if (params.HARBOR_PRUNE_OLD_TAGS == true) { def keepN = (params.HARBOR_KEEP_TAG_COUNT ?: '10').trim() as int catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') { pruneHarborUatTags( this, reg, proj, harborServices*.repo, keepN, 'uat-build-', buildTag, latestTag, ) } } } echo ">>> Harbor latest: ${env.UAT_HARBOR_LATEST_TAG}; archived tag this run: ${env.UAT_HARBOR_BUILD_TAG}" echo ">>> Prod promote: SOURCE_TAG=${env.UAT_HARBOR_LATEST_TAG}" } } } stage('Deploy Services') { steps { script { def services = [ 'alien-gateway:gateway-uat', 'alien-job:job-uat', 'alien-lawyer:lawyer-uat', 'alien-second:second-uat', 'alien-store:store-uat', 'alien-dining:dining-uat', 'alien-store-platform:store-platform-uat', ] def deployBranches = [:] services.each { item -> def parts = item.split(':') def moduleName = parts[0] def dirName = parts[1] deployBranches[dirName] = { deployOneUatService(this, moduleName, dirName, env.WORKSPACE) } } parallel deployBranches } } } } post { always { sh 'rm -f settings.xml || true' script { if (!params.PUSH_TO_HARBOR) { echo '>>> Harbor push SKIPPED: PUSH_TO_HARBOR is false. On "Build with Parameters" check PUSH_TO_HARBOR.' } } } success { script { if (params.PUSH_TO_HARBOR) { echo ">>> Harbor latest: ${env.UAT_HARBOR_LATEST_TAG}" echo ">>> Prod promote: SOURCE_TAG=${env.UAT_HARBOR_LATEST_TAG}" } } } } }