Files
huangjingfen/pro_v3.5.1/deploy/release-syj.sh
danaisuiyuan 9559031536 fix(deploy): bash 3.2 compat, sshpass auth, sync admin dist to public/admin
- Replace bash 4.2+ printf %(...)T with date subshell so script runs on
  the macOS-default bash 3.2.
- Add SSH_PASS_FILE / SSH_PASSWORD support via sshpass for environments
  where password auth is the only path.
- After admin npm build, rsync view/admin/dist/ into public/admin/
  (preserving UEditor and favicon) so the project rsync ships the
  admin to the path nginx actually serves from.
- Simplify rsync exclude list to drop view/admin/ wholesale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:47:24 +08:00

242 lines
8.2 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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_PASS_FILEsshpass -f其次 SSH_PASSWORD env再退回公钥
SSH_BASE=(ssh -p "$SSH_PORT" -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR)
if [ -n "${SSH_PASS_FILE:-}" ] && [ -f "${SSH_PASS_FILE/#~/$HOME}" ]; then
SSH_PASS_FILE="${SSH_PASS_FILE/#~/$HOME}"
[ "$(stat -f '%A' "$SSH_PASS_FILE" 2>/dev/null || stat -c '%a' "$SSH_PASS_FILE")" = "600" ] || \
warn "SSH_PASS_FILE permission is not 600 (consider: chmod 600 $SSH_PASS_FILE)"
command -v sshpass >/dev/null || { err "sshpass not installed (brew install hudochenkov/sshpass/sshpass)"; exit 1; }
SSH_AUTH=(sshpass -f "$SSH_PASS_FILE")
elif [ -n "${SSH_PASSWORD:-}" ]; then
command -v sshpass >/dev/null || { err "sshpass not installed"; exit 1; }
SSH_AUTH=(sshpass -e) # reads SSHPASS env
export SSHPASS="$SSH_PASSWORD"
else
SSH_AUTH=()
fi
ssh_run() { "${SSH_AUTH[@]}" "${SSH_BASE[@]}" "${SSH_USER}@${SSH_HOST}" "$@"; }
ssh_pipe() { "${SSH_AUTH[@]}" "${SSH_BASE[@]}" "${SSH_USER}@${SSH_HOST}" bash -se; }
# rsync 走相同认证
if [ -n "${SSH_AUTH+x}" ] && [ "${#SSH_AUTH[@]}" -gt 0 ]; then
RSYNC_RSH="${SSH_AUTH[*]} ssh -p $SSH_PORT -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
else
RSYNC_RSH="ssh -p $SSH_PORT"
fi
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; }
# admin 实际运行目录 = pro_v3.5.1/public/admin/(远端 nginx 指向同名路径)
# 把 build 产物同步到 public/admin/,再让项目根 rsync 整体推上去
log "Sync admin dist -> public/admin/"
mkdir -p "$PROJECT_ROOT/public/admin"
rsync -a --delete \
--exclude='UEditor/' --exclude='favicon.ico' \
"$PROJECT_ROOT/view/admin/dist/" "$PROJECT_ROOT/public/admin/"
# ---- 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 "$RSYNC_RSH")
[ "$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