手把手教你部署前端项目CI/CD 第三篇 Jenkins pipeline 篇
1 Jenkins pipeline 知识点
Jenkins Pipeline以代码的方式实现了持续集成和持续交付(CI/CD)的自动化,通过编写脚本,开发者可以定义包括构建、测试、部署和监控等步骤在内的复杂流程。
手把手教你部署前端项目CI/CD 第二篇 Jenkins freestyle 篇
学习pipeline前,请保证你已经掌握上面知识点,docker、jenkins、jenkins freestyle project。
1.1 Jenkins pipeline 主要特性
- 代码化配置:Pipeline 的配置通过代码(Jenkinsfile)进行管理,这意味着配置可以随着代码一起存储在版本控制系统中,便于版本控制和团队协作。
- 可视化界面:Jenkins 提供了直观的 Web 界面来显示 Pipeline 的执行状态和结果,包括每个阶段的执行时间、日志输出等详细信息。
- 灵活性:Pipeline 支持多种类型的节点(Node)和阶段(Stage),允许用户根据具体需求定制流水线的结构和行为。
- 并行执行:Pipeline 支持在同一流水线中并行执行多个阶段或任务,从而进一步提高执行效率。
- 条件执行:Pipeline 允许根据前置条件或执行结果来决定是否执行某个阶段或任务,增加了流程的灵活性和智能性。
1.2 Jenkins pipeline 流水线语法
流水线语法分为声明式(Declarative)和脚本化(Scripted)两种。
官网推荐声明式写法,理由:简洁、简单、社区流行(推动后续版本更好的支持)。
故本文会主要介绍 Declarative 声明式写法。
1.2.1 Declarative 声明式
// Jenkinsfile (Declarative Pipeline)
pipeline {
agent any // 1.在任何可用的代理上,执行流水线或它的任何阶段。
stages {
stage('Build') { // 2.定义 "Build" 阶段。
steps {
// 3.执行与 "Build" 阶段相关的步骤。
}
}
stage('Test') { // 4.定义"Test" 阶段。
steps {
// 5.执行与"Test" 阶段相关的步骤。
}
}
stage('Deploy') { // 6.定义 "Deploy" 阶段。
steps {
// 7.执行与 "Deploy" 阶段相关的步骤。
}
}
}
}
1.3.2 Scripted 脚本化
// Jenkinsfile (Scripted Pipeline)
node { // 1.在任何可用的代理上,执行流水线或它的任何阶段。
stage('Build') { // 2.定义 "Build" 阶段。 stage 块 在脚本化流水线语法中是可选的。 然而, 在脚本化流水线中实现 stage 块 ,可以清楚的显示Jenkins UI中的每个 stage 的任务子集。
// 3.执行与 "Build" 阶段相关的步骤。
}
stage('Test') { // 4.定义"Test" 阶段。
// 5.执行与 "Test" 阶段相关的步骤。
}
stage('Deploy') { // 6.定义 "Deploy" 阶段。
// 7.执行与 "Deploy" 阶段相关的步骤。
}
}
1.2 Jenkins pipeline 结构
- pipeline
- 顶层定义:整个持续集成/持续部署(CI/CD)流程的容器,整个流水线的顶层定义。
- 包含元素:agent, environment, triggers, libraries, options, parameters, tools, stages。
- agent
- 定义:用于指定所在模块(pipeline、stage)的运行环境,可以是Jenkins 节点机或者 docker 容器。
- 位置:可在Pipeline顶层定义,也可在Stage级别定义以覆盖顶层设置。
- environment
- 定义:指定所在模块(pipeline、stage)的环境变量。
- 位置:可在Pipeline顶层或Stage级别定义。
- triggers
- 定义:定义触发Pipeline运行的条件,如代码提交、定时任务等。
- 位置:通常位于Pipeline顶层。
- libraries
- 定义:引用外部Jenkins共享库,以便重用代码和Pipeline模板。
- 位置:在Pipeline顶层通过libraries指令引用。
- options
- 定义:配置Pipeline的运行选项,如重试失败的步骤、跳过默认阶段等。
- 位置:在Pipeline顶层定义。
- parameters
- 定义:定义Pipeline运行时可输入的参数,如分支名、版本号等。
- 位置:在Pipeline顶层定义,用于手动触发Pipeline时输入。
- tools
- 定义:指定Pipeline或Stage级别需要使用的工具,如Node、Maven、Gradle等。
- 位置:可在Pipeline顶层或Stage级别定义。
- stages
- 定义:Pipeline中的逻辑分组,代表了一系列任务的集合。
- 包含元素:至少包含一个stage。
- stage
- 定义:Pipeline中的一个阶段,包含一系列步骤(Steps)。
- 包含元素:agent, environment, tools, input, when, steps。
- agent (Stage级别)
- 定义(同Pipeline级别):指定当前Stage运行在哪个节点上。
- 位置:在Stage内部定义。
- environment (Stage级别)
- 定义(同Pipeline级别):设置当前Stage级别的环境变量。
- 位置:在Stage内部定义。
- tools (Stage级别)
- 定义(同Pipeline级别):指定当前Stage需要使用的工具。
- 位置:在Stage内部定义。
- input
- 定义:允许Pipeline在执行到某个Stage时暂停,等待用户输入或确认。
- 位置:在Stage内部定义。
- when
- 定义:根据条件判断是否执行当前Stage或步骤。
- 位置:在Stage或步骤(Steps)级别定义。
- steps
- 定义:Stage中的具体执行步骤,如编译代码、运行测试等。
- 执行语句:使用Pipeline DSL或Groovy脚本定义的具体命令或操作。
- post
- 定义:定义Pipeline或Stage执行完成后的操作,如发送通知、清理工作区等。- 位置:在Pipeline或Stage末尾定义。
- 包含条件:always, success, failure, unstable, changed, aborted等,用于指定不同执行结果下的操作。
上述结构只需要了解,使用的时候直接用生成器生成。
2 Jenkins pipeline 实战
2.1 新建pipeline任务
2.2 Git Paramater 参数化构建
2.3 Jenkins pipeline 生成方式
Jenkins 流水线的定义有两种方式:Pipeline script 和 Pipeline script from SCM。推荐Pipeline script from SCM
2.3.1 Pipeline script
直接在Jenkins页面上写,硬编码在Jenkins服务器上,在线编辑、固定内容不灵活
2.3.2 Pipeline script from SCM
创建一个 Jenkinsfile 文件 并将其检入源代码控制仓库是最佳实践
2.4 项目根目录新建 Jenkinsfile 文件
2.5 安装 Generic Webhook Trigger Plugin 插件
前面文章介绍安装github webhook 插件来完成代码提交触发流水线的功能,那个处理github的webhook。
这次换成gitee 的 webhook,所以需要安装另外个更通用的插件,这个插件可以处理大部分的webhook。
Jenkins 插件管理页面搜索 Generic Webhook Trigger Plugin
配置Generic Webhook Trigger Plugin
2.6 编写Jenkisnfile
回到项目里Jenkisnfile文件里
// Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
// 全局环境变量 为了和jenkins自带的环境变量区分。${env.GIT_REPO}
environment {
GIT_REPO = 'https://gitee.com/paul958320/React18_Vite4_Admin.git'
}
// 选择node,git版本
tools {
nodejs 'NodeJS 16.17.0'
git 'Default'
}
// 参数化共建
parameters {
gitParameter(
name: 'GIT_BRANCH',
branch: '',
branchFilter: 'origin/(.*)',
defaultValue: 'origin/dev',
description: '请选择部署分支',
quickFilterEnabled: false,
requiredParameter: true,
selectedValue: 'DEFAULT',
sortMode: 'ASCENDING_SMART',
tagFilter: '*',
type: 'GitParameterDefinition'
)
}
// webhook trigger触发器
triggers {
GenericTrigger(
causeString: 'Triggered on Gitee Webhook',
genericRequestVariables: [[key: '', regexpFilter: '']], // 请求体提取信息,暂不需要,有些插件版本不兼容genericVariables
genericHeaderVariables: [[key: '', regexpFilter: '']], // 请求头部提取信息,暂不需要
genericVariables: [ // 请求体body中提取信息
[
key: 'ref',
value: '$.ref',
regexpFilter: '^(refs/heads/|refs/remotes/origin/)',
expressionType:'JSONPath', //Optional, defaults to JSONPath
defaultValue: '', //Optional, defaults to empty string
],
[
key: 'clone_url',
value: '$.repository.clone_url',
regexpFilter: '',
expressionType:'JSONPath', //Optional, defaults to JSONPath
defaultValue: '', //Optional, defaults to empty string
],
[
key: 'changed_files',
value: '$.commits[*].[\'modified\',\'added\',\'removed\'][*]',
expressionType:'JSONPath', //Optional, defaults to JSONPath
regexpFilter: '',
defaultValue: '',
]
],
printContributedVariables: true, // 打印所有通过genericVariables提取的变量
printPostContent: true, // 打印触发构建时接收到的POST请求的内容
silentResponse: false, // 对收到的触发请求返回响应
shouldNotFlatten: false, // 不需要对提取的数据进行扁平化处理(嵌套数据结构)
tokenCredentialId: 'GITEE_WEBHOOK_SECRET', // github webhook secrect 配置后请求头会带上 X-Hub-Signature-256: sha256=203a39e9d753415ded57e09710ecf9e98b694a6f0c8d530b65ef90d35bede405
token: '', // 可选token,没有设置
regexpFilterExpression: '',
regexpFilterText: '',
)
}
stages {
stage('Cleanup Workspace') {
steps {
sh """
echo "查看当前所在文件路径"
pwd
"""
echo "开始清理工作空间"
cleanWs()
echo "清理工作空间结束"
// 或者,使用自定义配置来清理工作空间
// cleanWs(
// exclude: '**/some-directory/**', // 排除不删除的目录
// fingerprintCleanup: true, // 清理指纹信息
// when: 'before' // 在什么时候执行清理,默认为'after'
// )
}
}
stage('Pull Code') {
steps {
sh """
echo "查看当前所在文件路径"
pwd
"""
echo "开始clone代码"
echo "ref ${ref}"
// echo "clone_url ${clone_url}"
echo "GIT_REPO ${env.GIT_REPO}"
echo "GIT_BRANCH ${env.GIT_BRANCH}"
// checkout scmGit(
// branches: [[name: '$GIT_BRANCH']],
// extensions: [],
// userRemoteConfigs: [
// [url: "${clone_url}",credentialsId: "GITHUB_PERSONAL_ACCESS_TOKEN"]
// ]
// )
checkout scmGit(
branches: [[name: '$ref']],
extensions: [
cleanBeforeCheckout(deleteUntrackedNestedRepositories: true),
cleanAfterCheckout(deleteUntrackedNestedRepositories: true)
],
userRemoteConfigs: [
// [ url: '${clone_url}',name:'origin', credentialsId: 'GITHUB_PERSONAL_ACCESS_TOKEN',],
[
url: "${env.GIT_REPO}", // 仓库地址
name:"gitee", // 仓库remote
credentialsId: "GITEE_PERSONAL_ACCESS_TOKEN" // 仓库token
]
]
)
echo "clone代码成功"
}
// when {
// triggeredBy 'Triggered on GitHub Webhook'
// beforeOptions true
// beforeInput true
// beforeAgent true
// }
}
stage('Install Dependencies') {
steps {
echo "开始安装依赖"
sh """
echo "安装依赖前 检查环境..."
echo "查看当前所在文件路径"
pwd
echo "检查 Node.js 版本:"
node -v
npm config set registry https://registry.npmmirror.com
echo "检查 npm 镜像源:"
npm config get registry
npm install -g pnpm@8.6.0
echo "检查 pnpm 版本:"
pnpm -v
pnpm config set registry https://registry.npmmirror.com
echo "pnpm 镜像源:"
pnpm config get registry
echo "检查环境 通过..."
echo "开始安装依赖..."
pnpm install || { echo "安装依赖失败"; exit 1; }
echo "安装依赖成功"
"""
echo "安装依赖成功"
}
}
stage('Build Project') {
steps {
echo "开始构建项目"
sh """
echo "查看当前所在文件路径"
pwd
# 清理旧的 dist 目录
echo "清理dist"
rm -rf ./dist
# 这里是用你项目里的打包脚本,比如你的可能是npm run build
npm run build:jenkins || { echo "构建失败"; exit 1; }
"""
echo "构建项目成功"
echo "开始打包"
sh """
# 每次构建删除已存在的dist压缩包
rm -rf dist.tar
# 将dist文件压缩成dist.tar
tar -zcvf dist.tar ./dist
if [ \$? -eq 0 ]; then
echo "打包成功"
else
echo "打包失败"
exit 1
fi
"""
echo "构建和打包完成"
}
}
stage('Deploy') {
steps {
// ssh服务器发送文件
sshPublisher(
publishers: [
sshPublisherDesc(
configName: 'jenkins_node',
usePromotionTimestamp: false,
useWorkspaceInPromotion: false,
verbose: false,
transfers: [
sshTransfer(
cleanRemote: false, // 表示在传输文件之前不清除远程目录
excludes: '', // 要排除的文件模式,这里为空表示不排除任何文件
execCommand: // 远程服务器上执行的命令
'''
cd /docker/html/${GIT_BRANCH}
rm -rf dist/
tar zxvf dist.tar
rm dist.tar
''',
execTimeout: 120000, // 远程命令执行的超时时间,单位是毫秒
flatten: false, // 不将源文件目录结构展平到远程目录
makeEmptyDirs: false, // 不创建空的远程目录
noDefaultExcludes: false, // 是否应用默认的排除模式
patternSeparator: '[, ]+', // 定义了用于分隔多个源文件模式的分隔符
remoteDirectory: '/docker/html/${GIT_BRANCH}', // 远程目标目录
remoteDirectorySDF: false, // 可能是一个与日期时间格式化相关的选项
removePrefix: '', // 传输到远程目录之前,从源文件路径中移除的前缀
sourceFiles: 'dist.tar' // 要传输到远程服务器的源文件
)
]
)
]
)
}
}
}
post {
always {
script {
// 无论构建成功与否都不会影响整体结果
catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') {
sh '/var/jenkins_home/pipline_shell/dingding_jenkins --name="${JOB_NAME}" --id="${BUILD_ID}" --url="$JOB_URL" --branch="$refVar"'
}
}
}
}
}
洗尽铅华始见金,褪去浮华归本真