文章目录
- [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.实际效果

