331 lines
19 KiB
Plaintext
331 lines
19 KiB
Plaintext
|
// Groovy 辅助函数,用于发送钉钉通知。
|
|||
|
// 建议在 pipeline 块外部定义,或使用共享库。
|
|||
|
// @NonCPS 注解对于复杂的辅助函数可能有用。
|
|||
|
@NonCPS
|
|||
|
def sendDingTalkNotification(Map config) {
|
|||
|
def message = config.get('message', '来自 Jenkins 的通知')
|
|||
|
def webhookEnvVarName = config.get('webhookEnvVarName') // 存储 Webhook URL 的环境变量名称
|
|||
|
def author = config.get('author', '未知用户')
|
|||
|
def jobName = config.get('jobName', env.JOB_NAME ?: 'N/A')
|
|||
|
def buildNumber = config.get('buildNumber', env.BUILD_NUMBER ?: 'N/A')
|
|||
|
def enabled = config.get('enabled', false)
|
|||
|
|
|||
|
// 确保 webhookEnvVarName 被正确设置
|
|||
|
if (enabled && webhookEnvVarName) {
|
|||
|
// 从环境变量中获取 webhookUrl
|
|||
|
def webhookUrl = env[webhookEnvVarName]
|
|||
|
if (!webhookUrl) {
|
|||
|
echo "钉钉 Webhook URL 未通过环境变量 ${webhookEnvVarName} 找到。跳过通知。"
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
// 构造消息内容
|
|||
|
def finalMessage = "BZPT.发布 (Jenkins ${jobName}#${buildNumber}):\n${message}"
|
|||
|
if (author && author != "未知用户" && author.trim() != "") {
|
|||
|
finalMessage += "\n@${author.trim()}"
|
|||
|
}
|
|||
|
|
|||
|
// 准备 JSON payload
|
|||
|
def payload = groovy.json.JsonOutput.toJson([msgtype: "text", text: [content: finalMessage]])
|
|||
|
|
|||
|
// 使用 curl 发送 POST 请求到钉钉 Webhook URL
|
|||
|
// 使用 returnStatus: true 来捕获 curl 的退出码,以便判断是否成功
|
|||
|
def curlResult = sh script: """
|
|||
|
echo "正在发送钉钉通知..."
|
|||
|
# 使用单引号包裹 payload 和 webhookUrl 以处理特殊字符
|
|||
|
curl -X POST -H 'Content-Type: application/json' -d '${payload}' '${webhookUrl}' --silent --show-error --connect-timeout 10 --max-time 15
|
|||
|
""", returnStatus: true
|
|||
|
|
|||
|
// 根据 curl 的退出码判断通知是否发送成功
|
|||
|
if (curlResult != 0) {
|
|||
|
echo "警告:钉钉通知可能发送失败 (curl 退出码: ${curlResult})。"
|
|||
|
} else {
|
|||
|
echo "钉钉通知发送成功。"
|
|||
|
}
|
|||
|
} else {
|
|||
|
echo "钉钉通知已跳过 (可能已禁用、未设置 Webhook 凭证或未找到 Webhook URL 的环境变量)。"
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
pipeline {
|
|||
|
agent any
|
|||
|
|
|||
|
parameters {
|
|||
|
// Git 相关参数
|
|||
|
string(name: 'GIT_REPO_URL', defaultValue: 'http://111.230.114.47:3000/yidongliang/BZPT.SYS', description: 'Git 仓库 URL')
|
|||
|
string(name: 'GIT_BRANCH', defaultValue: 'stage', description: '要拉取的 Git 分支 (例如:develop, stage, master)')
|
|||
|
credentials(name: 'GIT_CREDENTIALS_ID', defaultValue: 'jenkins', description: 'Git 凭证 ID (用户名/密码或Token)。如果仓库是私有的则必需。', required: true)
|
|||
|
|
|||
|
// Docker 构建参数 (Dockerfile 负责 .NET 编译)
|
|||
|
string(name: 'DOCKERFILE_PATH_IN_REPO', defaultValue: 'BZPT.Api/Dockerfile', description: '仓库中 Dockerfile 的路径 (例如:Project.Api/Dockerfile)')
|
|||
|
// Docker 构建上下文将是工作空间根目录 '.'
|
|||
|
|
|||
|
// Docker Registry 和镜像参数
|
|||
|
string(name: 'DOCKER_REGISTRY_URL', defaultValue: 'https://106.52.199.114:5000', description: 'Docker 镜像仓库 URL (例如:docker.io/youruser)。如果推送到本地 Docker 守护进程,则留空。')
|
|||
|
string(name: 'DOCKER_IMAGE_NAME', defaultValue: 'bzpt.sys', description: 'Docker 镜像名称 (不含仓库地址部分)')
|
|||
|
string(name: 'IMAGE_BASE_TAG', defaultValue: '1.0', description: '镜像标签的基础部分 (例如:1.0 -> 1.0-构建号-commitID)')
|
|||
|
credentials(name: 'DOCKER_CREDENTIALS_ID', defaultValue: 'dockerregister', description: 'Docker 镜像仓库凭证 ID。如果推送到私有仓库则必需。', required: false) // 如果 DOCKER_REGISTRY_URL 为空则非必需
|
|||
|
booleanParam(name: 'PUSH_LATEST_TAG', defaultValue: true, description: '是否同时创建并推送 "latest" 标签的镜像?')
|
|||
|
|
|||
|
// 钉钉通知参数
|
|||
|
booleanParam(name: 'SEND_DINGTALK_NOTIFICATIONS', defaultValue: true, description: '是否发送钉钉通知?')
|
|||
|
credentials(name: 'DINGTALK_WEBHOOK_CREDENTIAL_ID', defaultValue: 'stage-publish-dingding', description: '存储完整钉钉 Webhook URL 的 Jenkins Secret Text 凭证 ID', required: false)
|
|||
|
}
|
|||
|
|
|||
|
// environment 块用于定义在整个 pipeline 中可用的环境变量
|
|||
|
environment {
|
|||
|
// IMAGE_TAG 将在 Checkout 后动态设置
|
|||
|
LAST_COMMIT_AUTHOR = "sys-stage" // 默认值
|
|||
|
DINGTALK_WEBHOOK_ENV_VAR_NAME = 'DINGTALK_WEBHOOK_URL_FROM_CREDS' // withCredentials 使用的环境变量名
|
|||
|
// PREPARED_IMAGE_NAME 将在 script 块中动态设置,用于表示带 Registry 的完整镜像名
|
|||
|
// (例如: https://106.52.199.114:5000/bzpt.sys)
|
|||
|
}
|
|||
|
|
|||
|
stages {
|
|||
|
// Stage 0: 初始化和环境准备
|
|||
|
stage('0. 初始化') {
|
|||
|
steps {
|
|||
|
script {
|
|||
|
// **核心改动 1:动态计算 PREPARED_IMAGE_NAME**
|
|||
|
// 这个变量将存储完整的镜像名,包括 Registry URL 和 Repository 名称。
|
|||
|
// 它会在后续的 docker 命令中使用,但 NOT 作为 docker build 的第一个参数(它需要 repository:tag 格式)。
|
|||
|
// def preparedImageNameWithRegistry = ""
|
|||
|
// if (params.DOCKER_REGISTRY_URL) {
|
|||
|
// // 移除可能的前后空格,并组合 Registry URL 和镜像名
|
|||
|
// preparedImageNameWithRegistry = "${params.DOCKER_IMAGE_NAME}"//"${params.DOCKER_REGISTRY_URL.trim()}/${params.DOCKER_IMAGE_NAME}"
|
|||
|
// } else {
|
|||
|
// // 如果 DOCKER_REGISTRY_URL 为空,则只使用镜像名
|
|||
|
// preparedImageNameWithRegistry = params.DOCKER_IMAGE_NAME
|
|||
|
// echo "注意:DOCKER_REGISTRY_URL 为空,镜像将不会推送到指定的仓库地址,而是可能推送到本地 Docker 守护进程的仓库。"
|
|||
|
// }
|
|||
|
preparedImageNameWithRegistry = params.DOCKER_IMAGE_NAME
|
|||
|
// 将计算出的完整镜像名(包含 registry)赋值给环境变量,以便后续阶段使用
|
|||
|
env.PREPARED_IMAGE_NAME = preparedImageNameWithRegistry
|
|||
|
echo "构建的镜像全名 (包括 Registry): ${env.PREPARED_IMAGE_NAME}"
|
|||
|
|
|||
|
// 添加一些额外的检查,提高健壮性
|
|||
|
if (params.DOCKER_REGISTRY_URL && !params.DOCKER_CREDENTIALS_ID) {
|
|||
|
echo "警告:已设置 DOCKER_REGISTRY_URL ('${params.DOCKER_REGISTRY_URL}') 但未提供 DOCKER_CREDENTIALS_ID。推送到私有仓库可能会失败。"
|
|||
|
}
|
|||
|
|
|||
|
// 检查 Git 仓库 URL 是否可能需要凭证
|
|||
|
if (!params.GIT_REPO_URL.toLowerCase().startsWith('http://') && !params.GIT_REPO_URL.toLowerCase().startsWith('https://') && !params.GIT_REPO_URL.toLowerCase().startsWith('git@') && !params.GIT_CREDENTIALS_ID) {
|
|||
|
echo "警告:Git 仓库 URL ('${params.GIT_REPO_URL}') 可能需要凭证,但未提供 GIT_CREDENTIALS_ID。"
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Stage 1: 拉取代码
|
|||
|
stage('1. 拉取代码') {
|
|||
|
steps {
|
|||
|
echo "清理工作空间..."
|
|||
|
cleanWs() // 清理工作空间以避免旧文件干扰
|
|||
|
|
|||
|
echo "从 ${params.GIT_REPO_URL} 拉取代码, 分支: ${params.GIT_BRANCH}"
|
|||
|
checkout([
|
|||
|
$class: 'GitSCM',
|
|||
|
branches: [[name: params.GIT_BRANCH]],
|
|||
|
userRemoteConfigs: [[
|
|||
|
url: params.GIT_REPO_URL,
|
|||
|
credentialsId: params.GIT_CREDENTIALS_ID // 使用指定的 Git 凭证
|
|||
|
]],
|
|||
|
extensions: [
|
|||
|
// 优化拉取:只拉取最近的提交,减少网络和磁盘 I/O
|
|||
|
[$class: 'CloneOption', shallow: true, noTags: true, depth: 1, timeout: 300],
|
|||
|
[$class: 'PruneStaleBranch'], // 清理本地过时的分支
|
|||
|
[$class: 'LocalBranch', localBranch: params.GIT_BRANCH] // 确保本地分支与远程匹配
|
|||
|
]
|
|||
|
])
|
|||
|
|
|||
|
script {
|
|||
|
// 获取当前提交的短哈希值
|
|||
|
def shortCommit = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
|
|||
|
|
|||
|
// **核心改动 2:生成 IMAGE_TAG**
|
|||
|
// IMAGE_TAG 由基础标签、构建号和提交哈希组成
|
|||
|
env.IMAGE_TAG = "${params.IMAGE_BASE_TAG}-${BUILD_NUMBER}-${shortCommit}"
|
|||
|
echo "生成的 IMAGE_TAG: ${env.IMAGE_TAG}"
|
|||
|
|
|||
|
// 确保 PREPARED_IMAGE_NAME 已经被设置,这是后续 Docker 操作的前提
|
|||
|
if (!env.PREPARED_IMAGE_NAME) {
|
|||
|
error "PREPARED_IMAGE_NAME 未在 '0. 初始化' 阶段正确设置。"
|
|||
|
}
|
|||
|
|
|||
|
// 尝试获取最后一次提交的作者信息
|
|||
|
try {
|
|||
|
env.LAST_COMMIT_AUTHOR = sh(script: 'git log -1 --pretty=format:"%an"', returnStdout: true).trim()
|
|||
|
} catch (e) {
|
|||
|
echo "警告:无法获取最后提交的作者。 ${e.getMessage()}"
|
|||
|
env.LAST_COMMIT_AUTHOR = "未知用户" // 设置一个默认值
|
|||
|
}
|
|||
|
echo "最后提交的作者: ${env.LAST_COMMIT_AUTHOR}"
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Stage 2: 构建 Docker 镜像
|
|||
|
// 此阶段假设您的 Dockerfile 负责 .NET Core 的编译和打包
|
|||
|
stage('2. 构建 Docker 镜像') {
|
|||
|
steps {
|
|||
|
script {
|
|||
|
def dockerfilePath = params.DOCKERFILE_PATH_IN_REPO
|
|||
|
// 验证 Dockerfile 是否存在
|
|||
|
if (!fileExists(dockerfilePath)) {
|
|||
|
error "在工作空间相对路径下未找到 Dockerfile: ${dockerfilePath}"
|
|||
|
}
|
|||
|
|
|||
|
// 确保镜像名和标签都已设置
|
|||
|
if (!env.PREPARED_IMAGE_NAME || !env.IMAGE_TAG) {
|
|||
|
error "构建 Docker 镜像所需的 PREPARED_IMAGE_NAME 或 IMAGE_TAG 未设置。"
|
|||
|
}
|
|||
|
|
|||
|
// **核心改动 3:正确使用 docker.build 函数**
|
|||
|
// docker.build 的第一个参数格式应为 [REGISTRY/]REPOSITORY:TAG
|
|||
|
// env.PREPARED_IMAGE_NAME 已经包含了 Registry URL 和 Repository 名称 (例如: https://106.52.199.114:5000/bzpt.sys)
|
|||
|
def fullImageNameWithRegistry = "${env.PREPARED_IMAGE_NAME}:${env.IMAGE_TAG}"
|
|||
|
echo "开始构建 Docker 镜像: ${fullImageNameWithRegistry}"
|
|||
|
echo "使用 Dockerfile: ${dockerfilePath}"
|
|||
|
echo "构建上下文: 工作空间根目录 (.)"
|
|||
|
|
|||
|
// 执行 docker build 命令
|
|||
|
docker.build(fullImageNameWithRegistry, "-f \"${dockerfilePath}\" .")
|
|||
|
echo "Docker 镜像 ${fullImageNameWithRegistry} 构建成功。"
|
|||
|
|
|||
|
// 如果启用了推送 latest 标签,则标记镜像
|
|||
|
if (params.PUSH_LATEST_TAG) {
|
|||
|
// def fullImageNameWithRegistry = "${env.PREPARED_IMAGE_NAME}:${env.IMAGE_TAG}"
|
|||
|
def imageNameWithoutRegistry = params.DOCKER_IMAGE_NAME
|
|||
|
|
|||
|
echo "使用 sh 命令将 ${fullImageNameWithRegistry} 标记为 ${imageNameWithoutRegistry}:latest"
|
|||
|
sh """
|
|||
|
echo "正在使用 docker tag 命令进行标记..."
|
|||
|
docker tag ${fullImageNameWithRegistry} ${imageNameWithoutRegistry}:latest
|
|||
|
echo "成功标记为 ${imageNameWithoutRegistry}:latest"
|
|||
|
"""
|
|||
|
// 移除下面的冗余 echo
|
|||
|
// echo "成功标记为 ${imageNameWithoutRegistry}:latest"
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Stage 3: 推送 Docker 镜像 (可选)
|
|||
|
// 这个阶段仅在 DOCKER_REGISTRY_URL 参数不为空时执行
|
|||
|
stage('3. 推送 Docker 镜像 (可选)') {
|
|||
|
when { expression { params.DOCKER_REGISTRY_URL != "" } }
|
|||
|
steps {
|
|||
|
echo "开始推送 Docker 镜像到 ${params.DOCKER_REGISTRY_URL}"
|
|||
|
script {
|
|||
|
// 再次确保镜像名和标签已设置
|
|||
|
if (!env.PREPARED_IMAGE_NAME || !env.IMAGE_TAG) {
|
|||
|
error "推送 Docker 镜像所需的 PREPARED_IMAGE_NAME 或 IMAGE_TAG 未设置。"
|
|||
|
}
|
|||
|
|
|||
|
// 定义需要推送的完整镜像名和 latest 镜像名
|
|||
|
def fullImageNameWithRegistry = "${env.PREPARED_IMAGE_NAME}:${env.IMAGE_TAG}"
|
|||
|
def repositoryName = params.DOCKER_IMAGE_NAME // 不含 Registry 的 Repository 名称
|
|||
|
def fullImageNameLatest = "${repositoryName}:latest"
|
|||
|
|
|||
|
// 处理没有提供 Docker Registry 凭证的情况
|
|||
|
if (!params.DOCKER_CREDENTIALS_ID) {
|
|||
|
echo "警告:已设置 DOCKER_REGISTRY_URL 但未设置 DOCKER_CREDENTIALS_ID。尝试匿名推送或推送到本地 Docker 守护进程。"
|
|||
|
// 直接推送,Docker 会尝试使用 Agent 的默认配置或匿名方式
|
|||
|
docker.image(fullImageNameWithRegistry).push()
|
|||
|
echo "镜像 ${fullImageNameWithRegistry} 尝试推送 (匿名/本地)。"
|
|||
|
if (params.PUSH_LATEST_TAG) {
|
|||
|
docker.image(fullImageNameLatest).push()
|
|||
|
echo "镜像 ${fullImageNameLatest} 尝试推送 (匿名/本地)。"
|
|||
|
}
|
|||
|
} else {
|
|||
|
// **核心改动 5:使用 docker.withRegistry 来处理 Registry 连接和认证**
|
|||
|
// docker.withRegistry 会在块内设置 Docker 的认证信息,并使用指定的 Registry URL
|
|||
|
docker.withRegistry(params.DOCKER_REGISTRY_URL, params.DOCKER_CREDENTIALS_ID) {
|
|||
|
echo "正在推送 ${fullImageNameWithRegistry}"
|
|||
|
// **核心改动 6:在 withRegistry 块内调用 push 时,参数应为 Repository:Tag 格式**
|
|||
|
// 此时,docker 工具会自动将 Registry URL 添加到前面
|
|||
|
docker.image(fullImageNameWithRegistry).push()
|
|||
|
echo "镜像 ${fullImageNameWithRegistry} 推送成功。"
|
|||
|
|
|||
|
// 如果需要推送 latest 标签
|
|||
|
if (params.PUSH_LATEST_TAG) {
|
|||
|
echo "正在推送 ${fullImageNameLatest}"
|
|||
|
docker.image(fullImageNameLatest).push()
|
|||
|
echo "镜像 ${fullImageNameLatest} 推送成功。"
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// post 块用于定义流水线结束后的操作(无论成功、失败或中止)
|
|||
|
post {
|
|||
|
// always: 无论流水线最终结果如何都执行
|
|||
|
always {
|
|||
|
echo "流水线结束。最终状态: ${currentBuild.result ?: 'IN PROGRESS'}"
|
|||
|
}
|
|||
|
|
|||
|
// success: 流水线成功时执行
|
|||
|
success {
|
|||
|
script {
|
|||
|
// 仅当启用了钉钉通知且提供了凭证时发送成功通知
|
|||
|
if (params.SEND_DINGTALK_NOTIFICATIONS && params.DINGTALK_WEBHOOK_CREDENTIAL_ID) {
|
|||
|
// 使用 withCredentials 获取 Webhook URL 并将其设置到环境变量中
|
|||
|
withCredentials([string(credentialsId: params.DINGTALK_WEBHOOK_CREDENTIAL_ID, variable: env.DINGTALK_WEBHOOK_ENV_VAR_NAME)]) {
|
|||
|
sendDingTalkNotification(
|
|||
|
message: "${params.DOCKER_IMAGE_NAME} 构建和推送成功。镜像: ${env.PREPARED_IMAGE_NAME}:${env.IMAGE_TAG}",
|
|||
|
webhookEnvVarName: env.DINGTALK_WEBHOOK_ENV_VAR_NAME,
|
|||
|
author: env.LAST_COMMIT_AUTHOR ?: '未知用户', // 提供默认值防止 null
|
|||
|
jobName: env.JOB_NAME,
|
|||
|
buildNumber: env.BUILD_NUMBER,
|
|||
|
enabled: params.SEND_DINGTALK_NOTIFICATIONS
|
|||
|
)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// failure: 流水线失败时执行
|
|||
|
failure {
|
|||
|
script {
|
|||
|
// 仅当启用了钉钉通知且提供了凭证时发送失败通知
|
|||
|
if (params.SEND_DINGTALK_NOTIFICATIONS && params.DINGTALK_WEBHOOK_CREDENTIAL_ID) {
|
|||
|
withCredentials([string(credentialsId: params.DINGTALK_WEBHOOK_CREDENTIAL_ID, variable: env.DINGTALK_WEBHOOK_ENV_VAR_NAME)]) {
|
|||
|
sendDingTalkNotification(
|
|||
|
// 报告失败信息,包含当前阶段和 Jenkins 构建 URL
|
|||
|
message: "${params.DOCKER_IMAGE_NAME} 构建失败。请检查控制台: ${env.BUILD_URL}console",
|
|||
|
webhookEnvVarName: env.DINGTALK_WEBHOOK_ENV_VAR_NAME,
|
|||
|
author: env.LAST_COMMIT_AUTHOR ?: '未知用户',
|
|||
|
jobName: env.JOB_NAME,
|
|||
|
buildNumber: env.BUILD_NUMBER,
|
|||
|
enabled: params.SEND_DINGTALK_NOTIFICATIONS
|
|||
|
)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// aborted: 流水线被中止时执行
|
|||
|
aborted {
|
|||
|
script {
|
|||
|
// 仅当启用了钉钉通知且提供了凭证时发送中止通知
|
|||
|
if (params.SEND_DINGTALK_NOTIFICATIONS && params.DINGTALK_WEBHOOK_CREDENTIAL_ID) {
|
|||
|
withCredentials([string(credentialsId: params.DINGTALK_WEBHOOK_CREDENTIAL_ID, variable: env.DINGTALK_WEBHOOK_ENV_VAR_NAME)]) {
|
|||
|
sendDingTalkNotification(
|
|||
|
// 报告中止信息,包含当前阶段和 Jenkins 构建 URL
|
|||
|
message: "${params.DOCKER_IMAGE_NAME} 构建已中止。请检查控制台: ${env.BUILD_URL}console",
|
|||
|
webhookEnvVarName: env.DINGTALK_WEBHOOK_ENV_VAR_NAME,
|
|||
|
author: env.LAST_COMMIT_AUTHOR ?: '未知用户',
|
|||
|
jobName: env.JOB_NAME,
|
|||
|
buildNumber: env.BUILD_NUMBER,
|
|||
|
enabled: params.SEND_DINGTALK_NOTIFICATIONS
|
|||
|
)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|