feat: add syj variant automated deploy script

One-shot release pipeline for syj-shop variant: admin build -> rsync to
8.140.50.89 -> remote .env switch + cache clear + Swoole reload ->
healthcheck with auto-rollback on failure. Includes operations manual
under docs/project-syj/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
danaisuiyuan
2026-05-10 09:13:29 +08:00
parent 79436c011c
commit b8643e085f
4 changed files with 390 additions and 0 deletions

211
pro_v3.5.1/deploy/release-syj.sh Executable file
View File

@@ -0,0 +1,211 @@
#!/usr/bin/env bash
# syj 商城变体一键发布脚本
# 用法:
# bash deploy/release-syj.sh # 正常发布
# bash deploy/release-syj.sh --dry-run # 干跑(不动远端)
# bash deploy/release-syj.sh --rollback <ts-sha> # 手动回滚到指定备份
# bash deploy/release-syj.sh --skip-build # 跳过 admin 构建(复用 dist/
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CONF_FILE="$SCRIPT_DIR/syj.conf"
EXCLUDE_FILE="$SCRIPT_DIR/rsync-exclude.txt"
LAST_RELEASE_FILE="$SCRIPT_DIR/.last-release"
[ -f "$CONF_FILE" ] || { echo "ERROR: $CONF_FILE not found"; exit 1; }
[ -f "$EXCLUDE_FILE" ] || { echo "ERROR: $EXCLUDE_FILE not found"; exit 1; }
# shellcheck disable=SC1090
source "$CONF_FILE"
DRY_RUN=0
SKIP_BUILD=0
ROLLBACK_TAG=""
while [ $# -gt 0 ]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
--skip-build) SKIP_BUILD=1; shift ;;
--rollback) ROLLBACK_TAG="${2:-}"; shift 2 ;;
-h|--help) sed -n '2,8p' "$0"; exit 0 ;;
*) echo "Unknown arg: $1"; exit 2 ;;
esac
done
_ts() { date +%H:%M:%S; }
log() { printf "\033[1;36m[%s]\033[0m %s\n" "$(_ts)" "$*"; }
warn() { printf "\033[1;33m[%s] WARN\033[0m %s\n" "$(_ts)" "$*"; }
err() { printf "\033[1;31m[%s] ERR\033[0m %s\n" "$(_ts)" "$*" >&2; }
ssh_run() { ssh -p "$SSH_PORT" "${SSH_USER}@${SSH_HOST}" "$@"; }
ssh_pipe() { ssh -p "$SSH_PORT" "${SSH_USER}@${SSH_HOST}" bash -se; }
check_health() {
local url code i
for url in "${HEALTH_URLS[@]}"; do
for ((i=1; i<=HEALTH_RETRY; i++)); do
code=$(curl -ksS -o /dev/null -w "%{http_code}" --max-time 10 "$url" || echo 000)
if [ "$code" = "200" ]; then
log "$url -> 200"
break
fi
if [ "$i" -eq "$HEALTH_RETRY" ]; then
err "$url -> $code (after $HEALTH_RETRY tries)"
return 1
fi
sleep "$HEALTH_INTERVAL"
done
done
return 0
}
remote_clear_and_reload() {
REMOTE_ROOT="$REMOTE_ROOT" ENV_SOURCE_FILE="$ENV_SOURCE_FILE" \
ssh_pipe <<'EOF'
set -euo pipefail
cd "$REMOTE_ROOT"
if [ ! -f "$ENV_SOURCE_FILE" ]; then
echo "ERR: $ENV_SOURCE_FILE missing on remote" >&2
exit 1
fi
cp -p "$ENV_SOURCE_FILE" .env
php think clear
PIDFILE=runtime/swoole/swoole.pid
if [ -f "$PIDFILE" ]; then
PID=$(cat "$PIDFILE")
if kill -0 "$PID" 2>/dev/null; then
kill -USR1 "$PID"
echo "Swoole reload via pidfile (master=$PID)"
else
echo "WARN: stale pidfile, falling back to ps"
PID=$(ps -eo pid,ppid,command | awk '/php think swoole/ && $2==1 {print $1; exit}')
[ -n "$PID" ] && kill -USR1 "$PID" && echo "Swoole reload via ps (master=$PID)"
fi
else
PID=$(ps -eo pid,ppid,command | awk '/php think swoole/ && $2==1 {print $1; exit}')
if [ -n "$PID" ]; then
kill -USR1 "$PID"
echo "Swoole reload via ps (master=$PID)"
else
echo "WARN: Swoole master not found; nothing reloaded"
fi
fi
EOF
}
prune_old_backups() {
REMOTE_BAK="$REMOTE_BAK" KEEP_BACKUPS="$KEEP_BACKUPS" ssh_pipe <<'EOF'
set -euo pipefail
mkdir -p "$REMOTE_BAK"
cd "$REMOTE_BAK"
ls -1t 2>/dev/null | tail -n +$((KEEP_BACKUPS + 1)) | while read -r d; do
[ -n "$d" ] && rm -rf -- "$d" && echo "Pruned $REMOTE_BAK/$d"
done
EOF
}
# ---- ROLLBACK MODE ----
if [ -n "$ROLLBACK_TAG" ]; then
log "ROLLBACK to $ROLLBACK_TAG"
REMOTE_ROOT="$REMOTE_ROOT" REMOTE_BAK="$REMOTE_BAK" TAG="$ROLLBACK_TAG" \
ssh_pipe <<'EOF'
set -euo pipefail
SRC="$REMOTE_BAK/$TAG"
[ -d "$SRC" ] || { echo "Backup $SRC not found"; exit 1; }
rsync -a --exclude='.env' "$SRC/" "$REMOTE_ROOT/"
echo "Rolled back files from $SRC"
EOF
remote_clear_and_reload
sleep "$HEALTH_INTERVAL"
check_health || { err "Healthcheck failed after rollback"; exit 1; }
log "Rollback OK"
exit 0
fi
# ---- PRE-FLIGHT ----
log "Pre-flight checks"
cd "$PROJECT_ROOT"
CURRENT_BRANCH=$(git -C "$PROJECT_ROOT/.." rev-parse --abbrev-ref HEAD 2>/dev/null || echo "?")
if [ "$CURRENT_BRANCH" != "$EXPECTED_BRANCH" ]; then
warn "Current branch is '$CURRENT_BRANCH', expected '$EXPECTED_BRANCH'"
if [ "$DRY_RUN" -ne 1 ]; then
read -r -p "Continue anyway? [y/N] " ans
[[ "$ans" =~ ^[Yy]$ ]] || { err "Aborted"; exit 1; }
fi
fi
[ -f "$PROJECT_ROOT/$ENV_SOURCE_FILE" ] || { err "$ENV_SOURCE_FILE missing locally"; exit 1; }
grep -q "syj.fsgx.cn" "$PROJECT_ROOT/view/admin/.env.production" || warn "view/admin/.env.production not pointing to syj.fsgx.cn"
ssh_run "test -d '$REMOTE_ROOT'" || { err "Remote root $REMOTE_ROOT does not exist"; exit 1; }
# ---- BUILD ADMIN ----
if [ "$SKIP_BUILD" -eq 1 ]; then
log "Skip admin build (--skip-build)"
else
log "Build admin"
(
cd "$PROJECT_ROOT/view/admin"
if [ ! -d node_modules ]; then
npm ci --silent
fi
npm run build
)
fi
[ -d "$PROJECT_ROOT/view/admin/dist" ] || { err "view/admin/dist missing; build failed?"; exit 1; }
# ---- VERSION ----
TS=$(date +%Y%m%d-%H%M%S)
SHA=$(git -C "$PROJECT_ROOT/.." rev-parse --short HEAD 2>/dev/null || echo "nogit")
TAG="${TS}-${SHA}"
log "Release tag: $TAG"
# ---- RSYNC ----
RSYNC_OPTS=(-az --delete --human-readable
--exclude-from="$EXCLUDE_FILE"
--backup --backup-dir="$REMOTE_BAK/$TAG"
-e "ssh -p $SSH_PORT")
[ "$DRY_RUN" -eq 1 ] && RSYNC_OPTS+=(--dry-run --itemize-changes)
log "rsync -> ${SSH_USER}@${SSH_HOST}:$REMOTE_ROOT (backup: $REMOTE_BAK/$TAG)"
ssh_run "mkdir -p '$REMOTE_BAK/$TAG'"
rsync "${RSYNC_OPTS[@]}" "$PROJECT_ROOT/" "${SSH_USER}@${SSH_HOST}:$REMOTE_ROOT/"
if [ "$DRY_RUN" -eq 1 ]; then
log "Dry-run done. No remote changes applied."
exit 0
fi
# ---- REMOTE: env switch + clear + reload ----
log "Remote: switch .env, php think clear, Swoole reload"
remote_clear_and_reload
# ---- HEALTHCHECK ----
sleep "$HEALTH_INTERVAL"
log "Healthcheck"
if check_health; then
echo "$TAG" > "$LAST_RELEASE_FILE"
log "Released $TAG"
prune_old_backups || warn "Prune step failed (non-fatal)"
exit 0
fi
# ---- AUTO ROLLBACK ----
err "Healthcheck failed. Auto-rolling back from $REMOTE_BAK/$TAG"
REMOTE_ROOT="$REMOTE_ROOT" REMOTE_BAK="$REMOTE_BAK" TAG="$TAG" \
ssh_pipe <<'EOF'
set -euo pipefail
rsync -a --exclude='.env' "$REMOTE_BAK/$TAG/" "$REMOTE_ROOT/"
EOF
remote_clear_and_reload
sleep "$HEALTH_INTERVAL"
if check_health; then
err "Rolled back to previous state. Release $TAG aborted."
exit 1
else
err "Rollback healthcheck STILL failing. Manual intervention required."
err "Backup at: ${SSH_USER}@${SSH_HOST}:$REMOTE_BAK/$TAG"
exit 2
fi

View File

@@ -0,0 +1,26 @@
.git/
.idea/
.vscode/
.DS_Store
runtime/
backup/
tests/
node_modules/
public/uploads/
.env
.env-fsgx
.env-huangjinfen
view/uniapp/
view/uniapp_v2/
view/admin/node_modules/
view/admin/src/
view/admin/public/
view/admin/.env*
view/admin/package*.json
view/admin/babel.config.js
view/admin/vue.config.js
view/admin/jest.config.js
view/admin/alias.config.js
view/admin/tests/
view/admin/dist/.DS_Store
deploy/.last-release

View File

@@ -0,0 +1,27 @@
# syj 商城变体发布参数(被 release-syj.sh source
# 修改后立即生效,不需要重新构建
SSH_USER=root
SSH_HOST=8.140.50.89
SSH_PORT=22
REMOTE_ROOT=/www/wwwroot/syj.fsgx.cn
REMOTE_BAK=/www/wwwroot/syj.fsgx.cn-bak
# 健康检查 URL任一非 200 即触发回滚
HEALTH_URLS=(
"https://syj.fsgx.cn/api/version"
"https://syj.fsgx.cn/admin/"
)
# 备份保留数量(远端 REMOTE_BAK 下保留最近 N 份)
KEEP_BACKUPS=10
# 期望分支;不一致时脚本会要求二次确认
EXPECTED_BRANCH=syj-bypass-auth
# 远端 .env 取自该文件(位于 pro_v3.5.1/ 根)
ENV_SOURCE_FILE=.env-syj
# 健康检查重试
HEALTH_RETRY=5
HEALTH_INTERVAL=3