diff --git a/Dockerfile/python/lessie-sourcing-agents/Dockerfile b/Dockerfile/python/lessie-sourcing-agents/Dockerfile index b4508a6..7775c02 100644 --- a/Dockerfile/python/lessie-sourcing-agents/Dockerfile +++ b/Dockerfile/python/lessie-sourcing-agents/Dockerfile @@ -1,3 +1,6 @@ +############################################### +# Stage 1: Builder (构建依赖 + venv) +############################################### FROM uswccr.ccs.tencentyun.com/lessie/python:3.12-slim AS builder ENV PYTHONDONTWRITEBYTECODE=1 \ @@ -5,32 +8,71 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ WORKDIR /data/webapps/lessie_sourcing_agents -# Install build prerequisites required for native wheels, then remove leftover apt metadata. -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - ca-certificates \ - cmake \ - curl \ - git \ - libomp-dev \ - libopenblas-dev \ - ninja-build \ - pkg-config \ - && \ +# ------------------------------------------------------------ +# 1. APT 源替换 + 安装依赖(合并为一个 RUN,最大化缓存) +# ------------------------------------------------------------ +RUN set -eux; \ + sed -i 's@deb.debian.org@mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list.d/debian.sources; \ + sed -i 's@security.debian.org@mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list.d/debian.sources; \ + \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + cmake \ + curl \ + git \ + libomp-dev \ + libopenblas-dev \ + ninja-build \ + pkg-config; \ rm -rf /var/lib/apt/lists/* +# ------------------------------------------------------------ +# 2. Pip 国内源设置(提前设置以便缓存命中) +# ------------------------------------------------------------ +RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ \ + && pip config set global.trusted-host mirrors.aliyun.com + +# ------------------------------------------------------------ +# 3. 安装 uv +# ------------------------------------------------------------ RUN python -m pip install --no-cache-dir "uv" +# ------------------------------------------------------------ +# 4. 拷贝依赖文件(只有这些变了才会重新跑 uv sync) +# ------------------------------------------------------------ COPY pyproject.toml uv.lock requirements.in requirements.txt ./ -# RUN python -m uv venv --python /usr/local/bin/python /opt/venv && \ -# python -m uv pip install --python /opt/venv/bin/python --no-cache -r requirements.txt - ENV UV_PROJECT_ENVIRONMENT=/opt/venv -RUN python -m uv venv /opt/venv && \ - python -m uv sync --frozen --no-dev --python /usr/local/bin/python +# ------------------------------------------------------------ +# 5. 设置 uv 变量(放在 COPY 前面确保有效缓存) +# ------------------------------------------------------------ +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + UV_HTTP_TIMEOUT=600 \ + UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \ + UV_EXTRA_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple + + +# ------------------------------------------------------------ +# 6. 创建 venv + 安装依赖(强烈建议使用 BuildKit 的 cache mount) +# ------------------------------------------------------------ +RUN --mount=type=cache,target=/root/.cache \ + python -m uv venv /opt/venv && \ + python -m uv sync --frozen --no-dev --python /usr/local/bin/python --index $UV_INDEX_URL + +# ------------------------------------------------------------ +# 7. llama_index 强烈推荐写入 pyproject.toml,而不是单独安装 +# ------------------------------------------------------------ +RUN python -m uv pip install --python /opt/venv/bin/python --no-cache-dir llama_index + + +############################################### +# Stage 2: Runtime (最小化生产镜像) +############################################### FROM uswccr.ccs.tencentyun.com/lessie/python:3.12-slim AS runtime ARG APP_PORT=8000 @@ -40,32 +82,35 @@ ENV APP_ENV=local \ WORKDIR /data/webapps/lessie_sourcing_agents -# Runtime dependencies required by packages like faiss-cpu. -RUN apt-get update && apt-get install -y --no-install-recommends \ - libgomp1 \ - libomp5 \ - libopenblas0 \ - && \ - rm -rf /var/lib/apt/lists/* - -# Bring in the pre-built virtual environment from the builder stage. +# ------------------------------------------------------------ +# 8. 拷贝 venv(仅包含已安装依赖) +# ------------------------------------------------------------ COPY --from=builder /opt/venv /opt/venv -# Ship only the application sources in the runtime image. +# ------------------------------------------------------------ +# 9. 仅拷贝业务代码(不会影响依赖缓存) +# ------------------------------------------------------------ COPY . . +# ------------------------------------------------------------ +# 10. 设置环境变量和 PATH +# ------------------------------------------------------------ ENV PATH="/opt/venv/bin:${PATH}" \ VIRTUAL_ENV="/opt/venv" EXPOSE ${APP_PORT} +# ------------------------------------------------------------ +# 11. 日志目录 +# ------------------------------------------------------------ RUN mkdir -p /data/webapps/lessie_sourcing_agents/logs +ENV LOG_DIR=/data/webapps/lessie_sourcing_agents/logs +VOLUME ["/data/webapps/lessie_sourcing_agents/logs"] +# ------------------------------------------------------------ +# 12. Entrypoint +# ------------------------------------------------------------ COPY scripts/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh -ENV LOG_DIR=/data/webapps/lessie_sourcing_agents/logs - -VOLUME ["/data/webapps/lessie_sourcing_agents/logs"] - ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/Dockerfile/web/lessie_official_web_Dockerfile b/Dockerfile/web/lessie_official_web_Dockerfile index 7da4a15..cedcee1 100644 --- a/Dockerfile/web/lessie_official_web_Dockerfile +++ b/Dockerfile/web/lessie_official_web_Dockerfile @@ -44,11 +44,8 @@ RUN pnpm run build RUN ls -a - - FROM node:20-slim AS runtime - # 安装 nginx RUN apt-get update && \ apt-get install -y --no-install-recommends nginx && \ diff --git a/SCM/构建镜像/v3/build_image_apex_v3.groovy b/SCM/构建镜像/v3/build_image_apex_v3.groovy new file mode 100644 index 0000000..11a1c20 --- /dev/null +++ b/SCM/构建镜像/v3/build_image_apex_v3.groovy @@ -0,0 +1,351 @@ +// --- 辅助函数:深拷贝对象以确保可序列化 --- +def deepCopyForSerialization(obj) { + if (obj instanceof Map) { + // 创建新的 LinkedHashMap,递归拷贝值 + return obj.collectEntries { k, v -> [(k): deepCopyForSerialization(v)] } + } else if (obj instanceof List) { + // 创建新的 ArrayList,递归拷贝元素 + return obj.collect { item -> deepCopyForSerialization(item) } + } else if (obj instanceof String || obj instanceof Number || obj instanceof Boolean || obj == null) { + return obj + } else { + return obj.toString() + } +} +// --- 结束辅助函数 --- + +pipeline { + agent any + + parameters { + gitParameter( + branchFilter: 'origin/(.*)', + defaultValue: 'dxin', + name: 'Code_branch', + type: 'PT_BRANCH_TAG', + selectedValue: 'DEFAULT', + sortMode: 'NONE', + description: '选择代码分支: ', + quickFilterEnabled: true, + tagFilter: '*', + listSize: "1" + ) + choice( + name: 'NAME_SPACES', + choices: ['sit', 'test', 'prod'], + description: '选择存放镜像的仓库命名空间:' + ) + string( + name: 'CUSTOM_TAG', + defaultValue: '', + description: '可选:自定义镜像 Tag (字母、数字、点、下划线、短横线), 如 v0.0.1, 留空则自动生成 “ v+构建次数_分支名_短哈希_构建时间 ”' + ) + booleanParam( + name: 'DEPLOY_TO_K8S', + defaultValue: false, + description: '构建成功后自动部署到 K8S 环境 (触发 job: DM_apex)' + ) + } + environment { + REGISTRY = "uswccr.ccs.tencentyun.com" // 镜像仓库地址 + NAMESPACE = "lessie${params.NAME_SPACES}" // 命名空间根据choices的选择拼接 + IMAGE_NAME = "apex" // 镜像名(固定前缀) + CREDENTIALS_ID = "dxin_img_hub_auth" // 容器仓库凭证ID + } + + stages { + stage('拉取代码') { + steps { + git branch: "${params.Code_branch}", + credentialsId: 'fly_gitlab_auth', + url: 'http://172.24.16.20/root/apex.git' + } + } + + stage('获取信息') { + steps { + script { + // 获取分支名 + env.Code_branch = "${params.Code_branch}" + // 获取最近一次提交的哈希值(短格式,前8位) + env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD',returnStdout: true).trim() + // 获取最近一次提交的哈希值(全格式) + env.GIT_COMMIT_LONG = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + // 获取最近一次提交的作者 + env.GIT_AUTHOR = sh(script: 'git log -1 --pretty=format:%an',returnStdout: true).trim() + // 获取最近一次提交的时间(格式化) + env.GIT_COMMIT_TIME = sh( + script: 'git log -1 --pretty=format:%ct | xargs -I {} date -d @{} +%Y%m%d-%H%M%S', + returnStdout: true + ).trim() + // 获取最近一次提交的备注信息(转义特殊字符,避免构建失败) + env.GIT_COMMIT_MSG = sh(script: 'git log -1 --pretty=format:%s | sed -e \'s/"/\\"/g\'', returnStdout: true).trim() + + // Jenkins构建次数 + def buildNumber = env.BUILD_NUMBER // Jenkins内置变量,直接获取当前Job的构建序号 + // 当前分支名(处理/为-,如feature/docker_1015 → feature-docker_1015) + def branchName = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim() + def formattedBranch = branchName.replace('/', '-').replace('_', '-') // 替换分支名中的/和_为- + // 构建时间(格式:202510181215,年-月-日-时-分,无分隔符) + def buildTime = sh(script: 'date +%Y%m%d%H%M', returnStdout: true).trim() + def defaultTag = "v${buildNumber}_${formattedBranch}_${GIT_COMMIT_SHORT}_${buildTime}" + + def customTag = params.CUSTOM_TAG?.trim() + def tagPattern = ~/^[a-zA-Z0-9._-]+$/ + + // 判断最终Tag + if (customTag && customTag ==~ tagPattern) { + echo "✅ 使用自定义镜像 Tag: ${customTag}" + env.IMAGE_TAG = customTag + } else if (customTag) { + echo "⚠️ 自定义 Tag '${customTag}' 不符合规范,将使用默认生成的 Tag: ${defaultTag}" + + def confirmed = true + timeout(time: 1, unit: 'MINUTES') { + try { + input( + message: """⚠️ Tag 命名不规范: + ${customTag} + + 将使用自动生成的 Tag: + ${defaultTag} + + 是否继续构建?""", + ok: '确认' + ) + } catch (err) { + // 用户点击“取消”或中断 + echo "🚫 用户取消构建" + confirmed = false + } + } + + if (confirmed) { + echo "✅ 用户确认使用自动生成的 Tag:${defaultTag}" + env.IMAGE_TAG = defaultTag + } else { + error("流水线已终止。") + } + } else { + env.IMAGE_TAG = defaultTag + echo "未输入自定义 Tag, 使用自动生成规则: ${env.IMAGE_TAG}" + } + } + } + } + + stage('登录仓库') { + steps { + withCredentials([usernamePassword( + credentialsId: env.CREDENTIALS_ID, + usernameVariable: 'REGISTRY_USER', + passwordVariable: 'REGISTRY_PWD' + )]) { + sh ''' + echo "$REGISTRY_PWD" | docker login ${REGISTRY} -u ${REGISTRY_USER} --password-stdin + ''' + } + } + } + + stage('构建镜像') { + steps { + script { + // 构建镜像,添加标签信息 + sh """ + docker build -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG} \ + --label "git-branch='${Code_branch}'" \ + --label "git-commit='${GIT_COMMIT_SHORT}'" \ + --label "git-author='${GIT_AUTHOR}'" \ + --label "git-message='${GIT_COMMIT_MSG}'" \ + --label "build-time='${GIT_COMMIT_TIME}'" \ + . + """ + } + } + } + + stage('推送仓库') { + steps { + script { + sh "docker push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + echo "推送镜像成功:${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + } + } + } + } + + post { + always { + script { + def keepCount = 3 + echo "开始清理本地旧镜像,仅保留最近 ${keepCount} 个构建版本" + def imagePrefix = "${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}" + + // 获取所有镜像(按创建时间排序,越新的越前) + // 格式:Repository:Tag ImageID CreatedAt + def allImagesRaw = sh(script: "docker images ${imagePrefix} --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedAt}}' | sort -rk3", returnStdout: true).trim() + if (!allImagesRaw) { + echo "未找到任何镜像,无需清理" + return + } + + def allImages = allImagesRaw.split('\n') + if (allImages.size() <= keepCount) { + echo "当前镜像数未超过 ${keepCount} 个,无需清理" + return + } + + def oldImages = allImages.drop(keepCount) + echo "发现 ${oldImages.size()} 个旧镜像需要清理" + oldImages.each { line -> + echo " ${line}" + } + + oldImages.each { line -> + def parts = line.split(' ') + def imageTag = parts[0] + def imageId = parts.size() > 1 ? parts[1] : "" + + // 对于标签为的无效镜像,使用镜像ID删除 + if (imageTag.contains("") && imageId) { + echo "删除无效镜像: ${imageId}" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else if (imageId) { + // 对于有标签的有效镜像,优先使用镜像ID删除 + echo "删除旧镜像: ${imageTag} (${imageId})" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else { + // 兜底方案,使用标签删除 + echo "删除旧镜像: ${imageTag}" + sh(returnStatus: true, script: "docker rmi -f ${imageTag} || true") + } + } + + echo "清理完成,当前镜像状态:" + sh """ + docker images ${imagePrefix} --format 'table {{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.Size}}' + """ + } + } + success { + script { + // 1. 准备元数据 (转换所有环境变量为 String) + def metadataDir = '/var/lib/jenkins/metadata' + def metadataFileRelativePath = "${env.NAMESPACE}-${env.IMAGE_NAME}.json" // 相对于 metadataDir 的文件名 + def fullMetadataPath = "${metadataDir}/${metadataFileRelativePath}" + + // --- 转换为 String --- + def registry = env.REGISTRY as String + def namespace = env.NAMESPACE as String + def imageName = env.IMAGE_NAME as String + def imageTag = env.IMAGE_TAG as String + def codeBranch = params.Code_branch as String // 使用 params,因为 Code_branch 是参数 + def gitCommit = env.GIT_COMMIT_LONG as String + def gitAuthor = env.GIT_AUTHOR as String + def gitCommitMsg = env.GIT_COMMIT_MSG as String + def gitCommitTime = env.GIT_COMMIT_TIME as String + def buildNumber = env.BUILD_NUMBER as String + // --- 转换为 String --- + + // 2. 准备新数据 + def newImageData = [ + image_tag: imageTag, // 使用转换后的变量 + full_image_name: "${registry}/${namespace}/${imageName}:${imageTag}", // 使用转换后的变量 + labels: [ + "git-branch": codeBranch, + "git-commit": gitCommit, + "git-author": gitAuthor, + "git-message": gitCommitMsg, + "build-time": gitCommitTime + ], + build_job_number: buildNumber, + build_time: new Date().format('yyyy-MM-dd HH:mm:ss') // Jenkins 构建完成时间 + ] + + // 2. 读取现有数据(如果文件存在) + def existingDataList = [] + try { + // 使用 readJSON 步骤读取文件内容 (readJSON 会自动处理 LazyMap 问题) + def rawExistingData = readJSON file: fullMetadataPath, default: [] // 如果文件不存在,则返回空列表 [] + + // --- 修复:深拷贝 rawExistingData (修正内联代码) --- + if (rawExistingData instanceof List) { + existingDataList = rawExistingData.collect { item -> + if (item instanceof Map) { + // 递归深拷贝 Map (使用辅助函数) + return deepCopyForSerialization(item) + } else { + return item + } + } + } else { + echo "警告: 元数据文件 ${fullMetadataPath} 格式不正确(非 List 类型),将被覆盖。" + existingDataList = [] + } + // --- 结束修复 --- + + } catch (Exception e) { + // readJSON 在文件不存在时通常会返回 default 值,但如果文件存在但格式错误,会抛出异常 + echo "警告: 读取元数据文件 ${fullMetadataPath} 失败或格式错误: ${e.getMessage()},将被覆盖。" + // 确保目录存在 + sh "mkdir -p ${metadataDir}" + existingDataList = [] // 重置为新列表 + } + + // 3. 将新数据添加到列表开头(最新的在前) + existingDataList.add(0, newImageData) + + // 4. 限制列表大小为 20 + if (existingDataList.size() > 20) { + existingDataList = existingDataList.take(20) + } + + // 5. 使用 writeJSON 步骤写入文件 (writeJSON 会自动处理 Map 的序列化) + writeJSON file: fullMetadataPath, json: existingDataList, pretty: 2 // pretty: 2 表示格式化 JSON (2 个空格缩进) + + echo "镜像元数据已存储到: ${fullMetadataPath}" + + // 输出构建结果 + echo """ + 镜像地址:${registry}/${namespace}/${imageName}:${imageTag} + 对应代码提交哈希:${gitCommit} + 对应代码分支:${codeBranch} + 代码提交者:${gitAuthor} + 提交备注:${gitCommitMsg} + """.stripIndent() + } + // 构建成功后,若选择则触发部署到 S1 环境 + script { + // 定义部署映射:参数名 → 部署 Job 名 + def deployMap = [ + 'DEPLOY_TO_K8S' : 'DM_apex' + ] + + // 获取完整镜像名(从已有 env 变量拼接) + def fullImageName = "${env.REGISTRY}/${env.NAMESPACE}/${env.IMAGE_NAME}:${env.IMAGE_TAG}" + + echo "构建完成镜像:${fullImageName}" + echo "检查部署触发项..." + + deployMap.each { paramKey, jobName -> + if (params."${paramKey}" == true) { + echo "触发部署:${jobName} ← ${fullImageName}" + // 异步触发 + build job: jobName, + parameters: [ + string(name: 'CUSTOM_IMAGE', value: fullImageName), + ], + wait: false // ← 异步(此 Job 结束,不等待被调用的 Job 结束) + } else { + echo "未勾选 ${paramKey}, 跳过触发部署:${jobName}" + } + } + } + } + failure { + // 输出构建结果 + echo "部署有错误,请检查!" + } + } +} \ No newline at end of file diff --git a/SCM/构建镜像/v3/build_image_apex_web_v3.groovy b/SCM/构建镜像/v3/build_image_apex_web_v3.groovy new file mode 100644 index 0000000..79de64b --- /dev/null +++ b/SCM/构建镜像/v3/build_image_apex_web_v3.groovy @@ -0,0 +1,354 @@ +// --- 辅助函数:深拷贝对象以确保可序列化 --- +def deepCopyForSerialization(obj) { + if (obj instanceof Map) { + // 创建新的 LinkedHashMap,递归拷贝值 + return obj.collectEntries { k, v -> [(k): deepCopyForSerialization(v)] } + } else if (obj instanceof List) { + // 创建新的 ArrayList,递归拷贝元素 + return obj.collect { item -> deepCopyForSerialization(item) } + } else if (obj instanceof String || obj instanceof Number || obj instanceof Boolean || obj == null) { + return obj + } else { + return obj.toString() + } +} +// --- 结束辅助函数 --- + +pipeline { + agent any + + parameters { + gitParameter( + branchFilter: 'origin/(.*)', + defaultValue: 'feat-tag', + name: 'Code_branch', + type: 'PT_BRANCH_TAG', + selectedValue: 'DEFAULT', + sortMode: 'NONE', + description: '选择代码分支: ', + quickFilterEnabled: true, + tagFilter: '*', + listSize: "1" + ) + choice( + name: 'NAME_SPACES', + choices: ['sit', 'test', 'prod'], + description: '选择存放镜像的仓库命名空间:' + ) + string( + name: 'CUSTOM_TAG', + defaultValue: '', + description: '可选:自定义镜像 Tag (字母、数字、点、下划线、短横线), 如 v0.0.1, 留空则自动生成 “ v+构建次数_分支名_短哈希_构建时间 ”' + ) + booleanParam( + name: 'DEPLOY_TO_K8S', + defaultValue: false, + description: '构建成功后自动部署到 K8S 环境 (触发 job: DM_apex_web)' + ) + } + environment { + REGISTRY = "uswccr.ccs.tencentyun.com" // 镜像仓库地址 + NAMESPACE = "lessie${params.NAME_SPACES}" // 命名空间根据choices的选择拼接 + IMAGE_NAME = "apex-web" // 镜像名(固定前缀) + CREDENTIALS_ID = "dxin_img_hub_auth" // 容器仓库凭证ID + } + + stages { + stage('拉取代码') { + steps { + git branch: "${params.Code_branch}", + credentialsId: 'fly_gitlab_auth', + url: 'http://172.24.16.20/web/apex-platform-fe.git' + } + } + + stage('获取信息') { + steps { + script { + // 获取分支名 + env.Code_branch = "${params.Code_branch}" + // 获取最近一次提交的哈希值(短格式,前8位) + env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD',returnStdout: true).trim() + // 获取最近一次提交的哈希值(全格式) + env.GIT_COMMIT_LONG = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + // 获取最近一次提交的作者 + env.GIT_AUTHOR = sh(script: 'git log -1 --pretty=format:%an',returnStdout: true).trim() + // 获取最近一次提交的时间(格式化) + env.GIT_COMMIT_TIME = sh( + script: 'git log -1 --pretty=format:%ct | xargs -I {} date -d @{} +%Y%m%d-%H%M%S', + returnStdout: true + ).trim() + // 获取最近一次提交的备注信息(转义特殊字符,避免构建失败) + env.GIT_COMMIT_MSG = sh(script: 'git log -1 --pretty=format:%s | sed -e \'s/"/\\"/g\'', returnStdout: true).trim() + + // Jenkins构建次数 + def buildNumber = env.BUILD_NUMBER // Jenkins内置变量,直接获取当前Job的构建序号 + // 当前分支名(处理/为-,如feature/docker_1015 → feature-docker_1015) + def branchName = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim() + def formattedBranch = branchName.replace('/', '-').replace('_', '-') // 替换分支名中的/和_为- + // 构建时间(格式:202510181215,年-月-日-时-分,无分隔符) + def buildTime = sh(script: 'date +%Y%m%d%H%M', returnStdout: true).trim() + def defaultTag = "v${buildNumber}_${formattedBranch}_${GIT_COMMIT_SHORT}_${buildTime}" + + def customTag = params.CUSTOM_TAG?.trim() + def tagPattern = ~/^[a-zA-Z0-9._-]+$/ + + // 判断最终Tag + if (customTag && customTag ==~ tagPattern) { + echo "✅ 使用自定义镜像 Tag: ${customTag}" + env.IMAGE_TAG = customTag + } else if (customTag) { + echo "⚠️ 自定义 Tag '${customTag}' 不符合规范,将使用默认生成的 Tag: ${defaultTag}" + + def confirmed = true + timeout(time: 1, unit: 'MINUTES') { + try { + input( + message: """⚠️ Tag 命名不规范: + ${customTag} + + 将使用自动生成的 Tag: + ${defaultTag} + + 是否继续构建?""", + ok: '确认' + ) + } catch (err) { + // 用户点击“取消”或中断 + echo "🚫 用户取消构建" + confirmed = false + } + } + + if (confirmed) { + echo "✅ 用户确认使用自动生成的 Tag:${defaultTag}" + env.IMAGE_TAG = defaultTag + } else { + error("流水线已终止。") + } + } else { + env.IMAGE_TAG = defaultTag + echo "未输入自定义 Tag, 使用自动生成规则: ${env.IMAGE_TAG}" + } + } + } + } + + stage('登录仓库') { + steps { + withCredentials([usernamePassword( + credentialsId: env.CREDENTIALS_ID, + usernameVariable: 'REGISTRY_USER', + passwordVariable: 'REGISTRY_PWD' + )]) { + sh ''' + echo "$REGISTRY_PWD" | docker login ${REGISTRY} -u ${REGISTRY_USER} --password-stdin + ''' + } + } + } + + stage('构建镜像') { + steps { + script { + // 构建镜像,添加标签信息 + sh """ + docker build -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG} \ + --label "git-branch='${Code_branch}'" \ + --label "git-commit='${GIT_COMMIT_SHORT}'" \ + --label "git-author='${GIT_AUTHOR}'" \ + --label "git-message='${GIT_COMMIT_MSG}'" \ + --label "build-time='${GIT_COMMIT_TIME}'" \ + . + """ + } + } + } + + stage('推送仓库') { + steps { + script { + sh "docker push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + echo "推送镜像成功:${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + } + } + } + } + + post { + always { + script { + def keepCount = 3 + echo "开始清理本地旧镜像,仅保留最近 ${keepCount} 个构建版本" + def imagePrefix = "${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}" + + // 获取所有镜像(按创建时间排序,越新的越前) + // 格式:Repository:Tag ImageID CreatedAt + def allImagesRaw = sh(script: "docker images ${imagePrefix} --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedAt}}' | sort -rk3", returnStdout: true).trim() + if (!allImagesRaw) { + echo "未找到任何镜像,无需清理" + return + } + + def allImages = allImagesRaw.split('\n') + if (allImages.size() <= keepCount) { + echo "当前镜像数未超过 ${keepCount} 个,无需清理" + return + } + + def oldImages = allImages.drop(keepCount) + echo "发现 ${oldImages.size()} 个旧镜像需要清理" + oldImages.each { line -> + echo " ${line}" + } + + oldImages.each { line -> + def parts = line.split(' ') + def imageTag = parts[0] + def imageId = parts.size() > 1 ? parts[1] : "" + + // 对于标签为的无效镜像,使用镜像ID删除 + if (imageTag.contains("") && imageId) { + echo "删除无效镜像: ${imageId}" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else if (imageId) { + // 对于有标签的有效镜像,优先使用镜像ID删除 + echo "删除旧镜像: ${imageTag} (${imageId})" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else { + // 兜底方案,使用标签删除 + echo "删除旧镜像: ${imageTag}" + sh(returnStatus: true, script: "docker rmi -f ${imageTag} || true") + } + } + + echo "清理完成,当前镜像状态:" + sh """ + docker images ${imagePrefix} --format 'table {{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.Size}}' + """ + + sh "docker logout ${REGISTRY}" + echo "容器仓库已登出,本地凭证已清理" + } + } + success { + script { + // 1. 准备元数据 (转换所有环境变量为 String) + def metadataDir = '/var/lib/jenkins/metadata' + def metadataFileRelativePath = "${env.NAMESPACE}-${env.IMAGE_NAME}.json" // 相对于 metadataDir 的文件名 + def fullMetadataPath = "${metadataDir}/${metadataFileRelativePath}" + + // --- 转换为 String --- + def registry = env.REGISTRY as String + def namespace = env.NAMESPACE as String + def imageName = env.IMAGE_NAME as String + def imageTag = env.IMAGE_TAG as String + def codeBranch = params.Code_branch as String // 使用 params,因为 Code_branch 是参数 + def gitCommit = env.GIT_COMMIT_LONG as String + def gitAuthor = env.GIT_AUTHOR as String + def gitCommitMsg = env.GIT_COMMIT_MSG as String + def gitCommitTime = env.GIT_COMMIT_TIME as String + def buildNumber = env.BUILD_NUMBER as String + // --- 转换为 String --- + + // 2. 准备新数据 + def newImageData = [ + image_tag: imageTag, // 使用转换后的变量 + full_image_name: "${registry}/${namespace}/${imageName}:${imageTag}", // 使用转换后的变量 + labels: [ + "git-branch": codeBranch, + "git-commit": gitCommit, + "git-author": gitAuthor, + "git-message": gitCommitMsg, + "build-time": gitCommitTime + ], + build_job_number: buildNumber, + build_time: new Date().format('yyyy-MM-dd HH:mm:ss') // Jenkins 构建完成时间 + ] + + // 2. 读取现有数据(如果文件存在) + def existingDataList = [] + try { + // 使用 readJSON 步骤读取文件内容 (readJSON 会自动处理 LazyMap 问题) + def rawExistingData = readJSON file: fullMetadataPath, default: [] // 如果文件不存在,则返回空列表 [] + + // --- ✅ 修复:深拷贝 rawExistingData (修正内联代码) --- + if (rawExistingData instanceof List) { + existingDataList = rawExistingData.collect { item -> + if (item instanceof Map) { + // 递归深拷贝 Map (使用辅助函数) + return deepCopyForSerialization(item) + } else { + return item + } + } + } else { + echo "警告: 元数据文件 ${fullMetadataPath} 格式不正确(非 List 类型),将被覆盖。" + existingDataList = [] + } + // --- 结束修复 --- + + } catch (Exception e) { + // readJSON 在文件不存在时通常会返回 default 值,但如果文件存在但格式错误,会抛出异常 + echo "警告: 读取元数据文件 ${fullMetadataPath} 失败或格式错误: ${e.getMessage()},将被覆盖。" + // 确保目录存在 + sh "mkdir -p ${metadataDir}" + existingDataList = [] // 重置为新列表 + } + + // 3. 将新数据添加到列表开头(最新的在前) + existingDataList.add(0, newImageData) + + // 4. 限制列表大小为 20 + if (existingDataList.size() > 20) { + existingDataList = existingDataList.take(20) + } + + // 5. 使用 writeJSON 步骤写入文件 (writeJSON 会自动处理 Map 的序列化) + writeJSON file: fullMetadataPath, json: existingDataList, pretty: 2 // pretty: 2 表示格式化 JSON (2 个空格缩进) + + echo "镜像元数据已存储到: ${fullMetadataPath}" + + // 输出构建结果 + echo """ + 镜像地址:${registry}/${namespace}/${imageName}:${imageTag} + 对应代码提交哈希:${gitCommit} + 对应代码分支:${codeBranch} + 代码提交者:${gitAuthor} + 提交备注:${gitCommitMsg} + """.stripIndent() + } + // 构建成功后,若选择则触发部署到 K8S 环境 + script { + // 定义部署映射:参数名 → 部署 Job 名 + def deployMap = [ + 'DEPLOY_TO_K8S' : 'DM_apex_web' + ] + + // 获取完整镜像名(从已有 env 变量拼接) + def fullImageName = "${env.REGISTRY}/${env.NAMESPACE}/${env.IMAGE_NAME}:${env.IMAGE_TAG}" + + echo "构建完成镜像:${fullImageName}" + echo "检查部署触发项..." + + deployMap.each { paramKey, jobName -> + if (params."${paramKey}" == true) { + echo "触发部署:${jobName} ← ${fullImageName}" + // 异步触发 + build job: jobName, + parameters: [ + string(name: 'CUSTOM_IMAGE', value: fullImageName), + ], + wait: false // ← 异步(此 Job 结束,不等待被调用的 Job 结束) + } else { + echo "未勾选 ${paramKey}, 跳过触发部署:${jobName}" + } + } + } + } + failure { + // 输出构建结果 + echo "部署有错误,请检查!" + } + } +} \ No newline at end of file diff --git a/SCM/构建镜像/v3/build_image_flymoon_admin_v3.groovy b/SCM/构建镜像/v3/build_image_flymoon_admin_v3.groovy new file mode 100644 index 0000000..afaf3a6 --- /dev/null +++ b/SCM/构建镜像/v3/build_image_flymoon_admin_v3.groovy @@ -0,0 +1,356 @@ +// --- 辅助函数:深拷贝对象以确保可序列化 --- +def deepCopyForSerialization(obj) { + if (obj instanceof Map) { + // 创建新的 LinkedHashMap,递归拷贝值 + return obj.collectEntries { k, v -> [(k): deepCopyForSerialization(v)] } + } else if (obj instanceof List) { + // 创建新的 ArrayList,递归拷贝元素 + return obj.collect { item -> deepCopyForSerialization(item) } + } else if (obj instanceof String || obj instanceof Number || obj instanceof Boolean || obj == null) { + return obj + } else { + return obj.toString() + } +} +// --- 结束辅助函数 --- + +pipeline { + agent any + tools{ + maven 'mvn3.8.8' + jdk 'jdk21' + } + parameters { + gitParameter( + branchFilter: 'origin/(.*)', + defaultValue: 'dxin', + name: 'Code_branch', + type: 'PT_BRANCH_TAG', + selectedValue: 'DEFAULT', + sortMode: 'NONE', + description: '选择代码分支: ', + quickFilterEnabled: true, + tagFilter: '*', + listSize: "1" + ) + choice( + name: 'NAME_SPACES', + choices: ['sit', 'test', 'prod'], + description: '选择存放镜像的仓库命名空间:' + ) + string( + name: 'CUSTOM_TAG', + defaultValue: '', + description: '可选:自定义镜像 Tag (字母、数字、点、下划线、短横线), 留空则自动生成 “ v+构建次数_分支名_短哈希_构建时间 ”' + ) + booleanParam( + name: 'DEPLOY_TO_S1', + defaultValue: false, + description: '可选:构建成功后部署到 S1 环境 (触发 job: DM_s1_flymoon_admin)' + ) + } + environment { + REGISTRY = "uswccr.ccs.tencentyun.com" // 镜像仓库地址 + NAMESPACE = "lessie${params.NAME_SPACES}" // 命名空间根据choices的选择拼接 + IMAGE_NAME = "flymoon-admin" // 镜像名(固定前缀) + CREDENTIALS_ID = "dxin_img_hub_auth" // 容器仓库凭证ID + } + + stages { + stage('拉取代码') { + steps { + git branch: "${params.Code_branch}", + credentialsId: 'fly_gitlab_auth', + url: 'http://172.24.16.20/root/fly_moon_admin.git' + } + } + + stage('获取信息') { + steps { + script { + // 获取分支名 + env.Code_branch = "${params.Code_branch}" + // 获取最近一次提交的哈希值(短格式,前8位) + env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD',returnStdout: true).trim() + // 获取最近一次提交的哈希值(全格式) + env.GIT_COMMIT_LONG = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + // 获取最近一次提交的作者 + env.GIT_AUTHOR = sh(script: 'git log -1 --pretty=format:%an',returnStdout: true).trim() + // 获取最近一次提交的时间(格式化) + env.GIT_COMMIT_TIME = sh( + script: 'git log -1 --pretty=format:%ct | xargs -I {} date -d @{} +%Y%m%d-%H%M%S', + returnStdout: true + ).trim() + // 获取最近一次提交的备注信息(转义特殊字符,避免构建失败) + env.GIT_COMMIT_MSG = sh(script: 'git log -1 --pretty=format:%s | sed -e \'s/"/\\"/g\'', returnStdout: true).trim() + + // Jenkins构建次数 + def buildNumber = env.BUILD_NUMBER // Jenkins内置变量,直接获取当前Job的构建序号 + // 当前分支名(处理/为-,如feature/docker_1015 → feature-docker_1015) + def branchName = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim() + def formattedBranch = branchName.replace('/', '-').replace('_', '-') // 替换分支名中的/和_为- + // 构建时间(格式:202510181215,年-月-日-时-分,无分隔符) + def buildTime = sh(script: 'date +%Y%m%d%H%M', returnStdout: true).trim() + def defaultTag = "v${buildNumber}_${formattedBranch}_${GIT_COMMIT_SHORT}_${buildTime}" + + def customTag = params.CUSTOM_TAG?.trim() + def tagPattern = ~/^[a-zA-Z0-9._-]+$/ + + // 判断最终Tag + if (customTag && customTag ==~ tagPattern) { + echo "✅ 使用自定义镜像 Tag: ${customTag}" + env.IMAGE_TAG = customTag + } else if (customTag) { + echo "⚠️ 自定义 Tag '${customTag}' 不符合规范,将使用默认生成的 Tag: ${defaultTag}" + + def confirmed = true + timeout(time: 1, unit: 'MINUTES') { + try { + input( + message: """⚠️ Tag 命名不规范: + ${customTag} + + 将使用自动生成的 Tag: + ${defaultTag} + + 是否继续构建?""", + ok: '确认' + ) + } catch (err) { + // 用户点击“取消”或中断 + echo "🚫 用户取消构建" + confirmed = false + } + } + + if (confirmed) { + echo "✅ 用户确认使用自动生成的 Tag:${defaultTag}" + env.IMAGE_TAG = defaultTag + } else { + error("流水线已终止。") + } + } else { + env.IMAGE_TAG = defaultTag + echo "未输入自定义 Tag, 使用自动生成规则: ${env.IMAGE_TAG}" + } + } + } + } + + stage('登录容器') { + steps { + withCredentials([usernamePassword( + credentialsId: env.CREDENTIALS_ID, + usernameVariable: 'REGISTRY_USER', + passwordVariable: 'REGISTRY_PWD' + )]) { + sh ''' + echo "$REGISTRY_PWD" | docker login ${REGISTRY} -u ${REGISTRY_USER} --password-stdin + ''' + } + } + } + + stage('构建镜像') { + steps { + script { + // 构建镜像,添加标签信息 + sh """ + docker build -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG} \ + --label "git-branch='${Code_branch}'" \ + --label "git-commit='${GIT_COMMIT_SHORT}'" \ + --label "git-author='${GIT_AUTHOR}'" \ + --label "git-message='${GIT_COMMIT_MSG}'" \ + --label "build-time='${GIT_COMMIT_TIME}'" \ + . + """ + } + } + } + + stage('推送镜像') { + steps { + script { + sh "docker push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + echo "推送镜像成功:${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + } + } + } + } + + post { + always { + script { + def keepCount = 3 + echo "开始清理本地旧镜像,仅保留最近 ${keepCount} 个构建版本" + def imagePrefix = "${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}" + + // 获取所有镜像(按创建时间排序,越新的越前) + // 格式:Repository:Tag ImageID CreatedAt + def allImagesRaw = sh(script: "docker images ${imagePrefix} --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedAt}}' | sort -rk3", returnStdout: true).trim() + if (!allImagesRaw) { + echo "未找到任何镜像,无需清理" + return + } + + def allImages = allImagesRaw.split('\n') + if (allImages.size() <= keepCount) { + echo "当前镜像数未超过 ${keepCount} 个,无需清理" + return + } + + def oldImages = allImages.drop(keepCount) + echo "发现 ${oldImages.size()} 个旧镜像需要清理" + oldImages.each { line -> + echo " ${line}" + } + + oldImages.each { line -> + def parts = line.split(' ') + def imageTag = parts[0] + def imageId = parts.size() > 1 ? parts[1] : "" + + // 对于标签为的无效镜像,使用镜像ID删除 + if (imageTag.contains("") && imageId) { + echo "删除无效镜像: ${imageId}" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else if (imageId) { + // 对于有标签的有效镜像,优先使用镜像ID删除 + echo "删除旧镜像: ${imageTag} (${imageId})" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else { + // 兜底方案,使用标签删除 + echo "删除旧镜像: ${imageTag}" + sh(returnStatus: true, script: "docker rmi -f ${imageTag} || true") + } + } + + echo "清理完成,当前镜像状态:" + sh """ + docker images ${imagePrefix} --format 'table {{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.Size}}' + """ + } + } + success { + script { + // 1. 准备元数据 (转换所有环境变量为 String) + def metadataDir = '/var/lib/jenkins/metadata' + def metadataFileRelativePath = "${env.NAMESPACE}-${env.IMAGE_NAME}.json" // 相对于 metadataDir 的文件名 + def fullMetadataPath = "${metadataDir}/${metadataFileRelativePath}" + + // --- 转换为 String --- + def registry = env.REGISTRY as String + def namespace = env.NAMESPACE as String + def imageName = env.IMAGE_NAME as String + def imageTag = env.IMAGE_TAG as String + def codeBranch = params.Code_branch as String // 使用 params,因为 Code_branch 是参数 + def gitCommit = env.GIT_COMMIT_LONG as String + def gitAuthor = env.GIT_AUTHOR as String + def gitCommitMsg = env.GIT_COMMIT_MSG as String + def gitCommitTime = env.GIT_COMMIT_TIME as String + def buildNumber = env.BUILD_NUMBER as String + // --- 转换为 String --- + + // 2. 准备新数据 + def newImageData = [ + image_tag: imageTag, // 使用转换后的变量 + full_image_name: "${registry}/${namespace}/${imageName}:${imageTag}", // 使用转换后的变量 + labels: [ + "git-branch": codeBranch, + "git-commit": gitCommit, + "git-author": gitAuthor, + "git-message": gitCommitMsg, + "build-time": gitCommitTime + ], + build_job_number: buildNumber, + build_time: new Date().format('yyyy-MM-dd HH:mm:ss') // Jenkins 构建完成时间 + ] + + // 2. 读取现有数据(如果文件存在) + def existingDataList = [] + try { + // 使用 readJSON 步骤读取文件内容 (readJSON 会自动处理 LazyMap 问题) + def rawExistingData = readJSON file: fullMetadataPath, default: [] // 如果文件不存在,则返回空列表 [] + + // --- ✅ 修复:深拷贝 rawExistingData (修正内联代码) --- + if (rawExistingData instanceof List) { + existingDataList = rawExistingData.collect { item -> + if (item instanceof Map) { + // 递归深拷贝 Map (使用辅助函数) + return deepCopyForSerialization(item) + } else { + return item + } + } + } else { + echo "警告: 元数据文件 ${fullMetadataPath} 格式不正确(非 List 类型),将被覆盖。" + existingDataList = [] + } + // --- 结束修复 --- + + } catch (Exception e) { + // readJSON 在文件不存在时通常会返回 default 值,但如果文件存在但格式错误,会抛出异常 + echo "警告: 读取元数据文件 ${fullMetadataPath} 失败或格式错误: ${e.getMessage()},将被覆盖。" + // 确保目录存在 + sh "mkdir -p ${metadataDir}" + existingDataList = [] // 重置为新列表 + } + + // 3. 将新数据添加到列表开头(最新的在前) + existingDataList.add(0, newImageData) + + // 4. 限制列表大小为 20 + if (existingDataList.size() > 20) { + existingDataList = existingDataList.take(20) + } + + // 5. 使用 writeJSON 步骤写入文件 (writeJSON 会自动处理 Map 的序列化) + writeJSON file: fullMetadataPath, json: existingDataList, pretty: 2 // pretty: 2 表示格式化 JSON (2 个空格缩进) + + echo "镜像元数据已存储到: ${fullMetadataPath}" + + // 输出构建结果 + echo """ + 镜像地址:${registry}/${namespace}/${imageName}:${imageTag} + 对应代码提交哈希:${gitCommit} + 对应代码分支:${codeBranch} + 代码提交者:${gitAuthor} + 提交备注:${gitCommitMsg} + """.stripIndent() + } + // 构建成功后,若选择则触发部署到 S1 环境 + script { + // 定义部署映射:参数名 → 部署 Job 名 + def deployMap = [ + 'DEPLOY_TO_S1' : 'DM_s1_flymoon_admin' + ] + + // 获取完整镜像名(从已有 env 变量拼接) + def fullImageName = "${env.REGISTRY}/${env.NAMESPACE}/${env.IMAGE_NAME}:${env.IMAGE_TAG}" + + echo "构建完成镜像:${fullImageName}" + echo "检查部署触发项..." + + deployMap.each { paramKey, jobName -> + if (params."${paramKey}" == true) { + echo "触发部署:${jobName} ← ${fullImageName}" + // 异步触发 + build job: jobName, + parameters: [ + string(name: 'CUSTOM_IMAGE', value: fullImageName), + ], + wait: false // ← 异步(此 Job 结束,不等待被调用的 Job 结束) + } else { + echo "未勾选 ${paramKey}, 跳过触发部署:${jobName}" + } + } + } + } + failure { + // 输出构建结果 + echo "有步骤失败,请检查!" + } + } +} + + diff --git a/SCM/构建镜像/v3/build_image_flymoon_admin_web_v3.groovy b/SCM/构建镜像/v3/build_image_flymoon_admin_web_v3.groovy new file mode 100644 index 0000000..c969ea2 --- /dev/null +++ b/SCM/构建镜像/v3/build_image_flymoon_admin_web_v3.groovy @@ -0,0 +1,362 @@ + +// --- 辅助函数:深拷贝对象以确保可序列化 --- +def deepCopyForSerialization(obj) { + if (obj instanceof Map) { + // 创建新的 LinkedHashMap,递归拷贝值 + return obj.collectEntries { k, v -> [(k): deepCopyForSerialization(v)] } + } else if (obj instanceof List) { + // 创建新的 ArrayList,递归拷贝元素 + return obj.collect { item -> deepCopyForSerialization(item) } + } else if (obj instanceof String || obj instanceof Number || obj instanceof Boolean || obj == null) { + return obj + } else { + return obj.toString() + } +} +// --- 结束辅助函数 --- + +pipeline { + agent any + + parameters { + gitParameter( + branchFilter: 'origin/(.*)', + defaultValue: 'dxin', + name: 'Code_branch', + type: 'PT_BRANCH_TAG', + selectedValue: 'DEFAULT', + sortMode: 'NONE', + description: '选择代码分支: ', + quickFilterEnabled: true, + tagFilter: '*', + listSize: "1" + ) + choice( + name: 'NAME_SPACES', + choices: ['sit', 'test', 'prod'], + description: '选择存放镜像的仓库命名空间:' + ) + choice( + name: 'BUILD_ENV', + choices: ['sit', 'test', 'prod'], + description: '选择构建的环境配置, 默认为 pnpm build:sit 构建' + ) + string( + name: 'CUSTOM_TAG', + defaultValue: '', + description: '可选:自定义镜像 Tag (字母、数字、点、下划线、短横线), 如 v0.0.1, 留空则自动生成 “ v+构建次数_分支名_短哈希_构建时间 ”' + ) + booleanParam( + name: 'DEPLOY_TO_S1', + defaultValue: false, + description: '可选:构建成功后部署到 S1 环境 (触发 job: DM_s1_flymoon_admin_web)' + ) + } + environment { + REGISTRY = "uswccr.ccs.tencentyun.com" // 镜像仓库地址 + NAMESPACE = "lessie${params.NAME_SPACES}" // 命名空间根据choices的选择拼接 + IMAGE_NAME = "flymoon-admin-web" // 镜像名(固定前缀) + CREDENTIALS_ID = "dxin_img_hub_auth" // 容器仓库凭证ID + } + + stages { + stage('拉取代码') { + steps { + git branch: "${params.Code_branch}", + credentialsId: 'fly_gitlab_auth', + url: 'http://172.24.16.20/root/fly_moon_web.git' + } + } + + stage('获取信息') { + steps { + script { + // 获取分支名 + env.Code_branch = "${params.Code_branch}" + // 获取最近一次提交的哈希值(短格式,前8位) + env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD',returnStdout: true).trim() + // 获取最近一次提交的哈希值(全格式) + env.GIT_COMMIT_LONG = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + // 获取最近一次提交的作者 + env.GIT_AUTHOR = sh(script: 'git log -1 --pretty=format:%an',returnStdout: true).trim() + // 获取最近一次提交的时间(格式化) + env.GIT_COMMIT_TIME = sh( + script: 'git log -1 --pretty=format:%ct | xargs -I {} date -d @{} +%Y%m%d-%H%M%S', + returnStdout: true + ).trim() + // 获取最近一次提交的备注信息(转义特殊字符,避免构建失败) + env.GIT_COMMIT_MSG = sh(script: 'git log -1 --pretty=format:%s | sed -e \'s/"/\\"/g\'', returnStdout: true).trim() + + // Jenkins构建次数 + def buildNumber = env.BUILD_NUMBER // Jenkins内置变量,直接获取当前Job的构建序号 + // 当前分支名(处理/为-,如feature/docker_1015 → feature-docker_1015) + def branchName = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim() + def formattedBranch = branchName.replace('/', '-').replace('_', '-') // 替换分支名中的/和_为- + // 构建时间(格式:202510181215,年-月-日-时-分,无分隔符) + def buildTime = sh(script: 'date +%Y%m%d%H%M', returnStdout: true).trim() + def defaultTag = "v${buildNumber}_${formattedBranch}_${GIT_COMMIT_SHORT}_${buildTime}" + + def customTag = params.CUSTOM_TAG?.trim() + def tagPattern = ~/^[a-zA-Z0-9._-]+$/ + + // 判断最终Tag + if (customTag && customTag ==~ tagPattern) { + echo "✅ 使用自定义镜像 Tag: ${customTag}" + env.IMAGE_TAG = customTag + } else if (customTag) { + echo "⚠️ 自定义 Tag '${customTag}' 不符合规范,将使用默认生成的 Tag: ${defaultTag}" + + def confirmed = true + timeout(time: 1, unit: 'MINUTES') { + try { + input( + message: """⚠️ Tag 命名不规范: + ${customTag} + + 将使用自动生成的 Tag: + ${defaultTag} + + 是否继续构建?""", + ok: '确认' + ) + } catch (err) { + // 用户点击“取消”或中断 + echo "🚫 用户取消构建" + confirmed = false + } + } + + if (confirmed) { + echo "✅ 用户确认使用自动生成的 Tag:${defaultTag}" + env.IMAGE_TAG = defaultTag + } else { + error("流水线已终止。") + } + } else { + env.IMAGE_TAG = defaultTag + echo "未输入自定义 Tag, 使用自动生成规则: ${env.IMAGE_TAG}" + } + } + } + } + + stage('登录容器') { + steps { + withCredentials([usernamePassword( + credentialsId: env.CREDENTIALS_ID, + usernameVariable: 'REGISTRY_USER', + passwordVariable: 'REGISTRY_PWD' + )]) { + sh ''' + echo "$REGISTRY_PWD" | docker login ${REGISTRY} -u ${REGISTRY_USER} --password-stdin + ''' + } + } + } + + stage('构建镜像') { + steps { + script { + // 构建镜像,添加标签信息 + sh """ + docker build \ + --build-arg BUILD_ENV=${params.BUILD_ENV} \ + -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG} \ + --label "git-branch='${Code_branch}'" \ + --label "git-commit='${GIT_COMMIT_SHORT}'" \ + --label "git-author='${GIT_AUTHOR}'" \ + --label "git-message='${GIT_COMMIT_MSG}'" \ + --label "build-time='${GIT_COMMIT_TIME}'" \ + . + """ + } + } + } + + stage('推送镜像') { + steps { + script { + sh "docker push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + echo "推送镜像成功:${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + } + } + } + } + + post { + always { + script { + def keepCount = 3 + echo "开始清理本地旧镜像,仅保留最近 ${keepCount} 个构建版本" + def imagePrefix = "${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}" + + // 获取所有镜像(按创建时间排序,越新的越前) + // 格式:Repository:Tag ImageID CreatedAt + def allImagesRaw = sh(script: "docker images ${imagePrefix} --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedAt}}' | sort -rk3", returnStdout: true).trim() + if (!allImagesRaw) { + echo "未找到任何镜像,无需清理" + return + } + + def allImages = allImagesRaw.split('\n') + if (allImages.size() <= keepCount) { + echo "当前镜像数未超过 ${keepCount} 个,无需清理" + return + } + + def oldImages = allImages.drop(keepCount) + echo "发现 ${oldImages.size()} 个旧镜像需要清理" + oldImages.each { line -> + echo " ${line}" + } + + oldImages.each { line -> + def parts = line.split(' ') + def imageTag = parts[0] + def imageId = parts.size() > 1 ? parts[1] : "" + + // 对于标签为的无效镜像,使用镜像ID删除 + if (imageTag.contains("") && imageId) { + echo "删除无效镜像: ${imageId}" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else if (imageId) { + // 对于有标签的有效镜像,优先使用镜像ID删除 + echo "删除旧镜像: ${imageTag} (${imageId})" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else { + // 兜底方案,使用标签删除 + echo "删除旧镜像: ${imageTag}" + sh(returnStatus: true, script: "docker rmi -f ${imageTag} || true") + } + } + + echo "清理完成,当前镜像状态:" + sh """ + docker images ${imagePrefix} --format 'table {{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.Size}}' + """ + + sh "docker logout ${REGISTRY}" + echo "容器仓库已登出,本地凭证已清理" + } + } + success { + script { + // 1. 准备元数据 (转换所有环境变量为 String) + def metadataDir = '/var/lib/jenkins/metadata' + def metadataFileRelativePath = "${env.NAMESPACE}-${env.IMAGE_NAME}.json" // 相对于 metadataDir 的文件名 + def fullMetadataPath = "${metadataDir}/${metadataFileRelativePath}" + + // --- 转换为 String --- + def registry = env.REGISTRY as String + def namespace = env.NAMESPACE as String + def imageName = env.IMAGE_NAME as String + def imageTag = env.IMAGE_TAG as String + def codeBranch = params.Code_branch as String // 使用 params,因为 Code_branch 是参数 + def gitCommit = env.GIT_COMMIT_LONG as String + def gitAuthor = env.GIT_AUTHOR as String + def gitCommitMsg = env.GIT_COMMIT_MSG as String + def gitCommitTime = env.GIT_COMMIT_TIME as String + def buildNumber = env.BUILD_NUMBER as String + // --- 转换为 String --- + + // 2. 准备新数据 + def newImageData = [ + image_tag: imageTag, // 使用转换后的变量 + full_image_name: "${registry}/${namespace}/${imageName}:${imageTag}", // 使用转换后的变量 + labels: [ + "git-branch": codeBranch, + "git-commit": gitCommit, + "git-author": gitAuthor, + "git-message": gitCommitMsg, + "build-time": gitCommitTime + ], + build_job_number: buildNumber, + build_time: new Date().format('yyyy-MM-dd HH:mm:ss') // Jenkins 构建完成时间 + ] + + // 2. 读取现有数据(如果文件存在) + def existingDataList = [] + try { + // 使用 readJSON 步骤读取文件内容 (readJSON 会自动处理 LazyMap 问题) + def rawExistingData = readJSON file: fullMetadataPath, default: [] // 如果文件不存在,则返回空列表 [] + + // --- ✅ 修复:深拷贝 rawExistingData (修正内联代码) --- + if (rawExistingData instanceof List) { + existingDataList = rawExistingData.collect { item -> + if (item instanceof Map) { + // 递归深拷贝 Map (使用辅助函数) + return deepCopyForSerialization(item) + } else { + return item + } + } + } else { + echo "警告: 元数据文件 ${fullMetadataPath} 格式不正确(非 List 类型),将被覆盖。" + existingDataList = [] + } + // --- 结束修复 --- + + } catch (Exception e) { + // readJSON 在文件不存在时通常会返回 default 值,但如果文件存在但格式错误,会抛出异常 + echo "警告: 读取元数据文件 ${fullMetadataPath} 失败或格式错误: ${e.getMessage()},将被覆盖。" + // 确保目录存在 + sh "mkdir -p ${metadataDir}" + existingDataList = [] // 重置为新列表 + } + + // 3. 将新数据添加到列表开头(最新的在前) + existingDataList.add(0, newImageData) + + // 4. 限制列表大小为 20 + if (existingDataList.size() > 20) { + existingDataList = existingDataList.take(20) + } + + // 5. 使用 writeJSON 步骤写入文件 (writeJSON 会自动处理 Map 的序列化) + writeJSON file: fullMetadataPath, json: existingDataList, pretty: 2 // pretty: 2 表示格式化 JSON (2 个空格缩进) + + echo "镜像元数据已存储到: ${fullMetadataPath}" + + // 输出构建结果 + echo """ + 镜像地址:${registry}/${namespace}/${imageName}:${imageTag} + 对应代码提交哈希:${gitCommit} + 对应代码分支:${codeBranch} + 代码提交者:${gitAuthor} + 提交备注:${gitCommitMsg} + """.stripIndent() + } + // 构建成功后,若选择则触发部署到 S1 环境 + script { + // 定义部署映射:参数名 → 部署 Job 名 + def deployMap = [ + 'DEPLOY_TO_S1' : 'DM_s1_flymoon_admin_web', + ] + + // 获取完整镜像名(从已有 env 变量拼接) + def fullImageName = "${env.REGISTRY}/${env.NAMESPACE}/${env.IMAGE_NAME}:${env.IMAGE_TAG}" + + echo "构建完成镜像:${fullImageName}" + echo "检查部署触发项..." + + deployMap.each { paramKey, jobName -> + if (params."${paramKey}" == true) { + echo "触发部署:${jobName} ← ${fullImageName}" + // 异步触发 + build job: jobName, + parameters: [ + string(name: 'CUSTOM_IMAGE', value: fullImageName), + ], + wait: false // ← 异步(此 Job 结束,不等待被调用的 Job 结束) + } else { + echo "未勾选 ${paramKey}, 跳过触发部署:${jobName}" + } + } + } + } + failure { + // 输出构建结果 + echo "部署有错误,请检查!" + } + } +} \ No newline at end of file diff --git a/SCM/构建镜像/v3/build_image_flymoon_agent_v3.groovy b/SCM/构建镜像/v3/build_image_flymoon_agent_v3.groovy new file mode 100644 index 0000000..e5ff555 --- /dev/null +++ b/SCM/构建镜像/v3/build_image_flymoon_agent_v3.groovy @@ -0,0 +1,354 @@ +// --- 辅助函数:深拷贝对象以确保可序列化 --- +def deepCopyForSerialization(obj) { + if (obj instanceof Map) { + // 创建新的 LinkedHashMap,递归拷贝值 + return obj.collectEntries { k, v -> [(k): deepCopyForSerialization(v)] } + } else if (obj instanceof List) { + // 创建新的 ArrayList,递归拷贝元素 + return obj.collect { item -> deepCopyForSerialization(item) } + } else if (obj instanceof String || obj instanceof Number || obj instanceof Boolean || obj == null) { + return obj + } else { + return obj.toString() + } +} +// --- 结束辅助函数 --- + +pipeline { + agent any + tools{ + maven 'mvn3.8.8' + jdk 'jdk21' + } + parameters { + gitParameter( + branchFilter: 'origin/(.*)', + defaultValue: 'dxin', + name: 'Code_branch', + type: 'PT_BRANCH_TAG', + selectedValue: 'DEFAULT', + sortMode: 'NONE', + description: '选择代码分支: ', + quickFilterEnabled: true, + tagFilter: '*', + listSize: "1" + ) + choice( + name: 'NAME_SPACES', + choices: ['sit', 'test', 'prod'], + description: '选择存放镜像的仓库命名空间:' + ) + string( + name: 'CUSTOM_TAG', + defaultValue: '', + description: '可选:自定义镜像 Tag (字母、数字、点、下划线、短横线), 留空则自动生成 “ v+构建次数_分支名_短哈希_构建时间 ”' + ) + booleanParam( + name: 'DEPLOY_TO_S1', + defaultValue: false, + description: '可选:构建成功后部署到 S1 环境 (触发 job: DM_s1_flymoon_agent)' + ) + } + environment { + REGISTRY = "uswccr.ccs.tencentyun.com" // 镜像仓库地址 + NAMESPACE = "lessie${params.NAME_SPACES}" // 命名空间根据choices的选择拼接 + IMAGE_NAME = "flymoon-agent" // 镜像名(固定前缀) + CREDENTIALS_ID = "dxin_img_hub_auth" // 容器仓库凭证ID + } + + stages { + stage('拉取代码') { + steps { + git branch: "${params.Code_branch}", + credentialsId: 'fly_gitlab_auth', + url: 'http://172.24.16.20/root/fly_moon_agent.git' + } + } + stage('获取信息') { + steps { + script { + // 获取分支名 + env.Code_branch = "${params.Code_branch}" + // 获取最近一次提交的哈希值(短格式,前8位) + env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD',returnStdout: true).trim() + // 获取最近一次提交的哈希值(全格式) + env.GIT_COMMIT_LONG = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + // 获取最近一次提交的作者 + env.GIT_AUTHOR = sh(script: 'git log -1 --pretty=format:%an',returnStdout: true).trim() + // 获取最近一次提交的时间(格式化) + env.GIT_COMMIT_TIME = sh( + script: 'git log -1 --pretty=format:%ct | xargs -I {} date -d @{} +%Y%m%d-%H%M%S', + returnStdout: true + ).trim() + // 获取最近一次提交的备注信息(转义特殊字符,避免构建失败) + env.GIT_COMMIT_MSG = sh(script: 'git log -1 --pretty=format:%s | sed -e \'s/"/\\"/g\'', returnStdout: true).trim() + + // Jenkins构建次数 + def buildNumber = env.BUILD_NUMBER // Jenkins内置变量,直接获取当前Job的构建序号 + // 当前分支名(处理/为-,如feature/docker_1015 → feature-docker_1015) + def branchName = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim() + def formattedBranch = branchName.replace('/', '-').replace('_', '-') // 替换分支名中的/和_为- + // 构建时间(格式:202510181215,年-月-日-时-分,无分隔符) + def buildTime = sh(script: 'date +%Y%m%d%H%M', returnStdout: true).trim() + def defaultTag = "v${buildNumber}_${formattedBranch}_${GIT_COMMIT_SHORT}_${buildTime}" + + def customTag = params.CUSTOM_TAG?.trim() + def tagPattern = ~/^[a-zA-Z0-9._-]+$/ + + // 判断最终Tag + if (customTag && customTag ==~ tagPattern) { + echo "✅ 使用自定义镜像 Tag: ${customTag}" + env.IMAGE_TAG = customTag + } else if (customTag) { + echo "⚠️ 自定义 Tag '${customTag}' 不符合规范,将使用默认生成的 Tag: ${defaultTag}" + + def confirmed = true + timeout(time: 1, unit: 'MINUTES') { + try { + input( + message: """⚠️ Tag 命名不规范: + ${customTag} + + 将使用自动生成的 Tag: + ${defaultTag} + + 是否继续构建?""", + ok: '确认' + ) + } catch (err) { + // 用户点击“取消”或中断 + echo "🚫 用户取消构建" + confirmed = false + } + } + + if (confirmed) { + echo "✅ 用户确认使用自动生成的 Tag:${defaultTag}" + env.IMAGE_TAG = defaultTag + } else { + error("流水线已终止。") + } + } else { + env.IMAGE_TAG = defaultTag + echo "未输入自定义 Tag, 使用自动生成规则: ${env.IMAGE_TAG}" + } + } + } + } + stage('登录仓库') { + steps { + withCredentials([usernamePassword( + credentialsId: env.CREDENTIALS_ID, + usernameVariable: 'REGISTRY_USER', + passwordVariable: 'REGISTRY_PWD' + )]) { + sh ''' + echo "$REGISTRY_PWD" | docker login ${REGISTRY} -u ${REGISTRY_USER} --password-stdin + ''' + } + } + } + stage('构建镜像') { + steps { + script { + // 构建镜像,添加标签信息 + sh """ + docker build -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG} \ + --label "git-branch='${Code_branch}'" \ + --label "git-commit='${GIT_COMMIT_SHORT}'" \ + --label "git-author='${GIT_AUTHOR}'" \ + --label "git-message='${GIT_COMMIT_MSG}'" \ + --label "build-time='${GIT_COMMIT_TIME}'" \ + . + """ + } + } + } + stage('推送镜像') { + steps { + script { + // 推送镜像(带唯一 Tag) + sh "docker push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + echo "推送镜像成功:${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + } + } + } + } + + post { + always { + script { + def keepCount = 3 + echo "开始清理本地旧镜像,仅保留最近 ${keepCount} 个构建版本" + def imagePrefix = "${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}" + + // 获取所有镜像(按创建时间排序,越新的越前) + // 格式:Repository:Tag ImageID CreatedAt + def allImagesRaw = sh(script: "docker images ${imagePrefix} --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedAt}}' | sort -rk3", returnStdout: true).trim() + if (!allImagesRaw) { + echo "未找到任何镜像,无需清理" + return + } + + def allImages = allImagesRaw.split('\n') + if (allImages.size() <= keepCount) { + echo "当前镜像数未超过 ${keepCount} 个,无需清理" + return + } + + def oldImages = allImages.drop(keepCount) + echo "发现 ${oldImages.size()} 个旧镜像需要清理" + oldImages.each { line -> + echo " ${line}" + } + + oldImages.each { line -> + def parts = line.split(' ') + def imageTag = parts[0] + def imageId = parts.size() > 1 ? parts[1] : "" + + // 对于标签为的无效镜像,使用镜像ID删除 + if (imageTag.contains("") && imageId) { + echo "删除无效镜像: ${imageId}" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else if (imageId) { + // 对于有标签的有效镜像,优先使用镜像ID删除 + echo "删除旧镜像: ${imageTag} (${imageId})" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else { + // 兜底方案,使用标签删除 + echo "删除旧镜像: ${imageTag}" + sh(returnStatus: true, script: "docker rmi -f ${imageTag} || true") + } + } + + echo "清理完成,当前镜像状态:" + sh """ + docker images ${imagePrefix} --format 'table {{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.Size}}' + """ + + sh "docker logout ${REGISTRY}" + echo "容器仓库已登出,本地凭证已清理" + } + } + success { + script { + // 1. 准备元数据 (转换所有环境变量为 String) + def metadataDir = '/var/lib/jenkins/metadata' + def metadataFileRelativePath = "${env.NAMESPACE}-${env.IMAGE_NAME}.json" // 相对于 metadataDir 的文件名 + def fullMetadataPath = "${metadataDir}/${metadataFileRelativePath}" + + // --- 转换为 String --- + def registry = env.REGISTRY as String + def namespace = env.NAMESPACE as String + def imageName = env.IMAGE_NAME as String + def imageTag = env.IMAGE_TAG as String + def codeBranch = params.Code_branch as String // 使用 params,因为 Code_branch 是参数 + def gitCommit = env.GIT_COMMIT_LONG as String + def gitAuthor = env.GIT_AUTHOR as String + def gitCommitMsg = env.GIT_COMMIT_MSG as String + def gitCommitTime = env.GIT_COMMIT_TIME as String + def buildNumber = env.BUILD_NUMBER as String + // --- 转换为 String --- + + // 2. 准备新数据 + def newImageData = [ + image_tag: imageTag, // 使用转换后的变量 + full_image_name: "${registry}/${namespace}/${imageName}:${imageTag}", // 使用转换后的变量 + labels: [ + "git-branch": codeBranch, + "git-commit": gitCommit, + "git-author": gitAuthor, + "git-message": gitCommitMsg, + "build-time": gitCommitTime + ], + build_job_number: buildNumber, + build_time: new Date().format('yyyy-MM-dd HH:mm:ss') // Jenkins 构建完成时间 + ] + + // 2. 读取现有数据(如果文件存在) + def existingDataList = [] + try { + // 使用 readJSON 步骤读取文件内容 (readJSON 会自动处理 LazyMap 问题) + def rawExistingData = readJSON file: fullMetadataPath, default: [] // 如果文件不存在,则返回空列表 [] + + // --- ✅ 修复:深拷贝 rawExistingData (修正内联代码) --- + if (rawExistingData instanceof List) { + existingDataList = rawExistingData.collect { item -> + if (item instanceof Map) { + // 递归深拷贝 Map (使用辅助函数) + return deepCopyForSerialization(item) + } else { + return item + } + } + } else { + echo "警告: 元数据文件 ${fullMetadataPath} 格式不正确(非 List 类型),将被覆盖。" + existingDataList = [] + } + // --- 结束修复 --- + + } catch (Exception e) { + // readJSON 在文件不存在时通常会返回 default 值,但如果文件存在但格式错误,会抛出异常 + echo "警告: 读取元数据文件 ${fullMetadataPath} 失败或格式错误: ${e.getMessage()},将被覆盖。" + // 确保目录存在 + sh "mkdir -p ${metadataDir}" + existingDataList = [] // 重置为新列表 + } + + // 3. 将新数据添加到列表开头(最新的在前) + existingDataList.add(0, newImageData) + + // 4. 限制列表大小为 20 + if (existingDataList.size() > 20) { + existingDataList = existingDataList.take(20) + } + + // 5. 使用 writeJSON 步骤写入文件 (writeJSON 会自动处理 Map 的序列化) + writeJSON file: fullMetadataPath, json: existingDataList, pretty: 2 // pretty: 2 表示格式化 JSON (2 个空格缩进) + + echo "镜像元数据已存储到: ${fullMetadataPath}" + + // 输出构建结果 + echo """ + 镜像地址:${registry}/${namespace}/${imageName}:${imageTag} + 对应代码提交哈希:${gitCommit} + 对应代码分支:${codeBranch} + 代码提交者:${gitAuthor} + 提交备注:${gitCommitMsg} + """.stripIndent() + } + // 构建成功后,若选择则触发部署到 S1 环境 + script { + // 定义部署映射:参数名 → 部署 Job 名 + def deployMap = [ + 'DEPLOY_TO_S1' : 'DM_s1_flymoon_agent', + ] + + // 获取完整镜像名(从已有 env 变量拼接) + def fullImageName = "${env.REGISTRY}/${env.NAMESPACE}/${env.IMAGE_NAME}:${env.IMAGE_TAG}" + + echo "构建完成镜像:${fullImageName}" + echo "检查部署触发项..." + + deployMap.each { paramKey, jobName -> + if (params."${paramKey}" == true) { + echo "触发部署:${jobName} ← ${fullImageName}" + // 异步触发 + build job: jobName, + parameters: [ + string(name: 'CUSTOM_IMAGE', value: fullImageName), + ], + wait: false // ← 异步(此 Job 结束,不等待被调用的 Job 结束) + } else { + echo "未勾选 ${paramKey}, 跳过触发部署:${jobName}" + } + } + } + } + failure { + // 输出构建结果 + echo "有步骤出错!" + } + } +} \ No newline at end of file diff --git a/SCM/构建镜像/v3/build_image_flymoon_email_v3.groovy b/SCM/构建镜像/v3/build_image_flymoon_email_v3.groovy new file mode 100644 index 0000000..8293306 --- /dev/null +++ b/SCM/构建镜像/v3/build_image_flymoon_email_v3.groovy @@ -0,0 +1,356 @@ + +// --- 辅助函数:深拷贝对象以确保可序列化 --- +def deepCopyForSerialization(obj) { + if (obj instanceof Map) { + // 创建新的 LinkedHashMap,递归拷贝值 + return obj.collectEntries { k, v -> [(k): deepCopyForSerialization(v)] } + } else if (obj instanceof List) { + // 创建新的 ArrayList,递归拷贝元素 + return obj.collect { item -> deepCopyForSerialization(item) } + } else if (obj instanceof String || obj instanceof Number || obj instanceof Boolean || obj == null) { + return obj + } else { + return obj.toString() + } +} +// --- 结束辅助函数 --- + +pipeline { + agent any + + parameters { + gitParameter( + branchFilter: 'origin/(.*)', + defaultValue: 'dxin', + name: 'Code_branch', + type: 'PT_BRANCH_TAG', + selectedValue: 'DEFAULT', + sortMode: 'NONE', + description: '选择代码分支: ', + quickFilterEnabled: true, + tagFilter: '*', + listSize: "1" + ) + choice( + name: 'NAME_SPACES', + choices: ['sit', 'test', 'prod'], + description: '选择存放镜像的仓库命名空间:' + ) + choice( + name: 'MAVEN_BUILD_PROFILE', + choices: ['us', 'cn'], + description: '选择MAVEN构建的配置文件, 默认为 us' + ) + string( + name: 'CUSTOM_TAG', + defaultValue: '', + description: '可选:自定义镜像 Tag (字母、数字、点、下划线、短横线), 如 v0.0.1, 留空则自动生成 “ v+构建次数_分支名_短哈希_构建时间 ”' + ) + booleanParam( + name: 'DEPLOY_TO_S1', + defaultValue: false, + description: '可选:构建成功后部署到 S1 环境 (触发 job: DM_s1_flymoon_email)' + ) + } + environment { + REGISTRY = "uswccr.ccs.tencentyun.com" // 镜像仓库地址 + NAMESPACE = "lessie${params.NAME_SPACES}" // 命名空间根据choices的选择拼接 + IMAGE_NAME = "flymoon-email" // 镜像名(固定前缀) + CREDENTIALS_ID = "dxin_img_hub_auth" // 容器仓库凭证ID + } + stages { + stage('拉取代码') { + steps { + git branch: "${params.Code_branch}", + credentialsId: 'fly_gitlab_auth', + url: 'http://172.24.16.20/root/fly_moon_email.git' + } + } + stage('获取信息') { + steps { + script { + // 获取分支名 + env.Code_branch = "${params.Code_branch}" + // 获取最近一次提交的哈希值(短格式,前8位) + env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD',returnStdout: true).trim() + // 获取最近一次提交的哈希值(全格式) + env.GIT_COMMIT_LONG = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + // 获取最近一次提交的作者 + env.GIT_AUTHOR = sh(script: 'git log -1 --pretty=format:%an',returnStdout: true).trim() + // 获取最近一次提交的时间(格式化) + env.GIT_COMMIT_TIME = sh( + script: 'git log -1 --pretty=format:%ct | xargs -I {} date -d @{} +%Y%m%d-%H%M%S', + returnStdout: true + ).trim() + // 获取最近一次提交的备注信息(转义特殊字符,避免构建失败) + env.GIT_COMMIT_MSG = sh(script: 'git log -1 --pretty=format:%s | sed -e \'s/"/\\"/g\'', returnStdout: true).trim() + + // Jenkins构建次数 + def buildNumber = env.BUILD_NUMBER // Jenkins内置变量,直接获取当前Job的构建序号 + // 当前分支名(处理/为-,如feature/docker_1015 → feature-docker_1015) + def branchName = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim() + def formattedBranch = branchName.replace('/', '-').replace('_', '-') // 替换分支名中的/和_为- + // 构建时间(格式:202510181215,年-月-日-时-分,无分隔符) + def buildTime = sh(script: 'date +%Y%m%d%H%M', returnStdout: true).trim() + def defaultTag = "v${buildNumber}_${formattedBranch}_${GIT_COMMIT_SHORT}_${buildTime}" + + def customTag = params.CUSTOM_TAG?.trim() + def tagPattern = ~/^[a-zA-Z0-9._-]+$/ + + // 判断最终Tag + if (customTag && customTag ==~ tagPattern) { + echo "✅ 使用自定义镜像 Tag: ${customTag}" + env.IMAGE_TAG = customTag + } else if (customTag) { + echo "⚠️ 自定义 Tag '${customTag}' 不符合规范,将使用默认生成的 Tag: ${defaultTag}" + + def confirmed = true + timeout(time: 1, unit: 'MINUTES') { + try { + input( + message: """⚠️ Tag 命名不规范: + ${customTag} + + 将使用自动生成的 Tag: + ${defaultTag} + + 是否继续构建?""", + ok: '确认' + ) + } catch (err) { + // 用户点击“取消”或中断 + echo "🚫 用户取消构建" + confirmed = false + } + } + if (confirmed) { + echo "✅ 用户确认使用自动生成的 Tag:${defaultTag}" + env.IMAGE_TAG = defaultTag + } else { + error("流水线已终止。") + } + } else { + env.IMAGE_TAG = defaultTag + echo "未输入自定义 Tag, 使用自动生成规则: ${env.IMAGE_TAG}" + } + } + } + } + stage('登录仓库') { + steps { + withCredentials([usernamePassword( + credentialsId: env.CREDENTIALS_ID, + usernameVariable: 'REGISTRY_USER', + passwordVariable: 'REGISTRY_PWD' + )]) { + sh ''' + echo "$REGISTRY_PWD" | docker login ${REGISTRY} -u ${REGISTRY_USER} --password-stdin + ''' + } + } + } + stage('构建镜像') { + steps { + script { + // 构建镜像,添加标签信息 + sh """ + docker build \ + --build-arg MAVEN_BUILD_PROFILE=${params.MAVEN_BUILD_PROFILE} \ + -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG} \ + --label "git-branch='${Code_branch}'" \ + --label "git-commit='${GIT_COMMIT_LONG}'" \ + --label "git-author='${GIT_AUTHOR}'" \ + --label "git-message='${GIT_COMMIT_MSG}'" \ + --label "build-time='${GIT_COMMIT_TIME}'" \ + . + """ + } + } + } + stage('推送镜像') { + steps { + script { + sh "docker push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + echo "推送镜像成功:${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + } + } + } + } + + post { + always { + script { + def keepCount = 3 + echo "开始清理本地旧镜像,仅保留最近 ${keepCount} 个构建版本" + def imagePrefix = "${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}" + + // 获取所有镜像(按创建时间排序,越新的越前) + // 格式:Repository:Tag ImageID CreatedAt + def allImagesRaw = sh(script: "docker images ${imagePrefix} --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedAt}}' | sort -rk3", returnStdout: true).trim() + if (!allImagesRaw) { + echo "未找到任何镜像,无需清理" + return + } + + def allImages = allImagesRaw.split('\n') + if (allImages.size() <= keepCount) { + echo "当前镜像数未超过 ${keepCount} 个,无需清理" + return + } + + def oldImages = allImages.drop(keepCount) + echo "发现 ${oldImages.size()} 个旧镜像需要清理" + oldImages.each { line -> + echo " ${line}" + } + + oldImages.each { line -> + def parts = line.split(' ') + def imageTag = parts[0] + def imageId = parts.size() > 1 ? parts[1] : "" + + // 对于标签为的无效镜像,使用镜像ID删除 + if (imageTag.contains("") && imageId) { + echo "删除无效镜像: ${imageId}" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else if (imageId) { + // 对于有标签的有效镜像,优先使用镜像ID删除 + echo "删除旧镜像: ${imageTag} (${imageId})" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else { + // 兜底方案,使用标签删除 + echo "删除旧镜像: ${imageTag}" + sh(returnStatus: true, script: "docker rmi -f ${imageTag} || true") + } + } + + echo "清理完成,当前镜像状态:" + sh """ + docker images ${imagePrefix} --format 'table {{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.Size}}' + """ + + sh "docker logout ${REGISTRY}" + echo "容器仓库已登出,本地凭证已清理" + } + } + success { + script { + // 1. 准备元数据 (转换所有环境变量为 String) + def metadataDir = '/var/lib/jenkins/metadata' + def metadataFileRelativePath = "${env.NAMESPACE}-${env.IMAGE_NAME}.json" // 相对于 metadataDir 的文件名 + def fullMetadataPath = "${metadataDir}/${metadataFileRelativePath}" + + // --- 转换为 String --- + def registry = env.REGISTRY as String + def namespace = env.NAMESPACE as String + def imageName = env.IMAGE_NAME as String + def imageTag = env.IMAGE_TAG as String + def codeBranch = params.Code_branch as String // 使用 params,因为 Code_branch 是参数 + def gitCommit = env.GIT_COMMIT_LONG as String + def gitAuthor = env.GIT_AUTHOR as String + def gitCommitMsg = env.GIT_COMMIT_MSG as String + def gitCommitTime = env.GIT_COMMIT_TIME as String + def buildNumber = env.BUILD_NUMBER as String + // --- 转换为 String --- + + // 2. 准备新数据 + def newImageData = [ + image_tag: imageTag, // 使用转换后的变量 + full_image_name: "${registry}/${namespace}/${imageName}:${imageTag}", // 使用转换后的变量 + labels: [ + "git-branch": codeBranch, + "git-commit": gitCommit, + "git-author": gitAuthor, + "git-message": gitCommitMsg, + "build-time": gitCommitTime + ], + build_job_number: buildNumber, + build_time: new Date().format('yyyy-MM-dd HH:mm:ss') // Jenkins 构建完成时间 + ] + + // 2. 读取现有数据(如果文件存在) + def existingDataList = [] + try { + // 使用 readJSON 步骤读取文件内容 (readJSON 会自动处理 LazyMap 问题) + def rawExistingData = readJSON file: fullMetadataPath, default: [] // 如果文件不存在,则返回空列表 [] + + // --- ✅ 修复:深拷贝 rawExistingData (修正内联代码) --- + if (rawExistingData instanceof List) { + existingDataList = rawExistingData.collect { item -> + if (item instanceof Map) { + // 递归深拷贝 Map (使用辅助函数) + return deepCopyForSerialization(item) + } else { + return item + } + } + } else { + echo "警告: 元数据文件 ${fullMetadataPath} 格式不正确(非 List 类型),将被覆盖。" + existingDataList = [] + } + // --- 结束修复 --- + + } catch (Exception e) { + // readJSON 在文件不存在时通常会返回 default 值,但如果文件存在但格式错误,会抛出异常 + echo "警告: 读取元数据文件 ${fullMetadataPath} 失败或格式错误: ${e.getMessage()},将被覆盖。" + // 确保目录存在 + sh "mkdir -p ${metadataDir}" + existingDataList = [] // 重置为新列表 + } + + // 3. 将新数据添加到列表开头(最新的在前) + existingDataList.add(0, newImageData) + + // 4. 限制列表大小为 20 + if (existingDataList.size() > 20) { + existingDataList = existingDataList.take(20) + } + + // 5. 使用 writeJSON 步骤写入文件 (writeJSON 会自动处理 Map 的序列化) + writeJSON file: fullMetadataPath, json: existingDataList, pretty: 2 // pretty: 2 表示格式化 JSON (2 个空格缩进) + + echo "镜像元数据已存储到: ${fullMetadataPath}" + + // 输出构建结果 + echo """ + 镜像地址:${registry}/${namespace}/${imageName}:${imageTag} + 对应代码提交哈希:${gitCommit} + 对应代码分支:${codeBranch} + 代码提交者:${gitAuthor} + 提交备注:${gitCommitMsg} + """.stripIndent() + } + // 构建成功后,若选择则触发部署到 S1 环境 + script { + // 定义部署映射:参数名 → 部署 Job 名 + def deployMap = [ + 'DEPLOY_TO_S1' : 'DM_s1_flymoon_email', + ] + + // 获取完整镜像名(从已有 env 变量拼接) + def fullImageName = "${env.REGISTRY}/${env.NAMESPACE}/${env.IMAGE_NAME}:${env.IMAGE_TAG}" + + echo "构建完成镜像:${fullImageName}" + echo "检查部署触发项..." + + deployMap.each { paramKey, jobName -> + if (params."${paramKey}" == true) { + echo "触发部署:${jobName} ← ${fullImageName}" + // 异步触发 + build job: jobName, + parameters: [ + string(name: 'CUSTOM_IMAGE', value: fullImageName), + ], + wait: false // ← 异步(此 Job 结束,不等待被调用的 Job 结束) + } else { + echo "未勾选 ${paramKey}, 跳过触发部署:${jobName}" + } + } + } + } + failure { + // 输出构建结果 + echo "构建失败,请检查!" + } + } +} diff --git a/SCM/构建镜像/v3/build_image_flymoon_payment_v3.groovy b/SCM/构建镜像/v3/build_image_flymoon_payment_v3.groovy new file mode 100644 index 0000000..af5108c --- /dev/null +++ b/SCM/构建镜像/v3/build_image_flymoon_payment_v3.groovy @@ -0,0 +1,353 @@ +// --- 辅助函数:深拷贝对象以确保可序列化 --- +def deepCopyForSerialization(obj) { + if (obj instanceof Map) { + // 创建新的 LinkedHashMap,递归拷贝值 + return obj.collectEntries { k, v -> [(k): deepCopyForSerialization(v)] } + } else if (obj instanceof List) { + // 创建新的 ArrayList,递归拷贝元素 + return obj.collect { item -> deepCopyForSerialization(item) } + } else if (obj instanceof String || obj instanceof Number || obj instanceof Boolean || obj == null) { + return obj + } else { + return obj.toString() + } +} +// --- 结束辅助函数 --- + +pipeline { + agent any + tools{ + maven 'mvn3.8.8' + jdk 'jdk21' + } + parameters { + gitParameter( + branchFilter: 'origin/(.*)', + defaultValue: 'dxin', + name: 'Code_branch', + type: 'PT_BRANCH_TAG', + selectedValue: 'DEFAULT', + sortMode: 'NONE', + description: '选择代码分支: ', + quickFilterEnabled: true, + tagFilter: '*', + listSize: "1" + ) + choice( + name: 'NAME_SPACES', + choices: ['sit', 'test', 'prod'], + description: '选择存放镜像的仓库命名空间:' + ) + string( + name: 'CUSTOM_TAG', + defaultValue: '', + description: '可选:自定义镜像 Tag (字母、数字、点、下划线、短横线), 留空则自动生成 “ v+构建次数_分支名_短哈希_构建时间 ”' + ) + booleanParam( + name: 'DEPLOY_TO_S1', + defaultValue: false, + description: '可选:构建成功后部署到 S1 环境 (触发 job: DM_s1_flymoon_payment)' + ) + } + environment { + REGISTRY = "uswccr.ccs.tencentyun.com" // 镜像仓库地址 + NAMESPACE = "lessie${params.NAME_SPACES}" // 命名空间根据choices的选择拼接 + IMAGE_NAME = "flymoon-payment" // 镜像名(固定前缀) + CREDENTIALS_ID = "dxin_img_hub_auth" // 容器仓库凭证ID + } + + stages { + stage('拉取代码') { + steps { + git branch: "${params.Code_branch}", + credentialsId: 'fly_gitlab_auth', + url: 'http://172.24.16.20/root/fly_moon_payment.git' + } + } + stage('获取信息') { + steps { + script { + // 获取分支名 + env.Code_branch = "${params.Code_branch}" + // 获取最近一次提交的哈希值(短格式,前8位) + env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD',returnStdout: true).trim() + // 获取最近一次提交的哈希值(全格式) + env.GIT_COMMIT_LONG = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + // 获取最近一次提交的作者 + env.GIT_AUTHOR = sh(script: 'git log -1 --pretty=format:%an',returnStdout: true).trim() + // 获取最近一次提交的时间(格式化) + env.GIT_COMMIT_TIME = sh( + script: 'git log -1 --pretty=format:%ct | xargs -I {} date -d @{} +%Y%m%d-%H%M%S', + returnStdout: true + ).trim() + // 获取最近一次提交的备注信息(转义特殊字符,避免构建失败) + env.GIT_COMMIT_MSG = sh(script: 'git log -1 --pretty=format:%s | sed -e \'s/"/\\"/g\'', returnStdout: true).trim() + + // Jenkins构建次数 + def buildNumber = env.BUILD_NUMBER // Jenkins内置变量,直接获取当前Job的构建序号 + // 当前分支名(处理/为-,如feature/docker_1015 → feature-docker_1015) + def branchName = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim() + def formattedBranch = branchName.replace('/', '-').replace('_', '-') // 替换分支名中的/和_为- + // 构建时间(格式:202510181215,年-月-日-时-分,无分隔符) + def buildTime = sh(script: 'date +%Y%m%d%H%M', returnStdout: true).trim() + def defaultTag = "v${buildNumber}_${formattedBranch}_${GIT_COMMIT_SHORT}_${buildTime}" + + def customTag = params.CUSTOM_TAG?.trim() + def tagPattern = ~/^[a-zA-Z0-9._-]+$/ + + // 最终Tag + if (customTag && customTag ==~ tagPattern) { + echo "✅ 使用自定义镜像 Tag: ${customTag}" + env.IMAGE_TAG = customTag + } else if (customTag) { + echo "⚠️ 自定义 Tag '${customTag}' 不符合规范,将使用默认生成的 Tag: ${defaultTag}" + + def confirmed = true + timeout(time: 1, unit: 'MINUTES') { + try { + input( + message: """⚠️ Tag 命名不规范: + ${customTag} + + 将使用自动生成的 Tag: + ${defaultTag} + + 是否继续构建?""", + ok: '确认' + ) + } catch (err) { + // 用户点击“取消”或中断 + echo "🚫 用户取消构建" + confirmed = false + } + } + + if (confirmed) { + echo "✅ 用户确认使用自动生成的 Tag:${defaultTag}" + env.IMAGE_TAG = defaultTag + } else { + error("流水线已终止。") + } + } else { + env.IMAGE_TAG = defaultTag + echo "未输入自定义 Tag, 使用自动生成规则: ${env.IMAGE_TAG}" + } + } + } + } + stage('登录仓库') { + steps { + withCredentials([usernamePassword( + credentialsId: env.CREDENTIALS_ID, + usernameVariable: 'REGISTRY_USER', + passwordVariable: 'REGISTRY_PWD' + )]) { + sh ''' + echo "$REGISTRY_PWD" | docker login ${REGISTRY} -u ${REGISTRY_USER} --password-stdin + ''' + } + } + } + stage('构建镜像') { + steps { + script { + // 构建镜像,添加标签信息 + sh """ + docker build -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG} \ + --label "git-branch='${Code_branch}'" \ + --label "git-commit='${GIT_COMMIT_SHORT}'" \ + --label "git-author='${GIT_AUTHOR}'" \ + --label "git-message='${GIT_COMMIT_MSG}'" \ + --label "build-time='${GIT_COMMIT_TIME}'" \ + . + """ + } + } + } + stage('推送镜像到仓库') { + steps { + script { + sh "docker push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + echo "推送镜像成功:${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + } + } + } + } + + post { + always { + script { + def keepCount = 2 + echo "开始清理本地旧镜像,仅保留最近 ${keepCount} 个构建版本" + def imagePrefix = "${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}" + + // 获取所有镜像(按创建时间排序,越新的越前) + // 格式:Repository:Tag ImageID CreatedAt + def allImagesRaw = sh(script: "docker images ${imagePrefix} --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedAt}}' | sort -rk3", returnStdout: true).trim() + if (!allImagesRaw) { + echo "未找到任何镜像,无需清理" + return + } + + def allImages = allImagesRaw.split('\n') + if (allImages.size() <= keepCount) { + echo "当前镜像数未超过 ${keepCount} 个,无需清理" + return + } + + def oldImages = allImages.drop(keepCount) + echo "发现 ${oldImages.size()} 个旧镜像需要清理" + oldImages.each { line -> + echo " ${line}" + } + + oldImages.each { line -> + def parts = line.split(' ') + def imageTag = parts[0] + def imageId = parts.size() > 1 ? parts[1] : "" + + // 对于标签为的无效镜像,使用镜像ID删除 + if (imageTag.contains("") && imageId) { + echo "删除无效镜像: ${imageId}" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else if (imageId) { + // 对于有标签的有效镜像,优先使用镜像ID删除 + echo "删除旧镜像: ${imageTag} (${imageId})" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else { + // 兜底方案,使用标签删除 + echo "删除旧镜像: ${imageTag}" + sh(returnStatus: true, script: "docker rmi -f ${imageTag} || true") + } + } + + echo "清理完成,当前镜像状态:" + sh """ + docker images ${imagePrefix} --format 'table {{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.Size}}' + """ + + sh "docker logout ${REGISTRY}" + echo "容器仓库已登出,本地凭证已清理" + } + } + success { + script { + // 1. 准备元数据 (转换所有环境变量为 String) + def metadataDir = '/var/lib/jenkins/metadata' + def metadataFileRelativePath = "${env.NAMESPACE}-${env.IMAGE_NAME}.json" // 相对于 metadataDir 的文件名 + def fullMetadataPath = "${metadataDir}/${metadataFileRelativePath}" + + // --- 转换为 String --- + def registry = env.REGISTRY as String + def namespace = env.NAMESPACE as String + def imageName = env.IMAGE_NAME as String + def imageTag = env.IMAGE_TAG as String + def codeBranch = params.Code_branch as String // 使用 params,因为 Code_branch 是参数 + def gitCommit = env.GIT_COMMIT_LONG as String + def gitAuthor = env.GIT_AUTHOR as String + def gitCommitMsg = env.GIT_COMMIT_MSG as String + def gitCommitTime = env.GIT_COMMIT_TIME as String + def buildNumber = env.BUILD_NUMBER as String + // --- 转换为 String --- + + // 2. 准备新数据 + def newImageData = [ + image_tag: imageTag, // 使用转换后的变量 + full_image_name: "${registry}/${namespace}/${imageName}:${imageTag}", // 使用转换后的变量 + labels: [ + "git-branch": codeBranch, + "git-commit": gitCommit, + "git-author": gitAuthor, + "git-message": gitCommitMsg, + "build-time": gitCommitTime + ], + build_job_number: buildNumber, + build_time: new Date().format('yyyy-MM-dd HH:mm:ss') // Jenkins 构建完成时间 + ] + + // 2. 读取现有数据(如果文件存在) + def existingDataList = [] + try { + // 使用 readJSON 步骤读取文件内容 (readJSON 会自动处理 LazyMap 问题) + def rawExistingData = readJSON file: fullMetadataPath, default: [] // 如果文件不存在,则返回空列表 [] + + // --- ✅ 修复:深拷贝 rawExistingData (修正内联代码) --- + if (rawExistingData instanceof List) { + existingDataList = rawExistingData.collect { item -> + if (item instanceof Map) { + // 递归深拷贝 Map (使用辅助函数) + return deepCopyForSerialization(item) + } else { + return item + } + } + } else { + echo "警告: 元数据文件 ${fullMetadataPath} 格式不正确(非 List 类型),将被覆盖。" + existingDataList = [] + } + // --- 结束修复 --- + + } catch (Exception e) { + // readJSON 在文件不存在时通常会返回 default 值,但如果文件存在但格式错误,会抛出异常 + echo "警告: 读取元数据文件 ${fullMetadataPath} 失败或格式错误: ${e.getMessage()},将被覆盖。" + // 确保目录存在 + sh "mkdir -p ${metadataDir}" + existingDataList = [] // 重置为新列表 + } + + // 3. 将新数据添加到列表开头(最新的在前) + existingDataList.add(0, newImageData) + + // 4. 限制列表大小为 20 + if (existingDataList.size() > 20) { + existingDataList = existingDataList.take(20) + } + + // 5. 使用 writeJSON 步骤写入文件 (writeJSON 会自动处理 Map 的序列化) + writeJSON file: fullMetadataPath, json: existingDataList, pretty: 2 // pretty: 2 表示格式化 JSON (2 个空格缩进) + + echo "镜像元数据已存储到: ${fullMetadataPath}" + + // 输出构建结果 + echo """ + 镜像地址:${registry}/${namespace}/${imageName}:${imageTag} + 对应代码提交哈希:${gitCommit} + 对应代码分支:${codeBranch} + 代码提交者:${gitAuthor} + 提交备注:${gitCommitMsg} + """.stripIndent() + } + // 构建成功后,若选择则触发部署到 S1 环境 + script { + // 定义部署映射:参数名 → 部署 Job 名 + def deployMap = [ + 'DEPLOY_TO_S1' : 'DM_s1_flymoon_payment', + ] + + // 获取完整镜像名(从已有 env 变量拼接) + def fullImageName = "${env.REGISTRY}/${env.NAMESPACE}/${env.IMAGE_NAME}:${env.IMAGE_TAG}" + + echo "构建完成镜像:${fullImageName}" + echo "检查部署触发项..." + + deployMap.each { paramKey, jobName -> + if (params."${paramKey}" == true) { + echo "触发部署:${jobName} ← ${fullImageName}" + // 异步触发 + build job: jobName, + parameters: [ + string(name: 'CUSTOM_IMAGE', value: fullImageName), + ], + wait: false // ← 异步(此 Job 结束,不等待被调用的 Job 结束) + } else { + echo "未勾选 ${paramKey}, 跳过触发部署:${jobName}" + } + } + } + } + failure { + // 输出构建结果 + echo "有片段失败!" + } + } +} \ No newline at end of file diff --git a/SCM/构建镜像/v3/build_image_lessie_agents_v3.groovy b/SCM/构建镜像/v3/build_image_lessie_agents_v3.groovy new file mode 100644 index 0000000..223d3a9 --- /dev/null +++ b/SCM/构建镜像/v3/build_image_lessie_agents_v3.groovy @@ -0,0 +1,371 @@ +// --- 辅助函数:深拷贝对象以确保可序列化 --- +def deepCopyForSerialization(obj) { + if (obj instanceof Map) { + // 创建新的 LinkedHashMap,递归拷贝值 + return obj.collectEntries { k, v -> [(k): deepCopyForSerialization(v)] } + } else if (obj instanceof List) { + // 创建新的 ArrayList,递归拷贝元素 + return obj.collect { item -> deepCopyForSerialization(item) } + } else if (obj instanceof String || obj instanceof Number || obj instanceof Boolean || obj == null) { + return obj + } else { + return obj.toString() + } +} +// --- 结束辅助函数 --- + +pipeline { + agent any + parameters { + gitParameter( + branchFilter: 'origin/(.*)', + defaultValue: 'dxin', + name: 'Code_branch', + type: 'PT_BRANCH_TAG', + selectedValue: 'DEFAULT', + sortMode: 'NONE', + description: '选择代码分支:', + quickFilterEnabled: true, + tagFilter: '*', + listSize: "1" + ) + choice( + name: 'NAME_SPACES', + choices: ['sit', 'test', 'prod'], + description: '选择存放镜像的仓库命名空间:' + ) + string( + name: 'CUSTOM_TAG', + defaultValue: '', + description: '可选:自定义镜像 Tag (字母、数字、点、下划线、短横线), 留空则自动生成 “ v+构建次数_分支名_短哈希_构建时间 ”' + ) + booleanParam( + name: 'DEPLOY_TO_S1', + defaultValue: false, + description: '可选:构建成功后部署到 S1 环境 (触发 job: DM_s1_lessie_ai)' + ) + booleanParam( + name: 'DEPLOY_TO_S2', + defaultValue: false, + description: '可选:构建成功后部署到 S2 环境 (触发 job: DM_s2_lessie_ai)' + ) + booleanParam( + name: 'DEPLOY_TO_S3', + defaultValue: false, + description: '可选:构建成功后部署到 S3 环境 (触发 job: DM_s3_lessie_ai)' + ) + booleanParam( + name: 'DEPLOY_TO_S4', + defaultValue: false, + description: '可选:构建成功后部署到 S4 环境 (触发 job: DM_s4_lessie_ai)' + ) + booleanParam( + name: 'DEPLOY_TO_S5', + defaultValue: false, + description: '可选:构建成功后部署到 S5 环境 (触发 job: DM_s5_lessie_ai)' + ) + booleanParam( + name: 'DEPLOY_TO_S6', + defaultValue: false, + description: '可选:构建成功后部署到 S6 环境 (触发 job: DM_s6_lessie_ai)' + ) + } + environment { + REGISTRY = "uswccr.ccs.tencentyun.com" // 镜像仓库地址 + NAMESPACE = "lessie${params.NAME_SPACES}" // 命名空间 + IMAGE_NAME = "lessie-sourcing-agents" // 镜像名(固定前缀) + CREDENTIALS_ID = "dxin_img_hub_auth" // 容器仓库凭证ID + } + + stages { + stage('拉取代码') { + steps { + // 拉取指定分支代码(通过参数 params.Code_branch 动态指定) + git branch: "${params.Code_branch}", + credentialsId: 'fly_gitlab_auth', + url: 'http://172.24.16.20/python/lessie-sourcing-agents.git' + } + } + + stage('获取信息') { + steps { + script { + env.Code_branch = "${params.Code_branch}" + env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + env.GIT_COMMIT_LONG = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + env.GIT_AUTHOR = sh(script: 'git log -1 --pretty=format:%an', returnStdout: true).trim() + env.GIT_COMMIT_TIME = sh(script: 'git log -1 --pretty=format:%ct | xargs -I {} date -d @{} +%Y%m%d-%H%M%S', returnStdout: true).trim() + env.GIT_COMMIT_MSG = sh(script: 'git log -1 --pretty=format:%s | sed -e \'s/"/\\"/g\'', returnStdout: true).trim() + + def buildNumber = env.BUILD_NUMBER + def branchName = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim() + def formattedBranch = branchName.replace('/', '-').replace('_', '-') + def buildTime = sh(script: 'date +%Y%m%d%H%M', returnStdout: true).trim() + def defaultTag = "v${buildNumber}_${formattedBranch}_${GIT_COMMIT_SHORT}_${buildTime}" + + def customTag = params.CUSTOM_TAG?.trim() + def tagPattern = ~/^[a-zA-Z0-9._-]+$/ + + if (customTag && customTag ==~ tagPattern) { + echo "✅ 使用自定义镜像 Tag: ${customTag}" + env.IMAGE_TAG = customTag + } else if (customTag) { + echo "⚠️ 自定义 Tag '${customTag}' 不符合规范,将使用默认生成的 Tag: ${defaultTag}" + + def confirmed = true + timeout(time: 1, unit: 'MINUTES') { + try { + input( + message: """⚠️ Tag 命名不规范: + ${customTag} + + 将使用自动生成的 Tag: + ${defaultTag} + + 是否继续构建?""", + ok: '确认' + ) + } catch (err) { + // 用户点击“取消”或中断 + echo "🚫 用户取消构建" + confirmed = false + } + } + + if (confirmed) { + echo "✅ 用户确认使用自动生成的 Tag:${defaultTag}" + env.IMAGE_TAG = defaultTag + } else { + error("流水线已终止。") + } + } else { + env.IMAGE_TAG = defaultTag + echo "未输入自定义 Tag, 使用自动生成规则: ${env.IMAGE_TAG}" + } + } + } + } + + stage('登录仓库') { + steps { + withCredentials([usernamePassword( + credentialsId: env.CREDENTIALS_ID, + usernameVariable: 'REGISTRY_USER', + passwordVariable: 'REGISTRY_PWD' + )]) { + sh ''' + echo "$REGISTRY_PWD" | docker login ${REGISTRY} -u ${REGISTRY_USER} --password-stdin + ''' + } + } + } + + stage('构建镜像') { + steps { + script { + // 构建镜像,添加标签信息 + sh """ + docker build -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG} \ + --label "git-branch='${Code_branch}'" \ + --label "git-commit='${GIT_COMMIT_SHORT}'" \ + --label "git-author='${GIT_AUTHOR}'" \ + --label "git-message='${GIT_COMMIT_MSG}'" \ + --label "build-time='${GIT_COMMIT_TIME}'" \ + . + """ + } + } + } + + stage('推送镜像') { + steps { + script { + sh "docker push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + echo "推送镜像成功:${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + } + } + } + } + + post { + always { + script { + def keepCount = 3 + echo "开始清理本地旧镜像,仅保留最近 ${keepCount} 个构建版本" + def imagePrefix = "${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}" + + // 获取所有镜像(按创建时间排序,越新的越前) + // 格式:Repository:Tag ImageID CreatedAt + def allImagesRaw = sh(script: "docker images ${imagePrefix} --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedAt}}' | sort -rk3", returnStdout: true).trim() + if (!allImagesRaw) { + echo "未找到任何镜像,无需清理" + return + } + + def allImages = allImagesRaw.split('\n') + if (allImages.size() <= keepCount) { + echo "当前镜像数未超过 ${keepCount} 个,无需清理" + return + } + + def oldImages = allImages.drop(keepCount) + echo "发现 ${oldImages.size()} 个旧镜像需要清理" + oldImages.each { line -> + echo " ${line}" + } + + oldImages.each { line -> + def parts = line.split(' ') + def imageTag = parts[0] + def imageId = parts.size() > 1 ? parts[1] : "" + + // 对于标签为的无效镜像,使用镜像ID删除 + if (imageTag.contains("") && imageId) { + echo "删除无效镜像: ${imageId}" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else if (imageId) { + // 对于有标签的有效镜像,优先使用镜像ID删除 + echo "删除旧镜像: ${imageTag} (${imageId})" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else { + // 兜底方案,使用标签删除 + echo "删除旧镜像: ${imageTag}" + sh(returnStatus: true, script: "docker rmi -f ${imageTag} || true") + } + } + + echo "清理完成,当前镜像状态:" + sh """ + docker images ${imagePrefix} --format 'table {{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.Size}}' + """ + + sh "docker logout ${REGISTRY}" + echo "容器仓库已登出,本地凭证已清理" + } + } + success { + script { + // 1. 准备元数据 (转换所有环境变量为 String) + def metadataDir = '/var/lib/jenkins/metadata' + def metadataFileRelativePath = "${env.NAMESPACE}-${env.IMAGE_NAME}.json" // 相对于 metadataDir 的文件名 + def fullMetadataPath = "${metadataDir}/${metadataFileRelativePath}" + + // --- 转换为 String --- + def registry = env.REGISTRY as String + def namespace = env.NAMESPACE as String + def imageName = env.IMAGE_NAME as String + def imageTag = env.IMAGE_TAG as String + def codeBranch = params.Code_branch as String // 使用 params,因为 Code_branch 是参数 + def gitCommit = env.GIT_COMMIT_LONG as String + def gitAuthor = env.GIT_AUTHOR as String + def gitCommitMsg = env.GIT_COMMIT_MSG as String + def gitCommitTime = env.GIT_COMMIT_TIME as String + def buildNumber = env.BUILD_NUMBER as String + // --- 转换为 String --- + + // 2. 准备新数据 + def newImageData = [ + image_tag: imageTag, // 使用转换后的变量 + full_image_name: "${registry}/${namespace}/${imageName}:${imageTag}", // 使用转换后的变量 + labels: [ + "git-branch": codeBranch, + "git-commit": gitCommit, + "git-author": gitAuthor, + "git-message": gitCommitMsg, + "build-time": gitCommitTime + ], + build_job_number: buildNumber, + build_time: new Date().format('yyyy-MM-dd HH:mm:ss') // Jenkins 构建完成时间 + ] + + // 2. 读取现有数据(如果文件存在) + def existingDataList = [] + try { + // 使用 readJSON 步骤读取文件内容 (readJSON 会自动处理 LazyMap 问题) + def rawExistingData = readJSON file: fullMetadataPath, default: [] // 如果文件不存在,则返回空列表 [] + + // --- ✅ 修复:深拷贝 rawExistingData (修正内联代码) --- + if (rawExistingData instanceof List) { + existingDataList = rawExistingData.collect { item -> + if (item instanceof Map) { + // 递归深拷贝 Map (使用辅助函数) + return deepCopyForSerialization(item) + } else { + return item + } + } + } else { + echo "警告: 元数据文件 ${fullMetadataPath} 格式不正确(非 List 类型),将被覆盖。" + existingDataList = [] + } + // --- 结束修复 --- + + } catch (Exception e) { + // readJSON 在文件不存在时通常会返回 default 值,但如果文件存在但格式错误,会抛出异常 + echo "警告: 读取元数据文件 ${fullMetadataPath} 失败或格式错误: ${e.getMessage()},将被覆盖。" + // 确保目录存在 + sh "mkdir -p ${metadataDir}" + existingDataList = [] // 重置为新列表 + } + + // 3. 将新数据添加到列表开头(最新的在前) + existingDataList.add(0, newImageData) + + // 4. 限制列表大小为 20 + if (existingDataList.size() > 20) { + existingDataList = existingDataList.take(20) + } + + // 5. 使用 writeJSON 步骤写入文件 (writeJSON 会自动处理 Map 的序列化) + writeJSON file: fullMetadataPath, json: existingDataList, pretty: 2 // pretty: 2 表示格式化 JSON (2 个空格缩进) + + echo "镜像元数据已存储到: ${fullMetadataPath}" + + // 输出构建结果 + echo """ + 镜像地址:${registry}/${namespace}/${imageName}:${imageTag} + 对应代码提交哈希:${gitCommit} + 对应代码分支:${codeBranch} + 代码提交者:${gitAuthor} + 提交备注:${gitCommitMsg} + """.stripIndent() + } + // 构建成功后,若选择则触发部署到 S1 环境 + script { + // 定义部署映射:参数名 → 部署 Job 名 + def deployMap = [ + 'DEPLOY_TO_S1' : 'DM_s1_lessie_ai', + 'DEPLOY_TO_S2' : 'DM_s2_lessie_ai', + 'DEPLOY_TO_S3' : 'DM_s3_lessie_ai', + 'DEPLOY_TO_S4' : 'DM_s4_lessie_ai', + 'DEPLOY_TO_S5' : 'DM_s5_lessie_ai', + 'DEPLOY_TO_S6' : 'DM_s6_lessie_ai', + ] + + // 获取完整镜像名(从已有 env 变量拼接) + def fullImageName = "${env.REGISTRY}/${env.NAMESPACE}/${env.IMAGE_NAME}:${env.IMAGE_TAG}" + + echo "构建完成镜像:${fullImageName}" + echo "检查部署触发项..." + + deployMap.each { paramKey, jobName -> + if (params."${paramKey}" == true) { + echo "触发部署:${jobName} ← ${fullImageName}" + // 异步触发 + build job: jobName, + parameters: [ + string(name: 'CUSTOM_IMAGE', value: fullImageName), + ], + wait: false // ← 异步(此 Job 结束,不等待被调用的 Job 结束) + } else { + echo "未勾选 ${paramKey}, 跳过触发部署:${jobName}" + } + } + } + } + failure { + // 输出构建结果 + echo "有失败步骤!" + } + } +} \ No newline at end of file diff --git a/SCM/构建镜像/v3/build_image_lessie_ai_web_v3.groovy b/SCM/构建镜像/v3/build_image_lessie_ai_web_v3.groovy new file mode 100644 index 0000000..d872197 --- /dev/null +++ b/SCM/构建镜像/v3/build_image_lessie_ai_web_v3.groovy @@ -0,0 +1,407 @@ +// --- 辅助函数:深拷贝对象以确保可序列化 --- +def deepCopyForSerialization(obj) { + if (obj instanceof Map) { + // 创建新的 LinkedHashMap,递归拷贝值 + return obj.collectEntries { k, v -> [(k): deepCopyForSerialization(v)] } + } else if (obj instanceof List) { + // 创建新的 ArrayList,递归拷贝元素 + return obj.collect { item -> deepCopyForSerialization(item) } + } else if (obj instanceof String || obj instanceof Number || obj instanceof Boolean || obj == null) { + return obj + } else { + return obj.toString() + } +} +// --- 结束辅助函数 --- + + +pipeline { + agent any + + parameters { + gitParameter( + branchFilter: 'origin/(.*)', + defaultValue: 'dxin', + name: 'Code_branch', + type: 'PT_BRANCH_TAG', + selectedValue: 'DEFAULT', + sortMode: 'NONE', + description: '选择代码分支: ', + quickFilterEnabled: true, + tagFilter: '*', + listSize: "1" + ) + choice( + name: 'NAME_SPACES', + choices: ['sit', 'test', 'prod'], + description: '选择存放镜像的仓库命名空间:' + ) + choice( + name: 'BUILD_ENV', + choices: ['im', 's2', 'prod'], + description: '选择构建的环境配置, 默认为 pnpm build:im 构建' + ) + string( + name: 'CUSTOM_TAG', + defaultValue: '', + description: '可选:自定义镜像 Tag (字母、数字、点、下划线、短横线), 如 v0.0.1, 留空则自动生成 “ v+构建次数_分支名_短哈希_构建时间 ”' + ) + booleanParam( + name: 'DEPLOY_TO_S1', + defaultValue: false, + description: '构建成功后部署到 S1 环境 (触发 job: DM_s1_lessie_ai_web)' + ) + booleanParam( + name: 'DEPLOY_TO_S2', + defaultValue: false, + description: '构建成功后部署到 S2 环境 (触发 job: DM_s2_lessie_ai_web)' + ) + booleanParam( + name: 'DEPLOY_TO_S3', + defaultValue: false, + description: '构建成功后部署到 S3 环境 (触发 job: DM_s3_lessie_ai_web)' + ) + booleanParam( + name: 'DEPLOY_TO_S4', + defaultValue: false, + description: '构建成功后部署到 S4 环境 (触发 job: DM_s4_lessie_ai_web)' + ) + booleanParam( + name: 'DEPLOY_TO_S5', + defaultValue: false, + description: '构建成功后部署到 S5 环境 (触发 job: DM_s5_lessie_ai_web)' + ) + booleanParam( + name: 'DEPLOY_TO_S6', + defaultValue: false, + description: '构建成功后部署到 S6 环境 (触发 job: DM_s6_lessie_ai_web)' + ) + } + environment { + REGISTRY = "uswccr.ccs.tencentyun.com" // 镜像仓库地址 + NAMESPACE = "lessie${params.NAME_SPACES}" // 命名空间根据choices的选择拼接 + IMAGE_NAME = "lessie-ai-web" // 镜像名(固定前缀) + CREDENTIALS_ID = "dxin_img_hub_auth" // 容器仓库凭证ID + } + + stages { + stage('拉取代码') { + steps { + git branch: "${params.Code_branch}", + credentialsId: 'fly_gitlab_auth', + url: 'http://172.24.16.20/web/jennie.git' + } + } + + stage('获取信息') { + steps { + script { + // 获取分支名 + env.Code_branch = "${params.Code_branch}" + // 获取最近一次提交的哈希值(短格式,前8位) + env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD',returnStdout: true).trim() + // 获取最近一次提交的哈希值(全格式) + env.GIT_COMMIT_LONG = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + // 获取最近一次提交的作者 + env.GIT_AUTHOR = sh(script: 'git log -1 --pretty=format:%an',returnStdout: true).trim() + // 获取最近一次提交的时间(格式化) + env.GIT_COMMIT_TIME = sh( + script: 'git log -1 --pretty=format:%ct | xargs -I {} date -d @{} +%Y%m%d-%H%M%S', + returnStdout: true + ).trim() + // 获取最近一次提交的备注信息(转义特殊字符,避免构建失败) + env.GIT_COMMIT_MSG = sh(script: 'git log -1 --pretty=format:%s | sed -e \'s/"/\\"/g\'', returnStdout: true).trim() + + // Jenkins构建次数 + def buildNumber = env.BUILD_NUMBER // Jenkins内置变量,直接获取当前Job的构建序号 + // 当前分支名(处理/为-,如feature/docker_1015 → feature-docker_1015) + def branchName = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim() + def formattedBranch = branchName.replace('/', '-').replace('_', '-') // 替换分支名中的/和_为- + // 构建时间(格式:202510181215,年-月-日-时-分,无分隔符) + def buildTime = sh(script: 'date +%Y%m%d%H%M', returnStdout: true).trim() + def defaultTag = "v${buildNumber}_${formattedBranch}_${GIT_COMMIT_SHORT}_${buildTime}" + + def customTag = params.CUSTOM_TAG?.trim() + def tagPattern = ~/^[a-zA-Z0-9._-]+$/ + + // 判断最终Tag + if (customTag && customTag ==~ tagPattern) { + echo "✅ 使用自定义镜像 Tag: ${customTag}" + env.IMAGE_TAG = customTag + } else if (customTag) { + echo "⚠️ 自定义 Tag '${customTag}' 不符合规范,将使用默认生成的 Tag: ${defaultTag}" + + def confirmed = true + timeout(time: 1, unit: 'MINUTES') { + try { + input( + message: """⚠️ Tag 命名不规范: + ${customTag} + + 将使用自动生成的 Tag: + ${defaultTag} + + 是否继续构建?""", + ok: '确认' + ) + } catch (err) { + // 用户点击“取消”或中断 + echo "🚫 用户取消构建" + confirmed = false + } + } + + if (confirmed) { + echo "✅ 用户确认使用自动生成的 Tag:${defaultTag}" + env.IMAGE_TAG = defaultTag + } else { + error("流水线已终止。") + } + } else { + env.IMAGE_TAG = defaultTag + echo "未输入自定义 Tag, 使用自动生成规则: ${env.IMAGE_TAG}" + } + } + } + } + + stage('pnpm i&b') { + steps { + script { + def buildEnv = params.BUILD_ENV // 获取参数 + sh """ + export PATH="/data/nvm/versions/node/v20.15.0/bin:$PATH" + echo "开始安装依赖包" + cd ${WORKSPACE}/ && rm -rf node_modules && pnpm install + echo "开始构建" + pnpm build:${buildEnv} + mv dist/main/index.html dist/ + chmod -R 755 dist/ + """ + } + } + } + + stage('登录容器') { + steps { + withCredentials([usernamePassword( + credentialsId: env.CREDENTIALS_ID, + usernameVariable: 'REGISTRY_USER', + passwordVariable: 'REGISTRY_PWD' + )]) { + sh ''' + echo "$REGISTRY_PWD" | docker login ${REGISTRY} -u ${REGISTRY_USER} --password-stdin + ''' + } + } + } + + stage('构建容器') { + steps { + script { + // 构建镜像,添加标签信息 + sh """ + docker build -t ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG} \ + --label "git-branch='${Code_branch}'" \ + --label "git-commit='${GIT_COMMIT_SHORT}'" \ + --label "git-author='${GIT_AUTHOR}'" \ + --label "git-message='${GIT_COMMIT_MSG}'" \ + --label "build-time='${GIT_COMMIT_TIME}'" \ + . + """ + } + } + } + + stage('推送镜像') { + steps { + script { + sh "docker push ${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + echo "推送镜像成功:${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + } + } + } + } + + post { + always { + script { + def keepCount = 3 + echo "开始清理本地旧镜像,仅保留最近 ${keepCount} 个构建版本" + def imagePrefix = "${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}" + + // 获取所有镜像(按创建时间排序,越新的越前) + // 格式:Repository:Tag ImageID CreatedAt + def allImagesRaw = sh(script: "docker images ${imagePrefix} --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedAt}}' | sort -rk3", returnStdout: true).trim() + if (!allImagesRaw) { + echo "未找到任何镜像,无需清理" + return + } + + def allImages = allImagesRaw.split('\n') + if (allImages.size() <= keepCount) { + echo "当前镜像数未超过 ${keepCount} 个,无需清理" + return + } + + def oldImages = allImages.drop(keepCount) + echo "发现 ${oldImages.size()} 个旧镜像需要清理" + oldImages.each { line -> + echo " ${line}" + } + + oldImages.each { line -> + def parts = line.split(' ') + def imageTag = parts[0] + def imageId = parts.size() > 1 ? parts[1] : "" + + // 对于标签为的无效镜像,使用镜像ID删除 + if (imageTag.contains("") && imageId) { + echo "删除无效镜像: ${imageId}" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else if (imageId) { + // 对于有标签的有效镜像,优先使用镜像ID删除 + echo "删除旧镜像: ${imageTag} (${imageId})" + sh(returnStatus: true, script: "docker rmi -f ${imageId} || true") + } else { + // 兜底方案,使用标签删除 + echo "删除旧镜像: ${imageTag}" + sh(returnStatus: true, script: "docker rmi -f ${imageTag} || true") + } + } + + echo "清理完成,当前镜像状态:" + sh """ + docker images ${imagePrefix} --format 'table {{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.Size}}' + """ + + sh "docker logout ${REGISTRY}" + echo "容器仓库已登出,本地凭证已清理" + } + } + success { + script { + // 1. 准备元数据 (转换所有环境变量为 String) + def metadataDir = '/var/lib/jenkins/metadata' + def metadataFileRelativePath = "${env.NAMESPACE}-${env.IMAGE_NAME}.json" // 相对于 metadataDir 的文件名 + def fullMetadataPath = "${metadataDir}/${metadataFileRelativePath}" + + // --- 转换为 String --- + def registry = env.REGISTRY as String + def namespace = env.NAMESPACE as String + def imageName = env.IMAGE_NAME as String + def imageTag = env.IMAGE_TAG as String + def codeBranch = params.Code_branch as String // 使用 params,因为 Code_branch 是参数 + def gitCommit = env.GIT_COMMIT_LONG as String + def gitAuthor = env.GIT_AUTHOR as String + def gitCommitMsg = env.GIT_COMMIT_MSG as String + def gitCommitTime = env.GIT_COMMIT_TIME as String + def buildNumber = env.BUILD_NUMBER as String + // --- 转换为 String --- + + // 2. 准备新数据 + def newImageData = [ + image_tag: imageTag, // 使用转换后的变量 + full_image_name: "${registry}/${namespace}/${imageName}:${imageTag}", // 使用转换后的变量 + labels: [ + "git-branch": codeBranch, + "git-commit": gitCommit, + "git-author": gitAuthor, + "git-message": gitCommitMsg, + "build-time": gitCommitTime + ], + build_job_number: buildNumber, + build_time: new Date().format('yyyy-MM-dd HH:mm:ss') // Jenkins 构建完成时间 + ] + + // 2. 读取现有数据(如果文件存在) + def existingDataList = [] + try { + // 使用 readJSON 步骤读取文件内容 (readJSON 会自动处理 LazyMap 问题) + def rawExistingData = readJSON file: fullMetadataPath, default: [] // 如果文件不存在,则返回空列表 [] + + // --- ✅ 修复:深拷贝 rawExistingData (修正内联代码) --- + if (rawExistingData instanceof List) { + existingDataList = rawExistingData.collect { item -> + if (item instanceof Map) { + // 递归深拷贝 Map (使用辅助函数) + return deepCopyForSerialization(item) + } else { + return item + } + } + } else { + echo "警告: 元数据文件 ${fullMetadataPath} 格式不正确(非 List 类型),将被覆盖。" + existingDataList = [] + } + // --- 结束修复 --- + + } catch (Exception e) { + // readJSON 在文件不存在时通常会返回 default 值,但如果文件存在但格式错误,会抛出异常 + echo "警告: 读取元数据文件 ${fullMetadataPath} 失败或格式错误: ${e.getMessage()},将被覆盖。" + // 确保目录存在 + sh "mkdir -p ${metadataDir}" + existingDataList = [] // 重置为新列表 + } + + // 3. 将新数据添加到列表开头(最新的在前) + existingDataList.add(0, newImageData) + + // 4. 限制列表大小为 20 + if (existingDataList.size() > 20) { + existingDataList = existingDataList.take(20) + } + + // 5. 使用 writeJSON 步骤写入文件 (writeJSON 会自动处理 Map 的序列化) + writeJSON file: fullMetadataPath, json: existingDataList, pretty: 2 // pretty: 2 表示格式化 JSON (2 个空格缩进) + + echo "镜像元数据已存储到: ${fullMetadataPath}" + + // 输出构建结果 + echo """ + 镜像地址:${registry}/${namespace}/${imageName}:${imageTag} + 对应代码提交哈希:${gitCommit} + 对应代码分支:${codeBranch} + 代码提交者:${gitAuthor} + 提交备注:${gitCommitMsg} + """.stripIndent() + } + // 构建成功后,若选择则触发部署到 S1 环境 + script { + // 定义部署映射:参数名 → 部署 Job 名 + def deployMap = [ + 'DEPLOY_TO_S1' : 'DM_s1_lessie_ai_web', + 'DEPLOY_TO_S2' : 'DM_s2_lessie_ai_web', + 'DEPLOY_TO_S3' : 'DM_s3_lessie_ai_web', + 'DEPLOY_TO_S4' : 'DM_s4_lessie_ai_web', + 'DEPLOY_TO_S5' : 'DM_s5_lessie_ai_web', + 'DEPLOY_TO_S6' : 'DM_s6_lessie_ai_web', + ] + + // 获取完整镜像名(从已有 env 变量拼接) + def fullImageName = "${env.REGISTRY}/${env.NAMESPACE}/${env.IMAGE_NAME}:${env.IMAGE_TAG}" + + echo "构建完成镜像:${fullImageName}" + echo "检查部署触发项..." + + deployMap.each { paramKey, jobName -> + if (params."${paramKey}" == true) { + echo "触发部署:${jobName} ← ${fullImageName}" + // 异步触发 + build job: jobName, + parameters: [ + string(name: 'CUSTOM_IMAGE', value: fullImageName), + ], + wait: false // ← 异步(此 Job 结束,不等待被调用的 Job 结束) + } else { + echo "未勾选 ${paramKey}, 跳过触发部署:${jobName}" + } + } + } + } + failure { + // 输出构建结果 + echo "部署有错误,请检查!" + } + } +} \ No newline at end of file diff --git a/SCM/部署镜像/apex/DM_apex.groovy b/SCM/部署镜像/apex/DM_apex.groovy index 4ae2051..c6c54ea 100644 --- a/SCM/部署镜像/apex/DM_apex.groovy +++ b/SCM/部署镜像/apex/DM_apex.groovy @@ -123,7 +123,7 @@ pipeline { ).trim() env.OLD_IMAGE_NAME = oldImg echo "--- 目前正常运行的旧镜像: ${OLD_IMAGE_NAME} ---" - echo "--- 所选择新的镜像: ${params.IMAGE_NAME} ---" + echo "--- 所选择新的镜像: ${env.IMAGE_FULL_NAME} ---" echo "--- 修改 Deployment YAML 中的镜像为新镜像版本 ---" sh """ sed -i 's#image:.*#image: ${IMAGE_FULL_NAME}#' ${Deployment_yaml} @@ -269,7 +269,7 @@ jenkins执行人: ${user} post { success { echo "成功!" - echo "=== 所选择镜像: ${params.IMAGE_NAME} ===" + echo "=== 所选择镜像: ${env.IMAGE_FULL_NAME} ===" } failure { echo "有步骤失败,请检查!" diff --git a/SCM/部署镜像/s1/DM_s1_flymoon_admin.groovy b/SCM/部署镜像/s1/v2/DM_s1_flymoon_admin.groovy similarity index 100% rename from SCM/部署镜像/s1/DM_s1_flymoon_admin.groovy rename to SCM/部署镜像/s1/v2/DM_s1_flymoon_admin.groovy diff --git a/SCM/部署镜像/s1/DM_s1_flymoon_admin_web.groovy b/SCM/部署镜像/s1/v2/DM_s1_flymoon_admin_web.groovy similarity index 100% rename from SCM/部署镜像/s1/DM_s1_flymoon_admin_web.groovy rename to SCM/部署镜像/s1/v2/DM_s1_flymoon_admin_web.groovy diff --git a/SCM/部署镜像/s1/DM_s1_flymoon_agent.groovy b/SCM/部署镜像/s1/v2/DM_s1_flymoon_agent.groovy similarity index 100% rename from SCM/部署镜像/s1/DM_s1_flymoon_agent.groovy rename to SCM/部署镜像/s1/v2/DM_s1_flymoon_agent.groovy diff --git a/SCM/部署镜像/s1/DM_s1_flymoon_email.groovy b/SCM/部署镜像/s1/v2/DM_s1_flymoon_email.groovy similarity index 100% rename from SCM/部署镜像/s1/DM_s1_flymoon_email.groovy rename to SCM/部署镜像/s1/v2/DM_s1_flymoon_email.groovy diff --git a/SCM/部署镜像/s1/DM_s1_flymoon_payment.groovy b/SCM/部署镜像/s1/v2/DM_s1_flymoon_payment.groovy similarity index 100% rename from SCM/部署镜像/s1/DM_s1_flymoon_payment.groovy rename to SCM/部署镜像/s1/v2/DM_s1_flymoon_payment.groovy diff --git a/SCM/部署镜像/s1/DM_s1_lessie_agent.groovy b/SCM/部署镜像/s1/v2/DM_s1_lessie_agent.groovy similarity index 100% rename from SCM/部署镜像/s1/DM_s1_lessie_agent.groovy rename to SCM/部署镜像/s1/v2/DM_s1_lessie_agent.groovy diff --git a/SCM/部署镜像/s1/DM_s1_lessie_ai_web.groovy b/SCM/部署镜像/s1/v2/DM_s1_lessie_ai_web.groovy similarity index 100% rename from SCM/部署镜像/s1/DM_s1_lessie_ai_web.groovy rename to SCM/部署镜像/s1/v2/DM_s1_lessie_ai_web.groovy diff --git a/SCM/部署镜像/s1/DM_s1_lessie_go_api.groovy b/SCM/部署镜像/s1/v2/DM_s1_lessie_go_api.groovy similarity index 100% rename from SCM/部署镜像/s1/DM_s1_lessie_go_api.groovy rename to SCM/部署镜像/s1/v2/DM_s1_lessie_go_api.groovy diff --git a/k8s_yaml/config/apex-user/apex-user-kubeconfig.yaml b/k8s_yaml/config/apex-user/apex-user-kubeconfig.yaml index 6fcd67e..596c66a 100644 --- a/k8s_yaml/config/apex-user/apex-user-kubeconfig.yaml +++ b/k8s_yaml/config/apex-user/apex-user-kubeconfig.yaml @@ -11,7 +11,12 @@ contexts: namespace: apex-evaluation user: apex-user name: apex-user-apex-evaluation # 给上下文起的名字 -current-context: apex-user-apex-evaluation # 当前使用的上下文 +- context: + cluster: cls-pl1yhr34 + namespace: sit + user: apex-user + name: apex-user-sit # 给上下文起的名字 +current-context: apex-user-apex-evaluation # 默认使用的上下文 users: - name: apex-user # 给用户起的名字 user: # kubectl get secret apex-user-longtoken -n apex-evaluation -o jsonpath='{.data.token}' | base64 --decode 查看创建的长token diff --git a/k8s_yaml/config/apex-user/apex-user-rbac-sit.yaml b/k8s_yaml/config/apex-user/apex-user-rbac-sit.yaml new file mode 100644 index 0000000..4db84d6 --- /dev/null +++ b/k8s_yaml/config/apex-user/apex-user-rbac-sit.yaml @@ -0,0 +1,73 @@ +# 创建 ServiceAccount(放在 apex-evaluation 命名空间) +apiVersion: v1 +kind: ServiceAccount +metadata: + name: apex-user + namespace: apex-evaluation # 明确 ServiceAccount 所在的命名空间(必填) +--- +# 创建 ServiceAccount(放在 sit 命名空间) +apiVersion: v1 +kind: ServiceAccount +metadata: + name: apex-user + namespace: sit # 在 sit 命名空间也创建相同的 ServiceAccount +--- + +# 为 apex-evaluation 命名空间创建 Role +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: apex-user-role + namespace: apex-evaluation +rules: +- apiGroups: ["", "apps", "extensions", "batch", "networking.k8s.io"] + resources: ["pods", "pods/log", "pods/exec", "statefulsets", "deployments", "daemonsets", "services", "configmaps", "secrets", "events", "replicasets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +- apiGroups: ["metrics.k8s.io"] + resources: ["pods","nodes"] + verbs: ["get","list","watch"] +--- +# 为 sit 命名空间创建 Role +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: apex-user-role + namespace: sit +rules: +- apiGroups: ["", "apps", "extensions", "batch", "networking.k8s.io"] + resources: ["pods", "pods/log", "pods/exec", "statefulsets", "deployments", "daemonsets", "services", "configmaps", "secrets", "events", "replicasets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +- apiGroups: ["metrics.k8s.io"] + resources: ["pods","nodes"] + verbs: ["get","list","watch"] +--- + +# 将 apex-evaluation 命名空间的 Role 绑定到 ServiceAccount +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: apex-user-binding + namespace: apex-evaluation +subjects: +- kind: ServiceAccount + name: apex-user + namespace: apex-evaluation +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: apex-user-role +--- +# 将 sit 命名空间的 Role 绑定到 ServiceAccount +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: apex-user-binding + namespace: sit +subjects: +- kind: ServiceAccount + name: apex-user + namespace: sit +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: apex-user-role \ No newline at end of file