Files
website/scripts/rsync-deploy.sh
T
eafonyang 3872d7d1dd feat(frontend): 新增首页新闻区块显示开关功能
- 首页新增 showNewsSection 字段,支持通过全局配置隐藏或显示新闻区块
- 主页组件根据 showNewsSection 判断是否渲染新闻区块组件
- 数据库添加 show_news_section 字段,默认值为 true
- 新增对应数据库迁移脚本以支持字段的添加和删除
- 更新 Payload 配置,新增“显示首页新闻区块”复选框,带默认选中说明
- 添加部署脚本 rsync-deploy.sh,支持项目文件的增量同步与便捷发布
- 优化 Dockerfile,调整用户组参数,新增 public 目录确保部署环境完整
2026-07-24 19:08:27 +08:00

306 lines
8.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Deploy source code to the production server via rsync.
#
# Usage:
# # Full deploy (sync entire project)
# ./scripts/rsync-deploy.sh
# ./scripts/rsync-deploy.sh --dry-run
#
# # Deploy a specific directory
# ./scripts/rsync-deploy.sh src/
# ./scripts/rsync-deploy.sh src/payload/
#
# # Deploy one or more files
# ./scripts/rsync-deploy.sh src/payload/globals/Homepage.ts
# ./scripts/rsync-deploy.sh src/payload/globals/Homepage.ts src/app/sitemap.ts
#
# # Mix directories and files
# ./scripts/rsync-deploy.sh src/migrations/ src/payload/globals/Homepage.ts
#
# Options:
# --dry-run, -n Show what would be transferred without making changes
# --target HOST SSH target (default: luxsin-website or $DEPLOY_TARGET)
# --port PORT SSH port (default: 22 or $SSH_PORT)
# --delete Enable --delete for specific-path syncs (full deploy always uses --delete)
# --help, -h Show this help message
#
# Environment variables:
# DEPLOY_TARGET SSH target (default: luxsin-website)
# REMOTE_DIR Remote project root (default: /data/project/eversolo-app)
# SSH_PORT Custom SSH port (default: 22)
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
DEPLOY_TARGET="${DEPLOY_TARGET:-luxsin-website}"
REMOTE_DIR="${REMOTE_DIR:-/data/project/eversolo-app}"
SSH_PORT="${SSH_PORT:-22}"
DRY_RUN=0
FORCE_DELETE=0
PATHS=()
# ── Parse arguments ──────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run|-n)
DRY_RUN=1
shift
;;
--target|-t)
DEPLOY_TARGET="$2"
shift 2
;;
--port|-p)
SSH_PORT="$2"
shift 2
;;
--delete)
FORCE_DELETE=1
shift
;;
--help|-h)
sed -n '3,/^$/p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
--*)
echo "ERROR: Unknown option: $1" >&2
exit 1
;;
*)
PATHS+=("$1")
shift
;;
esac
done
# ── Verify rsync is available ────────────────────────────────────
if ! command -v rsync >/dev/null 2>&1; then
echo "ERROR: rsync is not installed." >&2
exit 1
fi
SSH_CMD="ssh -p ${SSH_PORT}"
# ── Full-deploy excludes ─────────────────────────────────────────
# Only upload files needed to build and run the Docker deployment.
# Server-managed files (.env, data/, media/, etc.) are excluded and
# will NOT be deleted by --delete on the remote.
FULL_EXCLUDES=(
# Build artifacts
--exclude='.next/'
--exclude='.next-dev/'
--exclude='.turbo/'
--exclude='tsconfig.tsbuildinfo'
--exclude='out/'
# Dependencies (installed on server via npm ci / Docker)
--exclude='node_modules/'
# Environment files (managed on server)
--exclude='.env'
--exclude='.env.*'
# Server-managed data and uploads
--exclude='data/'
--exclude='media/'
--exclude='files/'
--exclude='videos/'
--exclude='public/'
# Local build / packaging artifacts
--exclude='deploy-packages/'
--exclude='output/'
--exclude='tmp-*'
--exclude='*.log'
--exclude='*.tar.gz'
--exclude='*.tgz'
--exclude='*.dump'
# Documentation (not needed for runtime)
--exclude='docs/'
--exclude='*.md'
# Cloudflare deployment (not used in Docker deployment)
--exclude='cloudflare/'
--exclude='Dockerfile.cloudflare'
--exclude='wrangler.jsonc'
# Version control
--exclude='.git/'
--exclude='.github/'
# Dev tools and editor configs
--exclude='.claude/'
--exclude='.playwright-cli/'
--exclude='.tmp/'
--exclude='.wrangler/'
--exclude='.vercel/'
--exclude='.vscode/'
--exclude='.idea/'
--exclude='.githooks/'
# OS artifacts
--exclude='.DS_Store'
--exclude='._*'
--exclude='__MACOSX/'
# Raw seed data (not needed in production)
--exclude='src/seed/raw/'
# TLS certificates (managed on server)
--exclude='nginx/certs/*.pem'
--exclude='nginx/certs/*.key'
)
# ── Minimal excludes for specific-path syncs ─────────────────────
# When uploading a specific directory or file, only filter out
# OS junk and dangerous directories. Project-level excludes like
# *.md or docs/ do not apply.
MINIMAL_EXCLUDES=(
--exclude='.DS_Store'
--exclude='._*'
--exclude='__MACOSX/'
--exclude='*.log'
--exclude='node_modules/'
--exclude='.next/'
--exclude='.next-dev/'
--exclude='nginx/certs/*.pem'
--exclude='nginx/certs/*.key'
)
# ── Build rsync flags ────────────────────────────────────────────
BASE_FLAGS=(-a -v -z)
if [[ $DRY_RUN -eq 1 ]]; then
BASE_FLAGS+=(--dry-run)
fi
# ── Summary header ───────────────────────────────────────────────
print_header() {
local scope="$1"
echo "=============================================="
echo " Eversolo Web — rsync Deploy"
echo "=============================================="
echo " Target: ${DEPLOY_TARGET}:${REMOTE_DIR}"
echo " SSH Port: ${SSH_PORT}"
echo " Scope: ${scope}"
echo " Mode: $([[ $DRY_RUN -eq 1 ]] && echo 'DRY RUN' || echo 'LIVE')"
echo "=============================================="
echo ""
}
# ── Full deploy ──────────────────────────────────────────────────
full_deploy() {
print_header "Full project"
rsync "${BASE_FLAGS[@]}" --delete \
-e "$SSH_CMD" \
"${FULL_EXCLUDES[@]}" \
./ "${DEPLOY_TARGET}:${REMOTE_DIR}/"
}
# ── Sync a single directory ──────────────────────────────────────
sync_directory() {
local dir="$1"
# Ensure no leading ./
dir="${dir#./}"
# Ensure trailing slash for rsync "contents" semantics
dir="${dir%/}/"
if [[ ! -d "$dir" ]]; then
echo "ERROR: Directory not found: $dir" >&2
return 1
fi
echo " → ${dir}${REMOTE_DIR}/${dir}"
local flags=("${BASE_FLAGS[@]}")
if [[ $FORCE_DELETE -eq 1 ]]; then
flags+=(--delete)
fi
rsync "${flags[@]}" \
-e "$SSH_CMD" \
"${MINIMAL_EXCLUDES[@]}" \
"./${dir}" "${DEPLOY_TARGET}:${REMOTE_DIR}/${dir}"
}
# ── Sync one or more files ───────────────────────────────────────
sync_files() {
# Use --relative so rsync preserves the full directory structure
# e.g. ./src/payload/globals/Homepage.ts → /remote/src/payload/globals/Homepage.ts
local file_args=()
for f in "$@"; do
f="${f#./}"
if [[ ! -f "$f" ]]; then
echo "ERROR: File not found: $f" >&2
return 1
fi
echo " → ${f}"
file_args+=("./${f}")
done
rsync "${BASE_FLAGS[@]}" --relative \
-e "$SSH_CMD" \
"${MINIMAL_EXCLUDES[@]}" \
"${file_args[@]}" \
"${DEPLOY_TARGET}:${REMOTE_DIR}/"
}
# ── Execute ─────────────────────────────────────────────────────
if [[ ${#PATHS[@]} -eq 0 ]]; then
# ── Full deploy ────────────────────────────────────────────────
full_deploy
else
# ── Specific paths: separate directories from files ───────────
DIRS=()
FILES=()
for p in "${PATHS[@]}"; do
# Strip leading ./
p="${p#./}"
if [[ -d "$p" ]]; then
DIRS+=("$p")
elif [[ -f "$p" ]]; then
FILES+=("$p")
else
echo "ERROR: Path does not exist: $p" >&2
exit 1
fi
done
print_header "Specific paths (${#PATHS[@]} item(s))"
# Sync directories
if [[ ${#DIRS[@]} -gt 0 ]]; then
for d in "${DIRS[@]}"; do
sync_directory "$d" || exit 1
done
fi
# Sync files (batch with --relative)
if [[ ${#FILES[@]} -gt 0 ]]; then
sync_files "${FILES[@]}" || exit 1
fi
fi
# ── Post-deploy instructions ────────────────────────────────────
echo ""
if [[ $DRY_RUN -eq 1 ]]; then
echo "✓ Dry run complete — no files were transferred."
echo " Run without --dry-run to apply changes."
else
echo "✓ Upload complete."
if [[ ${#PATHS[@]} -eq 0 ]]; then
echo ""
echo "Next steps (run on the production server):"
echo ""
echo " 1. Run database migrations:"
echo " cd ${REMOTE_DIR}"
echo " docker compose run --rm --no-deps bootstrap bash -lc 'npm ci --include=dev && npm run payload:migrate && npm run payload:migrate:status'"
echo ""
echo " 2. Rebuild and restart the app:"
echo " docker compose build app"
echo " docker compose up -d app"
fi
fi