Skip to content

一、Jenkins流水线发布项目

1.1 Jenkins 环境配置

1.1.1 项目所依赖的Jenkins插件

插件的安装部署过程不展开说明,具体可参考之前的“Jenkins 优秀插件讲解”章节

名称功能
Git Parameter此插件允许您在构建中分配 git 分支、标签、拉取请求或修订号作为参数。
Active Choices参数化构建,支持动态生成可交互参数(组合框等),支持Groovy脚本参数化Jenkins作业。
SonarQube Scanner for Jenkinsjenkins 上集成 sonarqube 代码质量检测功能
Build Name and Description Setter自定义构建的名称和描述
build-user-vars-plugin提供Jenkins构建用户相关的变量
Qy Wechat Notification Plugin提供企业微信 webhook 机器人插件
Email Extension TemplateJenkins复杂邮件推送功能,可自定义邮件主题,内容,定义邮件接收对象

1.1.2 Jenkins 流水线所需工具/组件

所需工具/组件安装部署过程不展开说明,具体可参考之前的"全局工具配置”章节

名称版本备注
Ansible2.9.27
Maven3.9.9
NodeV22.16.0

2.1 后端流水线-实现原理

流水线大致步骤👇 如下图所示:

2.1.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'
}

  1. 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 脚本大概分为发布前,和部署 两个阶段


发布前:

  1. 创建目标服务器上 带有版本的备份目录,一般为 /home/application/devops/backup/be/${project}/${_VERSION}
  2. 创建目标服务器上 项目工作目录, 一般为 /home/application/${project}
  3. 同步目标服务器上 项目工作目录下的jar包到机器上的备份目录下
  4. 同步keepfive.sh 脚本到目标服务器上

部署:

  1. 清理目标服务器上 项目工作目录下的jar包
  2. 同步 Jenkins 服务器上归档目录下最新的制品包 到 目标服务器上 项目工作目录下
  3. 执行目标服务器上 项目工作目录下的启动脚本
  4. 执行目标服务器上 保留脚本,保留最近 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步骤

  1. 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
    """
}

最终效果

最近更新

采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 运维小弟