#!/usr/bin/env bash # release.sh — claude-ecc 一键发布脚本(admin-xy + dev-assistant) # # 用法: # sudo ./release.sh # 构建镜像并部署全部服务 # sudo ./release.sh --no-cache # 强制全量重新构建镜像 # sudo ./release.sh --stop # 停止并移除容器(保留数据和配置) # sudo ./release.sh --restart # 重启已有容器(不重新构建) # sudo ./release.sh --skip-ts # 跳过 bridge TypeScript 构建(dist 已存在) # sudo ./release.sh --origin= # 覆盖默认生产域名(默认 https://oa.xiangyuetech.cn) # # 前提: # - 已安装 Docker / Docker Compose # - 已安装 node / npm(首次或修改 bridge 源码后需要 TS 构建) # - /opt/admin-xy/.env 已配置(首次运行会生成模板并退出) # # 发布目录与映射: # - 源码路径: 当前仓库根目录(脚本所在目录) # - 部署目录: /opt/admin-xy # - 环境变量: /opt/admin-xy/.env # - 日志目录: /opt/admin-xy/logs(挂载到 admin-xy-server:/app/logs) # - 后端端口: 127.0.0.1:9010 # - 前端端口: 127.0.0.1:9001(由 nginx 反代到站点根) # - 桥接端口: 127.0.0.1:9020(由 nginx 反代到 /dev-assistant/*) set -euo pipefail # ── 常量 ───────────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ADMIN_DIR="${SCRIPT_DIR}/admin-xy" DEV_ASSISTANT_DIR="${SCRIPT_DIR}/dev-assistant" BRIDGE_DIR="${DEV_ASSISTANT_DIR}/bridge" RELEASE_DIR="/opt/admin-xy" ENV_FILE="${RELEASE_DIR}/.env" COMPOSE_FILE="${RELEASE_DIR}/docker-compose.yml" LOG_DIR="${RELEASE_DIR}/logs" GIT_CONFIG_DIR="${RELEASE_DIR}/git" DEFAULT_ORIGIN="https://oa.xiangyuetech.cn" # ── 参数 ───────────────────────────────────────────────────────────────────── ACTION="deploy" NO_CACHE="" SKIP_TS=false ORIGIN="${DEFAULT_ORIGIN}" # ── 辅助 ───────────────────────────────────────────────────────────────────── log() { echo "[$(date '+%H:%M:%S')] $*"; } err() { echo "[ERROR] $*" >&2; exit 1; } warn() { echo "[WARN] $*" >&2; } require_root() { [[ $EUID -eq 0 ]] || err "该脚本需要 root 权限(请使用 sudo ./release.sh)" } require_cmd() { command -v "$1" >/dev/null 2>&1 || err "缺少命令:$1(请先安装)" } usage() { sed -n '2,25p' "$0" | sed 's/^# \?//' exit 0 } parse_args() { for arg in "$@"; do case "$arg" in --no-cache) NO_CACHE="--no-cache" ;; --stop) ACTION="stop" ;; --restart) ACTION="restart" ;; --skip-ts) SKIP_TS=true ;; --origin=*) ORIGIN="${arg#--origin=}" ;; --help|-h) usage ;; *) err "未知参数:$arg(--help 查看用法)" ;; esac done } # 将 https:// -> wss://,http:// -> ws:// to_ws_scheme() { local url="$1" case "$url" in https://*) printf 'wss://%s' "${url#https://}" ;; http://*) printf 'ws://%s' "${url#http://}" ;; *) printf '%s' "$url" ;; esac } # ── 发布目录 / .env 初始化 ─────────────────────────────────────────────────── load_env_file() { local line key value while IFS= read -r line || [[ -n "$line" ]]; do [[ "$line" =~ ^[[:space:]]*$ ]] && continue [[ "$line" =~ ^[[:space:]]*# ]] && continue [[ "$line" == *"="* ]] || continue key="${line%%=*}" value="${line#*=}" key="${key#"${key%%[![:space:]]*}"}" key="${key%"${key##*[![:space:]]}"}" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" if [[ "$value" != \"*\" && "$value" != \'*\' ]]; then value="${value%%[[:space:]]#*}" fi [[ "$value" =~ ^\"(.*)\"$ ]] && value="${BASH_REMATCH[1]}" [[ "$value" =~ ^\'(.*)\'$ ]] && value="${BASH_REMATCH[1]}" export "${key}=${value}" done < "${ENV_FILE}" } write_git_config_files() { local git_name git_email credentials_file git_name="${GIT_AUTHOR_NAME:-dev-assistant}" git_email="${GIT_AUTHOR_EMAIL:-dev-assistant@example.com}" credentials_file="${GIT_CONFIG_DIR}/.git-credentials" mkdir -p "${GIT_CONFIG_DIR}" chmod 700 "${GIT_CONFIG_DIR}" cat > "${GIT_CONFIG_DIR}/.gitconfig" < "${credentials_file}" if [[ -n "${GITEE_USERNAME:-}" && -n "${GITEE_TOKEN:-}" ]]; then printf 'https://%s:%s@gitee.com\n' "${GITEE_USERNAME}" "${GITEE_TOKEN}" > "${credentials_file}" else warn "未配置 GITEE_USERNAME/GITEE_TOKEN,容器内 git push 可能需要重新认证" fi chmod 600 "${GIT_CONFIG_DIR}/.gitconfig" "${credentials_file}" } init_release_dir() { log "初始化发布目录 ${RELEASE_DIR} ..." mkdir -p "${RELEASE_DIR}" mkdir -p "${LOG_DIR}" chmod 777 "${LOG_DIR}" # 首次运行:生成 .env 模板后退出,等待用户补全 if [[ ! -f "${ENV_FILE}" ]]; then log "生成 .env 配置模板 → ${ENV_FILE}" cat > "${ENV_FILE}" <