TemplatePro/Jenkinsfile

331 lines
19 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.

// 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
)
}
}
}
}
}
}