diff --git a/Jenkinsfile b/Jenkinsfile index 576bb25..6b6bc4a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,52 +1,35 @@ // ============================================ // AIOT Platform - Jenkins Pipeline -// 智能增量构建 + Docker 容器化部署 +// 并行构建 + Maven 依赖缓存优化 // ============================================ pipeline { agent any options { - // 保留最近 10 次构建 buildDiscarder(logRotator(numToKeepStr: '10')) - // 禁止并发构建 disableConcurrentBuilds() - // 超时设置 timeout(time: 60, unit: 'MINUTES') + timestamps() } environment { - // Gitea 仓库配置 - GIT_REPO = 'http://172.17.16.14:3000/XW-AIOT/aiot-platform-cloud.git' - - // Docker Registry 配置 REGISTRY = 'localhost:5000' - - // Maven 配置 - MAVEN_OPTS = '-Xmx2048m -Dmaven.repo.local=.m2/repository' - - // 镜像标签 IMAGE_TAG = "${env.BRANCH_NAME}-${env.BUILD_NUMBER}-${env.GIT_COMMIT?.take(8)}" - - // 服务列表(核心服务 - 不包括开发中的 ops-server) + 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 { - echo "=== 检出代码 ===" - checkout scm - - // 获取 Git 提交信息 - env.GIT_COMMIT_MSG = sh( - script: 'git log -1 --pretty=%B', - returnStdout: true - ).trim() - - echo "Commit: ${env.GIT_COMMIT}" - echo "Message: ${env.GIT_COMMIT_MSG}" + 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}" } } } @@ -54,117 +37,68 @@ pipeline { stage('Detect Changes') { steps { script { - echo "=== 检测变更的服务 ===" + env.SERVICES_TO_BUILD = detectChangedServices() + env.DEPS_CHANGED = checkDepsChanged() - // 获取变更的文件列表 - def changedFiles = sh( - script: ''' - # 获取上一次成功构建的提交 - PREV_COMMIT=$(git rev-parse HEAD~1 2>/dev/null || echo "") - - if [ -z "$PREV_COMMIT" ]; then - # 首次构建或只有一个提交,构建所有核心服务 - echo "all" - else - # 检测变更的文件 - git diff --name-only $PREV_COMMIT HEAD - fi - ''', - returnStdout: true - ).trim() - - echo "Changed files:\n${changedFiles}" - - // 分析需要构建的服务 - def servicesToBuild = [] - - if (changedFiles == 'all' || changedFiles.isEmpty()) { - // 首次构建或强制全量构建 - echo "首次构建或无法检测变更,构建所有核心服务" - servicesToBuild = CORE_SERVICES.split(',') - } else { - // 检测每个服务是否有变更 - CORE_SERVICES.split(',').each { service -> - def modulePath = service.replace('-server', '').replace('viewsh-module-', 'viewsh-module-').replace('viewsh-', '') - - if (changedFiles.contains(modulePath) || - changedFiles.contains('pom.xml') || - changedFiles.contains('viewsh-framework') || - changedFiles.contains('viewsh-dependencies') || - changedFiles.contains('Jenkinsfile') || - changedFiles.contains('docker/')) { - servicesToBuild.add(service) - } - } - - // 如果没有检测到变更,但有代码提交,构建所有服务 - if (servicesToBuild.isEmpty() && !changedFiles.isEmpty()) { - echo "检测到代码变更但未匹配到具体服务,构建所有服务" - servicesToBuild = CORE_SERVICES.split(',') - } - } - - env.SERVICES_TO_BUILD = servicesToBuild.join(',') - echo "Services to build: ${env.SERVICES_TO_BUILD}" - - if (servicesToBuild.isEmpty()) { - echo "No services need to be built. Skipping build." + if (env.SERVICES_TO_BUILD.isEmpty()) { + echo "⏭️ No changes detected, skipping build" currentBuild.result = 'SUCCESS' - error("No changes detected, skipping build") + error("No changes") } + echo "🔄 Services to build: ${env.SERVICES_TO_BUILD}" + echo "📦 Deps changed: ${env.DEPS_CHANGED}" } } } - stage('Docker Build & Push') { + stage('Build Dependencies Image') { when { - expression { env.SERVICES_TO_BUILD != '' } + expression { + env.DEPS_CHANGED == 'true' || !depsImageExists() + } } steps { script { - echo "=== Docker 镜像构建与推送 ===" - echo "使用 Docker 多阶段构建(包含 Maven 编译)" + echo "📦 Building dependencies base image..." - def services = env.SERVICES_TO_BUILD.split(',') + sh """ + docker build \ + -f docker/Dockerfile.deps \ + -t ${DEPS_IMAGE} \ + . + + docker push ${DEPS_IMAGE} + """ - // 串行构建,避免内存溢出 - services.each { service -> - def modulePath = getModulePath(service) - def jarName = service + 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(', ')}" - echo "=========================================" - echo "构建服务: ${service}" - echo "模块路径: ${modulePath}" - echo "=========================================" - - try { - // Docker 多阶段构建(包含 Maven 编译和镜像打包) - sh """ - docker build \ - -f docker/Dockerfile.template \ - --build-arg MODULE_NAME=${modulePath} \ - --build-arg JAR_NAME=${jarName} \ - --build-arg SKIP_TESTS=true \ - -t ${REGISTRY}/${service}:${IMAGE_TAG} \ - -t ${REGISTRY}/${service}:latest \ - . - """ - - echo "推送镜像: ${service}" - sh """ - docker push ${REGISTRY}/${service}:${IMAGE_TAG} - docker push ${REGISTRY}/${service}:latest - """ - - echo "✓ ${service} 构建成功" - } catch (Exception e) { - echo "✗ ${service} 构建失败: ${e.message}" - throw e + def parallelBuilds = [:] + batch.each { service -> + parallelBuilds[service] = { + buildAndPush(service) + } } + + parallel parallelBuilds } - // 清理悬空镜像 - echo "清理悬空镜像..." + // 清理 sh "docker image prune -f || true" } } @@ -172,46 +106,26 @@ pipeline { stage('Deploy') { when { - expression { env.SERVICES_TO_BUILD != '' && env.BRANCH_NAME == 'master' } + allOf { + expression { env.SERVICES_TO_BUILD != '' } + branch 'master' + } } steps { script { - echo "=== 部署到生产环境 ===" - - def services = env.SERVICES_TO_BUILD.split(',') + def services = env.SERVICES_TO_BUILD.split(',') as List + // 部署也可以并行 + def parallelDeploys = [:] services.each { service -> - echo "Deploying ${service}..." - - // 诊断信息 - sh """ - echo "=== 诊断信息 ===" - echo "当前用户: \$(whoami)" - echo "当前目录: \$(pwd)" - echo "测试 SSH 连接..." - ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 root@172.19.0.1 'echo "SSH 连接成功"; hostname; pwd' || echo "SSH 连接失败" - """ - - // 通过 SSH 在宿主机上执行部署命令 - // Jenkins 容器网络: 1panel-network, Gateway: 172.19.0.1 - sh """ - ssh -o StrictHostKeyChecking=no root@172.19.0.1 ' - cd /opt/aiot-platform-cloud || { echo "目录不存在,创建中..."; mkdir -p /opt/aiot-platform-cloud; exit 1; } - docker compose -f docker-compose.core.yml pull ${service} - docker compose -f docker-compose.core.yml up -d ${service} - ' - """ - - // 等待服务健康检查 - echo "Waiting for ${service} to be healthy..." - sh """ - ssh -o StrictHostKeyChecking=no root@172.19.0.1 ' - timeout 120 sh -c "until docker inspect --format=\\"{{.State.Health.Status}}\\" aiot-${service} 2>/dev/null | grep -q healthy; do sleep 5; done" || true - ' - """ + parallelDeploys[service] = { + deployService(service) + } } - echo "Deployment completed!" + parallel parallelDeploys + + echo "🚀 Deployment completed!" } } } @@ -219,23 +133,18 @@ pipeline { post { success { - echo "=== 构建成功 ===" - echo "Built services: ${env.SERVICES_TO_BUILD}" - echo "Image tag: ${IMAGE_TAG}" + echo """ + ✅ 构建成功 + 📦 Services: ${env.SERVICES_TO_BUILD} + 🏷️ Tag: ${IMAGE_TAG} + """ } - failure { - echo "=== 构建失败 ===" - echo "Please check the logs for details." + echo "❌ 构建失败,请检查日志" } - always { - // 清理工作空间(可选) - // cleanWs() - - // 显示磁盘使用情况 - sh 'df -h' - sh 'docker system df' + sh 'df -h | grep -E "/$|/var" || true' + sh 'docker system df || true' } } } @@ -244,16 +153,118 @@ pipeline { // 辅助函数 // ============================================ +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}..." + + sh """ + ssh -o StrictHostKeyChecking=no 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} + ' + """ + + // 健康检查 + sh """ + ssh -o StrictHostKeyChecking=no root@${DEPLOY_HOST} ' + for i in 1 2 3 4 5 6 7 8 9 10 11 12; do + STATUS=\$(docker inspect --format="{{.State.Health.Status}}" aiot-${service} 2>/dev/null || echo "starting") + [ "\$STATUS" = "healthy" ] && echo "${service} is healthy" && exit 0 + sleep 10 + done + echo "${service} health check timeout" + ' + """ + + echo "✅ ${service} deployed" +} + def getModulePath(String service) { - // 根据服务名获取 Maven 模块路径 - def moduleMap = [ + 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' - // viewsh-module-ops-server 暂未合并到 master,待开发完成后添加 ] - - return moduleMap[service] ?: service + return map[service] ?: service } diff --git a/docker/Dockerfile.deps b/docker/Dockerfile.deps new file mode 100644 index 0000000..a8bacbf --- /dev/null +++ b/docker/Dockerfile.deps @@ -0,0 +1,38 @@ +# ============================================ +# Maven 依赖基础镜像 +# 预下载所有依赖,供服务构建时复用 +# ============================================ + +FROM eclipse-temurin:17-jdk-alpine + +# 安装 Maven +RUN apk add --no-cache maven + +WORKDIR /build + +# 复制所有 pom 文件 +COPY pom.xml . +COPY viewsh-dependencies/pom.xml viewsh-dependencies/ +COPY viewsh-framework/pom.xml viewsh-framework/ +COPY viewsh-gateway/pom.xml viewsh-gateway/ +COPY viewsh-server/pom.xml viewsh-server/ +COPY viewsh-module-system/pom.xml viewsh-module-system/ +COPY viewsh-module-system/viewsh-module-system-api/pom.xml viewsh-module-system/viewsh-module-system-api/ +COPY viewsh-module-system/viewsh-module-system-server/pom.xml viewsh-module-system/viewsh-module-system-server/ +COPY viewsh-module-infra/pom.xml viewsh-module-infra/ +COPY viewsh-module-infra/viewsh-module-infra-api/pom.xml viewsh-module-infra/viewsh-module-infra-api/ +COPY viewsh-module-infra/viewsh-module-infra-server/pom.xml viewsh-module-infra/viewsh-module-infra-server/ +COPY viewsh-module-iot/pom.xml viewsh-module-iot/ +COPY viewsh-module-iot/viewsh-module-iot-core/pom.xml viewsh-module-iot/viewsh-module-iot-core/ +COPY viewsh-module-iot/viewsh-module-iot-api/pom.xml viewsh-module-iot/viewsh-module-iot-api/ +COPY viewsh-module-iot/viewsh-module-iot-server/pom.xml viewsh-module-iot/viewsh-module-iot-server/ +COPY viewsh-module-iot/viewsh-module-iot-gateway/pom.xml viewsh-module-iot/viewsh-module-iot-gateway/ + +# 下载所有依赖到本地仓库 +RUN mvn dependency:go-offline -B || true + +# 复制源代码 +COPY . . + +# 预编译 framework 和 dependencies(所有服务共享) +RUN mvn install -pl viewsh-dependencies,viewsh-framework -am -DskipTests -B -q || true diff --git a/docker/Dockerfile.service b/docker/Dockerfile.service new file mode 100644 index 0000000..4956b66 --- /dev/null +++ b/docker/Dockerfile.service @@ -0,0 +1,69 @@ +# ============================================ +# 服务构建 Dockerfile +# 基于依赖基础镜像,快速编译服务 +# ============================================ + +# 构建参数 +ARG DEPS_IMAGE=aiot-deps:latest + +# ============ 构建阶段 ============ +FROM ${DEPS_IMAGE} AS builder + +ARG MODULE_NAME +ARG JAR_NAME +ARG SKIP_TESTS=true + +WORKDIR /build + +# 复制最新源代码(覆盖基础镜像中的代码) +COPY . . + +# 编译指定模块(依赖已经在基础镜像中准备好) +RUN mvn package -pl ${MODULE_NAME} -am -DskipTests=${SKIP_TESTS} -B -q -o || \ + mvn package -pl ${MODULE_NAME} -am -DskipTests=${SKIP_TESTS} -B -q + +# ============ 运行阶段 ============ +FROM eclipse-temurin:17-jre-alpine + +# 安装必要工具 +RUN apk add --no-cache wget curl + +# 构建参数 +ARG MODULE_NAME +ARG JAR_NAME +ARG APP_PORT=48080 + +# 元数据标签 +LABEL maintainer="XW-AIOT Team" +LABEL service="${MODULE_NAME}" + +# 创建非 root 用户 +RUN addgroup -g 1000 appuser && \ + adduser -D -u 1000 -G appuser appuser + +# 创建应用目录 +RUN mkdir -p /app/logs /app/config && \ + chown -R appuser:appuser /app + +WORKDIR /app + +# 从构建阶段复制 JAR 文件 +COPY --from=builder --chown=appuser:appuser /build/${MODULE_NAME}/target/${JAR_NAME}.jar app.jar + +# 切换到非 root 用户 +USER appuser + +# 环境变量 +ENV TZ=Asia/Shanghai \ + JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200" \ + SPRING_PROFILES_ACTIVE=prod + +# 暴露端口 +EXPOSE ${APP_PORT} + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:${APP_PORT}/actuator/health || exit 1 + +# 启动应用 +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar app.jar"]