新增生产一键部署脚本

This commit is contained in:
wkc
2026-04-01 10:32:57 +08:00
parent db5735897d
commit 3ce3c438a9
4 changed files with 611 additions and 0 deletions

352
bin/prod/deploy_from_package.sh Executable file
View File

@@ -0,0 +1,352 @@
#!/bin/sh
set -eu
JAVA_BIN="/home/webapp/env/java/bin/java"
BACKEND_PORT=63310
SPRING_PROFILE="pro"
JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
BACKEND_DIR="$SCRIPT_DIR/backend"
FRONTEND_DIR="$SCRIPT_DIR/frontend"
BACKEND_JAR_TARGET="$BACKEND_DIR/ruoyi-admin.jar"
BACKEND_PID_FILE="$BACKEND_DIR/backend.pid"
BACKEND_LOG_FILE="$BACKEND_DIR/backend-console.log"
FRONTEND_DIST_ARCHIVE="$FRONTEND_DIR/dist.zip"
FRONTEND_DIST_DIR="$FRONTEND_DIR/dist"
BACKEND_MARKER="-Dloan.pricing.home=$SCRIPT_DIR"
usage() {
cat <<'EOF'
用法:
./deploy_from_package.sh
EOF
}
timestamp() {
date "+%Y%m%d%H%M%S"
}
log_info() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1"
}
log_error() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >&2
}
cleanup() {
if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then
rm -rf "$WORK_DIR"
fi
}
require_dir() {
if [ ! -d "$1" ]; then
log_error "缺少目录: $1"
exit 1
fi
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
log_error "缺少命令: $1"
exit 1
fi
}
require_port_command() {
if command -v ss >/dev/null 2>&1; then
return 0
fi
if command -v lsof >/dev/null 2>&1; then
return 0
fi
log_error "缺少端口检测命令: ss 或 lsof"
exit 1
}
find_release_archive() {
archives=$(find "$SCRIPT_DIR" -maxdepth 1 -type f -name '*.zip' ! -name 'dist.zip')
count=$(printf '%s\n' "$archives" | sed '/^$/d' | wc -l | tr -d ' ')
if [ "$count" -ne 1 ]; then
log_error "脚本同目录发布 zip 数量不正确,期望 1 个,实际 $count"
exit 1
fi
printf '%s\n' "$archives"
}
extract_release_package() {
release_archive="$1"
release_extract_dir="$2"
mkdir -p "$release_extract_dir"
unzip -oq "$release_archive" -d "$release_extract_dir"
}
assert_single_jar() {
search_dir="$1"
count=$(find "$search_dir" -type f -name '*.jar' | wc -l | tr -d ' ')
if [ "$count" -ne 1 ]; then
log_error "后端 jar 数量不正确,期望 1 个,实际 $count"
exit 1
fi
find "$search_dir" -type f -name '*.jar' | head -n 1
}
assert_single_dist_zip() {
search_dir="$1"
count=$(find "$search_dir" -type f -name 'dist.zip' | wc -l | tr -d ' ')
if [ "$count" -ne 1 ]; then
log_error "前端 dist.zip 数量不正确,期望 1 个,实际 $count"
exit 1
fi
find "$search_dir" -type f -name 'dist.zip' | head -n 1
}
backup_backend_jar() {
if [ -f "$BACKEND_JAR_TARGET" ]; then
mv "$BACKEND_JAR_TARGET" "$BACKEND_JAR_TARGET.$(timestamp).bak"
fi
}
backup_frontend_dist() {
if [ -d "$FRONTEND_DIST_DIR" ]; then
mv "$FRONTEND_DIST_DIR" "$FRONTEND_DIR/dist-$(timestamp)"
fi
}
deploy_backend_jar() {
source_jar="$1"
mv "$source_jar" "$BACKEND_JAR_TARGET"
}
deploy_frontend_archive() {
source_dist_zip="$1"
rm -f "$FRONTEND_DIST_ARCHIVE"
mv "$source_dist_zip" "$FRONTEND_DIST_ARCHIVE"
}
resolve_frontend_source_dir() {
unzip_dir="$1"
if [ -f "$unzip_dir/index.html" ]; then
printf '%s\n' "$unzip_dir"
return 0
fi
if [ -f "$unzip_dir/dist/index.html" ]; then
printf '%s\n' "$unzip_dir/dist"
return 0
fi
candidate=$(find "$unzip_dir" -type f -name 'index.html' | head -n 1)
if [ -z "${candidate:-}" ]; then
log_error "dist.zip 解压后未找到 index.html"
exit 1
fi
dirname "$candidate"
}
deploy_frontend_dist() {
dist_unpack_dir="$WORK_DIR/frontend"
mkdir -p "$dist_unpack_dir"
unzip -oq "$FRONTEND_DIST_ARCHIVE" -d "$dist_unpack_dir"
rm -rf "$FRONTEND_DIST_DIR"
mkdir -p "$FRONTEND_DIST_DIR"
cp -a "$(resolve_frontend_source_dir "$dist_unpack_dir")"/. "$FRONTEND_DIST_DIR"/
}
is_port_listening() {
port="$1"
if command -v ss >/dev/null 2>&1; then
ss -lnt 2>/dev/null | grep -q ":$port "
return $?
fi
lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1
}
is_managed_backend_pid() {
pid="$1"
if [ -z "${pid:-}" ] || ! kill -0 "$pid" 2>/dev/null; then
return 1
fi
args=$(ps -o args= -p "$pid" 2>/dev/null || true)
if [ -z "${args:-}" ]; then
return 1
fi
case "$args" in
*"$BACKEND_MARKER"*"$BACKEND_JAR_TARGET"*|*"$BACKEND_JAR_TARGET"*"$BACKEND_MARKER"*)
return 0
;;
esac
return 1
}
collect_backend_pids() {
pids=""
if [ -f "$BACKEND_PID_FILE" ]; then
file_pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || true)
if [ -n "${file_pid:-}" ] && is_managed_backend_pid "$file_pid"; then
pids="$pids $file_pid"
fi
fi
marker_pids=$(pgrep -f "$BACKEND_MARKER" 2>/dev/null || true)
if [ -n "${marker_pids:-}" ]; then
for pid in $marker_pids; do
if is_managed_backend_pid "$pid"; then
pids="$pids $pid"
fi
done
fi
printf '%s\n' "$(echo "$pids" | xargs 2>/dev/null || true)"
}
stop_backend() {
pids=$(collect_backend_pids)
if [ -z "${pids:-}" ]; then
rm -f "$BACKEND_PID_FILE"
log_info "未发现运行中的后端进程"
return 0
fi
log_info "停止后端进程: $pids"
for pid in $pids; do
kill -TERM "$pid" 2>/dev/null || true
done
elapsed=0
remaining="$pids"
while [ "$elapsed" -lt 30 ]; do
remaining=""
for pid in $pids; do
if kill -0 "$pid" 2>/dev/null; then
remaining="$remaining $pid"
fi
done
remaining=$(echo "$remaining" | xargs 2>/dev/null || true)
if [ -z "${remaining:-}" ]; then
break
fi
sleep 1
elapsed=$((elapsed + 1))
done
if [ -n "${remaining:-}" ]; then
log_info "执行强制停止: $remaining"
for pid in $remaining; do
kill -KILL "$pid" 2>/dev/null || true
done
fi
rm -f "$BACKEND_PID_FILE"
}
start_backend() {
if [ ! -x "$JAVA_BIN" ]; then
log_error "未检测到可执行 Java: $JAVA_BIN"
exit 1
fi
if [ ! -f "$BACKEND_JAR_TARGET" ]; then
log_error "未找到后端 jar: $BACKEND_JAR_TARGET"
exit 1
fi
if [ -n "$(collect_backend_pids)" ]; then
log_error "检测到后端已在运行,请先停止旧进程"
exit 1
fi
printf '\n===== %s deploy =====\n' "$(date '+%Y-%m-%d %H:%M:%S')" >> "$BACKEND_LOG_FILE"
nohup "$JAVA_BIN" $JAVA_OPTS "$BACKEND_MARKER" -jar "$BACKEND_JAR_TARGET" \
--spring.profiles.active="$SPRING_PROFILE" \
--server.port="$BACKEND_PORT" >> "$BACKEND_LOG_FILE" 2>&1 &
backend_pid=$!
printf '%s\n' "$backend_pid" > "$BACKEND_PID_FILE"
sleep 1
if ! kill -0 "$backend_pid" 2>/dev/null; then
log_error "后端启动失败,请检查日志: $BACKEND_LOG_FILE"
exit 1
fi
wait_seconds=0
while [ "$wait_seconds" -lt 30 ]; do
if is_port_listening "$BACKEND_PORT"; then
log_info "后端已监听端口: $BACKEND_PORT"
return 0
fi
sleep 1
wait_seconds=$((wait_seconds + 1))
done
log_error "后端未在预期时间内监听端口 $BACKEND_PORT"
exit 1
}
main() {
if [ "$#" -ne 0 ]; then
usage
exit 1
fi
require_dir "$BACKEND_DIR"
require_dir "$FRONTEND_DIR"
require_command unzip
require_command find
require_command pgrep
require_command ps
require_command nohup
require_port_command
release_archive=$(find_release_archive)
WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/deploy_from_package.XXXXXX")
trap cleanup EXIT INT TERM
extract_release_package "$release_archive" "$WORK_DIR/package"
backend_jar_source=$(assert_single_jar "$WORK_DIR/package")
frontend_dist_source=$(assert_single_dist_zip "$WORK_DIR/package")
backup_backend_jar
backup_frontend_dist
stop_backend
deploy_backend_jar "$backend_jar_source"
deploy_frontend_archive "$frontend_dist_source"
deploy_frontend_dist
start_backend
log_info "部署完成"
log_info "后端 jar: $BACKEND_JAR_TARGET"
log_info "前端目录: $FRONTEND_DIST_DIR"
}
main "$@"

View File

@@ -0,0 +1,192 @@
#!/bin/sh
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
SCRIPT_UNDER_TEST="$ROOT_DIR/bin/prod/deploy_from_package.sh"
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_file_exists() {
file_path="$1"
[ -e "$file_path" ] || fail "expected file to exist: $file_path"
}
assert_grep() {
pattern="$1"
target="$2"
if ! grep -Eq "$pattern" "$target"; then
fail "expected pattern [$pattern] in $target"
fi
}
create_fake_java() {
fake_java="$1"
cat > "$fake_java" <<'EOF'
#!/bin/sh
set -eu
port=""
for arg in "$@"; do
case "$arg" in
--server.port=*)
port=${arg#--server.port=}
;;
esac
done
if [ -z "$port" ]; then
echo "missing port" >&2
exit 1
fi
python3 -m http.server "$port" >/dev/null 2>&1 &
server_pid=$!
cleanup() {
kill "$server_pid" 2>/dev/null || true
wait "$server_pid" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
while kill -0 "$server_pid" 2>/dev/null; do
sleep 1
done
EOF
chmod +x "$fake_java"
}
create_release_zip() {
release_dir="$1"
release_zip_name="$2"
jar_name="$3"
mkdir -p "$release_dir/package/frontend_payload/dist"
printf 'new-jar\n' > "$release_dir/package/$jar_name"
printf '<html>new</html>\n' > "$release_dir/package/frontend_payload/dist/index.html"
(
cd "$release_dir/package/frontend_payload"
zip -qr "$release_dir/package/dist.zip" dist
)
(
cd "$release_dir/package"
zip -qr "$release_dir/$release_zip_name" "$jar_name" dist.zip
)
}
find_free_port() {
python3 - <<'PY'
import socket
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
print(sock.getsockname()[1])
sock.close()
PY
}
prepare_release_dir() {
release_dir="$1"
backend_port="$2"
mkdir -p "$release_dir/backend" "$release_dir/frontend" "$release_dir/fake-java-bin"
printf 'old-jar\n' > "$release_dir/backend/ruoyi-admin.jar"
mkdir -p "$release_dir/frontend/dist"
printf '<html>old</html>\n' > "$release_dir/frontend/dist/index.html"
create_fake_java "$release_dir/fake-java-bin/java"
create_release_zip "$release_dir" "loan-pricing-release.zip" "app.jar"
cp "$SCRIPT_UNDER_TEST" "$release_dir/deploy_from_package.sh"
perl -0pi -e "s#JAVA_BIN=\"/home/webapp/env/java/bin/java\"#JAVA_BIN=\"$release_dir/fake-java-bin/java\"#" \
"$release_dir/deploy_from_package.sh"
perl -0pi -e "s/BACKEND_PORT=63310/BACKEND_PORT=$backend_port/" \
"$release_dir/deploy_from_package.sh"
chmod +x "$release_dir/deploy_from_package.sh"
}
cleanup_release_dir() {
release_dir="$1"
if [ -f "$release_dir/backend/backend.pid" ]; then
backend_pid=$(cat "$release_dir/backend/backend.pid" 2>/dev/null || true)
if [ -n "${backend_pid:-}" ]; then
kill "$backend_pid" 2>/dev/null || true
wait "$backend_pid" 2>/dev/null || true
fi
fi
rm -rf "$release_dir"
}
test_deploy_success() {
release_dir=$(mktemp -d)
backend_port=$(find_free_port)
trap 'cleanup_release_dir "$release_dir"' EXIT INT TERM
prepare_release_dir "$release_dir" "$backend_port"
(
cd "$release_dir"
./deploy_from_package.sh
)
assert_file_exists "$release_dir/backend/ruoyi-admin.jar"
assert_file_exists "$release_dir/frontend/dist.zip"
assert_file_exists "$release_dir/frontend/dist/index.html"
assert_file_exists "$release_dir/backend/backend.pid"
assert_file_exists "$release_dir/backend/backend-console.log"
assert_grep 'new' "$release_dir/frontend/dist/index.html"
backup_jar_count=$(find "$release_dir/backend" -maxdepth 1 -type f -name 'ruoyi-admin.jar.*.bak' | wc -l | tr -d ' ')
[ "$backup_jar_count" -eq 1 ] || fail "expected one backup jar, got $backup_jar_count"
backup_dist_count=$(find "$release_dir/frontend" -maxdepth 1 -type d -name 'dist-*' | wc -l | tr -d ' ')
[ "$backup_dist_count" -eq 1 ] || fail "expected one backup dist dir, got $backup_dist_count"
backend_pid=$(cat "$release_dir/backend/backend.pid")
kill -0 "$backend_pid" 2>/dev/null || fail "expected backend pid to be running"
if ! lsof -nP -iTCP:"$backend_port" -sTCP:LISTEN >/dev/null 2>&1; then
fail "expected fake backend to listen on $backend_port"
fi
trap - EXIT INT TERM
cleanup_release_dir "$release_dir"
}
test_multiple_release_zip_should_fail() {
release_dir=$(mktemp -d)
backend_port=$(find_free_port)
trap 'cleanup_release_dir "$release_dir"' EXIT INT TERM
prepare_release_dir "$release_dir" "$backend_port"
cp "$release_dir/loan-pricing-release.zip" "$release_dir/loan-pricing-release-copy.zip"
if (
cd "$release_dir"
./deploy_from_package.sh >/tmp/deploy_from_package_test.stderr 2>&1
); then
fail "expected deploy_from_package.sh to fail when multiple release zips exist"
fi
assert_file_exists /tmp/deploy_from_package_test.stderr
assert_grep '发布 zip 数量不正确' /tmp/deploy_from_package_test.stderr
rm -f /tmp/deploy_from_package_test.stderr
trap - EXIT INT TERM
cleanup_release_dir "$release_dir"
}
main() {
[ -f "$SCRIPT_UNDER_TEST" ] || fail "script under test not found: $SCRIPT_UNDER_TEST"
test_deploy_success
test_multiple_release_zip_should_fail
printf 'PASS: deploy_from_package tests\n'
}
main "$@"