Commit a86b269ed71e2e3fc648b4a94525039ca8cf0248
1 parent
75b5d305
feat(deploy): 完善 Jenkinsfile 与 Jar 部署流程
- 参数化 Maven 构建、产物归档与 SSH 部署 - 补充运行时配置、备份、精确进程停止及启动检查 - 完善端口冲突处理与旧备份清理提示
Showing
1 changed file
with
307 additions
and
33 deletions
script/jenkins/Jenkinsfile
| 1 | 1 | #!groovy |
| 2 | +// yudao-server 的 Jenkins 部署流水线:Maven 打包 Jar,再通过 SSH 部署到一台 Linux 服务器 | |
| 3 | +// | |
| 4 | +// 使用说明见《Jenkins 部署》文档:https://doc.iocoder.cn/deployment-jenkins/ | |
| 5 | +// 1. Jenkins 新建「Pipeline(流水线)」类型的 Job,选择「Pipeline script from SCM」,Script Path 填写 script/jenkins/Jenkinsfile | |
| 6 | +// 2. Jenkins 中配置 Tools(jdk17、maven3),以及两个凭据(yudao-deploy-ssh、yudao-deploy-known-hosts) | |
| 7 | +// 3. 服务器上提前创建 <DEPLOY_BASE_DIR>/yudao-server 目录,并放入运行时配置 app.env(数据库、Redis 的地址与密码) | |
| 8 | + | |
| 9 | +// 在目标服务器上执行的脚本:Jenkins 通过 ssh 传入,用法为 bash -s -- <check|deploy> <部署根目录> <服务名> <默认端口> | |
| 10 | +def REMOTE_SCRIPT = ''' | |
| 11 | +set -euo pipefail | |
| 12 | + | |
| 13 | +action="$1" | |
| 14 | +app_name="$3" | |
| 15 | +app_dir="$2/$3" | |
| 16 | +app_jar="$app_dir/$app_name.jar" | |
| 17 | +new_jar="$app_dir/upload/$app_name.jar" | |
| 18 | +env_file="$app_dir/app.env" | |
| 19 | +default_port="$4" | |
| 20 | +stop_timeout=60 # 优雅停止的最长等待秒数,超时后 kill -9 | |
| 21 | +start_timeout=180 # 启动检查的最长等待秒数 | |
| 22 | +backup_keep=5 # backup 目录保留最近几个 Jar | |
| 23 | + | |
| 24 | +fail() { | |
| 25 | + echo "[deploy] 失败:$*" >&2 | |
| 26 | + exit 1 | |
| 27 | +} | |
| 28 | + | |
| 29 | +# 加载运行时配置 app.env:数据库、Redis 等地址和密码只保存在服务器上,不进入 Git 与 Jenkins | |
| 30 | +load_env() { | |
| 31 | + [ -r "$env_file" ] || fail "缺少运行时配置 $env_file,首次部署请参考文档创建" | |
| 32 | + set -a | |
| 33 | + . "$env_file" | |
| 34 | + set +a | |
| 35 | + [ -n "${SPRING_PROFILES_ACTIVE:-}" ] || fail "$env_file 中没有设置 SPRING_PROFILES_ACTIVE" | |
| 36 | + port="${SERVER_PORT:-$default_port}" | |
| 37 | + case "$port" in | |
| 38 | + '' | *[!0-9]*) fail "SERVER_PORT 必须是数字" ;; | |
| 39 | + esac | |
| 40 | + java_cmd=java | |
| 41 | + if [ -n "${JAVA_HOME:-}" ]; then | |
| 42 | + java_cmd="$JAVA_HOME/bin/java" | |
| 43 | + fi | |
| 44 | + command -v "$java_cmd" > /dev/null || fail "找不到 Java:$java_cmd" | |
| 45 | +} | |
| 46 | + | |
| 47 | +# 部署前检查:目录、app.env、Java、curl、pgrep、ss,以及端口是否被其它进程占用 | |
| 48 | +check() { | |
| 49 | + [ -d "$app_dir" ] || fail "目录 $app_dir 不存在,首次部署请先创建,并放入 app.env" | |
| 50 | + [ -w "$app_dir" ] || fail "用户 $(id -un) 没有 $app_dir 的写权限" | |
| 51 | + load_env | |
| 52 | + command -v curl > /dev/null || fail "服务器缺少 curl 命令" | |
| 53 | + command -v pgrep > /dev/null || fail "服务器缺少 pgrep 命令(procps 软件包)" | |
| 54 | + command -v ss > /dev/null || fail "服务器缺少 ss 命令(iproute 或 iproute2 软件包)" | |
| 55 | + mkdir -p "$app_dir/upload" "$app_dir/backup" | |
| 56 | + echo "[check] Java:$("$java_cmd" -version 2>&1 | head -n 1)" | |
| 57 | + echo "[check] Profile:$SPRING_PROFILES_ACTIVE,端口:$port" | |
| 58 | + case "$(stat -c %a "$env_file" 2> /dev/null || echo unknown)" in | |
| 59 | + 600 | 400) ;; | |
| 60 | + *) echo "[check] 警告:建议执行 chmod 600 $env_file,避免其它用户读取密码" ;; | |
| 61 | + esac | |
| 62 | + if [ -f "$app_jar" ]; then | |
| 63 | + echo "[check] 已存在 $app_jar,部署前会先备份" | |
| 64 | + else | |
| 65 | + echo "[check] 首次部署:$app_jar 不存在,无需备份" | |
| 66 | + fi | |
| 67 | + if [ -z "$(find_pids)" ] && port_in_use; then | |
| 68 | + fail "端口 $port 已被其它进程占用,请先执行 ss -ltnp 排查" | |
| 69 | + fi | |
| 70 | +} | |
| 71 | + | |
| 72 | +# 查找本服务的进程:java 进程中,-jar 后面的参数与 $app_jar 完全相等(逐个比较 /proc/<pid>/cmdline 的参数,不使用正则) | |
| 73 | +find_pids() { | |
| 74 | + local pid prev arg | |
| 75 | + for pid in $(pgrep -x java || true); do | |
| 76 | + prev="" | |
| 77 | + while IFS= read -r -d '' arg; do | |
| 78 | + if [ "$prev" = "-jar" ] && [ "$arg" = "$app_jar" ]; then | |
| 79 | + echo "$pid" | |
| 80 | + break | |
| 81 | + fi | |
| 82 | + prev="$arg" | |
| 83 | + done 2> /dev/null < "/proc/$pid/cmdline" || true | |
| 84 | + done | |
| 85 | +} | |
| 86 | + | |
| 87 | +# 等待指定的进程全部退出,超时返回 1 | |
| 88 | +wait_exit() { | |
| 89 | + local timeout="$1" deadline pid alive | |
| 90 | + shift | |
| 91 | + deadline=$((SECONDS + timeout)) | |
| 92 | + while true; do | |
| 93 | + alive="" | |
| 94 | + for pid in "$@"; do | |
| 95 | + if [ -e "/proc/$pid" ]; then | |
| 96 | + alive="$pid" | |
| 97 | + fi | |
| 98 | + done | |
| 99 | + [ -z "$alive" ] && return 0 | |
| 100 | + [ "$SECONDS" -ge "$deadline" ] && return 1 | |
| 101 | + sleep 1 | |
| 102 | + done | |
| 103 | +} | |
| 104 | + | |
| 105 | +# 端口是否已被监听:使用 ss 列出本机所有 IPv4、IPv6 地址(0.0.0.0、[::]、127.0.0.x、内网 IP 等)上监听该端口的 TCP 套接字 | |
| 106 | +# 先把完整输出保存到变量再判断,不使用管道,避免 grep -q 提前退出导致的误判;ss 执行失败时直接中止部署 | |
| 107 | +port_in_use() { | |
| 108 | + local out | |
| 109 | + out=$(ss -ltn "sport = :$port") || fail "执行 ss 命令失败,无法检查端口 $port 是否被占用" | |
| 110 | + case "$out" in | |
| 111 | + *LISTEN*) return 0 ;; | |
| 112 | + esac | |
| 113 | + return 1 | |
| 114 | +} | |
| 115 | + | |
| 116 | +# 停止:先 kill -15 优雅关闭,超时再 kill -9。确认进程退出后才返回,否则中止部署 | |
| 117 | +stop_app() { | |
| 118 | + local pids | |
| 119 | + pids=$(find_pids) | |
| 120 | + if [ -z "$pids" ]; then | |
| 121 | + echo "[stop] $app_name 未运行,无需停止" | |
| 122 | + return 0 | |
| 123 | + fi | |
| 124 | + pids=$(echo $pids) | |
| 125 | + echo "[stop] 停止进程:$pids" | |
| 126 | + kill -15 $pids 2> /dev/null || true | |
| 127 | + if ! wait_exit "$stop_timeout" $pids; then | |
| 128 | + echo "[stop] 等待 $stop_timeout 秒仍未停止,执行 kill -9" | |
| 129 | + kill -9 $pids 2> /dev/null || true | |
| 130 | + wait_exit 10 $pids || fail "进程 $pids 无法停止(可能属于其它用户),已中止部署,Jar 包未替换" | |
| 131 | + fi | |
| 132 | + echo "[stop] 已停止" | |
| 133 | +} | |
| 134 | + | |
| 135 | +# 启动:环境变量来自 app.env,Spring Boot 会用它们覆盖 Jar 包中的配置 | |
| 136 | +start_app() { | |
| 137 | + echo "[start] 启动 $app_jar,Profile:$SPRING_PROFILES_ACTIVE" | |
| 138 | + # JAVA_OPTS 需要拆分成多个 JVM 参数,所以这里不加引号 | |
| 139 | + nohup "$java_cmd" ${JAVA_OPTS:--Xms512m -Xmx512m} -jar "$app_jar" > "$app_dir/nohup.out" 2>&1 < /dev/null & | |
| 140 | + app_pid=$! | |
| 141 | + echo "[start] 进程号:$app_pid" | |
| 142 | +} | |
| 143 | + | |
| 144 | +# 启动检查:新进程存活,并且端口返回 HTTP 状态码(小于 500)。启动前已确认端口空闲,所以响应来自新进程 | |
| 145 | +# 它只说明服务已启动、端口可访问,不代表业务健康。项目默认不包含 actuator,所以不使用 /actuator/health | |
| 146 | +wait_started() { | |
| 147 | + local url="http://127.0.0.1:$port/" code | |
| 148 | + local deadline=$((SECONDS + start_timeout)) | |
| 149 | + while [ "$SECONDS" -lt "$deadline" ]; do | |
| 150 | + if [ ! -e "/proc/$app_pid" ]; then | |
| 151 | + echo "[start] 进程已退出" | |
| 152 | + return 1 | |
| 153 | + fi | |
| 154 | + code=$(curl -s -o /dev/null -m 3 -w '%{http_code}' "$url" || true) | |
| 155 | + if [ "$code" -ge 200 ] 2> /dev/null && [ "$code" -lt 500 ] && [ -e "/proc/$app_pid" ]; then | |
| 156 | + echo "[start] $url 已响应,启动完成" | |
| 157 | + return 0 | |
| 158 | + fi | |
| 159 | + sleep 3 | |
| 160 | + done | |
| 161 | + echo "[start] 等待 $start_timeout 秒后,$url 仍未响应" | |
| 162 | + return 1 | |
| 163 | +} | |
| 164 | + | |
| 165 | +# 只保留最近 backup_keep 个备份:按文件名中的时间戳排序(C 语言环境),而不是按修改时间(cp -p 保留的是原 Jar 的修改时间) | |
| 166 | +# 删除失败时(例如说没有权限),输出警告后继续删除其它备份,不影响已经成功的部署 | |
| 167 | +clean_backups() { | |
| 168 | + local LC_ALL=C | |
| 169 | + local backups=("$app_dir/backup/$app_name"-[0-9]*.jar) | |
| 170 | + local i | |
| 171 | + [ -e "${backups[0]}" ] || return 0 | |
| 172 | + for ((i = 0; i < ${#backups[@]} - backup_keep; i++)); do | |
| 173 | + rm -f -- "${backups[i]}" || echo "[backup] 警告:无法删除旧备份 ${backups[i]},请检查权限后手动清理" >&2 | |
| 174 | + done | |
| 175 | +} | |
| 176 | + | |
| 177 | +# 部署:备份 => 停止 => 替换 => 启动 => 启动检查。失败时不自动回滚,打印日志与备份路径,由人工确认后回滚 | |
| 178 | +deploy() { | |
| 179 | + [ -s "$new_jar" ] || fail "没有找到上传的 $new_jar" | |
| 180 | + load_env | |
| 181 | + cd "$app_dir" | |
| 182 | + backup_jar="" | |
| 183 | + if [ -f "$app_jar" ]; then | |
| 184 | + backup_jar="$app_dir/backup/$app_name-$(date +%Y%m%d%H%M%S).jar" | |
| 185 | + cp -p "$app_jar" "$backup_jar" | |
| 186 | + echo "[backup] 已备份到 $backup_jar" | |
| 187 | + else | |
| 188 | + echo "[backup] 首次部署,无需备份" | |
| 189 | + fi | |
| 190 | + stop_app | |
| 191 | + if port_in_use; then | |
| 192 | + fail "停止 $app_name 后,端口 $port 仍被其它进程占用,已中止部署:新 Jar 包未替换,但已停止的 $app_name 不会自动恢复。请执行 ss -ltnp 排查并处理端口后重新构建,或参考文档手动启动、回滚" | |
| 193 | + fi | |
| 194 | + mv -f "$new_jar" "$app_jar" | |
| 195 | + start_app | |
| 196 | + if wait_started; then | |
| 197 | + clean_backups | |
| 198 | + echo "[deploy] 部署成功:$app_name" | |
| 199 | + else | |
| 200 | + echo "[deploy] 启动检查失败,$app_dir/nohup.out 的最后 100 行:" | |
| 201 | + tail -n 100 "$app_dir/nohup.out" || true | |
| 202 | + if [ -n "$backup_jar" ]; then | |
| 203 | + echo "[deploy] 可以手动回滚到 $backup_jar,步骤见《Jenkins 部署》文档" | |
| 204 | + fi | |
| 205 | + exit 1 | |
| 206 | + fi | |
| 207 | +} | |
| 208 | + | |
| 209 | +case "$action" in | |
| 210 | + check) check ;; | |
| 211 | + deploy) deploy ;; | |
| 212 | + *) fail "未知操作:$action" ;; | |
| 213 | +esac | |
| 214 | +''' | |
| 215 | + | |
| 2 | 216 | pipeline { |
| 3 | 217 | |
| 4 | 218 | agent any |
| 5 | 219 | |
| 220 | + options { | |
| 221 | + skipDefaultCheckout() | |
| 222 | + disableConcurrentBuilds() | |
| 223 | + timeout(time: 60, unit: 'MINUTES') | |
| 224 | + buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '5')) | |
| 225 | + } | |
| 226 | + | |
| 6 | 227 | parameters { |
| 7 | - string(name: 'TAG_NAME', defaultValue: '', description: '') | |
| 228 | + string(name: 'DEPLOY_HOST', defaultValue: '', trim: true, description: '目标服务器的 IP 或域名(必填)') | |
| 229 | + string(name: 'DEPLOY_USER', defaultValue: 'root', trim: true, description: 'SSH 登录用户') | |
| 230 | + string(name: 'DEPLOY_SSH_PORT', defaultValue: '22', trim: true, description: 'SSH 端口') | |
| 231 | + string(name: 'DEPLOY_BASE_DIR', defaultValue: '/work/projects', trim: true, description: '部署根目录,实际部署到 <DEPLOY_BASE_DIR>/yudao-server') | |
| 232 | + } | |
| 233 | + | |
| 234 | + // 构建使用的 JDK、Maven,需要在 Jenkins 的 Tools 中配置同名工具。与 Jenkins 自身运行的 Java 版本无关 | |
| 235 | + tools { | |
| 236 | + jdk 'jdk17' | |
| 237 | + maven 'maven3' | |
| 8 | 238 | } |
| 9 | 239 | |
| 10 | 240 | environment { |
| 11 | - // DockerHub 凭证 ID(登录您的 DockerHub) | |
| 12 | - DOCKER_CREDENTIAL_ID = 'dockerhub-id' | |
| 13 | - // GitHub 凭证 ID (推送 tag 到 GitHub 仓库) | |
| 14 | - GITHUB_CREDENTIAL_ID = 'github-id' | |
| 15 | - // kubeconfig 凭证 ID (访问接入正在运行的 Kubernetes 集群) | |
| 16 | - KUBECONFIG_CREDENTIAL_ID = 'demo-kubeconfig' | |
| 17 | - // 镜像的推送 | |
| 18 | - REGISTRY = 'docker.io' | |
| 19 | - // DockerHub 账号名 | |
| 20 | - DOCKERHUB_NAMESPACE = 'docker_username' | |
| 21 | - // GitHub 账号名 | |
| 22 | - GITHUB_ACCOUNT = 'https://gitee.com/zhijiantianya/ruoyi-vue-pro' | |
| 23 | - // 应用名称 | |
| 241 | + // 应用名称,同时是服务器上的目录名、Jar 包名 | |
| 24 | 242 | APP_NAME = 'yudao-server' |
| 25 | - // 应用部署路径 | |
| 26 | - APP_DEPLOY_BASE_DIR = '/media/pi/KINGTON/data/work/projects/' | |
| 243 | + // Maven 模块与 Jar 包路径 | |
| 244 | + APP_MODULE = 'yudao-server' | |
| 245 | + APP_JAR = 'yudao-server/target/yudao-server.jar' | |
| 246 | + // 服务端口,用于启动检查。app.env 中设置了 SERVER_PORT 时,以 SERVER_PORT 为准 | |
| 247 | + APP_PORT = '48080' | |
| 248 | + // Jenkins 凭据 ID:SSH 私钥(SSH Username with private key)、known_hosts 文件(Secret file) | |
| 249 | + SSH_CREDENTIAL_ID = 'yudao-deploy-ssh' | |
| 250 | + KNOWN_HOSTS_CREDENTIAL_ID = 'yudao-deploy-known-hosts' | |
| 27 | 251 | } |
| 28 | 252 | |
| 29 | 253 | stages { |
| 30 | - stage('检出') { | |
| 254 | + stage('拉取代码') { | |
| 31 | 255 | steps { |
| 32 | - git url: "https://gitee.com/will-we/ruoyi-vue-pro.git", | |
| 33 | - branch: "devops" | |
| 256 | + checkout scm | |
| 34 | 257 | } |
| 35 | 258 | } |
| 36 | 259 | |
| 37 | - stage('构建') { | |
| 260 | + stage('检查参数') { | |
| 38 | 261 | steps { |
| 39 | - // TODO 解决多环境链接、密码不同配置临时方案 | |
| 40 | - sh 'if [ ! -d "' + "${env.HOME}" + '/resources" ];then\n' + | |
| 41 | - ' echo "配置文件不存在无需修改"\n' + | |
| 42 | - 'else\n' + | |
| 43 | - ' cp -rf ' + "${env.HOME}" + '/resources/*.yaml ' + "${env.APP_NAME}" + '/src/main/resources\n' + | |
| 44 | - ' echo "配置文件替换"\n' + | |
| 45 | - 'fi' | |
| 46 | - sh 'mvn clean package -Dmaven.test.skip=true' | |
| 262 | + script { | |
| 263 | + // 参数会拼接到 ssh、scp 命令中,必须严格校验,避免命令注入 | |
| 264 | + def host = params.DEPLOY_HOST ?: '' | |
| 265 | + if (!host.matches('[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?')) { | |
| 266 | + error('DEPLOY_HOST 必填,只能是 IPv4 地址或域名') | |
| 267 | + } | |
| 268 | + def user = params.DEPLOY_USER ?: '' | |
| 269 | + if (!user.matches('[a-z_][a-z0-9_-]{0,31}')) { | |
| 270 | + error('DEPLOY_USER 不合法,只能包含小写字母、数字、下划线、中划线') | |
| 271 | + } | |
| 272 | + def port = params.DEPLOY_SSH_PORT ?: '' | |
| 273 | + if (!port.matches('[0-9]{1,5}') || port.toInteger() < 1 || port.toInteger() > 65535) { | |
| 274 | + error('DEPLOY_SSH_PORT 必须是 1 ~ 65535 的数字') | |
| 275 | + } | |
| 276 | + def baseDir = params.DEPLOY_BASE_DIR ?: '' | |
| 277 | + if (!baseDir.matches('/[A-Za-z0-9._/-]*[A-Za-z0-9_-]') || baseDir.contains('..') || baseDir.contains('//')) { | |
| 278 | + error('DEPLOY_BASE_DIR 必须是绝对路径,只能包含字母、数字和 . _ - /,不能以 / 结尾') | |
| 279 | + } | |
| 280 | + echo "部署 ${env.APP_NAME} => ${user}@${host}:${baseDir}/${env.APP_NAME}" | |
| 281 | + } | |
| 282 | + } | |
| 283 | + } | |
| 284 | + | |
| 285 | + stage('编译') { | |
| 286 | + steps { | |
| 287 | + sh 'java -version && mvn -v' | |
| 288 | + sh 'mvn -B -ntp clean package -pl "$APP_MODULE" -am -Dmaven.test.skip=true' | |
| 289 | + sh 'test -f "$APP_JAR"' | |
| 290 | + } | |
| 291 | + } | |
| 292 | + | |
| 293 | + stage('归档') { | |
| 294 | + steps { | |
| 295 | + archiveArtifacts artifacts: env.APP_JAR, fingerprint: true | |
| 47 | 296 | } |
| 48 | 297 | } |
| 49 | 298 | |
| 50 | 299 | stage('部署') { |
| 51 | 300 | steps { |
| 52 | - sh 'cp -f ' + ' bin/deploy.sh ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" | |
| 53 | - sh 'cp -f ' + "${env.APP_NAME}" + '/target/*.jar ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" +'/build/' | |
| 54 | - archiveArtifacts "${env.APP_NAME}" + '/target/*.jar' | |
| 55 | - sh 'chmod +x ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" + '/deploy.sh' | |
| 56 | - sh 'bash ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" + '/deploy.sh' | |
| 301 | + writeFile file: '.jenkins-remote-deploy.sh', text: REMOTE_SCRIPT | |
| 302 | + withCredentials([file(credentialsId: env.KNOWN_HOSTS_CREDENTIAL_ID, variable: 'KNOWN_HOSTS_FILE')]) { | |
| 303 | + sshagent(credentials: [env.SSH_CREDENTIAL_ID]) { | |
| 304 | + // 只信任 known_hosts 凭据中的主机公钥:StrictHostKeyChecking=yes,并且不读取系统的 known_hosts | |
| 305 | + sh ''' | |
| 306 | + set -eu | |
| 307 | + case "$KNOWN_HOSTS_FILE" in | |
| 308 | + *[[:space:]]*) echo "known_hosts 文件路径不能包含空白字符,请修改 Job 名称" >&2; exit 1 ;; | |
| 309 | + esac | |
| 310 | + remote() { | |
| 311 | + ssh -p "$DEPLOY_SSH_PORT" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=yes -o GlobalKnownHostsFile=/dev/null -o "UserKnownHostsFile=$KNOWN_HOSTS_FILE" "$DEPLOY_USER@$DEPLOY_HOST" "$@" | |
| 312 | + } | |
| 313 | + # ① 检查服务器 | |
| 314 | + remote bash -s -- check "$DEPLOY_BASE_DIR" "$APP_NAME" "$APP_PORT" < .jenkins-remote-deploy.sh | |
| 315 | + # ② 上传 Jar 到 upload 目录 | |
| 316 | + scp -P "$DEPLOY_SSH_PORT" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=yes -o GlobalKnownHostsFile=/dev/null -o "UserKnownHostsFile=$KNOWN_HOSTS_FILE" "$APP_JAR" "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_BASE_DIR/$APP_NAME/upload/$APP_NAME.jar" | |
| 317 | + # ③ 备份、停止、替换、启动,并检查启动结果 | |
| 318 | + remote bash -s -- deploy "$DEPLOY_BASE_DIR" "$APP_NAME" "$APP_PORT" < .jenkins-remote-deploy.sh | |
| 319 | + ''' | |
| 320 | + } | |
| 321 | + } | |
| 57 | 322 | } |
| 58 | 323 | } |
| 59 | 324 | } |
| 325 | + | |
| 326 | + post { | |
| 327 | + success { | |
| 328 | + echo "部署成功:${env.APP_NAME}" | |
| 329 | + } | |
| 330 | + failure { | |
| 331 | + echo '部署失败:请查看上方 [check]、[stop]、[start]、[deploy] 开头的日志,回滚步骤见《Jenkins 部署》文档' | |
| 332 | + } | |
| 333 | + } | |
| 60 | 334 | } | ... | ... |