feat: 删除无用内容
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
# 部署服务器信息
|
||||
SERVER=162.14.96.191
|
||||
PORT=6000
|
||||
USER=root
|
||||
PASSWORD=a631223
|
||||
# KEYFILE=/home/user/.ssh/id_rsa
|
||||
#FINGERPRINT=SHA256:REPLACE_WITH_SERVER_FINGERPRINT
|
||||
|
||||
# 部署环境
|
||||
ENV=prod
|
||||
|
||||
# 构建工具
|
||||
BUILD_TOOL=maven
|
||||
PROJECT_DIR=G:\code\shw\hot\hot-platform-backend
|
||||
MAVEN_HOME=G:\soft\apache-maven-3.9.3-bin\apache-maven-3.9.3
|
||||
# GRADLE_HOME=
|
||||
# TARGET_JAR_NAME=
|
||||
|
||||
# 远端部署目录
|
||||
REMOTE_DIR=/apps/hot/
|
||||
|
||||
# JVM 启动参数
|
||||
JVM_OPTS=-Xms512m -Xmx1024m
|
||||
|
||||
# 部署验证接口地址
|
||||
DEPLOY_INFO_URL=http://api.xiaoshi98.top/deploy/info
|
||||
|
||||
# 回滚告警通知地址(可选)
|
||||
# ALERT_WEBHOOK=https://example.com/webhook
|
||||
|
||||
# 运行选项
|
||||
# 演示模式
|
||||
DRY_RUN=false
|
||||
# 跳过打包
|
||||
SKIP_BUILD=false
|
||||
@@ -1,30 +0,0 @@
|
||||
# 部署服务器信息
|
||||
SERVER=162.14.96.191
|
||||
PORT=6000
|
||||
USER=root
|
||||
PASSWORD=a631223
|
||||
# KEYFILE=/home/user/.ssh/id_rsa
|
||||
# FINGERPRINT=SHA256:REPLACE_WITH_SERVER_FINGERPRINT
|
||||
|
||||
# 部署环境
|
||||
ENV=prod
|
||||
|
||||
# 构建设置(前端)
|
||||
# 构建命令;未设置时,脚本默认使用:npm run build:prod
|
||||
BUILD_CMD=npm run build:prod
|
||||
# 本地前端项目目录(请按实际路径调整)
|
||||
PROJECT_DIR=G:\code\shw\hot\hot-platform-frontend
|
||||
# 构建产物目录(相对 PROJECT_DIR 或绝对路径),未设置时默认为 PROJECT_DIR\dist
|
||||
DIST_DIR=dist
|
||||
|
||||
# 远端部署目录(服务器上前端静态资源目录)
|
||||
REMOTE_DIR=/apps/hot/front-end/dist
|
||||
|
||||
# 传输与运行选项
|
||||
TRANSFER_TIMEOUT=300
|
||||
# 演示模式(不执行真实操作)
|
||||
DRY_RUN=false
|
||||
# 跳过构建
|
||||
SKIP_BUILD=false
|
||||
# 跳过清理远端目录
|
||||
SKIP_CLEAN_REMOTE=false
|
||||
@@ -1,734 +0,0 @@
|
||||
"""部署脚本(支持 .env/.json 配置)
|
||||
实现以下规范:
|
||||
- 构建:Maven/Gradle clean 并跳过测试,生成可执行 jar
|
||||
- 严格校验:校验 BOOT-INF/classes 配置文件与 MANIFEST 元数据
|
||||
- 安全传输:SCP 非 22 端口 + 指纹校验 + MD5 验证
|
||||
- 预检查:JDK17+、磁盘空间 >= 2GB、ulimit 满足 SpringBoot3
|
||||
- 进程管理:优雅停止(SIGTERM 30s)超时强杀(SIGKILL)
|
||||
- 备份保留:/opt/app/backups 保留最近 5 个版本
|
||||
- 启动与健康:nohup 启动、120s 日志无 ERROR、actuator/health=200
|
||||
- 回滚与告警:自动回滚最近可用版本并发送告警
|
||||
- 日志:JSON Lines 记录所有步骤与环境信息
|
||||
|
||||
配置优先级:
|
||||
- 优先读取 DEPLOY_CONFIG 指定的路径(.env 或 .json)
|
||||
- 未指定时优先加载同目录下 deployment.env,其次 deployment.config.json
|
||||
"""
|
||||
import base64
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import zipfile
|
||||
import paramiko
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
def _ts_ms():
|
||||
"""返回当前时间戳(毫秒)"""
|
||||
return int(time.time() * 1000)
|
||||
|
||||
|
||||
def _log_path():
|
||||
"""返回部署日志文件路径(JSONL 格式)"""
|
||||
return os.path.join(os.path.dirname(__file__), "deployment.log.jsonl")
|
||||
|
||||
|
||||
def log_step(step, status, details=None, extra=None, env=None):
|
||||
"""记录部署步骤到 JSONL 日志
|
||||
step:步骤名称;status:ok/fail/skip 等;
|
||||
details/extra:附加信息;env:环境信息(服务器、目录等)
|
||||
"""
|
||||
rec = {
|
||||
"timestamp_ms": _ts_ms(),
|
||||
"step": step,
|
||||
"status": status,
|
||||
"details": details or "",
|
||||
"extra": extra or {},
|
||||
}
|
||||
if env:
|
||||
rec["env"] = env
|
||||
with open(_log_path(), "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
def print_progress(percent, step, status="进行中", details=None):
|
||||
try:
|
||||
msg = f"[{str(percent).rjust(3)}%] {step} - {status}"
|
||||
if details:
|
||||
msg += f" | {details}"
|
||||
print(msg, flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def run_cmd(cmd, cwd=None, env=None, capture_output=True, timeout=None):
|
||||
"""运行本地命令并返回 (返回码, 标准输出, 标准错误)"""
|
||||
try:
|
||||
p = subprocess.run(
|
||||
cmd,
|
||||
shell=True,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE if capture_output else None,
|
||||
stderr=subprocess.PIPE if capture_output else None,
|
||||
timeout=timeout,
|
||||
)
|
||||
out = p.stdout.decode("utf-8", errors="replace") if capture_output and p.stdout else ""
|
||||
err = p.stderr.decode("utf-8", errors="replace") if capture_output and p.stderr else ""
|
||||
return p.returncode, out, err
|
||||
except subprocess.TimeoutExpired as te:
|
||||
out = te.stdout.decode("utf-8", errors="replace") if capture_output and te.stdout else ""
|
||||
err = te.stderr.decode("utf-8", errors="replace") if capture_output and te.stderr else "command timeout"
|
||||
return 124, out, err
|
||||
|
||||
|
||||
def md5_file(path):
|
||||
"""计算本地文件 MD5 摘要"""
|
||||
h = hashlib.md5()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def sha256_bytes(b):
|
||||
"""计算字节的 SHA256 并返回 base64 字符串(用于指纹)"""
|
||||
return base64.b64encode(hashlib.sha256(b).digest()).decode("ascii")
|
||||
|
||||
|
||||
def find_jar(project_dir, build_tool, target_jar_name=None):
|
||||
"""定位构建产物 jar 文件(支持指定文件名或自动选择最新)"""
|
||||
if target_jar_name:
|
||||
maven_path = os.path.join(project_dir, "target", target_jar_name)
|
||||
gradle_path = os.path.join(project_dir, "build", "libs", target_jar_name)
|
||||
if os.path.exists(maven_path):
|
||||
return maven_path
|
||||
if os.path.exists(gradle_path):
|
||||
return gradle_path
|
||||
return target_jar_name
|
||||
if build_tool == "maven":
|
||||
target = os.path.join(project_dir, "target")
|
||||
else:
|
||||
target = os.path.join(project_dir, "build", "libs")
|
||||
if not os.path.isdir(target):
|
||||
return None
|
||||
jars = []
|
||||
for name in os.listdir(target):
|
||||
if name.endswith(".jar"):
|
||||
jars.append(os.path.join(target, name))
|
||||
if not jars:
|
||||
return None
|
||||
jars.sort(key=lambda p: os.path.getmtime(p), reverse=True)
|
||||
return jars[0]
|
||||
|
||||
|
||||
def verify_jar_integrity(jar_path):
|
||||
"""校验 jar 完整性(支持 Boot Jar 与 Boot War):
|
||||
- 必须存在 application.yml、application-prod.yml、logback-plus.xml(在 classes 下)
|
||||
- MANIFEST 必须包含 Start-Class / Implementation-Version / Build-Time
|
||||
"""
|
||||
z = zipfile.ZipFile(jar_path)
|
||||
names = set(z.namelist())
|
||||
prefixes = ["BOOT-INF/classes/", "WEB-INF/classes/"]
|
||||
files = ["application.yml", "application-prod.yml", "logback-plus.xml"]
|
||||
missing = []
|
||||
for f in files:
|
||||
if not any((p + f) in names for p in prefixes):
|
||||
missing.append(f)
|
||||
ok = len(missing) == 0
|
||||
manifest = None
|
||||
if "META-INF/MANIFEST.MF" in names:
|
||||
manifest = z.read("META-INF/MANIFEST.MF").decode("utf-8", errors="replace")
|
||||
else:
|
||||
ok = False
|
||||
if manifest:
|
||||
has_start_class = bool(re.search(r"^Start-Class:\s*.+", manifest, re.MULTILINE))
|
||||
has_impl_ver = bool(re.search(r"^Implementation-Version:\s*.+", manifest, re.MULTILINE))
|
||||
has_build_time = bool(re.search(r"^Build-Time:\s*.+", manifest, re.MULTILINE))
|
||||
if not (has_start_class and has_impl_ver and has_build_time):
|
||||
ok = False
|
||||
if not has_start_class:
|
||||
missing.append("Start-Class")
|
||||
if not has_impl_ver:
|
||||
missing.append("Implementation-Version")
|
||||
if not has_build_time:
|
||||
missing.append("Build-Time")
|
||||
z.close()
|
||||
return ok, missing
|
||||
|
||||
|
||||
def parse_manifest_info(jar_path):
|
||||
"""解析 MANIFEST.MF 中的关键字段信息"""
|
||||
z = zipfile.ZipFile(jar_path)
|
||||
info = {}
|
||||
if "META-INF/MANIFEST.MF" in z.namelist():
|
||||
m = z.read("META-INF/MANIFEST.MF").decode("utf-8", errors="replace")
|
||||
for k in ["Start-Class", "Implementation-Version", "Build-Time"]:
|
||||
mobj = re.search(rf"^{k}:\s*(.+)$", m, re.MULTILINE)
|
||||
if mobj:
|
||||
info[k] = mobj.group(1).strip()
|
||||
z.close()
|
||||
return info
|
||||
|
||||
|
||||
def maven_build(project_dir, maven_home):
|
||||
"""执行 Maven 构建(跳过测试)"""
|
||||
mvn = os.path.join(maven_home, "bin", "mvn.cmd") if os.name == "nt" else os.path.join(maven_home, "bin", "mvn")
|
||||
cmd = f'"{mvn}" clean package -DskipTests=true'
|
||||
return run_cmd(cmd, cwd=project_dir, capture_output=False)
|
||||
|
||||
|
||||
def gradle_build(project_dir, gradle_home=None):
|
||||
"""执行 Gradle 构建(跳过测试,优先使用 gradlew)"""
|
||||
gradlew = os.path.join(project_dir, "gradlew.cmd") if os.name == "nt" else os.path.join(project_dir, "gradlew")
|
||||
if os.path.exists(gradlew):
|
||||
cmd = f'"{gradlew}" clean build -x test'
|
||||
return run_cmd(cmd, cwd=project_dir, capture_output=False)
|
||||
if gradle_home:
|
||||
gradle_bin = os.path.join(gradle_home, "bin", "gradle")
|
||||
if os.name == "nt":
|
||||
gradle_bin += ".bat"
|
||||
cmd = f'"{gradle_bin}" clean build -x test'
|
||||
return run_cmd(cmd, cwd=project_dir, capture_output=False)
|
||||
return 1, "", "gradle not found"
|
||||
|
||||
|
||||
def connect_ssh(host, port, user, password=None, keyfile=None):
|
||||
"""建立 SSH 连接(密码或密钥两种方式)"""
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
if keyfile:
|
||||
ssh.connect(host, port=port, username=user, key_filename=keyfile)
|
||||
else:
|
||||
ssh.connect(host, port=port, username=user, password=password)
|
||||
try:
|
||||
ssh.get_transport().set_keepalive(15)
|
||||
except Exception:
|
||||
pass
|
||||
return ssh
|
||||
|
||||
|
||||
def ensure_fingerprint(host, port, user, password, keyfile, expect_fp):
|
||||
"""校验服务器指纹(要求与配置的 SHA256 指纹一致)"""
|
||||
ssh = connect_ssh(host, port, user, password, keyfile)
|
||||
k = ssh.get_transport().get_remote_server_key()
|
||||
fp = "SHA256:" + sha256_bytes(base64.b64decode(k.get_base64()))
|
||||
ssh.close()
|
||||
return fp == expect_fp, fp
|
||||
|
||||
|
||||
def scp_transfer(local_path, host, port, user, remote_path, timeout=None):
|
||||
"""使用 scp 非标准端口传输文件到远端目录"""
|
||||
cmd = (
|
||||
f'scp -o BatchMode=yes '
|
||||
f'-o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=3 '
|
||||
f'-o StrictHostKeyChecking=accept-new -P {port} "{local_path}" {user}@{host}:"{remote_path}"'
|
||||
)
|
||||
return run_cmd(cmd, timeout=timeout)
|
||||
|
||||
|
||||
def sftp_transfer(ssh, local_path, remote_path):
|
||||
sftp = ssh.open_sftp()
|
||||
try:
|
||||
sftp.put(local_path, remote_path)
|
||||
return 0, "", ""
|
||||
finally:
|
||||
try:
|
||||
sftp.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def ssh_exec(ssh, cmd, timeout=None, print_output=True):
|
||||
"""执行远端命令并返回 (返回码, 标准输出, 标准错误)"""
|
||||
if print_output:
|
||||
print(f'SSH$ {cmd}', flush=True)
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd, timeout=timeout)
|
||||
out = stdout.read().decode("utf-8", errors="replace")
|
||||
err = stderr.read().decode("utf-8", errors="replace")
|
||||
rc = stdout.channel.recv_exit_status()
|
||||
if print_output:
|
||||
if out:
|
||||
try:
|
||||
print(out if out.endswith("\n") else out + "\n", end="", flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
if err:
|
||||
try:
|
||||
print(err if err.endswith("\n") else err + "\n", end="", flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
return rc, out, err
|
||||
|
||||
|
||||
def remote_md5(ssh, path):
|
||||
"""获取远端文件摘要(优先 md5sum,失败则尝试 sha256sum)"""
|
||||
rc, out, _ = ssh_exec(ssh, f'md5sum "{path}" || sha256sum "{path}"')
|
||||
algo = "md5"
|
||||
if " " in out.strip():
|
||||
val = out.strip().split()[0]
|
||||
if rc != 0:
|
||||
return None, algo
|
||||
return val, algo
|
||||
return None, algo
|
||||
|
||||
|
||||
def pre_checks(ssh, remote_dir):
|
||||
ok = True
|
||||
details = {}
|
||||
rc, out, _ = ssh_exec(ssh, "java -version")
|
||||
version_match = re.search(r'version \"(?P<v>[\\d\\.]+)', out) or re.search(r'openjdk version \"(?P<v>[\\d\\.]+)', out)
|
||||
if version_match:
|
||||
major = int(version_match.group("v").split(".")[0])
|
||||
details["java_major"] = major
|
||||
details["java_ok"] = major >= 17
|
||||
ok = ok and details["java_ok"]
|
||||
else:
|
||||
details["java_ok"] = False
|
||||
ok = False
|
||||
rc, out, _ = ssh_exec(ssh, f'df -Pk "{remote_dir}" | tail -1')
|
||||
parts = out.split()
|
||||
if len(parts) >= 4:
|
||||
try:
|
||||
avail_k = int(parts[3])
|
||||
except:
|
||||
avail_k = 0
|
||||
details["disk_avail_k"] = avail_k
|
||||
details["disk_ok"] = avail_k >= 2 * 1024 * 1024
|
||||
ok = ok and details["disk_ok"]
|
||||
else:
|
||||
details["disk_ok"] = False
|
||||
ok = False
|
||||
rc, out, _ = ssh_exec(ssh, "ulimit -n")
|
||||
try:
|
||||
nofile = int(out.strip())
|
||||
details["ulimit_nofile"] = nofile
|
||||
details["ulimit_nofile_ok"] = nofile >= 65535
|
||||
ok = ok and details["ulimit_nofile_ok"]
|
||||
except:
|
||||
details["ulimit_nofile_ok"] = False
|
||||
ok = False
|
||||
rc, out, _ = ssh_exec(ssh, "ulimit -u")
|
||||
try:
|
||||
nproc = int(out.strip())
|
||||
details["ulimit_nproc"] = nproc
|
||||
details["ulimit_nproc_ok"] = nproc >= 4096
|
||||
ok = ok and details["ulimit_nproc_ok"]
|
||||
except:
|
||||
details["ulimit_nproc_ok"] = False
|
||||
ok = False
|
||||
return ok, details
|
||||
|
||||
|
||||
def stop_process(ssh, jar_name):
|
||||
"""优雅停止现有服务:SIGTERM 等待 30s,超时则 SIGKILL"""
|
||||
rc, out, _ = ssh_exec(ssh, f"ps -ef | grep 'java.*{re.escape(jar_name)}' | grep -v grep | awk '{{print $2}}'")
|
||||
pids = [p.strip() for p in out.splitlines() if p.strip()]
|
||||
for pid in pids:
|
||||
ssh_exec(ssh, f"kill -15 {pid}")
|
||||
deadline = time.time() + 30
|
||||
remaining = pids[:]
|
||||
while time.time() < deadline and remaining:
|
||||
rc, out, _ = ssh_exec(ssh, f"ps -o pid= -p {' '.join(remaining)}")
|
||||
alive = [p.strip() for p in out.splitlines() if p.strip()]
|
||||
remaining = alive
|
||||
time.sleep(1)
|
||||
for pid in remaining:
|
||||
ssh_exec(ssh, f"kill -9 {pid}")
|
||||
return True
|
||||
|
||||
|
||||
def backup_remote(ssh, remote_dir, jar_name, impl_ver):
|
||||
"""备份当前版本至 /opt/app/backups,并保留最近 5 个版本"""
|
||||
ssh_exec(ssh, f'mkdir -p "{remote_dir}/backups"')
|
||||
ts = datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S")
|
||||
bn = os.path.splitext(jar_name)[0]
|
||||
ver = impl_ver or "unknown"
|
||||
backup_name = f"{bn}-{ver}-{ts}.jar"
|
||||
ssh_exec(ssh, f'if [ -f "{remote_dir}/{jar_name}" ]; then cp -f "{remote_dir}/{jar_name}" "{remote_dir}/backups/{backup_name}"; fi')
|
||||
rc, out, _ = ssh_exec(ssh, f'ls -1t "{remote_dir}/backups" | grep -E "^{re.escape(bn)}-.*\\.jar$"')
|
||||
lines = [l.strip() for l in out.splitlines() if l.strip()]
|
||||
if len(lines) > 5:
|
||||
to_del = lines[5:]
|
||||
names = " ".join([f'"{remote_dir}/backups/{x}"' for x in to_del])
|
||||
ssh_exec(ssh, f"rm -f {names}")
|
||||
return backup_name
|
||||
|
||||
|
||||
def start_service(ssh, remote_dir, jar_name, jvm_opts):
|
||||
"""使用 nohup 启动服务并返回 PID 与日志路径"""
|
||||
log_path = f"{remote_dir}/log.out"
|
||||
cmd = f'nohup java {jvm_opts} -jar "{remote_dir}/{jar_name}" > "{log_path}" 2>&1 &'
|
||||
ssh_exec(ssh, cmd)
|
||||
time.sleep(2)
|
||||
rc, out, _ = ssh_exec(ssh, f"ps -ef | grep 'java.*{re.escape(jar_name)}' | grep -v grep | awk '{{print $2}}' | head -1")
|
||||
pid = out.strip() if out.strip() else ""
|
||||
return pid, log_path
|
||||
|
||||
|
||||
def monitor_logs_no_error(ssh, log_path, seconds):
|
||||
"""在指定时长内监控启动日志,若出现 ERROR 则失败"""
|
||||
end = time.time() + seconds
|
||||
while time.time() < end:
|
||||
rc, out, _ = ssh_exec(ssh, f'tail -n 200 "{log_path}" || true', print_output=False)
|
||||
if "ERROR" in out:
|
||||
return False
|
||||
time.sleep(2)
|
||||
return True
|
||||
|
||||
|
||||
def detect_crash_within(ssh, jar_name, seconds):
|
||||
"""监控指定时长,若进程曾启动后又消失,判定为崩溃"""
|
||||
end = time.time() + seconds
|
||||
alive_once = False
|
||||
while time.time() < end:
|
||||
rc, out, _ = ssh_exec(ssh, f"ps -ef | grep 'java.*{re.escape(jar_name)}' | grep -v grep | awk '{{print $2}}'")
|
||||
pids = [p.strip() for p in out.splitlines() if p.strip()]
|
||||
if pids:
|
||||
alive_once = True
|
||||
else:
|
||||
if alive_once:
|
||||
return True
|
||||
time.sleep(5)
|
||||
return False
|
||||
|
||||
|
||||
def verify_deploy_info(ssh, url, expected_version=None, expected_build_time=None):
|
||||
"""调用部署验证接口并校验版本与构建时间"""
|
||||
rc, code_str, _ = ssh_exec(ssh, f'curl -s -o /dev/null -w "%{{http_code}}" "{url}"')
|
||||
rc2, body, _ = ssh_exec(ssh, f'curl -s "{url}"')
|
||||
http_code = code_str.strip()
|
||||
ok_http = http_code == "200"
|
||||
version = None
|
||||
build_time = None
|
||||
try:
|
||||
data = json.loads(body)
|
||||
payload = data.get("data") if isinstance(data, dict) and "data" in data else data
|
||||
if isinstance(payload, dict):
|
||||
version = payload.get("version")
|
||||
build_time = payload.get("buildTime")
|
||||
except:
|
||||
pass
|
||||
match_ver = (expected_version is None) or (version == expected_version)
|
||||
match_bt = (not expected_build_time) or (build_time == expected_build_time)
|
||||
ok = ok_http and match_ver and match_bt
|
||||
return ok, {"http_code": http_code, "version": version, "buildTime": build_time}
|
||||
|
||||
|
||||
def rollback(ssh, remote_dir, jar_name, jvm_opts):
|
||||
"""自动回滚到最近可用备份并重启服务"""
|
||||
rc, out, _ = ssh_exec(ssh, f'ls -1t "{remote_dir}/backups" | grep -E "^{re.escape(os.path.splitext(jar_name)[0])}-.*\\.jar$" | head -1')
|
||||
last = out.strip()
|
||||
if not last:
|
||||
return False
|
||||
ssh_exec(ssh, f'cp -f "{remote_dir}/backups/{last}" "{remote_dir}/{jar_name}"')
|
||||
pid, log_path = start_service(ssh, remote_dir, jar_name, jvm_opts)
|
||||
return True
|
||||
|
||||
|
||||
def send_alert(webhook, payload):
|
||||
"""向运维告警地址发送 JSON 通知(可选)"""
|
||||
if not webhook:
|
||||
return False
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(webhook, data=data, headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
return True
|
||||
except urllib.error.URLError:
|
||||
return False
|
||||
|
||||
|
||||
def parse_bool(s):
|
||||
"""解析布尔字符串(支持 true/false/1/0/yes/no/on/off)"""
|
||||
if s is None:
|
||||
return False
|
||||
v = str(s).strip().lower()
|
||||
return v in ("1", "true", "yes", "y", "on")
|
||||
|
||||
|
||||
def load_env_file(path):
|
||||
"""加载 .env 配置文件:支持注释与引号包裹的值"""
|
||||
data = {}
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for raw in f:
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("#") or line.startswith(";"):
|
||||
continue
|
||||
if "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
k = k.strip()
|
||||
v = v.strip()
|
||||
if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")):
|
||||
v = v[1:-1]
|
||||
data[k] = v
|
||||
return {
|
||||
"server": data.get("SERVER"),
|
||||
"port": int(data.get("PORT")) if data.get("PORT") else None,
|
||||
"user": data.get("USER"),
|
||||
"password": data.get("PASSWORD"),
|
||||
"keyfile": data.get("KEYFILE"),
|
||||
"fingerprint": data.get("FINGERPRINT"),
|
||||
"env": data.get("ENV"),
|
||||
"build_tool": data.get("BUILD_TOOL"),
|
||||
"project_dir": data.get("PROJECT_DIR"),
|
||||
"maven_home": data.get("MAVEN_HOME"),
|
||||
"gradle_home": data.get("GRADLE_HOME"),
|
||||
"target_jar_name": data.get("TARGET_JAR_NAME"),
|
||||
"remote_dir": data.get("REMOTE_DIR") or "/opt/app",
|
||||
"jvm_opts": data.get("JVM_OPTS") or "-Xms512m -Xmx1024m",
|
||||
"transfer_method": (data.get("TRANSFER_METHOD") or "sftp").lower(),
|
||||
"transfer_timeout": int(data.get("TRANSFER_TIMEOUT")) if data.get("TRANSFER_TIMEOUT") else 300,
|
||||
"deploy_info_url": data.get("DEPLOY_INFO_URL") or "http://localhost:8080/deploy/info",
|
||||
"alert_webhook": data.get("ALERT_WEBHOOK"),
|
||||
"dry_run": parse_bool(data.get("DRY_RUN")),
|
||||
"skip_build": parse_bool(data.get("SKIP_BUILD")),
|
||||
"skip_prechecks": parse_bool(data.get("SKIP_PRECHECKS")),
|
||||
"precheck_strict": parse_bool(data.get("PRECHECK_STRICT")),
|
||||
"crash_monitor_seconds": int(data.get("CRASH_MONITOR_SECONDS")) if data.get("CRASH_MONITOR_SECONDS") else 60,
|
||||
"skip_crash_monitor": parse_bool(data.get("SKIP_CRASH_MONITOR")),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""主流程:加载配置 -> 构建 -> 校验 -> 传输 -> 预检 -> 停止 -> 备份 -> 启动 -> 健康 -> 回滚/完成"""
|
||||
default_env = os.path.join(os.path.dirname(__file__), "deployment.env")
|
||||
cfg_path = os.getenv("DEPLOY_CONFIG") or default_env
|
||||
# 仅支持 .env 配置文件,并校验必填字段
|
||||
if not os.path.isfile(cfg_path):
|
||||
log_step("config_load", "fail", {"path": cfg_path})
|
||||
print(f"配置文件未找到:{cfg_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
cfg = load_env_file(cfg_path)
|
||||
required = ["server", "port", "user", "env", "build_tool", "project_dir"]
|
||||
missing = []
|
||||
for k in required:
|
||||
v = cfg.get(k)
|
||||
if v is None or (isinstance(v, str) and v.strip() == ""):
|
||||
missing.append(k)
|
||||
if missing:
|
||||
log_step("config_validate", "fail", {"missing": missing, "path": cfg_path})
|
||||
print(f"配置错误:缺少必填项 {', '.join(missing)};配置文件:{cfg_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print_progress(0, "启动部署", "进行中", f"配置文件:{cfg_path}")
|
||||
|
||||
args = SimpleNamespace(
|
||||
server=cfg["server"],
|
||||
port=int(cfg["port"]),
|
||||
user=cfg["user"],
|
||||
password=cfg.get("password"),
|
||||
keyfile=cfg.get("keyfile"),
|
||||
fingerprint=cfg["fingerprint"],
|
||||
env=cfg["env"],
|
||||
build_tool=cfg["build_tool"],
|
||||
project_dir=cfg["project_dir"],
|
||||
maven_home=cfg.get("maven_home"),
|
||||
gradle_home=cfg.get("gradle_home"),
|
||||
target_jar_name=cfg.get("target_jar_name"),
|
||||
remote_dir=cfg.get("remote_dir", "/opt/app"),
|
||||
jvm_opts=cfg.get("jvm_opts", "-Xms512m -Xmx1024m"),
|
||||
deploy_info_url=cfg.get("deploy_info_url", "http://localhost:8080/deploy/info"),
|
||||
alert_webhook=cfg.get("alert_webhook"),
|
||||
dry_run=bool(cfg.get("dry_run", False)),
|
||||
skip_build=bool(cfg.get("skip_build", False)),
|
||||
transfer_method=cfg.get("transfer_method", "sftp"),
|
||||
transfer_timeout=int(cfg.get("transfer_timeout", 300)),
|
||||
skip_prechecks=bool(cfg.get("skip_prechecks", False)),
|
||||
precheck_strict=bool(cfg.get("precheck_strict", False)),
|
||||
crash_monitor_seconds=int(cfg.get("crash_monitor_seconds", 60)),
|
||||
skip_crash_monitor=bool(cfg.get("skip_crash_monitor", False)),
|
||||
)
|
||||
|
||||
env_info = {
|
||||
"server": args.server,
|
||||
"port": args.port,
|
||||
"env": args.env,
|
||||
"build_tool": args.build_tool,
|
||||
"project_dir": args.project_dir,
|
||||
"remote_dir": args.remote_dir,
|
||||
}
|
||||
|
||||
try:
|
||||
if args.dry_run:
|
||||
log_step("start", "dry_run", {"config_path": cfg_path}, env=env_info)
|
||||
print_progress(5, "加载配置", "完成")
|
||||
else:
|
||||
log_step("start", "running", {"config_path": cfg_path}, env=env_info)
|
||||
print_progress(5, "加载配置", "完成")
|
||||
|
||||
if not args.skip_build:
|
||||
print_progress(10, "开始构建", "进行中", args.build_tool)
|
||||
if args.build_tool == "maven":
|
||||
rc, out, err = maven_build(args.project_dir, args.maven_home or "")
|
||||
else:
|
||||
rc, out, err = gradle_build(args.project_dir, args.gradle_home)
|
||||
log_step("build", "ok" if rc == 0 else "fail", {"stdout": out[-1000:], "stderr": err[-1000:]}, env=env_info)
|
||||
print_progress(20, "构建完成", "成功" if rc == 0 else "失败")
|
||||
if rc != 0 and not args.dry_run:
|
||||
sys.exit(1)
|
||||
else:
|
||||
print_progress(10, "跳过构建", "已跳过")
|
||||
|
||||
jar_path = find_jar(args.project_dir, args.build_tool, args.target_jar_name)
|
||||
if not jar_path or not os.path.isfile(jar_path):
|
||||
log_step("find_jar", "fail", {"path": jar_path}, env=env_info)
|
||||
print_progress(25, "查找构建产物", "失败")
|
||||
if not args.dry_run:
|
||||
sys.exit(1)
|
||||
else:
|
||||
log_step("find_jar", "ok", {"path": jar_path}, env=env_info)
|
||||
print_progress(25, "查找构建产物", "成功", os.path.basename(jar_path))
|
||||
|
||||
if jar_path and os.path.isfile(jar_path):
|
||||
ok, missing = verify_jar_integrity(jar_path)
|
||||
manifest_info = parse_manifest_info(jar_path)
|
||||
log_step("jar_integrity", "ok" if ok else "fail", {"manifest": manifest_info, "missing": missing}, env=env_info)
|
||||
print_progress(35, "校验产物完整性", "成功" if ok else f"失败(缺失: {', '.join(missing)})")
|
||||
if not ok and not args.dry_run:
|
||||
sys.exit(1)
|
||||
|
||||
if args.dry_run:
|
||||
log_step("fingerprint_check", "skip", env=env_info)
|
||||
log_step("transfer", "skip", env=env_info)
|
||||
log_step("pre_checks", "skip", env=env_info)
|
||||
log_step("stop_process", "skip", env=env_info)
|
||||
log_step("backup", "skip", env=env_info)
|
||||
log_step("start_service", "skip", env=env_info)
|
||||
log_step("monitor_crash", "skip", env=env_info)
|
||||
print_progress(100, "完成", "DRY-RUN")
|
||||
log_step("done", "ok", env=env_info)
|
||||
return
|
||||
|
||||
if args.fingerprint:
|
||||
print_progress(40, "校验服务器指纹", "进行中")
|
||||
ok_fp, actual_fp = ensure_fingerprint(args.server, args.port, args.user, args.password, args.keyfile, args.fingerprint)
|
||||
log_step("fingerprint_check", "ok" if ok_fp else "fail", {"actual": actual_fp}, env=env_info)
|
||||
print_progress(45, "校验服务器指纹", "成功" if ok_fp else "失败")
|
||||
if not ok_fp:
|
||||
print(f"服务器指纹不匹配:期望 {args.fingerprint},实际 {actual_fp}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
log_step("fingerprint_check", "skip", {"reason": "no_fingerprint_provided"}, env=env_info)
|
||||
print_progress(40, "校验服务器指纹", "已跳过")
|
||||
|
||||
print_progress(50, "连接服务器", "进行中")
|
||||
ssh = connect_ssh(args.server, args.port, args.user, args.password, args.keyfile)
|
||||
ssh.exec_command(f'mkdir -p "{args.remote_dir}"')
|
||||
print_progress(55, "连接服务器", "成功")
|
||||
|
||||
local_md5 = md5_file(jar_path)
|
||||
print_progress(60, "传输文件", "进行中", os.path.basename(jar_path))
|
||||
if args.transfer_method == "scp":
|
||||
rc, out, err = scp_transfer(
|
||||
jar_path,
|
||||
args.server,
|
||||
args.port,
|
||||
args.user,
|
||||
f'{args.remote_dir}/{os.path.basename(jar_path)}',
|
||||
timeout=args.transfer_timeout,
|
||||
)
|
||||
else:
|
||||
rc, out, err = sftp_transfer(
|
||||
ssh,
|
||||
jar_path,
|
||||
f'{args.remote_dir}/{os.path.basename(jar_path)}',
|
||||
)
|
||||
log_step("transfer", "ok" if rc == 0 else "fail", {"stdout": out[-1000:], "stderr": err[-1000:]}, env=env_info)
|
||||
print_progress(65, "传输文件", "成功" if rc == 0 else "失败")
|
||||
if rc != 0:
|
||||
ssh.close()
|
||||
sys.exit(1)
|
||||
|
||||
remote_hash, algo = remote_md5(ssh, f'{args.remote_dir}/{os.path.basename(jar_path)}')
|
||||
match = remote_hash == local_md5 if algo == "md5" and remote_hash else True
|
||||
log_step("md5_verify", "ok" if match else "fail", {"local_md5": local_md5, "remote_hash": remote_hash, "algo": algo}, env=env_info)
|
||||
print_progress(70, "校验文件摘要", "成功" if match else "失败")
|
||||
if not match:
|
||||
ssh.close()
|
||||
sys.exit(1)
|
||||
|
||||
if args.skip_prechecks:
|
||||
log_step("pre_checks", "skip", env=env_info)
|
||||
print_progress(75, "预部署检查", "已跳过")
|
||||
else:
|
||||
checks_ok, check_details = pre_checks(ssh, args.remote_dir)
|
||||
status = "ok" if checks_ok else ("fail" if args.precheck_strict else "warn")
|
||||
log_step("pre_checks", status, check_details, env=env_info)
|
||||
print_progress(75, "预部署检查", "成功" if checks_ok else ("警告" if not args.precheck_strict else "失败"))
|
||||
if not checks_ok and args.precheck_strict:
|
||||
ssh.close()
|
||||
sys.exit(1)
|
||||
|
||||
jar_name = os.path.basename(jar_path)
|
||||
print_progress(78, "停止现有服务", "进行中")
|
||||
stop_process(ssh, jar_name)
|
||||
log_step("stop_process", "ok", env=env_info)
|
||||
print_progress(80, "停止现有服务", "完成")
|
||||
|
||||
backup_name = backup_remote(ssh, args.remote_dir, jar_name, parse_manifest_info(jar_path).get("Implementation-Version"))
|
||||
log_step("backup", "ok", {"backup": backup_name}, env=env_info)
|
||||
print_progress(82, "备份旧版本", "完成", backup_name)
|
||||
|
||||
pid, log_path = start_service(ssh, args.remote_dir, jar_name, args.jvm_opts)
|
||||
log_step("start_service", "ok" if pid else "fail", {"pid": pid, "log": log_path}, env=env_info)
|
||||
print_progress(85, "启动服务", "成功" if pid else "失败", f"pid={pid}")
|
||||
if not pid:
|
||||
ssh.close()
|
||||
sys.exit(1)
|
||||
|
||||
log_step("startup_logs", "skip", env=env_info)
|
||||
print_progress(90, "监控启动日志", "已跳过")
|
||||
|
||||
last_metrics = {}
|
||||
|
||||
if args.skip_crash_monitor:
|
||||
log_step("monitor_crash", "skip", env=env_info)
|
||||
print_progress(97, f"崩溃监控({args.crash_monitor_seconds}秒)", "已跳过")
|
||||
else:
|
||||
crashed = detect_crash_within(ssh, jar_name, args.crash_monitor_seconds)
|
||||
log_step("monitor_crash", "fail" if crashed else "ok", env=env_info)
|
||||
print_progress(97, f"崩溃监控({args.crash_monitor_seconds}秒)", "失败" if crashed else "通过")
|
||||
|
||||
ver_expected = parse_manifest_info(jar_path).get("Implementation-Version")
|
||||
bt_expected = parse_manifest_info(jar_path).get("Build-Time")
|
||||
print_progress(98, "验证部署接口", "进行中")
|
||||
ok_deploy, deploy_info = verify_deploy_info(ssh, args.deploy_info_url, ver_expected, bt_expected)
|
||||
log_step("deploy_info_check", "ok" if ok_deploy else "fail", {"expect_version": ver_expected, "expect_build_time": bt_expected, "response": deploy_info}, env=env_info)
|
||||
print_progress(98, "验证部署接口", "成功" if ok_deploy else "失败")
|
||||
|
||||
if crashed or not ok_deploy:
|
||||
print_progress(98, "触发回滚", "进行中")
|
||||
rb_ok = rollback(ssh, args.remote_dir, jar_name, args.jvm_opts)
|
||||
log_step("rollback", "ok" if rb_ok else "fail", env=env_info)
|
||||
print_progress(99, "回滚完成", "成功" if rb_ok else "失败")
|
||||
send_alert(args.alert_webhook, {"event": "rollback", "server": args.server, "env": args.env, "reason": "deploy_info_fail_or_crash"})
|
||||
ssh.close()
|
||||
if not rb_ok:
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
log_step("done", "ok", {"metrics": last_metrics}, env=env_info)
|
||||
print_progress(100, "部署完成", "成功")
|
||||
ssh.close()
|
||||
except Exception as e:
|
||||
import traceback
|
||||
log_step("exception", "fail", {"error": str(e), "trace": traceback.format_exc()}, env=env_info)
|
||||
print_progress(100, "部署失败", "异常", str(e))
|
||||
print(traceback.format_exc(), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,193 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
|
||||
from deployment import (
|
||||
log_step,
|
||||
print_progress,
|
||||
run_cmd,
|
||||
connect_ssh,
|
||||
ssh_exec,
|
||||
parse_bool,
|
||||
)
|
||||
|
||||
|
||||
def load_frontend_env(path):
|
||||
data = {}
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for raw in f:
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("#") or line.startswith(";"):
|
||||
continue
|
||||
if "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
k = k.strip()
|
||||
v = v.strip()
|
||||
if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")):
|
||||
v = v[1:-1]
|
||||
data[k] = v
|
||||
return {
|
||||
"server": data.get("SERVER"),
|
||||
"port": int(data.get("PORT")) if data.get("PORT") else None,
|
||||
"user": data.get("USER"),
|
||||
"password": data.get("PASSWORD"),
|
||||
"keyfile": data.get("KEYFILE"),
|
||||
"fingerprint": data.get("FINGERPRINT"),
|
||||
"env": data.get("ENV"),
|
||||
"project_dir": data.get("PROJECT_DIR"),
|
||||
"build_cmd": data.get("BUILD_CMD") or "npm run build:prod",
|
||||
"dist_dir": data.get("DIST_DIR"),
|
||||
"remote_dir": data.get("REMOTE_DIR") or "/opt/app/frontend",
|
||||
"transfer_timeout": int(data.get("TRANSFER_TIMEOUT")) if data.get("TRANSFER_TIMEOUT") else 300,
|
||||
"skip_build": parse_bool(data.get("SKIP_BUILD")),
|
||||
"dry_run": parse_bool(data.get("DRY_RUN")),
|
||||
"skip_clean_remote": parse_bool(data.get("SKIP_CLEAN_REMOTE")),
|
||||
}
|
||||
|
||||
|
||||
def sftp_upload_dir(ssh, local_dir, remote_dir):
|
||||
sftp = ssh.open_sftp()
|
||||
try:
|
||||
try:
|
||||
sftp.stat(remote_dir)
|
||||
except IOError:
|
||||
sftp.mkdir(remote_dir)
|
||||
for root, dirs, files in os.walk(local_dir):
|
||||
rel = os.path.relpath(root, local_dir)
|
||||
if rel == ".":
|
||||
remote_root = remote_dir
|
||||
else:
|
||||
remote_root = remote_dir.rstrip("/") + "/" + rel.replace(os.sep, "/")
|
||||
try:
|
||||
sftp.stat(remote_root)
|
||||
except IOError:
|
||||
sftp.mkdir(remote_root)
|
||||
for name in files:
|
||||
lp = os.path.join(root, name)
|
||||
rp = remote_root.rstrip("/") + "/" + name
|
||||
sftp.put(lp, rp)
|
||||
finally:
|
||||
try:
|
||||
sftp.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
default_env = os.path.join(os.path.dirname(__file__), "deployment.frontend.env")
|
||||
cfg_path = os.getenv("DEPLOY_FRONTEND_CONFIG") or default_env
|
||||
if not os.path.isfile(cfg_path):
|
||||
log_step("fe_config_load", "fail", {"path": cfg_path})
|
||||
print(f"前端配置文件未找到:{cfg_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
cfg = load_frontend_env(cfg_path)
|
||||
required = ["server", "port", "user", "project_dir", "remote_dir"]
|
||||
missing = []
|
||||
for k in required:
|
||||
v = cfg.get(k)
|
||||
if v is None or (isinstance(v, str) and v.strip() == ""):
|
||||
missing.append(k)
|
||||
if missing:
|
||||
log_step("fe_config_validate", "fail", {"missing": missing, "path": cfg_path})
|
||||
print(f"前端配置错误:缺少必填项 {', '.join(missing)};配置文件:{cfg_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
args = SimpleNamespace(
|
||||
server=cfg["server"],
|
||||
port=int(cfg["port"]),
|
||||
user=cfg["user"],
|
||||
password=cfg.get("password"),
|
||||
keyfile=cfg.get("keyfile"),
|
||||
fingerprint=cfg.get("fingerprint"),
|
||||
env=cfg.get("env") or "prod",
|
||||
project_dir=cfg["project_dir"],
|
||||
build_cmd=cfg["build_cmd"],
|
||||
dist_dir=cfg.get("dist_dir"),
|
||||
remote_dir=cfg["remote_dir"],
|
||||
transfer_timeout=int(cfg.get("transfer_timeout", 300)),
|
||||
skip_build=bool(cfg.get("skip_build", False)),
|
||||
dry_run=bool(cfg.get("dry_run", False)),
|
||||
skip_clean_remote=bool(cfg.get("skip_clean_remote", False)),
|
||||
)
|
||||
|
||||
if not args.dist_dir:
|
||||
args.dist_dir = os.path.join(args.project_dir, "dist")
|
||||
elif not os.path.isabs(args.dist_dir):
|
||||
args.dist_dir = os.path.join(args.project_dir, args.dist_dir)
|
||||
|
||||
env_info = {
|
||||
"server": args.server,
|
||||
"port": args.port,
|
||||
"env": args.env,
|
||||
"project_dir": args.project_dir,
|
||||
"dist_dir": args.dist_dir,
|
||||
"remote_dir": args.remote_dir,
|
||||
}
|
||||
|
||||
log_step("fe_start", "running", {"config_path": cfg_path}, env=env_info)
|
||||
print_progress(0, "前端部署启动", "进行中", f"配置文件:{cfg_path}")
|
||||
|
||||
try:
|
||||
if not args.skip_build:
|
||||
print_progress(10, "前端构建", "进行中", args.build_cmd)
|
||||
rc, out, err = run_cmd(args.build_cmd, cwd=args.project_dir, capture_output=False)
|
||||
log_step("fe_build", "ok" if rc == 0 else "fail", {}, env=env_info)
|
||||
print_progress(20, "前端构建完成", "成功" if rc == 0 else "失败")
|
||||
if rc != 0 and not args.dry_run:
|
||||
sys.exit(1)
|
||||
else:
|
||||
print_progress(10, "跳过前端构建", "已跳过")
|
||||
|
||||
if not os.path.isdir(args.dist_dir):
|
||||
log_step("fe_dist_check", "fail", {"path": args.dist_dir}, env=env_info)
|
||||
print_progress(25, "检查前端构建目录", "失败")
|
||||
if not args.dry_run:
|
||||
sys.exit(1)
|
||||
else:
|
||||
log_step("fe_dist_check", "ok", {"path": args.dist_dir}, env=env_info)
|
||||
print_progress(25, "检查前端构建目录", "成功")
|
||||
|
||||
if args.dry_run:
|
||||
log_step("fe_connect", "skip", env=env_info)
|
||||
log_step("fe_upload", "skip", env=env_info)
|
||||
print_progress(100, "前端部署完成", "DRY-RUN")
|
||||
log_step("fe_done", "ok", env=env_info)
|
||||
return
|
||||
|
||||
print_progress(40, "连接前端服务器", "进行中")
|
||||
ssh = connect_ssh(args.server, args.port, args.user, args.password, args.keyfile)
|
||||
print_progress(50, "连接前端服务器", "成功")
|
||||
|
||||
ssh_exec(ssh, f'mkdir -p "{args.remote_dir}"')
|
||||
|
||||
if not args.skip_clean_remote:
|
||||
print_progress(60, "清理远端目录", "进行中")
|
||||
ssh_exec(ssh, f'find "{args.remote_dir}" -mindepth 1 -maxdepth 1 -exec rm -rf {{}} + || true')
|
||||
print_progress(65, "清理远端目录", "完成")
|
||||
|
||||
print_progress(70, "上传前端静态资源", "进行中")
|
||||
sftp_upload_dir(ssh, args.dist_dir, args.remote_dir)
|
||||
log_step("fe_upload", "ok", {"dist": args.dist_dir}, env=env_info)
|
||||
print_progress(90, "上传前端静态资源", "完成")
|
||||
|
||||
log_step("fe_done", "ok", env=env_info)
|
||||
print_progress(100, "前端部署完成", "成功")
|
||||
ssh.close()
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
log_step("fe_exception", "fail", {"error": str(e), "trace": traceback.format_exc()}, env=env_info)
|
||||
print_progress(100, "前端部署失败", "异常", str(e))
|
||||
print(traceback.format_exc(), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"basics": [
|
||||
"system:oss:upload",
|
||||
"system:oss:query"
|
||||
],
|
||||
"dependencies": {
|
||||
"securityManagement:vehicleThreeInspect:list": ["driverManagement:driver:list", "resourceManagement:vehicle:list", "config:vehicleThreeInspectConfig:list", "config:signAuditPermissionConfig:list", "resourceManagement:companySafetyManager:list"],
|
||||
"securityManagement:hiddenDangerPlan:list": ["config:hiddenDangerCheckConfig:list"],
|
||||
"securityManagement:accidentArchive:list": ["config:accidentLevelConfig:list", "config:accidentSeverityLevelConfig:list", "config:accidentLikelihoodLevelConfig:list"],
|
||||
"noticeManagerment:noticeRecord:list": ["system:user:list"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
-- 动态生成清理脚本的SQL
|
||||
-- 运行此查询,它会返回一系列 TRUNCATE 语句
|
||||
-- 逻辑:
|
||||
-- 1. 包含所有 hot_ 开头的表
|
||||
-- 2. 包含 sys_flow_ 开头的表
|
||||
-- 3. 排除 _config 结尾的配置表
|
||||
-- 4. 排除 _template 结尾的模板表
|
||||
-- 5. 排除特定的系统/资源保留表
|
||||
|
||||
SELECT CONCAT('TRUNCATE TABLE ', TABLE_NAME, ';') AS cleanup_script
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = (SELECT DATABASE())
|
||||
AND (
|
||||
-- 规则1:所有 hot_ 开头的表
|
||||
(TABLE_NAME LIKE 'hot_%'
|
||||
-- 排除配置表
|
||||
AND TABLE_NAME NOT LIKE '%_config'
|
||||
-- 排除模板表
|
||||
AND TABLE_NAME NOT LIKE '%_template'
|
||||
-- 排除特定保留表
|
||||
AND TABLE_NAME NOT IN (
|
||||
'hot_course_resource',
|
||||
'hot_media_resource',
|
||||
'hot_question_bank',
|
||||
'hot_exam_paper',
|
||||
'hot_system_agreement',
|
||||
'hot_law_regulation',
|
||||
'hot_vehicle_type',
|
||||
'hot_vehicle_brand_model',
|
||||
'hot_company_basic_config',
|
||||
'hot_company_dept_config',
|
||||
'hot_driver_annual_assessment_config',
|
||||
'hot_sign_audit_permission_config',
|
||||
'hot_vehicle_three_inspect_config',
|
||||
'hot_hidden_danger_inspect_store',
|
||||
'hot_hidden_danger_check_config',
|
||||
'hot_company_basic_config',
|
||||
'hot_vehicle_inspection_config',
|
||||
'hot_vehicle_secondary_maintenance_config',
|
||||
'hot_accident_likelihood_level_config',
|
||||
'hot_accident_severity_level_config',
|
||||
'hot_accident_level_config',
|
||||
'hot_risk_level_config',
|
||||
'hot_risk_score_level',
|
||||
'hot_vehicle_brand_model',
|
||||
'hot_system_template',
|
||||
'hot_file_type_config'
|
||||
)
|
||||
)
|
||||
OR
|
||||
-- 规则2:流程相关表
|
||||
TABLE_NAME IN ('sys_flow_instance', 'sys_flow_task', 'sys_flow_task_his')
|
||||
)
|
||||
ORDER BY TABLE_NAME;
|
||||
@@ -1,22 +0,0 @@
|
||||
-- 使用动态生成清理脚本的sql
|
||||
|
||||
|
||||
delete from hot_system_template where company_id != 1;
|
||||
delete from hot_vehicle_three_inspect_config where company_id != 1;
|
||||
delete from hot_company_dept_config where company_id != 1;
|
||||
delete from hot_driver_annual_assessment_config where company_id != 1;
|
||||
delete from hot_hidden_danger_check_config where company_id != 1;
|
||||
|
||||
|
||||
delete from sys_user where phonenumber not in ('15888888888', '15588888888');
|
||||
delete from sys_user_role where user_id not in(2011415737717006338, 1);
|
||||
delete from sys_user_login_port where user_id not in(2011415737717006338, 1);
|
||||
|
||||
delete
|
||||
from sys_company_role;
|
||||
delete
|
||||
from sys_company;
|
||||
delete
|
||||
from hot_gov_enterprise_unit;
|
||||
delete
|
||||
from hot_personnel_config;
|
||||
@@ -1,83 +0,0 @@
|
||||
DELIMITER
|
||||
$$
|
||||
|
||||
DROP PROCEDURE IF EXISTS truncate_hot_tables $$
|
||||
CREATE PROCEDURE truncate_hot_tables()
|
||||
BEGIN
|
||||
DECLARE
|
||||
done INT DEFAULT 0;
|
||||
DECLARE
|
||||
v_table_name VARCHAR(128);
|
||||
|
||||
DECLARE
|
||||
cur CURSOR FOR
|
||||
SELECT TABLE_NAME
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND (
|
||||
(
|
||||
LEFT(TABLE_NAME, 4) = 'hot_'
|
||||
AND RIGHT (TABLE_NAME, 7) <> '_config'
|
||||
AND RIGHT (TABLE_NAME, 9) <> '_template'
|
||||
AND TABLE_NAME NOT IN (
|
||||
'hot_course_resource',
|
||||
'hot_media_resource',
|
||||
'hot_question_bank',
|
||||
'hot_exam_paper',
|
||||
'hot_system_agreement',
|
||||
'hot_law_regulation',
|
||||
'hot_vehicle_type',
|
||||
'hot_vehicle_brand_model',
|
||||
'hot_company_basic_config',
|
||||
'hot_company_dept_config',
|
||||
'hot_driver_annual_assessment_config',
|
||||
'hot_sign_audit_permission_config',
|
||||
'hot_vehicle_three_inspect_config',
|
||||
'hot_hidden_danger_inspect_store',
|
||||
'hot_hidden_danger_check_config',
|
||||
'hot_vehicle_inspection_config',
|
||||
'hot_vehicle_secondary_maintenance_config',
|
||||
'hot_accident_likelihood_level_config',
|
||||
'hot_accident_severity_level_config',
|
||||
'hot_accident_level_config',
|
||||
'hot_risk_level_config',
|
||||
'hot_risk_score_level',
|
||||
'hot_system_template',
|
||||
'hot_file_type_config'
|
||||
)
|
||||
)
|
||||
OR TABLE_NAME IN ('sys_flow_instance', 'sys_flow_task', 'sys_flow_task_his')
|
||||
)
|
||||
ORDER BY TABLE_NAME;
|
||||
|
||||
DECLARE
|
||||
CONTINUE HANDLER FOR NOT FOUND SET done = 1;
|
||||
|
||||
SET
|
||||
FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
OPEN cur;
|
||||
read_loop
|
||||
: LOOP
|
||||
FETCH cur INTO v_table_name;
|
||||
IF
|
||||
done = 1 THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
|
||||
SET
|
||||
@sql = CONCAT('TRUNCATE TABLE `', v_table_name, '`');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END LOOP;
|
||||
CLOSE cur;
|
||||
|
||||
SET
|
||||
FOREIGN_KEY_CHECKS = 1;
|
||||
END $$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
CALL truncate_hot_tables();
|
||||
DROP PROCEDURE truncate_hot_tables;
|
||||
Reference in New Issue
Block a user