主题
一、Jenkins流水线发布项目
1.1 Jenkins 环境配置
1.1.1 项目所依赖的Jenkins插件
插件的安装部署过程不展开说明,具体可参考之前的“Jenkins 优秀插件讲解”章节
名称 | 功能 |
---|---|
Git Parameter | 此插件允许您在构建中分配 git 分支、标签、拉取请求或修订号作为参数。 |
Active Choices | 参数化构建,支持动态生成可交互参数(组合框等),支持Groovy脚本参数化Jenkins作业。 |
SonarQube Scanner for Jenkins | jenkins 上集成 sonarqube 代码质量检测功能 |
Build Name and Description Setter | 自定义构建的名称和描述 |
build-user-vars-plugin | 提供Jenkins构建用户相关的变量 |
Qy Wechat Notification Plugin | 提供企业微信 webhook 机器人插件 |
Email Extension Template | Jenkins复杂邮件推送功能,可自定义邮件主题,内容,定义邮件接收对象 |
1.1.2 Jenkins 流水线所需工具/组件
所需工具/组件安装部署过程不展开说明,具体可参考之前的"全局工具配置”章节
名称 | 版本 | 备注 |
---|---|---|
Ansible | 2.9.27 | |
Maven | 3.9.9 | |
Node | V22.16.0 |
2.1 后端流水线-实现原理
流水线大致步骤👇 如下图所示:
2.1.1 参数化构建
- Git Parameter 插件,动态拉取代码分支
TIP
使用 Git Parameter Plug-In
插件,实现在参数化构建的时候,可以勾选代码分支,指定分支进行构建; 具体的场景使用技巧,可参考Git Parameter插件使用说明
groovy
parameters {
gitParameter branch: '', branchFilter: '.*', defaultValue: 'main', description: '构建分支', name: 'GIT_BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'GitParameterDefinition'
}
- Active Choices 插件,实现动态参数联动功能
TIP
使用Active Choices
插件主要用于实现动态参数联动功能,支持自定义Groovy脚本参数化Jenkins作业;
这里我讲演示通过参数化联动,如何构建单个的微服务,以及指定构建的微服务 pom.xml 文件
具体的场景使用技巧,可参考“Active Choices插件使用说明”
groovy
parameters {
gitParameter branch: '', branchFilter: '.*', defaultValue: 'main', description: '构建分支', name: 'GIT_BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'GitParameterDefinition'
choice choices: ['false', 'true'], description: '是否跳过sonar扫描', name: 'skipSonar'
activeChoice choiceType: 'PT_RADIO', description: '选择要对应的项目', filterLength: 1, filterable: false, name: 'project', randomName: 'choice-parameter-2668886408691049', script: groovyScript(fallbackScript: [classpath: [], oldScript: '', sandbox: true, script: ''], script: [classpath: [], oldScript: '', sandbox: true, script: '''return[
"pig-auth",
"pig-gateway",
"pig-upms-biz",
"pig-monitor",
"pig-codegen",
"pig-quartz"]'''])
reactiveChoice choiceType: 'PT_SINGLE_SELECT', description: 'pom.xml 路径', filterLength: 1, filterable: false, name: 'pom_path', randomName: 'choice-parameter-2668886412881229', referencedParameters: 'project', script: groovyScript(fallbackScript: [classpath: [], oldScript: '', sandbox: true, script: 'return["选择 project 项目后显示"]'], script: [classpath: [], oldScript: '', sandbox: true, script: '''
def A = ["./pig-auth"]
def B = ["./pig-gateway"]
def C = ["./pig-upms/pig-upms-biz"]
def D = ["./pig-visual/pig-monitor"]
def E = ["./pig-visual/pig-codegen"]
def F = ["./pig-visual/pig-quartz"]
if (project.equals("pig-auth")) {
return A
} else if (project.equals("pig-gateway")) {
return B
} else if (project.equals("pig-upms-biz")) {
return C
} else if (project.equals("pig-monitor")) {
return D
} else if (project.equals("pig-codegen")) {
return E
} else if (project.equals("pig-quartz")) {
return F
}'''])
}
2.1.2 自定义构建名称和描述
TIP
使用 Build Name and Description Setter
插件,实现自定义构建名称和描述. 具体的场景使用技巧,可参考Build Name and Description Setter插件使用说明
groovy
environment {
_VERSION = sh(script: "echo `date '+%Y%m%d'`" + "-${env.BUILD_ID}", returnStdout: true).trim() //对应构建的版本 时间+commitID+buildID
}
buildName "#${BUILD_NUMBER}:${GIT_BRANCH}" // 更改构建名称
buildDescription "任务运行在@ master节点<br/> 构建者: ${BUILD_USER} <br/> 版本号: ${_VERSION} <br/> 归档下载地址: <a href='http://172.22.33.201/be/${_VERSION}'>🌸点我下载</a> <br/>"
2.1.3 同步制品包&归档
TIP
为了在打包制品之后,留存,我这里使用了一个 在线文件服务器小工具 dufs 可实现在线上传下载制品包
Dufs文件服务部署
bash
wget https://github.com/sigoden/dufs/releases/download/v0.43.0/dufs-v0.43.0-x86_64-unknown-linux-musl.tar.gz
tar -xf dufs-v0.43.0-x86_64-unknown-linux-musl.tar.gz
mv dufs /usr/local/bin
chmod +x /usr/local/bin/dufs
bash
##创建制品包工作路径
mkdir -p /home/application/jd/{fe,be}
##创建 dufs 启动脚本
##dufs服务监听在80 端口,忽略.git,.DS_Store,tmp 文件夹
##匿名访问,不需要用户名密码
cat >> /etc/profile.d/start-dufs.sh << 'EOF'
#!/bin/bash
nohup /usr/local/bin/dufs -p 80 -A --hidden .git,.DS_Store,tmp /home/application/jd > /dev/null 2>&1 &
EOF
##赋予脚本执行权限
chmod +x /etc/profile.d/start-dufs.sh
##执行脚本
bash /etc/profile.d/start-dufs.sh
效果
制品包归档
TIP
Dufs文件服务器上 只保留10个版本的制品包,多余的制品包,会自动删除,配合keepfive.sh
脚本实现
groovy 脚本
groovy
stage('同步制品包&归档') {
steps {
script {
def archive_path = "/home/application/jd/be/${project}/${_VERSION}"
sh """
ssh root@172.22.33.201 mkdir -p ${archive_path}
"""
//执行tar命令创建tar.gz压缩文件,且排除javadoc,sources的jar包
sh """
cd ${pom_path}/target
tar -czvf ${project}.tar.gz --exclude='./*-javadoc.jar' --exclude='./*-sources.jar' ./*.jar
scp -rp ./*.tar.gz root@172.22.33.201:/home/application/jd/be/${project}/${_VERSION}
rm -rf ./*.tar.gz
"""
//保留10个版本
def keepfive_path = "/home/application/jd/be/${project}"
def keepfiveContent = '''
while true; do
dirs_to_remove=$(ls -td -- */ | tail -n +4)
if [ -z "$dirs_to_remove" ]; then
break
fi
echo "$dirs_to_remove" | xargs rm -rf
done
'''
// 将脚本内容写入临时文件
writeFile file: 'temp_script.sh', text: keepfiveContent
// 将临时文件复制到远程主机上的目标文件
sh 'scp temp_script.sh root@172.22.33.201:/tmp/keepfive.sh'
// 删除临时文件
sh 'rm temp_script.sh'
// 执行保留脚本
sh "ssh root@172.22.33.201 'cd ${keepfive_path} && chmod +x /tmp/keepfive.sh && /tmp/keepfive.sh'"
}
}
}
2.1.4 Ansible 自动化发布
TIP
Ansible 是一款自动化运维工具,可以用来做自动化部署,自动化发布,自动化配置,无需代理,使用 ssh 直接连接;
Ansible 提供众多的模块,可以完成各种任务,我这里使用到了 file,unarchive,shell 等模块。有关Ansible 的更多用法,可以参考官方文档 Ansible
这里我使用了 Ansible 纯命令的方式,没有使用 playbook 的方式,有关 playbook 的方式 见后面 jenkins 共享库那块的内容。
bash
##安装 ansible
yum install -y ansible
##修改ansible配置文件
vim /etc/ansible/ansible.cfg
host_key_checking = False
log_path = /var/log/ansible.log
bash
##在Jenkins服务器上生成密钥
ssh-keygen -t rsa
##拷贝公钥到目标服务器
ssh-copy-id -i ~/.ssh/id_rsa.pub root@172.22.33.214
ssh-copy-id -i ~/.ssh/id_rsa.pub root@172.22.33.215
bash
ansible all -i '172.22.33.214,172.22.33.215' -m ping
ansible all -i '172.22.33.214,172.22.33.215' -m shell -a "date"
ansible 脚本
ansible 脚本大概分为发布前,和部署 两个阶段
发布前:
- 创建目标服务器上 带有版本的备份目录,一般为
/home/application/devops/backup/be/${project}/${_VERSION}
- 创建目标服务器上 项目工作目录, 一般为
/home/application/${project}
- 同步目标服务器上 项目工作目录下的jar包到机器上的备份目录下
- 同步keepfive.sh 脚本到目标服务器上
部署:
- 清理目标服务器上 项目工作目录下的jar包
- 同步 Jenkins 服务器上归档目录下最新的制品包 到 目标服务器上 项目工作目录下
- 执行目标服务器上 项目工作目录下的启动脚本
- 执行目标服务器上 保留脚本,保留最近 3 个带有版本的备份目录
groovy
stage('Ansible发布前任务') { //bak 备份目录,项目运行目录,同步备份包
steps {
script {
// 执行Ansible命令(纯命令方式)
sh """
ansible all -i '172.22.33.214,172.22.33.215' \
-m file \
-a "path='/home/application/devops/backup/be/${project}/${_VERSION}' state=directory mode=0755"
ansible all -i '172.22.33.214,172.22.33.215' \
-m file \
-a "path='/home/application/${project}' state=directory mode=0755"
ansible all -i '172.22.33.214,172.22.33.215' \
-m shell \
-a "rsync -av --delete /home/application/${project}/*.jar /home/application/devops/backup/be/${project}/${_VERSION}"
"""
//保留10个版本
def backup_keepfive_path = "/home/application/devops/backup/be/${project}/"
def backup_keepfiveContent = '''
while true; do
dirs_to_remove=$(ls -td -- */ | tail -n +4)
if [ -z "$dirs_to_remove" ]; then
break
fi
echo "$dirs_to_remove" | xargs rm -rf
done
'''
// 将脚本内容写入临时文件
writeFile file: 'services_keepfive.sh', text: backup_keepfiveContent
// 将临时文件复制到远程主机上的目标文件
sh 'scp services_keepfive.sh root@172.22.33.214:/home/application/devops/backup/be/keepfive.sh'
sh 'scp services_keepfive.sh root@172.22.33.215:/home/application/devops/backup/be/keepfive.sh'
// 删除临时文件
sh 'rm services_keepfive.sh'
// 执行保留脚本
sh "ssh root@172.22.33.214 'cd ${backup_keepfive_path} && chmod +x /home/application/devops/backup/be/keepfive.sh && /home/application/devops/backup/be/keepfive.sh'"
sh "ssh root@172.22.33.215 'cd ${backup_keepfive_path} && chmod +x /home/application/devops/backup/be/keepfive.sh && /home/application/devops/backup/be/keepfive.sh'"
}
}
}
stage('Ansible部署') { //清理旧包,同步新包,,执行远程启动脚本, 清理旧版本(保留最近 10 个版本目录)
steps {
script {
// 执行Ansible命令(纯命令方式)
sh """
ansible all -i '172.22.33.214,172.22.33.215' -m file \
-a "path=/home/application/${project}/*.jar state=absent"
ansible all -i 172.22.33.214,172.22.33.215 -m unarchive \
-a "src=http://172.22.33.201/be/${project}/${_VERSION}/${project}.tar.gz \
dest=/home/application/${project} remote_src=yes force=yes"
ansible all -i '172.22.33.214,172.22.33.215' -m shell -a "cd /home/application/ && ./${project}/startup.sh ${project}"
ansible all -i '172.22.33.214,172.22.33.215' -m shell -a "cd /home/application/devops/backup/be/${project} && chmod +x /home/application/devops/backup/be/keepfive.sh && /home/application/devops/backup/be/keepfive.sh"
"""
}
}
}
2.1.5 构建后动作
1、版本号记录
TIP
为了方便后面回退的流水线,需要将版本号记录下来;记录在 Jenkins 的 version
文件中,只保留最后 3 个最新的版本号。 version
的文件在 Jenkins 的/home/application/jd/{be,fe}/${project}/
目录下
groovy
stage("版本号写入") {
steps {
script {
try {
// 创建存放版本文件的目录(注意目录名需与Jenkins文件夹一致)
sh ''' mkdir -p "/home/application/jd/be/${project}" '''
// 写入新版本号(直接追加,文件不存在时会自动创建)
sh "echo '${_VERSION}' >> /home/application/jd/be/${project}/version"
def versionFile = "/home/application/jd/be/${project}/version"
def lineCount = sh(script: "wc -l < '${versionFile}'", returnStdout: true).trim()
def lineCountInt = lineCount.toInteger() // 显式转为整数
echo "当前版本文件中的版本号数量为:${lineCountInt}"
// 如果行数超过3行,删除最旧的版本
if (lineCountInt > 3) {
sh "tail -n 3 ${versionFile} > ${versionFile}.tmp && mv -f ${versionFile}.tmp ${versionFile}"
echo "版本号超过3个,已删除最旧版本,当前保留最新3个版本"
} else {
echo "版本号未超过3个(当前${lineCountInt}个),无需清理"
}
}catch(err) {
echo "🚨🚨🚨版本号写入出错🚨🚨🚨"
}
}
}
}
2.1.6 其他重要stage步骤
- sonarqube 检测
TIP
可参考之前Jenkins章节中的Sonarqube&Jenkins集成,这里不再赘述
stage('SonarQube 扫描') {
when {
environment name: 'skipSonar', value: 'false'
}
steps {
script{
withSonarQubeEnv(credentialsId: '3ce7da72-90ae-4af9-a2e4-2b8eb83ec0df') {
sh """
source /etc/profile > /dev/null 2>&1
cd ${pom_path}
sonar-scanner \
-Dsonar.token=${SONAR_AUTH_TOKEN} \
-Dsonar.projectKey=${project} \
-Dsonar.projectName=${project} \
-Dsonar.sourceEncoding=UTF-8 \
-Dsonar.sources=./src/main/java \
-Dsonar.language=java \
-Dsonar.java.binaries=./target/classes \
-Dsonar.host.url=${SONAR_HOST_URL}
"""
}
}
// 等待 SonarQube 质量门结果并处理
script {
def qg = waitForQualityGate()
if (qg.status != 'OK') {
error "SonarQube 检查未通过!状态:${qg.status},报告地址:http://172.22.33.207:9000/dashboard?id=${project}"
}
}
}
}
2、邮件通知/企业微信通知
关于邮件通知 和 企业微信 插件的具体使用方法,可以见插件模块
groovy
pipeline {
agent any
environment {
BUILD_TIME = sh(script: "echo `date '+%Y-%m-%d %H:%M:%S'`", returnStdout: true).trim()
}
stages {
stage('构建') {
steps {
echo "执行构建步骤..."
}
}
}
post {
always {
wrap([$class: 'BuildUser']) {
script {
// 调用邮件发送
sendEmailNotification("${currentBuild.currentResult}")
// 调用企业微信通知
sendWeChatNotification("${currentBuild.currentResult}")
// 设置构建名称
buildName "#${BUILD_NUMBER} - ${env.BUILD_USER_ID ?: '系统'}"
}
}
}
}
}
// 邮件通知函数
def sendEmailNotification(STATUS) {
// 根据构建状态设置颜色
def statusColor = STATUS == 'SUCCESS' ? '#0B610B' : '#FF0000'
emailext body: """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4" offset="0">
<table width="95%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
<tr>
<td>(本邮件是Jenkins程序自动下发的,请勿回复!)</td>
</tr>
<tr>
<td><h2><font color="${statusColor}">构建结果:"${STATUS}"</font></h2></td>
</tr>
<tr>
<td><br />
<b><font color="#0B610B">构建信息:</font></b><hr size="2" width="100%" align="center" /></td>
</tr>
<tr>
<td>
<ul>
<li>项目名称:${JOB_NAME}</li>
<li>构建编号:${BUILD_ID}</li>
<li>构建状态: ${STATUS} </li>
<li>项目地址:<a href="${BUILD_URL}">${BUILD_URL}</a></li>
<li>构建日志:<a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
<li>历史变更记录 : <a href="${BUILD_URL}changes">${BUILD_URL}changes</a></li>
</ul>
</td>
</tr>
<tr>
</table>
</body>
</html> """,
recipientProviders: [buildUser(), developers()],
subject: 'jenkins2.0 【构建通知】: Build # $BUILD_NUMBER - $BUILD_STATUS!',
to: 'admin@srebro.cn' //默认接收对象
}
// 企业微信通知函数
def sendWeChatNotification(STATUS) {
// 构建状态相关信息
def buildUser = env.BUILD_USER ?: '系统自动'
def buildStatus = STATUS
def statusIcon = buildStatus == 'SUCCESS' ? '✅' : '❌'
// 创建临时 JSON 文件
writeFile file: 'template_card.json', text: """
{
"msgtype": "template_card",
"template_card": {
"card_type": "text_notice",
"source": {
"icon_url": "https://jenkins.io/images/logos/jenkins/jenkins.png",
"desc": "Jenkins构建通知",
"desc_color": 0
},
"main_title": {
"title": "Jenkins构建完成通知",
"desc": "${env.JOB_NAME} - #${BUILD_NUMBER}"
},
"emphasis_content": {
"title": "${statusIcon} ${buildStatus}",
"desc": "构建状态"
},
"quote_area": {
"type": 1,
"url": "${env.BUILD_URL}",
"title": "构建日志摘要",
"quote_text": "构建分支: ${env.GIT_BRANCH ?: 'master'}\\n构建时间: ${BUILD_TIME}\\n构建用户: ${buildUser}"
},
"sub_title_text": "构建详细信息",
"horizontal_content_list": [
{
"keyname": "构建分支",
"value": "${env.GIT_BRANCH ?: 'master'}"
},
{
"keyname": "构建编号",
"value": "#${BUILD_NUMBER}"
},
{
"keyname": "触发用户",
"value": "${buildUser}"
},
{
"keyname": "构建日志",
"value": "点击查看",
"type": 1,
"url": "${env.BUILD_URL}console"
}
],
"jump_list": [
{
"type": 1,
"url": "${env.BUILD_URL}",
"title": "查看构建详情"
},
{
"type": 1,
"url": "${env.BUILD_URL}changes",
"title": "查看变更记录"
}
],
"card_action": {
"type": 1,
"url": "${env.BUILD_URL}"
}
}
}
"""
// 发送企业微信通知
sh """
#!/bin/sh
# 企业微信机器人 Webhook 地址
WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx你的keyxxxxxx"
# 发送请求
curl -s -H "Content-Type: application/json" -X POST -d @template_card.json \$WEBHOOK_URL
# 清理临时文件
rm template_card.json
"""
}
2.2 后端Pipeline流水线
groovy
pipeline {
options{
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '10'))
skipDefaultCheckout()
timeout(time: 1, unit: 'HOURS')
timestamps()
}
agent any
environment {
_VERSION = sh(script: "echo `date '+%Y%m%d'`" + "-${env.BUILD_ID}", returnStdout: true).trim() //对应构建的版本 时间+commitID+buildID
BUILD_TIME = sh(script: "echo `date '+%Y-%m-%d %H:%M:%S'`", returnStdout: true).trim()
}
parameters {
gitParameter branch: '', branchFilter: '.*', defaultValue: 'main', description: '构建分支', name: 'GIT_BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'GitParameterDefinition'
choice choices: ['true', 'false'],description: '是否跳过sonar扫描', name: 'skipSonar'
activeChoice choiceType: 'PT_RADIO', description: '选择要对应的项目', filterLength: 1, filterable: false, name: 'project', randomName: 'choice-parameter-2668886408691049', script: groovyScript(fallbackScript: [classpath: [], oldScript: '', sandbox: true, script: ''], script: [classpath: [], oldScript: '', sandbox: true, script: '''return[
"pig-auth",
"pig-gateway",
"pig-upms-biz",
"pig-monitor",
"pig-codegen",
"pig-quartz"]'''])
reactiveChoice choiceType: 'PT_SINGLE_SELECT', description: 'pom.xml 路径', filterLength: 1, filterable: false, name: 'pom_path', randomName: 'choice-parameter-2668886412881229', referencedParameters: 'project', script: groovyScript(fallbackScript: [classpath: [], oldScript: '', sandbox: true, script: 'return["选择 project 项目后显示"]'], script: [classpath: [], oldScript: '', sandbox: true, script: '''def A = ["./pig-auth"]
def B = ["./pig-gateway"]
def C = ["./pig-upms/pig-upms-biz"]
def D = ["./pig-visual/pig-monitor"]
def E = ["./pig-visual/pig-codegen"]
def F = ["./pig-visual/pig-quartz"]
if (project.equals("pig-auth")) {
return A
} else if (project.equals("pig-gateway")) {
return B
} else if (project.equals("pig-upms-biz")) {
return C
} else if (project.equals("pig-monitor")) {
return D
} else if (project.equals("pig-codegen")) {
return E
} else if (project.equals("pig-quartz")) {
return F
}'''])
}
stages {
stage('Checkout 代码') {
steps {
script {
checkout scmGit(
branches: [[name: "${GIT_BRANCH}"]],
extensions: [],
userRemoteConfigs: [
[
credentialsId: 'a11aa99b-7bba-48fd-bd01-46b68732578a',
url: 'http://code.srebro.cn/opforge/cicd-demo-pig-backend.git'
]
]
)
}
}
}
stage('Maven 编译 & 测试') {
when {
environment name: 'skipSonar', value: 'false' // 仅当扫描通过时打包
}
steps {
script {
// 清理旧构建并编译(生成 .class 文件)
sh """
echo ${env._VERSION}
source /etc/profile > /dev/null 2>&1
cd ${pom_path}
mvn clean compile test # 编译、运行测试
"""
}
}
}
stage('SonarQube 扫描') {
when {
environment name: 'skipSonar', value: 'false'
}
steps {
script{
withSonarQubeEnv(credentialsId: '3ce7da72-90ae-4af9-a2e4-2b8eb83ec0df') {
sh """
source /etc/profile > /dev/null 2>&1
cd ${pom_path}
sonar-scanner \
-Dsonar.token=${SONAR_AUTH_TOKEN} \
-Dsonar.projectKey=${project} \
-Dsonar.projectName=${project} \
-Dsonar.sourceEncoding=UTF-8 \
-Dsonar.sources=./src/main/java \
-Dsonar.language=java \
-Dsonar.java.binaries=./target/classes \
-Dsonar.host.url=${SONAR_HOST_URL}
"""
}
}
// 等待 SonarQube 质量门结果并处理
script {
def qg = waitForQualityGate()
if (qg.status != 'OK') {
error "SonarQube 检查未通过!状态:${qg.status},报告地址:http://172.22.33.207:9000/dashboard?id=${project}"
}
}
}
}
stage('Maven 打包') {
steps {
script {
sh """
source /etc/profile > /dev/null 2>&1
cd ${pom_path}
mvn clean install -Pcloud
"""
}
}
}
stage('同步制品包&归档') {
steps {
script {
def archive_path = "/home/application/jd/be/${project}/${_VERSION}"
sh """
ssh root@172.22.33.201 mkdir -p ${archive_path}
"""
//执行tar命令创建tar.gz压缩文件,且排除javadoc,sources的jar包
sh """
cd ${pom_path}/target
tar -czvf ${project}.tar.gz --exclude='./*-javadoc.jar' --exclude='./*-sources.jar' ./*.jar
scp -rp ./*.tar.gz root@172.22.33.201:/home/application/jd/be/${project}/${_VERSION}
rm -rf ./*.tar.gz
"""
//保留3个版本
def keepfive_path = "/home/application/jd/be/${project}"
def keepfiveContent = '''
while true; do
dirs_to_remove=$(find . -maxdepth 1 -type d | ls -td -- */ | tail -n +4)
if [ -z "$dirs_to_remove" ]; then
break
fi
echo "$dirs_to_remove" | xargs rm -rf
done
'''
// 将脚本内容写入临时文件
writeFile file: 'temp_script.sh', text: keepfiveContent
// 将临时文件复制到远程主机上的目标文件
sh 'scp temp_script.sh root@172.22.33.201:/tmp/keepfive.sh'
// 删除临时文件
sh 'rm temp_script.sh'
// 执行保留脚本
sh "ssh root@172.22.33.201 'cd ${keepfive_path} && chmod +x /tmp/keepfive.sh && /tmp/keepfive.sh'"
}
}
}
stage('Ansible发布前任务') { //bak 备份目录,项目运行目录,同步备份包
steps {
script {
// 执行Ansible命令(纯命令方式)
sh """
ansible all -i '172.22.33.214,172.22.33.215' \
-m file \
-a "path='/home/application/devops/backup/be/${project}/${_VERSION}' state=directory mode=0755"
ansible all -i '172.22.33.214,172.22.33.215' \
-m file \
-a "path='/home/application/${project}' state=directory mode=0755"
ansible all -i '172.22.33.214,172.22.33.215' \
-m shell \
-a "rsync -av --delete /home/application/${project}/*.jar /home/application/devops/backup/be/${project}/${_VERSION}"
"""
//保留3个版本
def backup_keepfive_path = "/home/application/devops/backup/be/${project}/"
def backup_keepfiveContent = '''
while true; do
dirs_to_remove=$(find . -maxdepth 1 -type d | ls -td -- */ | tail -n +4)
if [ -z "$dirs_to_remove" ]; then
break
fi
echo "$dirs_to_remove" | xargs rm -rf
done
'''
// 将脚本内容写入临时文件
writeFile file: 'services_keepfive.sh', text: backup_keepfiveContent
// 将临时文件复制到远程主机上的目标文件
sh 'scp services_keepfive.sh root@172.22.33.214:/home/application/devops/backup/be/keepfive.sh'
sh 'scp services_keepfive.sh root@172.22.33.215:/home/application/devops/backup/be/keepfive.sh'
// 删除临时文件
sh 'rm services_keepfive.sh'
// 执行保留脚本
sh "ssh root@172.22.33.214 'cd ${backup_keepfive_path} && chmod +x /home/application/devops/backup/be/keepfive.sh && /home/application/devops/backup/be/keepfive.sh'"
sh "ssh root@172.22.33.215 'cd ${backup_keepfive_path} && chmod +x /home/application/devops/backup/be/keepfive.sh && /home/application/devops/backup/be/keepfive.sh'"
}
}
}
stage('Ansible部署') { //清理旧包,同步新包,,执行远程启动脚本, 清理旧版本(保留最近 10 个版本目录)
steps {
script {
// 执行Ansible命令(纯命令方式)
sh """
ansible all -i '172.22.33.214,172.22.33.215' -m file \
-a "path=/home/application/${project}/*.jar state=absent"
ansible all -i 172.22.33.214,172.22.33.215 -m unarchive \
-a "src=http://172.22.33.201/be/${project}/${_VERSION}/${project}.tar.gz \
dest=/home/application/${project} remote_src=yes force=yes"
ansible all -i '172.22.33.214,172.22.33.215' -m shell -a "cd /home/application/ && ./${project}/startup.sh ${project}"
ansible all -i '172.22.33.214,172.22.33.215' -m shell -a "cd /home/application/devops/backup/be/${project} && chmod +x /home/application/devops/backup/be/keepfive.sh && /home/application/devops/backup/be/keepfive.sh"
"""
}
}
}
stage('服务运行日志') { //打印 100 行启动日志
steps {
script {
// 执行Ansible命令(纯命令方式)
sh """
ansible all -i '172.22.33.214,172.22.33.215' -m shell -a "sleep 10s && tail -n 1000 /home/application/logs/${project}/debug.log"
"""
}
}
}
stage("版本号写入") {
steps {
script {
try {
// 创建存放版本文件的目录(注意目录名需与Jenkins文件夹一致)
sh ''' mkdir -p "/home/application/jd/be/${project}" '''
// 写入新版本号(直接追加,文件不存在时会自动创建)
sh "echo '${_VERSION}' >> /home/application/jd/be/${project}/version"
// ====================== 修正:判断并保留最多3个版本 ======================
def versionFile = "/home/application/jd/be/${project}/version"
def lineCount = sh(script: "wc -l < '${versionFile}'", returnStdout: true).trim()
def lineCountInt = lineCount.toInteger() // 显式转为整数
echo "当前版本文件中的版本号数量为:${lineCountInt}"
// 如果行数超过3行,删除最旧的版本
if (lineCountInt > 3) {
sh "tail -n 3 ${versionFile} > ${versionFile}.tmp && mv -f ${versionFile}.tmp ${versionFile}"
echo "版本号超过3个,已删除最旧版本,当前保留最新3个版本"
} else {
echo "版本号未超过3个(当前${lineCountInt}个),无需清理"
}
}catch(err) {
echo "🚨🚨🚨版本号写入出错🚨🚨🚨"
}
}
}
}
}
post {
always {
wrap([$class: 'BuildUser']) {
script {
// 调用邮件发送
sendEmailNotification("${currentBuild.currentResult}")
// 调用企业微信通知
sendWeChatNotification("${currentBuild.currentResult}")
// 清理工作空间
cleanWs()
}
}
}
aborted {
script{
echo "aborted"
}
}
success {
wrap([$class: 'BuildUser']){
script{
buildName "#${BUILD_NUMBER}-${project}:${GIT_BRANCH}" // 更改构建名称
buildDescription "任务运行在@ master节点<br/> 构建者: ${BUILD_USER} <br/> 版本号: ${_VERSION} <br/> 归档下载地址: <a href='http://172.22.33.201/be/${project}/${_VERSION}'>🌸点我下载</a> <br/>"
}
}
}
failure {
wrap([$class: 'BuildUser']){
script{
buildName "#${BUILD_NUMBER}-${project}:${GIT_BRANCH}"
}
}
}
}
}
// 邮件通知函数
def sendEmailNotification(STATUS) {
// 根据构建状态设置颜色
def statusColor = STATUS == 'SUCCESS' ? '#0B610B' : '#FF0000'
emailext body: """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4" offset="0">
<table width="95%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
<tr>
<td>(本邮件是Jenkins程序自动下发的,请勿回复!)</td>
</tr>
<tr>
<td><h2><font color="${statusColor}">构建结果:"${STATUS}"</font></h2></td>
</tr>
<tr>
<td><br />
<b><font color="#0B610B">构建信息:</font></b><hr size="2" width="100%" align="center" /></td>
</tr>
<tr>
<td>
<ul>
<li>项目名称:${JOB_NAME}</li>
<li>构建编号:${BUILD_ID}</li>
<li>构建状态: ${STATUS} </li>
<li>选择项目: ${project}</li>
<li>构建分支: ${GIT_BRANCH}</li>
<li>版本号: ${_VERSION}</li>
<li>项目地址:<a href="${BUILD_URL}">${BUILD_URL}</a></li>
<li>构建日志:<a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
<li>历史变更记录 : <a href="${BUILD_URL}changes">${BUILD_URL}changes</a></li>
<li>归档下载地址: <a href="http://172.22.33.201/be/${project}/${_VERSION}">🌸点我下载</a></li>
</ul>
</td>
</tr>
<tr>
</table>
</body>
</html> """,
recipientProviders: [buildUser(), developers()],
subject: 'Jenkins构建通知 【${project}】: Build # $BUILD_NUMBER - $BUILD_STATUS!',
to: 'admin@srebro.cn' //默认接收对象
}
// 企业微信通知函数
def sendWeChatNotification(STATUS) {
// 构建状态相关信息
def buildUser = env.BUILD_USER ?: '系统自动'
def buildStatus = STATUS
def statusIcon = buildStatus == 'SUCCESS' ? '✅' : '❌'
// 创建临时 JSON 文件
writeFile file: 'template_card.json', text: """
{
"msgtype": "template_card",
"template_card": {
"card_type": "text_notice",
"source": {
"icon_url": "https://jenkins.io/images/logos/jenkins/jenkins.png",
"desc": "Jenkins构建通知",
"desc_color": 0
},
"main_title": {
"title": "Jenkins构建完成通知",
"desc": "${env.JOB_NAME} - #${BUILD_NUMBER}"
},
"emphasis_content": {
"title": "${statusIcon} ${buildStatus}",
"desc": "构建状态"
},
"quote_area": {
"type": 1,
"url": "${env.BUILD_URL}",
"title": "构建日志摘要",
"quote_text": "项目名称: ${project}\\n构建分支: ${GIT_BRANCH}\\n构建时间: ${BUILD_TIME}\\n构建用户: ${buildUser}\\n版本号: ${_VERSION}"
},
"sub_title_text": "构建详细信息",
"horizontal_content_list": [
{
"keyname": "项目名称",
"value": "${project}"
},
{
"keyname": "构建分支",
"value": "${GIT_BRANCH}"
},
{
"keyname": "构建编号",
"value": "#${BUILD_NUMBER}"
},
{
"keyname": "版本号",
"value": "${_VERSION}"
},
{
"keyname": "触发用户",
"value": "${buildUser}"
},
{
"keyname": "构建日志",
"value": "点击查看",
"type": 1,
"url": "${env.BUILD_URL}console"
}
],
"jump_list": [
{
"type": 1,
"url": "${env.BUILD_URL}",
"title": "查看构建详情"
},
{
"type": 1,
"url": "${env.BUILD_URL}changes",
"title": "查看变更记录"
},
{
"type": 1,
"url": "http://172.22.33.201/be/${project}/${_VERSION}",
"title": "下载构建产物"
}
],
"card_action": {
"type": 1,
"url": "${env.BUILD_URL}"
}
}
}
"""
// 发送企业微信通知
sh """
#!/bin/sh
# 企业微信机器人 Webhook 地址
WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx 你的 key xxxx"
# 发送请求
curl -s -H "Content-Type: application/json" -X POST -d @template_card.json \$WEBHOOK_URL
# 清理临时文件
rm template_card.json
"""
}
最终效果