#!/usr/bin/env bash set -Eeuo pipefail # 日志源目录:由日志生产服务写入 # 默认值:/home/basil/bot/data/git-summary LOG_SOURCE_DIR="${LOG_SOURCE_DIR:-/home/basil/bot/data/git-summary}" # 静态资源同步清单 # 默认值:$SITE_DIR/src/content/static-assets/index.json STATIC_ASSET_MANIFEST="${STATIC_ASSET_MANIFEST:-src/content/static-assets/index.json}" # 个人主页仓库目录 # 默认值:/home/basil/source/personal-homepage SITE_DIR="${SITE_DIR:-/home/basil/source/personal-homepage}" # 日志目标目录:复制到 homepage 仓库内 # 默认值:$SITE_DIR/src/content/logs LOG_TARGET_DIR="${LOG_TARGET_DIR:-${SITE_DIR}/src/content/logs}" # Docker Compose 配置与服务名 # 默认 compose 文件:$SITE_DIR/docker-compose.yml # 默认服务名:homepage DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-${SITE_DIR}/docker-compose.yml}" DOCKER_SERVICE_NAME="${DOCKER_SERVICE_NAME:-homepage}" # 是否在同步时删除目标目录中源目录已不存在的文件 # 默认值:false RSYNC_DELETE="${RSYNC_DELETE:-false}" # 是否先在宿主机执行一次 npm run rebuild # 默认值:true RUN_LOCAL_REBUILD="${RUN_LOCAL_REBUILD:-true}" # 是否在 log 同步前先同步静态资源 # 默认值:true RUN_STATIC_ASSET_SYNC="${RUN_STATIC_ASSET_SYNC:-true}" # 是否执行日志同步 # 默认值:true RUN_LOG_SYNC="${RUN_LOG_SYNC:-true}" # 刷新 Docker 服务时是否强制重新构建镜像 # 默认值:true DOCKER_FORCE_BUILD="${DOCKER_FORCE_BUILD:-true}" # 锁文件路径,避免定时任务重入 # 默认值:/tmp/personal-homepage-deploy.lock LOCK_FILE="${LOCK_FILE:-/tmp/personal-homepage-deploy.lock}" timestamp() { date '+%Y-%m-%d %H:%M:%S' } log() { printf '[%s] %s\n' "$(timestamp)" "$*" } fail() { log "ERROR: $*" exit 1 } usage() { cat <<'EOF' 用法: deploy-homepage.sh [options] 选项: --preset 使用预设部署档位 --dev-deploy 等价于 --preset dev --full-deploy 等价于 --preset full --run-static-asset-sync 是否同步静态资源 --run-log-sync 是否同步日志目录 --run-local-rebuild 是否在宿主机执行 npm run rebuild --docker-force-build docker compose up 时是否加 --build --rsync-delete rsync 时是否启用 --delete --fast 快速模式(仅刷新容器,不做同步与宿主机 rebuild) -h, --help 显示帮助 说明: - 预设档位: dev = 仅代码/样式部署(不做静态资源同步与日志同步;仍会 docker --build) full = 全量部署(静态资源 + 日志 + 宿主机 rebuild + docker --build) fast = 仅快速刷新容器(不做同步、不本地 rebuild、且不 docker --build) - 命令行参数优先级高于环境变量。 - 布尔值支持: true/false/1/0/yes/no/on/off EOF } require_command() { command -v "$1" >/dev/null 2>&1 || fail "缺少命令: $1" } is_true() { case "${1,,}" in 1|true|yes|on) return 0 ;; *) return 1 ;; esac } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --preset) [[ $# -ge 2 ]] || fail "参数缺少值: $1" case "${2,,}" in dev) RUN_STATIC_ASSET_SYNC=false RUN_LOG_SYNC=false RUN_LOCAL_REBUILD=false DOCKER_FORCE_BUILD=true ;; full) RUN_STATIC_ASSET_SYNC=true RUN_LOG_SYNC=true RUN_LOCAL_REBUILD=true DOCKER_FORCE_BUILD=true ;; fast) RUN_STATIC_ASSET_SYNC=false RUN_LOG_SYNC=false RUN_LOCAL_REBUILD=false DOCKER_FORCE_BUILD=false ;; *) fail "不支持的 preset: $2(可选: dev|full|fast)" ;; esac shift 2 ;; --dev-deploy) RUN_STATIC_ASSET_SYNC=false RUN_LOG_SYNC=false RUN_LOCAL_REBUILD=false DOCKER_FORCE_BUILD=true shift ;; --full-deploy) RUN_STATIC_ASSET_SYNC=true RUN_LOG_SYNC=true RUN_LOCAL_REBUILD=true DOCKER_FORCE_BUILD=true shift ;; --run-static-asset-sync) [[ $# -ge 2 ]] || fail "参数缺少值: $1" RUN_STATIC_ASSET_SYNC="$2" shift 2 ;; --run-log-sync) [[ $# -ge 2 ]] || fail "参数缺少值: $1" RUN_LOG_SYNC="$2" shift 2 ;; --run-local-rebuild) [[ $# -ge 2 ]] || fail "参数缺少值: $1" RUN_LOCAL_REBUILD="$2" shift 2 ;; --docker-force-build) [[ $# -ge 2 ]] || fail "参数缺少值: $1" DOCKER_FORCE_BUILD="$2" shift 2 ;; --rsync-delete) [[ $# -ge 2 ]] || fail "参数缺少值: $1" RSYNC_DELETE="$2" shift 2 ;; --fast) RUN_STATIC_ASSET_SYNC=false RUN_LOG_SYNC=false RUN_LOCAL_REBUILD=false DOCKER_FORCE_BUILD=false shift ;; -h|--help) usage exit 0 ;; *) fail "未知参数: $1(使用 --help 查看可用参数)" ;; esac done } sync_logs() { mkdir -p "$LOG_TARGET_DIR" if command -v rsync >/dev/null 2>&1; then local -a rsync_args=(-av) if is_true "$RSYNC_DELETE"; then rsync_args+=(--delete) fi log "同步日志: $LOG_SOURCE_DIR -> $LOG_TARGET_DIR" rsync "${rsync_args[@]}" "${LOG_SOURCE_DIR}/" "${LOG_TARGET_DIR}/" return 0 fi log "未找到 rsync,改用 cp -a 复制日志" if is_true "$RSYNC_DELETE"; then find "$LOG_TARGET_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf {} + fi cp -a "${LOG_SOURCE_DIR}/." "$LOG_TARGET_DIR/" } sync_static_assets() { log "同步静态资源清单: $STATIC_ASSET_MANIFEST" ( cd "$SITE_DIR" STATIC_ASSET_MANIFEST="$STATIC_ASSET_MANIFEST" \ node --env-file-if-exists=.env --experimental-strip-types ./scripts/sync-static-assets.ts ) } run_local_rebuild() { log "开始宿主机构建校验: npm run rebuild" ( cd "$SITE_DIR" npm run rebuild ) } refresh_docker_service() { log "刷新 Docker 服务: $DOCKER_SERVICE_NAME" ( cd "$SITE_DIR" if is_true "$DOCKER_FORCE_BUILD"; then docker compose -f "$DOCKER_COMPOSE_FILE" up -d --build "$DOCKER_SERVICE_NAME" else docker compose -f "$DOCKER_COMPOSE_FILE" up -d "$DOCKER_SERVICE_NAME" fi ) } main_impl() { require_command docker if is_true "$RUN_STATIC_ASSET_SYNC"; then require_command node fi if is_true "$RUN_LOCAL_REBUILD"; then require_command npm fi docker compose version >/dev/null 2>&1 || fail "当前环境不可用 docker compose" [[ -d "$LOG_SOURCE_DIR" ]] || fail "日志源目录不存在: $LOG_SOURCE_DIR" [[ -d "$SITE_DIR" ]] || fail "站点目录不存在: $SITE_DIR" [[ -f "$DOCKER_COMPOSE_FILE" ]] || fail "docker compose 文件不存在: $DOCKER_COMPOSE_FILE" log "部署开始" log "LOG_SOURCE_DIR=$LOG_SOURCE_DIR" log "LOG_TARGET_DIR=$LOG_TARGET_DIR" log "SITE_DIR=$SITE_DIR" log "STATIC_ASSET_MANIFEST=$STATIC_ASSET_MANIFEST" log "DOCKER_COMPOSE_FILE=$DOCKER_COMPOSE_FILE" log "DOCKER_SERVICE_NAME=$DOCKER_SERVICE_NAME" log "RSYNC_DELETE=$RSYNC_DELETE" log "RUN_LOG_SYNC=$RUN_LOG_SYNC" log "RUN_STATIC_ASSET_SYNC=$RUN_STATIC_ASSET_SYNC" log "RUN_LOCAL_REBUILD=$RUN_LOCAL_REBUILD" log "DOCKER_FORCE_BUILD=$DOCKER_FORCE_BUILD" log "LOCK_FILE=$LOCK_FILE" if is_true "$RUN_STATIC_ASSET_SYNC"; then sync_static_assets else log "跳过静态资源同步(RUN_STATIC_ASSET_SYNC=false)" fi if is_true "$RUN_LOG_SYNC"; then sync_logs else log "跳过日志同步(RUN_LOG_SYNC=false)" fi if is_true "$RUN_LOCAL_REBUILD"; then run_local_rebuild else log "跳过宿主机 npm run rebuild(RUN_LOCAL_REBUILD=false)" fi refresh_docker_service log "部署完成" } main() { parse_args "$@" if ! command -v flock >/dev/null 2>&1; then log "未找到 flock,跳过锁保护" main_impl return 0 fi mkdir -p "$(dirname "$LOCK_FILE")" exec 9>"$LOCK_FILE" if ! flock -n 9; then fail "已有部署任务在执行中,锁文件: $LOCK_FILE" fi log "已获取部署锁: $LOCK_FILE" main_impl } main "$@"