@NonCPS函数不能调用Pipeline步骤(sh, echo等) 移除以下函数的@NonCPS: - detectChangedServices (调用sh) - checkDepsChanged (调用sh) - depsImageExists (调用sh) - sortServicesByDependency (避免序列化问题) 保留纯函数的@NonCPS: - getContainerNameForService - getModulePathForService
410 lines
13 KiB
Groovy
410 lines
13 KiB
Groovy
// ============================================
|
||
// AIOT Platform - Jenkins Pipeline (Optimized)
|
||
// ä¿®å¤<C3A5>åº<C3A5>列化问é¢?+ 并行构建 + 优化部署
|
||
// ============================================
|
||
|
||
pipeline {
|
||
agent any
|
||
|
||
options {
|
||
buildDiscarder(logRotator(numToKeepStr: '10'))
|
||
disableConcurrentBuilds()
|
||
timeout(time: 60, unit: 'MINUTES')
|
||
timestamps()
|
||
}
|
||
|
||
environment {
|
||
// 镜åƒ<C3A5>仓库é…<C3A9>ç½®
|
||
REGISTRY = 'localhost:5000'
|
||
IMAGE_TAG = "${env.BRANCH_NAME}-${env.BUILD_NUMBER}-${env.GIT_COMMIT?.take(8)}"
|
||
DEPS_IMAGE = "${REGISTRY}/aiot-deps:latest"
|
||
|
||
// æœ<C3A6>务é…<C3A9>ç½®
|
||
CORE_SERVICES = 'viewsh-gateway,viewsh-module-system-server,viewsh-module-infra-server,viewsh-module-iot-server,viewsh-module-iot-gateway'
|
||
|
||
// 部署é…<C3A9>ç½®
|
||
DEPLOY_HOST = '172.19.0.1'
|
||
DEPLOY_PATH = '/opt/aiot-platform-cloud'
|
||
SSH_KEY = '/var/jenkins_home/.ssh/id_rsa'
|
||
|
||
// 性能é…<C3A9>ç½®
|
||
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 "â<>ï¸<C3AF> 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})"
|
||
|
||
// 并行构建æœ<C3A6>务
|
||
def buildTasks = [:]
|
||
servicesToBuild.each { service ->
|
||
buildTasks[service] = {
|
||
buildService(service)
|
||
}
|
||
}
|
||
|
||
// é™<C3A9>制并å<C2B6>‘æ•?
|
||
parallel buildTasks
|
||
|
||
echo "�All services built successfully"
|
||
|
||
// 清ç<E280A6>†æ—§é•œåƒ?
|
||
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(',')
|
||
|
||
// 按ä¾<C3A4>赖顺åº<C3A5>排åº?
|
||
def sortedServices = sortServicesByDependency(servicesToDeploy)
|
||
|
||
echo "🚀 Deploying ${sortedServices.size()} services in order"
|
||
|
||
// 串行部署(ä¿<C3A4>è¯<C3A8>ä¾<C3A4>赖关系)
|
||
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 "ðŸ<C3B0>¥ 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)
|
||
}
|
||
}
|
||
|
||
// 并行å<C592>¥åº·æ£€æŸ?
|
||
parallel healthCheckTasks
|
||
|
||
echo "�All services are healthy"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
post {
|
||
success {
|
||
echo """
|
||
âœ?构建æˆ<C3A6>功
|
||
📦 Services: ${env.SERVICES_TO_BUILD}
|
||
ðŸ<C3B0>·ï¸? Tag: ${IMAGE_TAG}
|
||
"""
|
||
}
|
||
failure {
|
||
echo "â<>?构建失败,请检查日å¿?
|
||
}
|
||
always {
|
||
sh 'df -h | grep -E "/$|/var" || true'
|
||
sh 'docker system df || true'
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// 辅助函数(使ç”?@NonCPS é<>¿å…<C3A5>åº<C3A5>列化问题)
|
||
// ============================================
|
||
|
||
@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
|
||
}
|
||
|
||
// 触å<C2A6>‘å…¨é‡<C3A9>构建的文ä»?
|
||
def triggerAll = ['pom.xml', 'viewsh-framework', 'viewsh-dependencies', 'Jenkinsfile', 'docker/']
|
||
if (triggerAll.any { changedFiles.contains(it) }) {
|
||
return env.CORE_SERVICES
|
||
}
|
||
|
||
// 检测å<E280B9>˜æ›´çš„æœ<C3A6>务
|
||
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
|
||
}
|
||
|
||
// 构建å<C2BA>•个æœ<C3A6>务
|
||
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"
|
||
}
|
||
|
||
// 部署å<C2B2>•个æœ<C3A6>务
|
||
def deployService(String service) {
|
||
echo "🚀 Deploying ${service}..."
|
||
|
||
def containerName = getContainerNameForService(service)
|
||
def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${SSH_KEY}"
|
||
|
||
// 部署æœ<C3A6>务
|
||
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}
|
||
'
|
||
"""
|
||
|
||
// ç‰å¾…æœ<C3A6>务å<C2A1>¥åº·
|
||
waitForServiceHealthy(containerName, service, sshOpts)
|
||
|
||
echo "�${service} deployed successfully"
|
||
}
|
||
|
||
// ç‰å¾…æœ<C3A6>务å<C2A1>¥åº·
|
||
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
|
||
'
|
||
"""
|
||
}
|
||
|
||
// 检查æœ<C3A6>务å<C2A1>¥åº·çжæ€?
|
||
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 "âš ï¸<C3AF> ${serviceName} not found (may not be deployed)"
|
||
else
|
||
echo "â<>?${serviceName} is \$STATUS"
|
||
docker logs --tail 50 ${containerName}
|
||
exit 1
|
||
fi
|
||
'
|
||
"""
|
||
}
|
||
|
||
// 按ä¾<C3A4>赖顺åº<C3A5>排åº<C3A5>æœ<C3A6>åŠ?
|
||
@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)
|
||
}
|
||
}
|
||
|
||
// 获å<C2B7>–æœ<C3A6>务对应的容器å<C2A8><C3A5>ç§?
|
||
@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}"
|
||
}
|
||
}
|
||
|
||
// 获å<C2B7>–æœ<C3A6>务对应的模å<C2A1>—è·¯å¾?
|
||
@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
|
||
}
|
||
}
|