2026-01-13 09:49:19 +08:00
|
|
|
|
// ============================================
|
2026-01-14 09:13:48 +08:00
|
|
|
|
// AIOT Platform - Jenkins Pipeline (Optimized Edition)
|
2026-01-13 09:49:19 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
|
|
pipeline {
|
|
|
|
|
|
agent any
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-13 09:49:19 +08:00
|
|
|
|
options {
|
2026-01-14 01:16:16 +08:00
|
|
|
|
buildDiscarder(logRotator(
|
|
|
|
|
|
numToKeepStr: '10',
|
|
|
|
|
|
artifactNumToKeepStr: '5'
|
|
|
|
|
|
))
|
2026-01-13 09:49:19 +08:00
|
|
|
|
disableConcurrentBuilds()
|
2026-01-14 01:16:16 +08:00
|
|
|
|
timeout(time: 90, unit: 'MINUTES')
|
2026-01-13 16:05:24 +08:00
|
|
|
|
timestamps()
|
2026-01-14 09:13:48 +08:00
|
|
|
|
retry(1)
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-13 09:49:19 +08:00
|
|
|
|
environment {
|
2026-02-13 10:41:54 +08:00
|
|
|
|
// 镜像仓库配置(Infra 服务器内网地址,Prod 服务器可通过内网拉取)
|
|
|
|
|
|
REGISTRY = '172.17.16.7:5000'
|
2026-01-13 16:05:24 +08:00
|
|
|
|
DEPS_IMAGE = "${REGISTRY}/aiot-deps:latest"
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-14 00:35:32 +08:00
|
|
|
|
// 服务配置
|
2026-01-15 16:30:15 +08:00
|
|
|
|
CORE_SERVICES = 'viewsh-gateway,viewsh-module-system-server,viewsh-module-infra-server,viewsh-module-iot-server,viewsh-module-iot-gateway,viewsh-module-ops-server'
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-02-13 10:41:54 +08:00
|
|
|
|
// 部署配置(Prod 服务器内网地址)
|
|
|
|
|
|
DEPLOY_HOST = '172.17.16.14'
|
2026-01-13 16:05:24 +08:00
|
|
|
|
DEPLOY_PATH = '/opt/aiot-platform-cloud'
|
2026-01-14 00:35:32 +08:00
|
|
|
|
SSH_KEY = '/var/jenkins_home/.ssh/id_rsa'
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
// 性能配置 - 将动态调整
|
|
|
|
|
|
BUILD_TIMEOUT = 45
|
|
|
|
|
|
DEPLOY_TIMEOUT = 10
|
|
|
|
|
|
HEALTH_CHECK_TIMEOUT = 180
|
|
|
|
|
|
HEALTH_CHECK_INTERVAL = 10
|
|
|
|
|
|
|
|
|
|
|
|
// 【优化1】Maven 缓存配置
|
|
|
|
|
|
MAVEN_CACHE_VOLUME = 'jenkins-maven-cache'
|
|
|
|
|
|
MAVEN_OPTS = '-Dmaven.repo.local=/var/jenkins_home/.m2/repository'
|
|
|
|
|
|
|
|
|
|
|
|
// 【优化7】回滚配置
|
|
|
|
|
|
ROLLBACK_ENABLED = 'true'
|
|
|
|
|
|
KEEP_PREVIOUS_IMAGES = 'true'
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-13 09:49:19 +08:00
|
|
|
|
stages {
|
2026-01-14 01:16:16 +08:00
|
|
|
|
stage('Initialize') {
|
|
|
|
|
|
steps {
|
|
|
|
|
|
script {
|
2026-01-14 09:13:48 +08:00
|
|
|
|
// 【优化6】记录开始时间
|
|
|
|
|
|
env.PIPELINE_START_TIME = System.currentTimeMillis()
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
echo "=========================================="
|
2026-01-14 09:13:48 +08:00
|
|
|
|
echo " AIOT Platform - CI/CD Pipeline (Optimized)"
|
2026-01-14 01:16:16 +08:00
|
|
|
|
echo "=========================================="
|
|
|
|
|
|
echo "Branch: ${env.BRANCH_NAME}"
|
|
|
|
|
|
echo "Build: #${env.BUILD_NUMBER}"
|
|
|
|
|
|
echo "Workspace: ${env.WORKSPACE}"
|
2026-01-14 09:13:48 +08:00
|
|
|
|
echo "Start Time: ${new Date()}"
|
2026-01-14 01:16:16 +08:00
|
|
|
|
echo "=========================================="
|
2026-01-14 09:13:48 +08:00
|
|
|
|
|
|
|
|
|
|
// 【优化2】动态检测系统资源
|
|
|
|
|
|
detectSystemResources()
|
2026-01-14 01:16:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 09:49:19 +08:00
|
|
|
|
stage('Checkout') {
|
|
|
|
|
|
steps {
|
|
|
|
|
|
script {
|
2026-01-14 09:13:48 +08:00
|
|
|
|
def stageStartTime = System.currentTimeMillis()
|
|
|
|
|
|
|
|
|
|
|
|
retry(3) {
|
|
|
|
|
|
checkout scm
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
def shortCommit = sh(
|
|
|
|
|
|
script: 'git rev-parse --short HEAD',
|
|
|
|
|
|
returnStdout: true
|
|
|
|
|
|
).trim()
|
|
|
|
|
|
|
2026-01-15 16:55:44 +08:00
|
|
|
|
// 清理分支名:将斜杠替换为连字符,确保 Docker 标签格式有效
|
|
|
|
|
|
def sanitizedBranchName = env.BRANCH_NAME.replaceAll('/', '-')
|
|
|
|
|
|
env.IMAGE_TAG = "${sanitizedBranchName}-${env.BUILD_NUMBER}-${shortCommit}"
|
2026-01-14 09:13:48 +08:00
|
|
|
|
env.PREVIOUS_IMAGE_TAG = getPreviousImageTag()
|
2026-01-14 01:16:16 +08:00
|
|
|
|
env.GIT_COMMIT_MSG = sh(
|
|
|
|
|
|
script: 'git log -1 --pretty=%B',
|
|
|
|
|
|
returnStdout: true
|
|
|
|
|
|
).trim()
|
|
|
|
|
|
|
|
|
|
|
|
echo "📦 Commit: ${shortCommit}"
|
|
|
|
|
|
echo "📝 Message: ${env.GIT_COMMIT_MSG}"
|
2026-01-14 09:13:48 +08:00
|
|
|
|
echo "🏷️ Current Tag: ${env.IMAGE_TAG}"
|
|
|
|
|
|
echo "🔖 Previous Tag: ${env.PREVIOUS_IMAGE_TAG}"
|
|
|
|
|
|
|
|
|
|
|
|
// 【优化6】记录阶段耗时
|
|
|
|
|
|
recordStageMetrics('Checkout', stageStartTime)
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-13 09:49:19 +08:00
|
|
|
|
stage('Detect Changes') {
|
|
|
|
|
|
steps {
|
|
|
|
|
|
script {
|
2026-01-14 09:13:48 +08:00
|
|
|
|
def stageStartTime = System.currentTimeMillis()
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
def changedFiles = getChangedFiles()
|
|
|
|
|
|
echo "📝 Changed files: ${changedFiles.size()} files"
|
|
|
|
|
|
|
|
|
|
|
|
env.SERVICES_TO_BUILD = detectServicesToBuild(changedFiles)
|
|
|
|
|
|
env.DEPS_CHANGED = checkIfDepsChanged(changedFiles)
|
2026-01-14 09:19:22 +08:00
|
|
|
|
|
|
|
|
|
|
// Check if deps image exists
|
|
|
|
|
|
if (env.DEPS_CHANGED == 'false') {
|
|
|
|
|
|
def depsExists = sh(
|
|
|
|
|
|
script: "docker manifest inspect ${env.DEPS_IMAGE} > /dev/null 2>&1",
|
|
|
|
|
|
returnStatus: true
|
|
|
|
|
|
)
|
|
|
|
|
|
if (depsExists != 0) {
|
|
|
|
|
|
env.DEPS_CHANGED = 'true'
|
|
|
|
|
|
echo "📦 Deps image not found, will rebuild"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-13 16:05:24 +08:00
|
|
|
|
if (env.SERVICES_TO_BUILD.isEmpty()) {
|
2026-01-14 00:35:32 +08:00
|
|
|
|
echo "⏭️ No changes detected, skipping build"
|
2026-01-13 09:49:19 +08:00
|
|
|
|
currentBuild.result = 'SUCCESS'
|
2026-01-14 09:13:48 +08:00
|
|
|
|
return
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-13 16:05:24 +08:00
|
|
|
|
echo "🔄 Services to build: ${env.SERVICES_TO_BUILD}"
|
|
|
|
|
|
echo "📦 Deps changed: ${env.DEPS_CHANGED}"
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
|
|
|
|
|
env.SERVICES_TO_BUILD.split(',').each { service ->
|
|
|
|
|
|
def module = getModulePathForService(service)
|
|
|
|
|
|
echo " - ${service} (${module})"
|
|
|
|
|
|
}
|
2026-01-14 09:13:48 +08:00
|
|
|
|
|
|
|
|
|
|
recordStageMetrics('Detect Changes', stageStartTime)
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
|
|
|
|
|
stage('Pre-build Check') {
|
|
|
|
|
|
when {
|
|
|
|
|
|
expression { env.SERVICES_TO_BUILD != '' }
|
|
|
|
|
|
}
|
|
|
|
|
|
steps {
|
|
|
|
|
|
script {
|
2026-01-14 09:13:48 +08:00
|
|
|
|
def stageStartTime = System.currentTimeMillis()
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
echo "🔍 Running pre-build checks..."
|
|
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
// 检查 Docker
|
2026-01-14 01:16:16 +08:00
|
|
|
|
sh "docker version >/dev/null 2>&1 || { echo '❌ Docker not available'; exit 1; }"
|
|
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
// 【优化1】检查并创建 Maven 缓存卷
|
|
|
|
|
|
sh """
|
|
|
|
|
|
if ! docker volume inspect ${env.MAVEN_CACHE_VOLUME} >/dev/null 2>&1; then
|
|
|
|
|
|
echo "📦 Creating Maven cache volume: ${env.MAVEN_CACHE_VOLUME}"
|
|
|
|
|
|
docker volume create ${env.MAVEN_CACHE_VOLUME}
|
|
|
|
|
|
else
|
|
|
|
|
|
echo "✅ Maven cache volume exists: ${env.MAVEN_CACHE_VOLUME}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
// 检查磁盘空间
|
|
|
|
|
|
def diskUsage = sh(
|
|
|
|
|
|
script: "df ${env.WORKSPACE} | tail -1 | awk '{print \$5}' | sed 's/%//'",
|
|
|
|
|
|
returnStdout: true
|
|
|
|
|
|
).trim() as int
|
|
|
|
|
|
|
|
|
|
|
|
if (diskUsage > 80) {
|
|
|
|
|
|
echo "⚠️ Disk usage is ${diskUsage}%, cleaning up..."
|
|
|
|
|
|
sh "docker system prune -f --volumes || true"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查镜像仓库连接
|
|
|
|
|
|
sh """
|
|
|
|
|
|
curl -f ${env.REGISTRY}/v2/ >/dev/null 2>&1 || \
|
|
|
|
|
|
{ echo '⚠️ Registry not accessible, will continue...'; }
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
echo "✅ Pre-build checks passed"
|
2026-01-14 09:13:48 +08:00
|
|
|
|
recordStageMetrics('Pre-build Check', stageStartTime)
|
2026-01-14 01:16:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 16:05:24 +08:00
|
|
|
|
stage('Build Dependencies Image') {
|
2026-01-13 09:49:19 +08:00
|
|
|
|
when {
|
2026-01-14 01:16:16 +08:00
|
|
|
|
expression {
|
|
|
|
|
|
env.SERVICES_TO_BUILD != '' &&
|
2026-01-14 09:19:22 +08:00
|
|
|
|
env.DEPS_CHANGED == 'true'
|
2026-01-13 16:05:24 +08:00
|
|
|
|
}
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
steps {
|
|
|
|
|
|
script {
|
2026-01-14 09:13:48 +08:00
|
|
|
|
def stageStartTime = System.currentTimeMillis()
|
|
|
|
|
|
|
|
|
|
|
|
echo "📦 Building dependencies base image with Maven cache..."
|
2026-01-14 01:16:16 +08:00
|
|
|
|
timeout(time: 15, unit: 'MINUTES') {
|
2026-01-14 09:13:48 +08:00
|
|
|
|
// 【优化1】使用 Maven 缓存卷加速依赖下载
|
2026-01-14 01:16:16 +08:00
|
|
|
|
sh """
|
|
|
|
|
|
set -e
|
2026-01-14 09:13:48 +08:00
|
|
|
|
echo "Building ${env.DEPS_IMAGE} with cache..."
|
2026-01-14 01:16:16 +08:00
|
|
|
|
docker build \
|
|
|
|
|
|
-f docker/Dockerfile.deps \
|
|
|
|
|
|
-t ${env.DEPS_IMAGE} \
|
|
|
|
|
|
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
2026-01-14 09:13:48 +08:00
|
|
|
|
--build-arg MAVEN_OPTS="${env.MAVEN_OPTS}" \
|
2026-01-14 01:16:16 +08:00
|
|
|
|
.
|
|
|
|
|
|
|
|
|
|
|
|
docker push ${env.DEPS_IMAGE}
|
|
|
|
|
|
echo "✅ Dependencies image built and pushed"
|
|
|
|
|
|
"""
|
|
|
|
|
|
}
|
2026-01-14 09:13:48 +08:00
|
|
|
|
|
|
|
|
|
|
recordStageMetrics('Build Dependencies Image', stageStartTime)
|
2026-01-13 16:05:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-13 16:05:24 +08:00
|
|
|
|
stage('Build Services') {
|
2026-01-14 01:16:16 +08:00
|
|
|
|
when {
|
|
|
|
|
|
expression { env.SERVICES_TO_BUILD != '' }
|
|
|
|
|
|
}
|
2026-01-13 16:05:24 +08:00
|
|
|
|
steps {
|
|
|
|
|
|
script {
|
2026-01-14 09:13:48 +08:00
|
|
|
|
def stageStartTime = System.currentTimeMillis()
|
|
|
|
|
|
|
2026-01-14 00:35:32 +08:00
|
|
|
|
def servicesToBuild = env.SERVICES_TO_BUILD.split(',')
|
2026-01-14 09:13:48 +08:00
|
|
|
|
echo "🔨 Building ${servicesToBuild.size()} services (parallelism: ${env.MAX_PARALLEL_BUILDS})"
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
// 【优化2】动态并行构建
|
2026-01-14 00:35:32 +08:00
|
|
|
|
def buildTasks = [:]
|
|
|
|
|
|
servicesToBuild.each { service ->
|
|
|
|
|
|
buildTasks[service] = {
|
2026-01-14 01:16:16 +08:00
|
|
|
|
buildServiceWithRetry(service)
|
2026-01-14 00:35:32 +08:00
|
|
|
|
}
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-14 00:35:32 +08:00
|
|
|
|
parallel buildTasks
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
|
|
|
|
|
echo "✅ All services built successfully"
|
|
|
|
|
|
|
|
|
|
|
|
// 显示构建后的镜像信息
|
|
|
|
|
|
sh """
|
|
|
|
|
|
echo "📊 Built images:"
|
|
|
|
|
|
${env.SERVICES_TO_BUILD.split(',').collect { "docker images ${env.REGISTRY}/${it} --format ' {{.Repository}}:{{.Tag}} - {{.Size}}'" }.join('\n ')}
|
|
|
|
|
|
"""
|
2026-01-14 09:13:48 +08:00
|
|
|
|
|
|
|
|
|
|
recordStageMetrics('Build Services', stageStartTime)
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-13 09:49:19 +08:00
|
|
|
|
stage('Deploy') {
|
|
|
|
|
|
when {
|
2026-01-13 16:05:24 +08:00
|
|
|
|
allOf {
|
|
|
|
|
|
expression { env.SERVICES_TO_BUILD != '' }
|
|
|
|
|
|
branch 'master'
|
|
|
|
|
|
}
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
steps {
|
|
|
|
|
|
script {
|
2026-01-14 09:13:48 +08:00
|
|
|
|
def stageStartTime = System.currentTimeMillis()
|
|
|
|
|
|
|
2026-01-14 00:35:32 +08:00
|
|
|
|
def servicesToDeploy = env.SERVICES_TO_BUILD.split(',')
|
|
|
|
|
|
def sortedServices = sortServicesByDependency(servicesToDeploy)
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
|
|
|
|
|
echo "🚀 Deploying ${sortedServices.size()} services in dependency order"
|
|
|
|
|
|
sortedServices.eachWithIndex { service, index ->
|
|
|
|
|
|
echo " ${index + 1}. ${service}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
// 【优化7】部署前备份当前镜像标签
|
|
|
|
|
|
if (env.ROLLBACK_ENABLED == 'true') {
|
|
|
|
|
|
backupCurrentDeployment(sortedServices)
|
2026-01-13 19:13:30 +08:00
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-14 10:42:13 +08:00
|
|
|
|
// 【新增】同步最新的 docker-compose.core.yml 到部署服务器
|
|
|
|
|
|
echo "📂 Syncing docker-compose.core.yml to deploy host..."
|
|
|
|
|
|
sh "scp -o StrictHostKeyChecking=no -i ${env.SSH_KEY} docker-compose.core.yml root@${env.DEPLOY_HOST}:${env.DEPLOY_PATH}/"
|
|
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 串行部署(保证依赖关系)
|
|
|
|
|
|
sortedServices.each { service ->
|
|
|
|
|
|
deployServiceWithTimeout(service)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
echo "🚀 All services deployed successfully!"
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
// 【优化7】部署失败时自动回滚
|
|
|
|
|
|
if (env.ROLLBACK_ENABLED == 'true') {
|
|
|
|
|
|
echo "❌ Deployment failed: ${e.message}"
|
|
|
|
|
|
echo "🔄 Initiating automatic rollback..."
|
|
|
|
|
|
rollbackDeployment(sortedServices)
|
|
|
|
|
|
}
|
|
|
|
|
|
throw e
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
recordStageMetrics('Deploy', stageStartTime)
|
2026-01-13 19:13:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stage('Final Health Check') {
|
|
|
|
|
|
when {
|
|
|
|
|
|
allOf {
|
|
|
|
|
|
expression { env.SERVICES_TO_BUILD != '' }
|
|
|
|
|
|
branch 'master'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
steps {
|
|
|
|
|
|
script {
|
2026-01-14 09:13:48 +08:00
|
|
|
|
def stageStartTime = System.currentTimeMillis()
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
echo "🏥 Running final health check for all services..."
|
|
|
|
|
|
|
2026-01-14 00:35:32 +08:00
|
|
|
|
def servicesToCheck = env.SERVICES_TO_BUILD.split(',')
|
|
|
|
|
|
def healthCheckTasks = [:]
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-14 00:35:32 +08:00
|
|
|
|
servicesToCheck.each { service ->
|
|
|
|
|
|
healthCheckTasks[service] = {
|
2026-01-14 01:16:16 +08:00
|
|
|
|
checkServiceHealthWithRetry(service)
|
2026-01-14 00:35:32 +08:00
|
|
|
|
}
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
try {
|
|
|
|
|
|
parallel healthCheckTasks
|
|
|
|
|
|
echo "✅ All services are healthy!"
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
// 【优化7】健康检查失败时回滚
|
|
|
|
|
|
if (env.ROLLBACK_ENABLED == 'true') {
|
|
|
|
|
|
echo "❌ Health check failed: ${e.message}"
|
|
|
|
|
|
echo "🔄 Initiating automatic rollback..."
|
|
|
|
|
|
rollbackDeployment(servicesToCheck)
|
|
|
|
|
|
}
|
|
|
|
|
|
throw e
|
|
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
|
|
|
|
|
// 显示最终状态
|
|
|
|
|
|
sh """
|
|
|
|
|
|
echo "📊 Service Status:"
|
|
|
|
|
|
${servicesToCheck.collect {
|
|
|
|
|
|
def container = getContainerNameForService(it)
|
|
|
|
|
|
"docker inspect --format='${it}: {{.State.Status}} ({{.State.Health.Status}})' ${container} 2>/dev/null || echo '${it}: not found'"
|
|
|
|
|
|
}.join('\n ')}
|
|
|
|
|
|
"""
|
2026-01-14 09:13:48 +08:00
|
|
|
|
|
|
|
|
|
|
recordStageMetrics('Final Health Check', stageStartTime)
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-13 09:49:19 +08:00
|
|
|
|
post {
|
|
|
|
|
|
success {
|
2026-01-14 01:16:16 +08:00
|
|
|
|
script {
|
2026-01-14 09:13:48 +08:00
|
|
|
|
// 【优化6】计算总耗时并生成性能报告
|
|
|
|
|
|
def totalDuration = System.currentTimeMillis() - env.PIPELINE_START_TIME.toLong()
|
|
|
|
|
|
generatePerformanceReport(totalDuration)
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
echo """
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
✅ BUILD SUCCESS
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
📦 Services: ${env.SERVICES_TO_BUILD}
|
|
|
|
|
|
🏷️ Tag: ${env.IMAGE_TAG}
|
2026-01-14 09:13:48 +08:00
|
|
|
|
⏱️ Duration: ${formatDuration(totalDuration)}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
==========================================
|
|
|
|
|
|
"""
|
|
|
|
|
|
}
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
failure {
|
2026-01-14 01:16:16 +08:00
|
|
|
|
script {
|
2026-01-14 09:13:48 +08:00
|
|
|
|
def totalDuration = System.currentTimeMillis() - env.PIPELINE_START_TIME.toLong()
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
echo """
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
❌ BUILD FAILED
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
📦 Services: ${env.SERVICES_TO_BUILD ?: 'None'}
|
|
|
|
|
|
🏷️ Tag: ${env.IMAGE_TAG ?: 'Unknown'}
|
2026-01-14 09:13:48 +08:00
|
|
|
|
🔖 Rollback Tag: ${env.PREVIOUS_IMAGE_TAG ?: 'N/A'}
|
|
|
|
|
|
⏱️ Duration: ${formatDuration(totalDuration)}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
⚠️ Please check the logs above
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
// 失败时收集诊断信息
|
|
|
|
|
|
sh '''
|
|
|
|
|
|
echo "=== Docker System Info ==="
|
|
|
|
|
|
docker system df
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo "=== Disk Usage ==="
|
|
|
|
|
|
df -h | grep -E "/$|/var"
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo "=== Recent Containers ==="
|
|
|
|
|
|
docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}" | head -10
|
|
|
|
|
|
'''
|
|
|
|
|
|
}
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
always {
|
2026-01-14 01:16:16 +08:00
|
|
|
|
script {
|
|
|
|
|
|
echo "🧹 Cleaning up..."
|
|
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
// 清理悬空的镜像(但保留带标签的镜像用于回滚)
|
|
|
|
|
|
if (env.KEEP_PREVIOUS_IMAGES == 'true') {
|
|
|
|
|
|
sh "docker image prune -f --filter 'dangling=true' || true"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sh "docker image prune -f || true"
|
|
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
|
|
|
|
|
// 清理超过30天的构建日志
|
|
|
|
|
|
sh """
|
|
|
|
|
|
find ${env.WORKSPACE} -name '*.log' -mtime +30 -delete 2>/dev/null || true
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
echo "📊 Final System Status:"
|
|
|
|
|
|
sh 'df -h | grep -E "/$|/var" || true'
|
|
|
|
|
|
sh 'docker system df || true'
|
2026-01-14 09:13:48 +08:00
|
|
|
|
|
|
|
|
|
|
// 【优化6】保存性能指标到文件
|
|
|
|
|
|
archivePerformanceMetrics()
|
2026-01-14 01:16:16 +08:00
|
|
|
|
}
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
2026-01-14 01:16:16 +08:00
|
|
|
|
// 辅助函数
|
2026-01-13 09:49:19 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
|
2026-01-22 10:30:02 +08:00
|
|
|
|
// 获取变更的文件列表(对比上次成功构建,支持多 commit)
|
2026-01-14 01:16:16 +08:00
|
|
|
|
def getChangedFiles() {
|
2026-01-13 16:05:24 +08:00
|
|
|
|
def changedFiles = sh(
|
|
|
|
|
|
script: '''
|
2026-01-22 10:30:02 +08:00
|
|
|
|
# 优先使用上次成功构建的 commit
|
|
|
|
|
|
if [ -n "$GIT_PREVIOUS_SUCCESSFUL_COMMIT" ]; then
|
|
|
|
|
|
PREV="$GIT_PREVIOUS_SUCCESSFUL_COMMIT"
|
|
|
|
|
|
else
|
|
|
|
|
|
# 回退到 origin/master(如果有)
|
|
|
|
|
|
PREV=$(git rev-parse origin/master 2>/dev/null || git rev-parse HEAD~1 2>/dev/null || echo "")
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [ -z "$PREV" ]; then
|
|
|
|
|
|
echo "all"
|
|
|
|
|
|
else
|
|
|
|
|
|
git diff --name-only $PREV HEAD
|
|
|
|
|
|
fi
|
2026-01-13 16:05:24 +08:00
|
|
|
|
''',
|
|
|
|
|
|
returnStdout: true
|
|
|
|
|
|
).trim()
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
|
|
|
|
|
if (changedFiles == 'all') {
|
|
|
|
|
|
return ['all'] as List<String>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return changedFiles.split('\n') as List<String>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检测是否需要重建依赖镜像
|
|
|
|
|
|
@NonCPS
|
|
|
|
|
|
def checkIfDepsChanged(List<String> changedFiles) {
|
|
|
|
|
|
if (changedFiles.contains('all')) {
|
|
|
|
|
|
return 'true'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def depsFiles = ['pom.xml', 'viewsh-dependencies', 'viewsh-framework', 'docker/Dockerfile.deps']
|
|
|
|
|
|
return depsFiles.any { changedFiles.contains(it) } ? 'true' : 'false'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检测需要构建的服务
|
|
|
|
|
|
def detectServicesToBuild(List<String> changedFiles) {
|
|
|
|
|
|
if (changedFiles.contains('all')) {
|
2026-01-13 16:05:24 +08:00
|
|
|
|
return env.CORE_SERVICES
|
|
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
|
|
|
|
|
def triggerAllFiles = ['pom.xml', 'viewsh-framework', 'viewsh-dependencies', 'Jenkinsfile', 'docker/']
|
|
|
|
|
|
if (triggerAllFiles.any { triggerFile ->
|
|
|
|
|
|
changedFiles.any { changedFile ->
|
|
|
|
|
|
changedFile.startsWith(triggerFile) || changedFile == triggerFile
|
|
|
|
|
|
}
|
|
|
|
|
|
}) {
|
2026-01-13 16:05:24 +08:00
|
|
|
|
return env.CORE_SERVICES
|
|
|
|
|
|
}
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-14 00:35:32 +08:00
|
|
|
|
def changedServices = []
|
|
|
|
|
|
def allServices = env.CORE_SERVICES.split(',')
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-14 00:35:32 +08:00
|
|
|
|
allServices.each { service ->
|
|
|
|
|
|
def modulePath = getModulePathForService(service)
|
|
|
|
|
|
def moduleDir = modulePath.split('/')[0]
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
|
|
|
|
|
if (changedFiles.any { it.startsWith(moduleDir) }) {
|
2026-01-14 00:35:32 +08:00
|
|
|
|
changedServices.add(service)
|
2026-01-13 16:05:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
return changedServices.isEmpty() ? env.CORE_SERVICES : changedServices.join(',')
|
2026-01-13 16:05:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
// 检查依赖镜像是否存在
|
2026-01-13 16:05:24 +08:00
|
|
|
|
def depsImageExists() {
|
|
|
|
|
|
def result = sh(
|
|
|
|
|
|
script: "docker manifest inspect ${env.DEPS_IMAGE} > /dev/null 2>&1",
|
|
|
|
|
|
returnStatus: true
|
|
|
|
|
|
)
|
|
|
|
|
|
return result == 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
// 【优化2】动态检测系统资源并设置并行度
|
|
|
|
|
|
def detectSystemResources() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取 CPU 核心数
|
|
|
|
|
|
def cpuCores = sh(
|
|
|
|
|
|
script: 'nproc 2>/dev/null || echo "2"',
|
|
|
|
|
|
returnStdout: true
|
|
|
|
|
|
).trim() as int
|
|
|
|
|
|
|
|
|
|
|
|
// 获取可用内存(GB)
|
|
|
|
|
|
def availableMemory = sh(
|
|
|
|
|
|
script: 'free -g | grep Mem | awk \'{print $7}\'',
|
|
|
|
|
|
returnStdout: true
|
|
|
|
|
|
).trim() as int
|
|
|
|
|
|
|
|
|
|
|
|
// 动态计算并行度
|
|
|
|
|
|
// 规则:每个构建任务需要至少 2GB 内存和 1 个 CPU 核心
|
|
|
|
|
|
def maxParallelByMemory = Math.max(1, (availableMemory / 2) as int)
|
|
|
|
|
|
def maxParallelByCpu = Math.max(1, cpuCores - 1) // 保留一个核心给系统
|
|
|
|
|
|
|
|
|
|
|
|
env.MAX_PARALLEL_BUILDS = Math.min(maxParallelByMemory, maxParallelByCpu).toString()
|
|
|
|
|
|
|
|
|
|
|
|
echo """
|
|
|
|
|
|
========================================
|
|
|
|
|
|
📊 System Resources Detected:
|
|
|
|
|
|
========================================
|
|
|
|
|
|
CPU Cores: ${cpuCores}
|
|
|
|
|
|
Available Memory: ${availableMemory} GB
|
|
|
|
|
|
Max Parallel Builds: ${env.MAX_PARALLEL_BUILDS}
|
|
|
|
|
|
========================================
|
|
|
|
|
|
"""
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
echo "⚠️ Failed to detect system resources: ${e.message}"
|
|
|
|
|
|
echo "Using default parallelism: 2"
|
|
|
|
|
|
env.MAX_PARALLEL_BUILDS = '2'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 【优化6】记录阶段性能指标
|
|
|
|
|
|
def recordStageMetrics(String stageName, long startTime) {
|
|
|
|
|
|
def duration = System.currentTimeMillis() - startTime
|
|
|
|
|
|
def durationStr = formatDuration(duration)
|
|
|
|
|
|
|
|
|
|
|
|
if (!env.STAGE_METRICS) {
|
|
|
|
|
|
env.STAGE_METRICS = ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
env.STAGE_METRICS += "${stageName}:${duration}|"
|
|
|
|
|
|
echo "⏱️ Stage '${stageName}' completed in ${durationStr}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 【优化6】格式化时长
|
|
|
|
|
|
@NonCPS
|
|
|
|
|
|
def formatDuration(long milliseconds) {
|
|
|
|
|
|
def seconds = (milliseconds / 1000) as int
|
|
|
|
|
|
def minutes = (seconds / 60) as int
|
|
|
|
|
|
def hours = (minutes / 60) as int
|
|
|
|
|
|
|
|
|
|
|
|
if (hours > 0) {
|
|
|
|
|
|
return "${hours}h ${minutes % 60}m ${seconds % 60}s"
|
|
|
|
|
|
} else if (minutes > 0) {
|
|
|
|
|
|
return "${minutes}m ${seconds % 60}s"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return "${seconds}s"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 【优化6】生成性能报告
|
|
|
|
|
|
def generatePerformanceReport(long totalDuration) {
|
|
|
|
|
|
echo """
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
📊 PERFORMANCE REPORT
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
Total Duration: ${formatDuration(totalDuration)}
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
if (env.STAGE_METRICS) {
|
|
|
|
|
|
echo "Stage Breakdown:"
|
|
|
|
|
|
env.STAGE_METRICS.split('\\|').each { metric ->
|
|
|
|
|
|
if (metric) {
|
|
|
|
|
|
def parts = metric.split(':')
|
|
|
|
|
|
if (parts.size() == 2) {
|
|
|
|
|
|
def stageName = parts[0]
|
|
|
|
|
|
def duration = parts[1] as long
|
|
|
|
|
|
def percentage = (duration * 100 / totalDuration) as int
|
|
|
|
|
|
echo " - ${stageName.padRight(25)}: ${formatDuration(duration).padRight(10)} (${percentage}%)"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 【优化6】归档性能指标
|
|
|
|
|
|
def archivePerformanceMetrics() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
def metricsFile = "${env.WORKSPACE}/build-metrics-${env.BUILD_NUMBER}.json"
|
|
|
|
|
|
def metricsData = [
|
|
|
|
|
|
buildNumber: env.BUILD_NUMBER,
|
|
|
|
|
|
timestamp: new Date().format('yyyy-MM-dd HH:mm:ss'),
|
|
|
|
|
|
branch: env.BRANCH_NAME,
|
|
|
|
|
|
imageTag: env.IMAGE_TAG,
|
|
|
|
|
|
servicesToBuild: env.SERVICES_TO_BUILD,
|
|
|
|
|
|
totalDuration: System.currentTimeMillis() - env.PIPELINE_START_TIME.toLong(),
|
|
|
|
|
|
stages: [:]
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if (env.STAGE_METRICS) {
|
|
|
|
|
|
env.STAGE_METRICS.split('\\|').each { metric ->
|
|
|
|
|
|
if (metric) {
|
|
|
|
|
|
def parts = metric.split(':')
|
|
|
|
|
|
if (parts.size() == 2) {
|
|
|
|
|
|
metricsData.stages[parts[0]] = parts[1] as long
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 09:30:24 +08:00
|
|
|
|
// writeJSON file: metricsFile, json: metricsData
|
|
|
|
|
|
writeFile file: metricsFile, text: groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(metricsData))
|
2026-01-14 09:13:48 +08:00
|
|
|
|
archiveArtifacts artifacts: "build-metrics-${env.BUILD_NUMBER}.json", allowEmptyArchive: true
|
|
|
|
|
|
|
|
|
|
|
|
echo "✅ Performance metrics archived: ${metricsFile}"
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
echo "⚠️ Failed to archive performance metrics: ${e.message}"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 【优化7】获取上一次成功部署的镜像标签
|
|
|
|
|
|
def getPreviousImageTag() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 从部署主机获取当前运行的镜像标签
|
|
|
|
|
|
def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${env.SSH_KEY}"
|
|
|
|
|
|
def previousTag = sh(
|
|
|
|
|
|
script: """
|
|
|
|
|
|
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
|
|
|
|
|
cd ${env.DEPLOY_PATH}
|
|
|
|
|
|
docker compose -f docker-compose.core.yml images --format json | \
|
|
|
|
|
|
jq -r ".[0].Tag" | head -1
|
|
|
|
|
|
' 2>/dev/null || echo "latest"
|
|
|
|
|
|
""",
|
|
|
|
|
|
returnStdout: true
|
|
|
|
|
|
).trim()
|
|
|
|
|
|
|
|
|
|
|
|
return previousTag ?: 'latest'
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
echo "⚠️ Failed to get previous image tag: ${e.message}"
|
|
|
|
|
|
return 'latest'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 【优化7】备份当前部署
|
|
|
|
|
|
def backupCurrentDeployment(def services) {
|
|
|
|
|
|
echo "💾 Backing up current deployment state..."
|
|
|
|
|
|
|
|
|
|
|
|
def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${env.SSH_KEY}"
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
sh """
|
|
|
|
|
|
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
|
|
|
|
|
cd ${env.DEPLOY_PATH}
|
|
|
|
|
|
|
|
|
|
|
|
# 保存当前 docker-compose 配置
|
|
|
|
|
|
cp docker-compose.core.yml docker-compose.core.yml.backup-${env.BUILD_NUMBER}
|
|
|
|
|
|
|
|
|
|
|
|
# 记录当前运行的镜像
|
|
|
|
|
|
docker compose -f docker-compose.core.yml images > deployment-state-${env.BUILD_NUMBER}.txt
|
|
|
|
|
|
|
|
|
|
|
|
echo "✅ Backup completed: deployment-state-${env.BUILD_NUMBER}.txt"
|
|
|
|
|
|
'
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
echo "✅ Current deployment backed up successfully"
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
echo "⚠️ Failed to backup current deployment: ${e.message}"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 【优化7】回滚部署
|
|
|
|
|
|
def rollbackDeployment(def services) {
|
|
|
|
|
|
echo """
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
🔄 INITIATING ROLLBACK
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
Rolling back to: ${env.PREVIOUS_IMAGE_TAG}
|
|
|
|
|
|
Services: ${services.join(', ')}
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${env.SSH_KEY}"
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
services.each { service ->
|
|
|
|
|
|
echo "🔄 Rolling back ${service}..."
|
|
|
|
|
|
|
|
|
|
|
|
sh """
|
|
|
|
|
|
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
|
|
|
|
|
cd ${env.DEPLOY_PATH}
|
|
|
|
|
|
|
|
|
|
|
|
# 设置回滚镜像标签
|
|
|
|
|
|
export IMAGE_TAG=${env.PREVIOUS_IMAGE_TAG}
|
|
|
|
|
|
|
|
|
|
|
|
# 拉取旧版本镜像
|
|
|
|
|
|
docker compose -f docker-compose.core.yml pull ${service}
|
|
|
|
|
|
|
|
|
|
|
|
# 重启服务
|
|
|
|
|
|
docker compose -f docker-compose.core.yml up -d ${service}
|
|
|
|
|
|
|
|
|
|
|
|
echo "✅ ${service} rolled back to ${env.PREVIOUS_IMAGE_TAG}"
|
|
|
|
|
|
'
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
// 等待服务启动
|
|
|
|
|
|
sleep 5
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
echo """
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
✅ ROLLBACK COMPLETED
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
All services have been rolled back to: ${env.PREVIOUS_IMAGE_TAG}
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
echo """
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
❌ ROLLBACK FAILED
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
Error: ${e.message}
|
|
|
|
|
|
Manual intervention required!
|
|
|
|
|
|
==========================================
|
|
|
|
|
|
"""
|
|
|
|
|
|
throw e
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
// 构建服务(带重试)
|
|
|
|
|
|
def buildServiceWithRetry(String service) {
|
|
|
|
|
|
retry(2) {
|
|
|
|
|
|
timeout(time: env.BUILD_TIMEOUT.toInteger(), unit: 'MINUTES') {
|
|
|
|
|
|
buildService(service)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 00:35:32 +08:00
|
|
|
|
// 构建单个服务
|
|
|
|
|
|
def buildService(String service) {
|
|
|
|
|
|
def modulePath = getModulePathForService(service)
|
2026-01-14 09:13:48 +08:00
|
|
|
|
def buildStartTime = System.currentTimeMillis()
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
|
echo "🔨 Building ${service}"
|
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
|
echo "Module: ${modulePath}"
|
|
|
|
|
|
echo "Registry: ${env.REGISTRY}"
|
|
|
|
|
|
echo "Tag: ${env.IMAGE_TAG}"
|
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-14 09:13:48 +08:00
|
|
|
|
// 【优化1】使用 Maven 缓存卷
|
2026-01-14 01:16:16 +08:00
|
|
|
|
sh """
|
|
|
|
|
|
set -e
|
|
|
|
|
|
set -x
|
|
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
# 构建镜像(使用 Maven 缓存)
|
2026-01-14 01:16:16 +08:00
|
|
|
|
docker build \\
|
|
|
|
|
|
-f docker/Dockerfile.service \\
|
|
|
|
|
|
--build-arg DEPS_IMAGE=${env.DEPS_IMAGE} \\
|
|
|
|
|
|
--build-arg MODULE_NAME=${modulePath} \\
|
|
|
|
|
|
--build-arg JAR_NAME=${service} \\
|
|
|
|
|
|
--build-arg SKIP_TESTS=true \\
|
2026-01-14 09:13:48 +08:00
|
|
|
|
--build-arg MAVEN_OPTS="${env.MAVEN_OPTS}" \\
|
2026-01-14 01:16:16 +08:00
|
|
|
|
-t ${env.REGISTRY}/${service}:${env.IMAGE_TAG} \\
|
|
|
|
|
|
-t ${env.REGISTRY}/${service}:latest \\
|
|
|
|
|
|
.
|
|
|
|
|
|
|
|
|
|
|
|
# 推送镜像
|
|
|
|
|
|
docker push ${env.REGISTRY}/${service}:${env.IMAGE_TAG}
|
|
|
|
|
|
docker push ${env.REGISTRY}/${service}:latest
|
|
|
|
|
|
|
|
|
|
|
|
set +x
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
def buildDuration = System.currentTimeMillis() - buildStartTime
|
|
|
|
|
|
echo "✅ ${service} built and pushed successfully in ${formatDuration(buildDuration)}"
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取镜像大小
|
|
|
|
|
|
def imageSize = sh(
|
|
|
|
|
|
script: "docker images ${env.REGISTRY}/${service}:latest --format '{{.Size}}'",
|
|
|
|
|
|
returnStdout: true
|
|
|
|
|
|
).trim()
|
|
|
|
|
|
|
|
|
|
|
|
echo "📊 Image size: ${imageSize}"
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
echo "❌ Failed to build ${service}: ${e.message}"
|
|
|
|
|
|
|
|
|
|
|
|
// 打印构建日志以便调试
|
|
|
|
|
|
sh """
|
|
|
|
|
|
echo "=== Docker Build Logs for ${service} ==="
|
|
|
|
|
|
docker logs ${service}-builder 2>/dev/null || true
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
throw e
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 部署服务(带超时)
|
|
|
|
|
|
def deployServiceWithTimeout(String service) {
|
|
|
|
|
|
timeout(time: env.DEPLOY_TIMEOUT.toInteger(), unit: 'MINUTES') {
|
|
|
|
|
|
deployService(service)
|
|
|
|
|
|
}
|
2026-01-13 16:05:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 00:35:32 +08:00
|
|
|
|
// 部署单个服务
|
2026-01-13 16:05:24 +08:00
|
|
|
|
def deployService(String service) {
|
2026-01-14 00:35:32 +08:00
|
|
|
|
def containerName = getContainerNameForService(service)
|
2026-01-14 01:16:16 +08:00
|
|
|
|
def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${env.SSH_KEY}"
|
2026-01-14 00:35:32 +08:00
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
echo ""
|
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
|
echo "🚀 Deploying ${service}"
|
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
|
echo "Container: ${containerName}"
|
|
|
|
|
|
echo "Host: ${env.DEPLOY_HOST}"
|
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 部署服务
|
|
|
|
|
|
sh """
|
|
|
|
|
|
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
|
|
|
|
|
set -e
|
|
|
|
|
|
cd ${env.DEPLOY_PATH}
|
|
|
|
|
|
|
|
|
|
|
|
echo "📥 Pulling ${service}..."
|
|
|
|
|
|
docker compose -f docker-compose.core.yml pull ${service}
|
|
|
|
|
|
|
|
|
|
|
|
echo "🔄 Restarting ${service}..."
|
|
|
|
|
|
docker compose -f docker-compose.core.yml up -d ${service}
|
|
|
|
|
|
|
|
|
|
|
|
echo "⏳ Waiting for container to start..."
|
|
|
|
|
|
sleep 5
|
|
|
|
|
|
'
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
// 等待服务健康
|
|
|
|
|
|
waitForServiceHealthy(containerName, service, sshOpts)
|
|
|
|
|
|
|
|
|
|
|
|
echo "✅ ${service} deployed successfully"
|
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
echo "❌ Failed to deploy ${service}: ${e.message}"
|
|
|
|
|
|
|
|
|
|
|
|
// 收集诊断信息
|
|
|
|
|
|
sh """
|
|
|
|
|
|
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
|
|
|
|
|
echo "=== Container Status ==="
|
|
|
|
|
|
docker ps -a | grep ${containerName} || true
|
|
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo "=== Container Logs (last 50 lines) ==="
|
|
|
|
|
|
docker logs --tail 50 ${containerName} 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo "=== Service Health Status ==="
|
|
|
|
|
|
docker inspect --format="{{.State.Health.Status}}" ${containerName} 2>/dev/null || echo "not_found"
|
|
|
|
|
|
'
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
throw e
|
|
|
|
|
|
}
|
2026-01-14 00:35:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 等待服务健康
|
|
|
|
|
|
def waitForServiceHealthy(String containerName, String serviceName, String sshOpts) {
|
2026-01-14 17:48:00 +08:00
|
|
|
|
// iot-gateway 是轻量化网关,不需要健康检查,只检查容器是否运行
|
|
|
|
|
|
if (serviceName == 'viewsh-module-iot-gateway') {
|
|
|
|
|
|
echo "⏳ Waiting for ${serviceName} to start (lightweight gateway, no health check)..."
|
|
|
|
|
|
sh """
|
|
|
|
|
|
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
|
|
|
|
|
set -e
|
|
|
|
|
|
sleep 5
|
|
|
|
|
|
if docker ps | grep -q ${containerName}; then
|
|
|
|
|
|
echo "✅ ${serviceName} is running"
|
|
|
|
|
|
else
|
|
|
|
|
|
echo "❌ ${serviceName} is not running"
|
|
|
|
|
|
docker logs --tail 50 ${containerName} 2>/dev/null || true
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
'
|
|
|
|
|
|
"""
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 00:35:32 +08:00
|
|
|
|
def maxAttempts = env.HEALTH_CHECK_TIMEOUT.toInteger() / env.HEALTH_CHECK_INTERVAL.toInteger()
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
|
|
|
|
|
echo "⏳ Waiting for ${serviceName} to be healthy (max ${env.HEALTH_CHECK_TIMEOUT}s)..."
|
|
|
|
|
|
|
2026-01-13 16:05:24 +08:00
|
|
|
|
sh """
|
2026-01-14 01:16:16 +08:00
|
|
|
|
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
|
|
|
|
|
set -e
|
|
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
for i in \$(seq 1 ${maxAttempts}); do
|
|
|
|
|
|
STATUS=\$(docker inspect --format="{{.State.Health.Status}}" ${containerName} 2>/dev/null || echo "starting")
|
2026-01-13 19:13:30 +08:00
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
case "\$STATUS" in
|
2026-01-14 01:16:16 +08:00
|
|
|
|
healthy)
|
|
|
|
|
|
echo "✅ ${serviceName} is healthy"
|
|
|
|
|
|
exit 0
|
|
|
|
|
|
;;
|
|
|
|
|
|
unhealthy)
|
|
|
|
|
|
echo "❌ ${serviceName} is unhealthy"
|
|
|
|
|
|
echo "=== Last 100 lines of logs ==="
|
|
|
|
|
|
docker logs --tail 100 ${containerName}
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
;;
|
|
|
|
|
|
starting)
|
2026-01-14 09:13:48 +08:00
|
|
|
|
ELAPSED=\$((i * ${env.HEALTH_CHECK_INTERVAL}))
|
2026-01-14 01:16:16 +08:00
|
|
|
|
echo "⏳ ${serviceName} is starting... (\${ELAPSED}s/${env.HEALTH_CHECK_TIMEOUT}s)"
|
|
|
|
|
|
;;
|
|
|
|
|
|
*)
|
|
|
|
|
|
echo "⚠️ ${serviceName} status: \$STATUS"
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
|
|
sleep ${env.HEALTH_CHECK_INTERVAL}
|
2026-01-13 16:05:24 +08:00
|
|
|
|
done
|
2026-01-13 19:13:30 +08:00
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
echo "❌ ${serviceName} health check timeout after ${env.HEALTH_CHECK_TIMEOUT}s"
|
2026-01-13 19:13:30 +08:00
|
|
|
|
echo "=== Full logs ==="
|
|
|
|
|
|
docker logs ${containerName}
|
|
|
|
|
|
exit 1
|
2026-01-13 16:05:24 +08:00
|
|
|
|
'
|
|
|
|
|
|
"""
|
2026-01-14 00:35:32 +08:00
|
|
|
|
}
|
2026-01-13 19:13:30 +08:00
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
// 检查服务健康(带重试)
|
|
|
|
|
|
def checkServiceHealthWithRetry(String service) {
|
|
|
|
|
|
def containerName = getContainerNameForService(service)
|
|
|
|
|
|
def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${env.SSH_KEY}"
|
|
|
|
|
|
|
|
|
|
|
|
retry(3) {
|
|
|
|
|
|
timeout(time: 2, unit: 'MINUTES') {
|
|
|
|
|
|
checkServiceHealth(containerName, service, sshOpts)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查服务健康状态
|
|
|
|
|
|
def checkServiceHealth(String containerName, String serviceName, String sshOpts) {
|
2026-01-14 17:48:00 +08:00
|
|
|
|
// iot-gateway 是轻量化网关,不需要健康检查,只检查容器是否运行
|
|
|
|
|
|
if (serviceName == 'viewsh-module-iot-gateway') {
|
|
|
|
|
|
sh """
|
|
|
|
|
|
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
|
|
|
|
|
if docker ps | grep -q ${containerName}; then
|
|
|
|
|
|
echo "✅ ${serviceName} is running (lightweight gateway)"
|
|
|
|
|
|
else
|
|
|
|
|
|
echo "❌ ${serviceName} is not running"
|
|
|
|
|
|
docker logs --tail 50 ${containerName} 2>/dev/null || true
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
'
|
|
|
|
|
|
"""
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 00:35:32 +08:00
|
|
|
|
sh """
|
2026-01-14 01:16:16 +08:00
|
|
|
|
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
2026-01-14 09:13:48 +08:00
|
|
|
|
STATUS=\$(docker inspect --format="{{.State.Health.Status}}" ${containerName} 2>/dev/null || echo "not_found")
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-14 09:13:48 +08:00
|
|
|
|
case "\$STATUS" in
|
2026-01-14 01:16:16 +08:00
|
|
|
|
healthy)
|
|
|
|
|
|
echo "✅ ${serviceName} is healthy"
|
|
|
|
|
|
;;
|
|
|
|
|
|
not_found)
|
|
|
|
|
|
echo "⚠️ ${serviceName} not found (may not be deployed)"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
;;
|
|
|
|
|
|
*)
|
|
|
|
|
|
echo "❌ ${serviceName} is \$STATUS"
|
|
|
|
|
|
echo "=== Last 50 lines of logs ==="
|
|
|
|
|
|
docker logs --tail 50 ${containerName}
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
2026-01-14 00:35:32 +08:00
|
|
|
|
'
|
|
|
|
|
|
"""
|
2026-01-13 16:05:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
// 按依赖顺序排序服务
|
2026-01-14 00:35:32 +08:00
|
|
|
|
@NonCPS
|
|
|
|
|
|
def sortServicesByDependency(def services) {
|
|
|
|
|
|
def deployOrder = [
|
|
|
|
|
|
'viewsh-gateway',
|
|
|
|
|
|
'viewsh-module-system-server',
|
|
|
|
|
|
'viewsh-module-infra-server',
|
|
|
|
|
|
'viewsh-module-iot-server',
|
2026-01-15 16:30:15 +08:00
|
|
|
|
'viewsh-module-iot-gateway',
|
|
|
|
|
|
'viewsh-module-ops-server'
|
2026-01-13 17:24:09 +08:00
|
|
|
|
]
|
2026-01-14 01:16:16 +08:00
|
|
|
|
|
2026-01-14 00:35:32 +08:00
|
|
|
|
return services.sort { a, b ->
|
|
|
|
|
|
deployOrder.indexOf(a) <=> deployOrder.indexOf(b)
|
|
|
|
|
|
}
|
2026-01-13 17:24:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
// 获取服务对应的容器名称
|
2026-01-14 00:35:32 +08:00
|
|
|
|
@NonCPS
|
|
|
|
|
|
def getContainerNameForService(String service) {
|
2026-01-14 01:16:16 +08:00
|
|
|
|
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',
|
2026-01-15 16:30:15 +08:00
|
|
|
|
'viewsh-module-iot-gateway': 'aiot-iot-gateway',
|
|
|
|
|
|
'viewsh-module-ops-server': 'aiot-ops-server'
|
2026-01-14 01:16:16 +08:00
|
|
|
|
]
|
|
|
|
|
|
return map.get(service, "aiot-${service}")
|
2026-01-14 00:35:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 01:16:16 +08:00
|
|
|
|
// 获取服务对应的模块路径
|
2026-01-14 00:35:32 +08:00
|
|
|
|
@NonCPS
|
|
|
|
|
|
def getModulePathForService(String service) {
|
2026-01-14 01:16:16 +08:00
|
|
|
|
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',
|
2026-01-15 16:30:15 +08:00
|
|
|
|
'viewsh-module-iot-gateway': 'viewsh-module-iot/viewsh-module-iot-gateway',
|
|
|
|
|
|
'viewsh-module-ops-server': 'viewsh-module-ops/viewsh-module-ops-server'
|
2026-01-14 01:16:16 +08:00
|
|
|
|
]
|
|
|
|
|
|
return map.get(service, service)
|
2026-01-13 09:49:19 +08:00
|
|
|
|
}
|