// ============================================ // AIOT Platform - Jenkins Pipeline (Optimized) // 修复序列化问?+ 并行构建 + 优化部署 // ============================================ pipeline { agent any options { buildDiscarder(logRotator(numToKeepStr: '10')) disableConcurrentBuilds() timeout(time: 60, unit: 'MINUTES') timestamps() } environment { // 镜像仓库配置 REGISTRY = 'localhost:5000' IMAGE_TAG = "${env.BRANCH_NAME}-${env.BUILD_NUMBER}-${env.GIT_COMMIT?.take(8)}" DEPS_IMAGE = "${REGISTRY}/aiot-deps:latest" // 服务配置 CORE_SERVICES = 'viewsh-gateway,viewsh-module-system-server,viewsh-module-infra-server,viewsh-module-iot-server,viewsh-module-iot-gateway' // 部署配置 DEPLOY_HOST = '172.19.0.1' DEPLOY_PATH = '/opt/aiot-platform-cloud' SSH_KEY = '/var/jenkins_home/.ssh/id_rsa' // 性能配置 MAX_PARALLEL_BUILDS = 2 HEALTH_CHECK_TIMEOUT = 120 // ? HEALTH_CHECK_INTERVAL = 5 // ? } stages { stage('Checkout') { steps { checkout scm script { env.GIT_COMMIT_MSG = sh(script: 'git log -1 --pretty=%B', returnStdout: true).trim() echo "📦 Commit: ${env.GIT_COMMIT?.take(8)} - ${env.GIT_COMMIT_MSG}" } } } stage('Detect Changes') { steps { script { env.SERVICES_TO_BUILD = detectChangedServices() env.DEPS_CHANGED = checkDepsChanged() if (env.SERVICES_TO_BUILD.isEmpty()) { echo "⏭️ No changes detected, skipping build" currentBuild.result = 'SUCCESS' error("No changes") } echo "🔄 Services to build: ${env.SERVICES_TO_BUILD}" echo "📦 Deps changed: ${env.DEPS_CHANGED}" } } } stage('Build Dependencies Image') { when { expression { env.DEPS_CHANGED == 'true' || !depsImageExists() } } steps { script { echo "📦 Building dependencies base image..." sh """ docker build \\ -f docker/Dockerfile.deps \\ -t ${DEPS_IMAGE} \\ . docker push ${DEPS_IMAGE} """ echo "?Dependencies image built and pushed" } } } stage('Build Services') { when { expression { env.SERVICES_TO_BUILD != '' } } steps { script { def servicesToBuild = env.SERVICES_TO_BUILD.split(',') echo "🔨 Building ${servicesToBuild.size()} services in parallel (max ${MAX_PARALLEL_BUILDS})" // 并行构建服务 def buildTasks = [:] servicesToBuild.each { service -> buildTasks[service] = { buildService(service) } } // 限制并发? parallel buildTasks echo "?All services built successfully" // 清理旧镜? sh "docker image prune -f || true" } } } stage('Deploy') { when { allOf { expression { env.SERVICES_TO_BUILD != '' } branch 'master' } } steps { script { def servicesToDeploy = env.SERVICES_TO_BUILD.split(',') // 按依赖顺序排? def sortedServices = sortServicesByDependency(servicesToDeploy) echo "🚀 Deploying ${sortedServices.size()} services in order" // 串行部署(保证依赖关系) sortedServices.each { service -> deployService(service) } echo "🚀 All services deployed successfully!" } } } stage('Final Health Check') { when { allOf { expression { env.SERVICES_TO_BUILD != '' } branch 'master' } } steps { script { echo "🏥 Running final health check..." def servicesToCheck = env.SERVICES_TO_BUILD.split(',') def healthCheckTasks = [:] servicesToCheck.each { service -> def containerName = getContainerNameForService(service) healthCheckTasks[service] = { checkServiceHealth(containerName, service) } } // 并行健康检? parallel healthCheckTasks echo "?All services are healthy" } } } } post { success { echo """ ?构建成功 📦 Services: ${env.SERVICES_TO_BUILD} 🏷? Tag: ${IMAGE_TAG} """ } failure { echo "?构建失败,请检查日? } always { sh 'df -h | grep -E "/$|/var" || true' sh 'docker system df || true' } } } // ============================================ // 辅助函数(使?@NonCPS 避免序列化问题) // ============================================ @NonCPS def detectChangedServices() { def changedFiles = sh( script: ''' PREV=$(git rev-parse HEAD~1 2>/dev/null || echo "") [ -z "$PREV" ] && echo "all" || git diff --name-only $PREV HEAD ''', returnStdout: true ).trim() if (changedFiles == 'all' || changedFiles.isEmpty()) { return env.CORE_SERVICES } // 触发全量构建的文? def triggerAll = ['pom.xml', 'viewsh-framework', 'viewsh-dependencies', 'Jenkinsfile', 'docker/'] if (triggerAll.any { changedFiles.contains(it) }) { return env.CORE_SERVICES } // 检测变更的服务 def changedServices = [] def allServices = env.CORE_SERVICES.split(',') allServices.each { service -> def modulePath = getModulePathForService(service) def moduleDir = modulePath.split('/')[0] if (changedFiles.contains(moduleDir)) { changedServices.add(service) } } return changedServices.isEmpty() ? env.CORE_SERVICES : changedServices.join(',') } @NonCPS def checkDepsChanged() { def changedFiles = sh( script: ''' PREV=$(git rev-parse HEAD~1 2>/dev/null || echo "") [ -z "$PREV" ] && echo "all" || git diff --name-only $PREV HEAD ''', returnStdout: true ).trim() if (changedFiles == 'all') { return 'true' } def depsFiles = ['pom.xml', 'viewsh-dependencies', 'viewsh-framework'] return depsFiles.any { changedFiles.contains(it) } ? 'true' : 'false' } @NonCPS def depsImageExists() { def result = sh( script: "docker manifest inspect ${env.DEPS_IMAGE} > /dev/null 2>&1", returnStatus: true ) return result == 0 } // 构建单个服务 def buildService(String service) { def modulePath = getModulePathForService(service) echo "🔨 Building ${service}..." sh """ docker build \\ -f docker/Dockerfile.service \\ --build-arg DEPS_IMAGE=${DEPS_IMAGE} \\ --build-arg MODULE_NAME=${modulePath} \\ --build-arg JAR_NAME=${service} \\ --build-arg SKIP_TESTS=true \\ -t ${REGISTRY}/${service}:${IMAGE_TAG} \\ -t ${REGISTRY}/${service}:latest \\ . docker push ${REGISTRY}/${service}:${IMAGE_TAG} docker push ${REGISTRY}/${service}:latest """ echo "?${service} built and pushed" } // 部署单个服务 def deployService(String service) { echo "🚀 Deploying ${service}..." def containerName = getContainerNameForService(service) def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${SSH_KEY}" // 部署服务 sh """ ssh ${sshOpts} root@${DEPLOY_HOST} ' cd ${DEPLOY_PATH} echo "Pulling ${service}..." docker compose -f docker-compose.core.yml pull ${service} echo "Starting ${service}..." docker compose -f docker-compose.core.yml up -d ${service} ' """ // 等待服务健康 waitForServiceHealthy(containerName, service, sshOpts) echo "?${service} deployed successfully" } // 等待服务健康 def waitForServiceHealthy(String containerName, String serviceName, String sshOpts) { def maxAttempts = env.HEALTH_CHECK_TIMEOUT.toInteger() / env.HEALTH_CHECK_INTERVAL.toInteger() sh """ ssh ${sshOpts} root@${DEPLOY_HOST} ' echo "Waiting for ${serviceName} to be healthy..." for i in \$(seq 1 ${maxAttempts}); do STATUS=\$(docker inspect --format="{{.State.Health.Status}}" ${containerName} 2>/dev/null || echo "starting") if [ "\$STATUS" = "healthy" ]; then echo "?${serviceName} is healthy" exit 0 elif [ "\$STATUS" = "unhealthy" ]; then echo "?${serviceName} is unhealthy" echo "=== Last 100 lines of logs ===" docker logs --tail 100 ${containerName} exit 1 fi ELAPSED=\$((i * ${HEALTH_CHECK_INTERVAL})) echo "?${serviceName} status: \$STATUS (\${ELAPSED}s/${HEALTH_CHECK_TIMEOUT}s)" sleep ${HEALTH_CHECK_INTERVAL} done echo "?${serviceName} health check timeout after ${HEALTH_CHECK_TIMEOUT}s" echo "=== Full logs ===" docker logs ${containerName} exit 1 ' """ } // 检查服务健康状? def checkServiceHealth(String containerName, String serviceName) { def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${SSH_KEY}" sh """ ssh ${sshOpts} root@${DEPLOY_HOST} ' echo "Checking ${serviceName}..." STATUS=\$(docker inspect --format="{{.State.Health.Status}}" ${containerName} 2>/dev/null || echo "not_found") if [ "\$STATUS" = "healthy" ]; then echo "?${serviceName} is healthy" elif [ "\$STATUS" = "not_found" ]; then echo "⚠️ ${serviceName} not found (may not be deployed)" else echo "?${serviceName} is \$STATUS" docker logs --tail 50 ${containerName} exit 1 fi ' """ } // 按依赖顺序排序服? @NonCPS def sortServicesByDependency(def services) { def deployOrder = [ 'viewsh-gateway', 'viewsh-module-system-server', 'viewsh-module-infra-server', 'viewsh-module-iot-server', 'viewsh-module-iot-gateway' ] return services.sort { a, b -> deployOrder.indexOf(a) <=> deployOrder.indexOf(b) } } // 获取服务对应的容器名? @NonCPS def getContainerNameForService(String service) { switch(service) { case 'viewsh-gateway': return 'aiot-gateway' case 'viewsh-module-system-server': return 'aiot-system-server' case 'viewsh-module-infra-server': return 'aiot-infra-server' case 'viewsh-module-iot-server': return 'aiot-iot-server' case 'viewsh-module-iot-gateway': return 'aiot-iot-gateway' default: return "aiot-${service}" } } // 获取服务对应的模块路? @NonCPS def getModulePathForService(String service) { switch(service) { case 'viewsh-gateway': return 'viewsh-gateway' case 'viewsh-module-system-server': return 'viewsh-module-system/viewsh-module-system-server' case 'viewsh-module-infra-server': return 'viewsh-module-infra/viewsh-module-infra-server' case 'viewsh-module-iot-server': return 'viewsh-module-iot/viewsh-module-iot-server' case 'viewsh-module-iot-gateway': return 'viewsh-module-iot/viewsh-module-iot-gateway' default: return service } }