- 修复健康检查 IP 地址 (172.17.16.14 -> localhost) - 禁用微信自动配置避免 appid 错误 - 添加服务依赖关系和启动顺序 - 优化 Jenkinsfile 部署流程 - 添加 Quartz 优雅关闭配置 - 注释 XXL-JOB Admin 配置(暂不部署)
353 lines
12 KiB
Groovy
353 lines
12 KiB
Groovy
// ============================================
|
|
// AIOT Platform - Jenkins Pipeline
|
|
// 并行构建 + Maven 依赖缓存优化
|
|
// ============================================
|
|
|
|
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'
|
|
MAX_PARALLEL = 2 // 最大并行构建数
|
|
}
|
|
|
|
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 services = env.SERVICES_TO_BUILD.split(',') as List
|
|
def batchSize = MAX_PARALLEL.toInteger()
|
|
|
|
echo "🔨 Building ${services.size()} services with parallelism=${batchSize}"
|
|
|
|
// 分批并行构建
|
|
services.collate(batchSize).each { batch ->
|
|
echo "📦 Building batch: ${batch.join(', ')}"
|
|
|
|
def parallelBuilds = [:]
|
|
batch.each { service ->
|
|
parallelBuilds[service] = {
|
|
buildAndPush(service)
|
|
}
|
|
}
|
|
|
|
parallel parallelBuilds
|
|
}
|
|
|
|
// 清理
|
|
sh "docker image prune -f || true"
|
|
}
|
|
}
|
|
}
|
|
|
|
stage('Deploy') {
|
|
when {
|
|
allOf {
|
|
expression { env.SERVICES_TO_BUILD != '' }
|
|
branch 'master'
|
|
}
|
|
}
|
|
steps {
|
|
script {
|
|
def services = env.SERVICES_TO_BUILD.split(',') as List
|
|
|
|
// 按依赖顺序排序
|
|
def deployOrder = [
|
|
'viewsh-gateway',
|
|
'viewsh-module-system-server',
|
|
'viewsh-module-infra-server',
|
|
'viewsh-module-iot-server',
|
|
'viewsh-module-iot-gateway'
|
|
]
|
|
|
|
def sortedServices = services.sort { a, b ->
|
|
deployOrder.indexOf(a) <=> deployOrder.indexOf(b)
|
|
}
|
|
|
|
// 串行部署(保证依赖关系)
|
|
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 {
|
|
// 验证所有核心服务健康
|
|
def coreContainers = ['aiot-gateway', 'aiot-system-server', 'aiot-infra-server', 'aiot-iot-server', 'aiot-iot-gateway']
|
|
|
|
coreContainers.each { container ->
|
|
sh """
|
|
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@${DEPLOY_HOST} '
|
|
echo "Checking ${container}..."
|
|
STATUS=\$(docker inspect --format="{{.State.Health.Status}}" ${container} 2>/dev/null || echo "not_found")
|
|
if [ "\$STATUS" = "healthy" ]; then
|
|
echo "✅ ${container} is healthy"
|
|
elif [ "\$STATUS" = "not_found" ]; then
|
|
echo "⚠️ ${container} not found (may not be deployed)"
|
|
else
|
|
echo "❌ ${container} is \$STATUS"
|
|
docker logs --tail 50 ${container}
|
|
exit 1
|
|
fi
|
|
'
|
|
"""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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'
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// 辅助函数
|
|
// ============================================
|
|
|
|
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 services = []
|
|
env.CORE_SERVICES.split(',').each { service ->
|
|
def path = getModulePath(service).split('/')[0]
|
|
if (changedFiles.contains(path)) {
|
|
services.add(service)
|
|
}
|
|
}
|
|
|
|
return services.isEmpty() ? env.CORE_SERVICES : services.join(',')
|
|
}
|
|
|
|
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()
|
|
|
|
// 如果 pom.xml 或基础模块变化,需要重建依赖镜像
|
|
def depsFiles = ['pom.xml', 'viewsh-dependencies', 'viewsh-framework']
|
|
|
|
if (changedFiles == 'all') {
|
|
return 'true'
|
|
}
|
|
|
|
return depsFiles.any { changedFiles.contains(it) } ? 'true' : 'false'
|
|
}
|
|
|
|
def depsImageExists() {
|
|
def result = sh(
|
|
script: "docker manifest inspect ${env.DEPS_IMAGE} > /dev/null 2>&1",
|
|
returnStatus: true
|
|
)
|
|
return result == 0
|
|
}
|
|
|
|
def buildAndPush(String service) {
|
|
def modulePath = getModulePath(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}..."
|
|
|
|
// SSH 诊断
|
|
sh """
|
|
echo "=== SSH Diagnostic ==="
|
|
whoami
|
|
ls -la /var/jenkins_home/.ssh/
|
|
ssh -V
|
|
"""
|
|
|
|
// 使用明确的 SSH 密钥路径
|
|
def sshKey = '/var/jenkins_home/.ssh/id_rsa'
|
|
def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${sshKey}"
|
|
|
|
sh """
|
|
ssh ${sshOpts} root@${DEPLOY_HOST} '
|
|
cd ${DEPLOY_PATH}
|
|
docker compose -f docker-compose.core.yml pull ${service}
|
|
docker compose -f docker-compose.core.yml up -d ${service}
|
|
'
|
|
"""
|
|
|
|
// 健康检查(增加超时和更好的错误处理)
|
|
def containerName = getContainerName(service)
|
|
sh """
|
|
ssh ${sshOpts} root@${DEPLOY_HOST} '
|
|
echo "Waiting for ${service} to be healthy..."
|
|
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
|
|
STATUS=\$(docker inspect --format="{{.State.Health.Status}}" ${containerName} 2>/dev/null || echo "starting")
|
|
|
|
if [ "\$STATUS" = "healthy" ]; then
|
|
echo "✅ ${service} is healthy"
|
|
exit 0
|
|
elif [ "\$STATUS" = "unhealthy" ]; then
|
|
echo "❌ ${service} is unhealthy"
|
|
echo "=== Last 100 lines of logs ==="
|
|
docker logs --tail 100 ${containerName}
|
|
exit 1
|
|
fi
|
|
|
|
echo "⏳ ${service} status: \$STATUS (\$((i*10))s/200s)"
|
|
sleep 10
|
|
done
|
|
|
|
echo "❌ ${service} health check timeout after 200s"
|
|
echo "=== Full logs ==="
|
|
docker logs ${containerName}
|
|
exit 1
|
|
'
|
|
"""
|
|
|
|
echo "✅ ${service} deployed successfully"
|
|
}
|
|
|
|
def getContainerName(String service) {
|
|
// 服务名到容器名的映射
|
|
def map = [
|
|
'viewsh-gateway': 'aiot-gateway',
|
|
'viewsh-module-system-server': 'aiot-system-server',
|
|
'viewsh-module-infra-server': 'aiot-infra-server',
|
|
'viewsh-module-iot-server': 'aiot-iot-server',
|
|
'viewsh-module-iot-gateway': 'aiot-iot-gateway'
|
|
]
|
|
return map[service] ?: "aiot-${service}"
|
|
}
|
|
|
|
def getModulePath(String service) {
|
|
def map = [
|
|
'viewsh-gateway': 'viewsh-gateway',
|
|
'viewsh-module-system-server': 'viewsh-module-system/viewsh-module-system-server',
|
|
'viewsh-module-infra-server': 'viewsh-module-infra/viewsh-module-infra-server',
|
|
'viewsh-module-iot-server': 'viewsh-module-iot/viewsh-module-iot-server',
|
|
'viewsh-module-iot-gateway': 'viewsh-module-iot/viewsh-module-iot-gateway'
|
|
]
|
|
return map[service] ?: service
|
|
}
|