Jenkins流水线部署+webhook2.0

文章目录

  • [1. 环境](#1. 环境)
  • [2. 用到的插件](#2. 用到的插件)
  • [3. 流水线部署脚本](#3. 流水线部署脚本)
      • [一 后端接口](#一 后端接口)
      • [二 前端VUE项目](#二 前端VUE项目)
  • 4.遇到的问题
  • 5.实际效果

1. 环境

  • Centos7
  • Jenkins2.520
  • JDKopen17
  • 阿里云仓库

注意:这个版本兼容需要特别注意,要不然会很麻烦

2. 用到的插件

  • Generic Webhook Trigger
  • HTTP Request
  • Extra Columns
  • Pipeline: Stage View
  • NodeJS

3. 流水线部署脚本

一 后端接口

  • 兼容钩子部署(webhook)和手动参数化部署
  • 优先取钩子的推送参数,没有的话取手动参数
  • 根据推送的分支进行不同的部署操作
  • 推送结果到钉钉通知
groovy 复制代码
pipeline {
    agent any

    // 定义参数化构建
    parameters {
        string(
            name: 'MANUAL_BRANCH', 
            defaultValue: '', 
            description: '手动指定要部署的分支(如 master, dev, test)'
        )
    }

    triggers {
        GenericTrigger(
            genericVariables: [
                [key: 'ref', value: '$.ref'], // 获取分支信息
                [key: 'repository_name', value: '$.repository.name'] // 获取仓库名称
            ],
            causeString: 'Triggered by push event on branch $ref',
            token: 'AZWSDD2555SSWS', // 自定义的Token
            printContributedVariables: true, // 打印传递的变量
            silentResponse: false,
            regexpFilterText: '$ref', // 匹配分支名
            regexpFilterExpression: '^refs/heads/(dev|test)$' // 只处理特定分支
        )
    }

   stages {
        stage('Determine Branch') {
            steps {
                script {
                    // 优先使用Webhook传递的分支信息
                    if (env.ref) {
                        env.BRANCH_NAME = env.ref.tokenize('/')[-1]
                        echo "Using webhook-triggered branch: ${env.BRANCH_NAME}"
                    } 
                    // 如果没有Webhook信息,检查是否有手动输入的分支
                    else if (params.MANUAL_BRANCH?.trim()) {
                        env.BRANCH_NAME = params.MANUAL_BRANCH.trim()
                        echo "Using manually specified branch: ${env.BRANCH_NAME}"
                    } 
                    // 如果两者都没有,抛出错误
                    else {
                        error "No branch specified! Please provide a branch via manual input or webhook."
                    }
                }
            }
        }

        stage('Checkout Code') {
            steps {
                git branch: env.BRANCH_NAME,
                    url: 'git@codeup.aliyun.com:test.git',
                    credentialsId: 'jenkins密钥ID'
            }
        }

         stage('Build and Deploy') {
            steps {
                echo "Performing actions for branch: ${env.BRANCH_NAME}"
                script {
                    if (env.BRANCH_NAME == 'master') {
                        sh '''
                            ssh 1.11.11.11 "cd /home/test && git pull"
                        '''
                    } else if (env.BRANCH_NAME == 'dev') {
                        sh '''
                            cd /home/dev && git pull
                        '''
                    } else if (env.BRANCH_NAME == 'test') {
                        sh '''
                            cd /home/test && git pull
                        '''
                    } else {
                        error "Unsupported branch: ${env.BRANCH_NAME}. No deployment logic defined."
                    }
                }
            }
        }
    }
    
    post {
        success {
            notifyDingTalk("SUCCESS") // 构建成功时通知
        }

        failure {
            notifyDingTalk("FAILURE") // 构建失败时通知
        }
    }
}

// 定义通用的钉钉通知方法
def notifyDingTalk(String buildStatus) {
    script {
        // 获取构建信息
        def branchName = env.BRANCH_NAME
        def duration = currentBuild.durationString
        def executor = currentBuild.getBuildCauses('hudson.model.Cause$UserIdCause')?.userId ?: 'webhook'
        def commitHash = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
        def committer = sh(script: 'git log -1 --pretty=format:"%an"', returnStdout: true).trim()
        def commitMessage = sh(script: 'git log -1 --pretty=format:"%s"', returnStdout: true).trim()
        def rawCommitTime = sh(script: 'git log -1 --pretty=format:"%cd" --date=iso', returnStdout: true).trim()
        def formattedCommitTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(
            new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(rawCommitTime)
        )

        // 获取变更内容(修改的文件列表)
        def changedFiles = sh(script: 'git diff --name-only HEAD~1 HEAD', returnStdout: true).trim()

        // 动态生成标题和状态颜色
        def (statusTitle, statusColor) = buildStatus == "SUCCESS" ? ["构建成功", "#00FF00"] : ["构建失败", "#FF0000"]

        // 获取项目名(从环境变量或静态配置中)
        def projectName = env.JOB_NAME ?: "未知项目"

        // 构造钉钉消息内容
        def message = """
        ## Jenkins <font color="${statusColor}">${statusTitle}</font>通知
 - **项目**: ${projectName}
 - **分支**: ${branchName}
 - **状态**: <font color="${statusColor}">${buildStatus}</font>
 - **持续时间**: ${duration}
 - **执行人**: ${executor}
 - **代码推送人**: ${committer}
 - **提交哈希**: ${commitHash}
 - **提交时间**: ${formattedCommitTime}
 - **提交信息**: ${commitMessage}
 - **变更内容**: ${changedFiles ?: "无变更内容"}
""".stripIndent()

        // 钉钉机器人配置
        def dingtalkWebhookUrl = 'https://oapi.dingtalk.com/robot/send?access_token=your_token'

           // 使用 JsonOutput 生成 JSON 数据
        def payload = groovy.json.JsonOutput.toJson([
            msgtype: "markdown",
            markdown: [
                title: "[${projectName}] Jenkins ${statusTitle}通知",
                text: message
            ],
            at: [
                isAtAll: true
            ]
        ])
        httpRequest(
            url: dingtalkWebhookUrl,
            httpMode: 'POST',
            contentType: 'APPLICATION_JSON_UTF8',  // 确保 UTF-8 编码
            requestBody: payload,
            validResponseCodes: '200:299'  // 接受 200-299 状态码
        )
    }
}

二 前端VUE项目

  • 环境配置:设置 npm 镜像源和缓存目录。

  • 参数化构建:支持选择分支和强制重装依赖。

  • 代码检出:从指定分支拉取代码。

  • 依赖缓存管理:通过哈希值判断是否需要重新安装依赖,并缓存 node_modules。

  • 项目构建:根据分支执行不同构建命令(如生产、测试、开发环境)。 产物验证:检查构建产物是否存在。

  • 部署:将构建产物通过 rsync 部署到本地或远程服务器。

  • 通知:构建成功或失败时通过钉钉机器人发送通知。

groovy 复制代码
pipeline {
    agent any
    environment {
        NPM_REGISTRY = "https://registry.npmmirror.com"
        CACHE_DIR = "${JENKINS_HOME}/cache/${JOB_NAME}"
        
        }
    
    parameters {
        choice(
            name: 'BRANCH',
            choices: ['test', 'master'],
            description: '选择要构建部署的分支'
        )
        
        booleanParam(
            name: 'FORCE_REINSTALL',
            defaultValue: false,
            description: '强制重新安装所有依赖(忽略缓存)'
        )
  
    }
    
    tools {
        // 指定 Node.js 版本为 14.16.1
        nodejs "NodeJS-14.16.1"
    }
    
    triggers {
    // 阿里云Code Webhook配置
    GenericTrigger(
        genericVariables: [
            [key: 'ref', value: '$.ref'], // 获取分支信息
            [key: 'repository_name', value: '$.repository.name'] // 获取仓库名称
        ],
        causeString: 'Triggered by push event on branch $ref',
        token: '15444AWQWQSS75221GRTHESDDSFSFSFS', // 自定义的Token
        printContributedVariables: true, // 打印传递的变量
        printPostContent: true,
        silentResponse: false,
        regexpFilterText: '$ref', // 匹配分支名
        regexpFilterExpression: '^refs/heads/(main|dev|test)$' // 只处理特定分支
    )
}
    
    stages {
        stage('初始化') {
            steps {
                script {
                    // 确定构建分支(Webhook优先)
                    if (env.ref) {
                        env.BUILD_BRANCH = env.ref.tokenize('/')[-1]
                        echo "Webhook触发构建分支: ${env.BUILD_BRANCH}"
                    } else {
                        env.BUILD_BRANCH = params.BRANCH
                        echo "手动触发构建分支: ${params.BRANCH}"
                    }
                    
                    // 验证 Node.js 版本
                    sh 'node -v && npm -v'
                }
            }
        }
        
        stage('检出代码') {
            steps {
                git branch: env.BUILD_BRANCH,
                    url: '阿里云代码仓库地址',
                    credentialsId: 'jenkin凭证ID'
            }
        }
        
        stage('依赖变更检测') {
            steps {
                script {
                    // 强制刷新模式
                    if (params.FORCE_REINSTALL) {
                        env.DEPENDENCIES_CHANGED = "true"
                        echo "强制刷新依赖缓存"
                        return
                    }
                    
                    // 确保缓存目录存在
                    sh "mkdir -p ${CACHE_DIR}"
                    
                    // 计算package.json哈希值
                    def lockFile = fileExists("yarn.lock") ? "yarn.lock" : "package-lock.json"
                    def currentHash = sh(
                        script: "sha256sum package.json ${lockFile} 2>/dev/null | sha256sum | cut -d ' ' -f1",
                        returnStdout: true
                    ).trim()
                    
                    // 获取上次哈希值
                    def cachedHash = ""
                    if (fileExists("${CACHE_DIR}/package_hash.txt")) {
                        cachedHash = readFile("${CACHE_DIR}/package_hash.txt").trim()
                    }
                    
                    // 判断依赖变更
                    env.DEPENDENCIES_CHANGED = (currentHash != cachedHash) ? "true" : "false"
                    echo "依赖变更状态: ${env.DEPENDENCIES_CHANGED}"
                    
                    // 保存当前哈希值
                    if (currentHash) {
                        writeFile file: "${CACHE_DIR}/package_hash.txt", text: currentHash
                    }
                }
            }
        }
        
        stage('恢复缓存') {
            when {
                expression {
                    return env.DEPENDENCIES_CHANGED == "false" && fileExists("${CACHE_DIR}/node_modules.tar")
                }
            }
            steps {
                sh """
                    mkdir -p node_modules
                    tar -xf ${CACHE_DIR}/node_modules.tar -C .
                """
            }
        }
        
        stage('安装依赖') {
            steps {
                script {
                    sh """
                        # 配置镜像源加速
                        npm config set registry ${NPM_REGISTRY}
                        npm config set sass_binary_site ${NPM_REGISTRY}/mirrors/node-sass/
                    """
                    
                    if (env.DEPENDENCIES_CHANGED == "true" || !fileExists("node_modules")) {
                        echo "执行完整依赖安装..."
                        sh "npm ci --prefer-offline --no-audit --silent"
                    } else {
                        echo "依赖未变更,跳过安装..."
                    }
                }
            }
        }
        
      stage('构建') {
            steps {
                sh '''
                    # 优化构建参数
                    export NODE_OPTIONS="--max-old-space-size=4096"
                    export GENERATE_SOURCEMAP=false
                    
                    echo "开始构建项目..."
                    echo "Node version: $(node -v)"
                    echo "NPM version: $(npm -v)"
                    
                    # 根据不同环境使用不同构建配置
                    if [ "${BUILD_BRANCH}" = "master" ]; then
                        echo "生产环境构建"
                        npm run build
                    elif [ "${BUILD_BRANCH}" = "test" ]; then
                        echo "test环境构建"
                        npm run test
                    else
                        echo "dev环境构建"
                        npm run dev
                    fi
                    
                    # 检查构建结果
                    if [ -d "dist" ]; then
                        echo "构建成功完成"
                        echo "构建产物大小: $(du -sh dist)"
                    else
                        echo "构建失败:未找到构建产物"
                        exit 1
                    fi
                '''
            }
         }
        
        stage('验证构建产物') {
            steps {
                sh '''
                    if [ ! -d "dist" ]; then
                        echo "构建产物目录不存在"
                        exit 1
                    fi
                    
                    if [ ! -f "dist/index.html" ]; then
                        echo "入口文件不存在"
                        exit 1
                    fi
                    
                    echo "构建产物验证通过"
                    ls -la dist/
                '''
            }
        }
        
        stage('保存缓存') {
            when {
                expression {
                    return env.DEPENDENCIES_CHANGED == "true"
                }
            }
            steps {
                sh """
                    mkdir -p ${CACHE_DIR}
                    tar -cf ${CACHE_DIR}/node_modules.tar node_modules
                    
                    # 清理策略:保留最近5次缓存
                    cd ${CACHE_DIR}
                    ls -t node_modules.tar 2>/dev/null | tail -n +6 | xargs rm -f
                """
            }
        }
        
        stage('部署') {
            steps {
                script {
                    def DEPLOY_MAP = [
                        "dev": "/home/wwwroot/dev",
                        "test": "/home/wwwroot/test",
                        "master": "IP:/home/wwwroot/master"
                     ]
                    echo "确定部署目标..............."
                    // 确定部署目标
                    def deployTarget = ""
                    def isRemoteDeploy = false
                    deployTarget = DEPLOY_MAP[env.BUILD_BRANCH]
                    echo "部署目录为...............${deployTarget}"
                    if (!deployTarget) {
                            error "未配置分支 ${env.BUILD_BRANCH} 的部署映射"
                        }
                    if (env.BUILD_BRANCH == 'master') {
                            isRemoteDeploy = true
                        }
                   
                    if (isRemoteDeploy) {
                        echo "远程部署中..............."
                        // 远程部署(SSH方式)
                        // 拆分服务器和路径
                        def (server, path) = deployTarget.tokenize(':')
                         // 安全检查
                        if (!server || !path || path == '/') {
                            error "无效的部署路径配置: server=${server}, path=${path}"
                        }
                        sh """
                                rsync -avz --delete \\
                                  --exclude='*.map' \\
                                  --checksum \\
                                  --compress \\
                                  --partial \\
                                  dist/ ${server}:${path}/
                        """
                    } else {
                        echo "本地部署中..............."
                        // 本地部署
                        def path = deployTarget
                        if (!path || path == '/') {
                            error "无效的本地部署路径: ${path}"
                        }
                        sh """
                            rsync -avz --delete \\
                              --exclude='*.map' \\
                              --checksum \\
                              dist/ ${path}/
                        """
                    }
                }
            }
        }
    }
    
   post {
	    
        success {
            notifyDingTalk("SUCCESS") // 构建成功时通知
        }

        failure {
            notifyDingTalk("FAILURE") // 构建失败时通知
        }
    }
}

// 定义通用的钉钉通知方法
def notifyDingTalk(String buildStatus) {
    script {
        // 获取构建信息
        def branchName = env.BUILD_BRANCH
        def duration = currentBuild.durationString
        def executor = currentBuild.getBuildCauses('hudson.model.Cause$UserIdCause')?.userId ?: 'webhook'
        def commitHash = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
        def committer = sh(script: 'git log -1 --pretty=format:"%an"', returnStdout: true).trim()
        def commitMessage = sh(script: 'git log -1 --pretty=format:"%s"', returnStdout: true).trim()
        def rawCommitTime = sh(script: 'git log -1 --pretty=format:"%cd" --date=iso', returnStdout: true).trim()
        def formattedCommitTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(
            new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(rawCommitTime)
        )

        // 获取变更内容(修改的文件列表)
        def changedFiles = sh(script: 'git diff --name-only HEAD~1 HEAD', returnStdout: true).trim()

        // 动态生成标题和状态颜色
        def (statusTitle, statusColor) = buildStatus == "SUCCESS" ? ["构建成功", "#00FF00"] : ["构建失败", "#FF0000"]

        // 获取项目名(从环境变量或静态配置中)
        def projectName = env.JOB_NAME ?: "未知项目"

        // 构造钉钉消息内容
        def message = """
        ## Jenkins <font color="${statusColor}">${statusTitle}</font>通知
- **项目**: ${projectName}
- **分支**: ${branchName}
- **状态**: <font color="${statusColor}">${buildStatus}</font>
- **持续时间**: ${duration}
- **执行人**: ${executor}
- **代码推送人**: ${committer}
- **提交哈希**: ${commitHash}
- **提交时间**: ${formattedCommitTime}
- **提交信息**: ${commitMessage}
- **变更内容**: ${changedFiles ?: "无变更内容"}
""".stripIndent()

        // 钉钉机器人配置
        def dingtalkWebhookUrl = 'webhook地址'

           // 使用 JsonOutput 生成 JSON 数据
        def payload = groovy.json.JsonOutput.toJson([
            msgtype: "markdown",
            markdown: [
                title: "[${projectName}] Jenkins ${statusTitle}通知",
                text: message
            ],
            at: [
                isAtAll: true
            ]
        ])
        httpRequest(
            url: dingtalkWebhookUrl,
            httpMode: 'POST',
            contentType: 'APPLICATION_JSON_UTF8',  // 确保 UTF-8 编码
            requestBody: payload,
            validResponseCodes: '200:299'  // 接受 200-299 状态码
        )
        cleanWs()
    }
}

4.遇到的问题

  • 1.环境兼容问题-linux版本/jenkins版本/java版本三者之间的兼容

  • 2.环境变量作用范围的问题-切换java版本配置环境的生效问题

    通过/etc/profile或/etc/environment配置的环境变量仅对当前终端会话有效,而通过/etc/systemd/system设置的环境变量可确保服务启动时自动加载‌

5.实际效果

相关推荐
KIDULT°7 小时前
MySQL系列 (1):数据库原理
运维·数据库·mysql·云计算
wewowilling9 小时前
认识微服务
java·运维·微服务·springcloud
玖石书10 小时前
Ubuntu 24.04 UFW 全命令攻略:从入门到端口开放实战
linux·运维·ubuntu
weifont11 小时前
自动化抓取和上架商品:能不动手,就少动手
运维·前端·自动化
拂拉氏12 小时前
【知识讲解】 Linux之动静态库的了解
linux·运维·服务器·动静态库
Mikko713 小时前
Linux 重启后日志没了?进程被 OOM 杀了去哪看?logrotate 之后日志还往旧文件写?三台真机实测日志与性能排查
linux·运维·centos
光依旧13 小时前
发版就掉请求?Spring Boot优雅停机的四步
运维·spring boot·nginx·高可用·优雅停机·mcp server
yichengerp14 小时前
电子行业常用erp系统有哪些?推荐适合中小电子厂的易呈erp软件,操作简单,易落地
大数据·运维·人工智能·云计算·制造
天国梦14 小时前
国密门禁品牌怎么选?2026年最新3个避坑要点
运维·服务器·数据库·python
信可维15 小时前
自动化操作多平台账号:登录态、双会话与 User Data 的 3 个坑
运维·自动化