Files
jenkins-pipeline/SCM/构建镜像/build_image_lessie_ai_web_v2.groovy
2025-12-03 15:31:02 +08:00

345 lines
16 KiB
Groovy
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// --- 辅助函数:深拷贝对象以确保可序列化 ---
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+构建次数_分支名_短哈希_构建时间 ”'
)
}
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] : ""
// 对于标签为<none>的无效镜像使用镜像ID删除
if (imageTag.contains("<none>") && 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()
}
}
failure {
// 输出构建结果
echo "部署有错误,请检查!"
}
}
}