xykj-plus/claude-ecc/release.sh

321 lines
12 KiB
Bash
Raw Normal View History

#!/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=<url> # 覆盖默认生产域名(默认 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" <<EOF
[user]
name = ${git_name}
email = ${git_email}
[credential]
helper = store
[safe]
directory = /workspace
EOF
: > "${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}" <<EOF
# =============================================================================
# claude-ecc 生产环境变量
# 编辑本文件后重新执行sudo ./release.sh
# =============================================================================
# ── admin-xy 业务数据库(保留字段,将来业务升级使用) ──────────────────────
SPRING_DATASOURCE_URL=
SPRING_DATASOURCE_USERNAME=
SPRING_DATASOURCE_PASSWORD=
# ── admin-xy 管理库(必填) ────────────────────────────────────────────────
SPRING_DATASOURCE_ADMIN_URL=jdbc:mysql://127.0.0.1:3306/adminxy?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia%2FShanghai
SPRING_DATASOURCE_ADMIN_USERNAME=adminxy
SPRING_DATASOURCE_ADMIN_PASSWORD=changeme
# ── JWT 密钥(至少 32 字节,必填) ─────────────────────────────────────────
JWT_SECRET=change-me-in-production-32bytes-min-secret-key-please
# ── dev-assistant 桥接(按部署实际情况调整) ──────────────────────────────
# 浏览器访问的前端域名,用于 bridge 的 CORS origin 与 JWT 签发对齐
BRIDGE_ORIGIN=${DEFAULT_ORIGIN}
# 仓库源码在宿主机的绝对路径,以 rw 挂载到 bridge 容器的 /workspace
WORKSPACE_ROOT=${SCRIPT_DIR}
# 生产必须为 false仅本机调试时设为 true 可跳过鉴权
BRIDGE_AUTH_DISABLED=false
# ── 容器内 Git 身份与 Gitee HTTPS 凭据(可选,用于 Agent 提交/推送)────────
GIT_AUTHOR_NAME=dev-assistant
GIT_AUTHOR_EMAIL=dev-assistant@example.com
GITEE_USERNAME=
GITEE_TOKEN=
# ── 宿主机日志目录(一般无需修改) ─────────────────────────────────────────
ADMINXY_LOG_HOST_DIR=${LOG_DIR}
EOF
warn ".env 模板已生成,请编辑后重新执行:${ENV_FILE}"
exit 0
fi
# 安全读取 .env逐行解析避免 & 等特殊字符被 bash source 解释
# docker compose 自身也用此方式解析 .env行为一致
load_env_file
# 校验必填
local missing=()
[[ -z "${SPRING_DATASOURCE_ADMIN_URL:-}" ]] && missing+=("SPRING_DATASOURCE_ADMIN_URL")
[[ -z "${SPRING_DATASOURCE_ADMIN_USERNAME:-}" ]] && missing+=("SPRING_DATASOURCE_ADMIN_USERNAME")
[[ -z "${SPRING_DATASOURCE_ADMIN_PASSWORD:-}" ]] && missing+=("SPRING_DATASOURCE_ADMIN_PASSWORD")
[[ -z "${JWT_SECRET:-}" ]] && missing+=("JWT_SECRET")
if [[ ${#missing[@]} -gt 0 ]]; then
err "以下必填项未配置:${missing[*]}(请检查 ${ENV_FILE}"
fi
if [[ ${#JWT_SECRET} -lt 32 ]]; then
err "JWT_SECRET 至少 32 字节,当前 ${#JWT_SECRET} 字节(${ENV_FILE}"
fi
write_git_config_files
# 同步 compose 文件到发布目录(保持与 admin-xy/deploy.sh 行为一致)
log "同步 docker-compose.yml → ${COMPOSE_FILE}"
cp "${ADMIN_DIR}/docker-compose.yml" "${COMPOSE_FILE}"
}
# ── dev-assistant/bridge TypeScript 编译 ─────────────────────────────────────
build_bridge_dist() {
if ${SKIP_TS}; then
log "跳过 bridge TypeScript 构建(--skip-ts"
[[ -d "${BRIDGE_DIR}/dist" ]] || err "bridge dist/ 不存在,无法跳过 TS 构建"
return
fi
require_cmd node
require_cmd npm
log "安装 dev-assistant 依赖 ..."
( cd "${DEV_ASSISTANT_DIR}" && npm install --no-audit --no-fund )
log "编译 dev-assistant/bridge TypeScript ..."
( cd "${BRIDGE_DIR}" && npm run build )
[[ -d "${BRIDGE_DIR}/dist" ]] || err "bridge 构建失败dist/ 不存在"
}
# ── 构建 Docker 镜像 ─────────────────────────────────────────────────────────
build_images() {
local ws_origin
ws_origin="$(to_ws_scheme "${ORIGIN}")"
log "=== [1/3] 构建后端镜像 admin-xy-server:latest ==="
docker build ${NO_CACHE} -t admin-xy-server:latest "${ADMIN_DIR}/server"
log "=== [2/3] 构建前端镜像 admin-xy-web:latestorigin=${ORIGIN}==="
# build context 为仓库根Dockerfile 单独指定;
# 这样 COPY dev-assistant/web-sdk/src 能拿到 vite alias 所需的源码
docker build ${NO_CACHE} \
--file "${ADMIN_DIR}/web/Dockerfile" \
--build-arg "VITE_DEV_ASSISTANT_HTTP=${ORIGIN}/dev-assistant" \
--build-arg "VITE_DEV_ASSISTANT_WS=${ws_origin}/dev-assistant/copilot" \
-t admin-xy-web:latest "${SCRIPT_DIR}"
log "=== [3/3] 构建桥接镜像 dev-assistant-bridge:latest ==="
docker build ${NO_CACHE} -t dev-assistant-bridge:latest "${BRIDGE_DIR}"
log "镜像列表:"
docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}' \
| grep -E '(admin-xy|dev-assistant)' || true
}
# ── docker compose 封装 ──────────────────────────────────────────────────────
compose() {
docker compose \
--project-directory "${RELEASE_DIR}" \
--file "${COMPOSE_FILE}" \
--env-file "${ENV_FILE}" \
"$@"
}
deploy() {
log "启动服务 ..."
compose up -d --remove-orphans
log "当前容器状态:"
compose ps
}
stop_services() {
[[ -f "${COMPOSE_FILE}" ]] || err "未找到 ${COMPOSE_FILE}(是否已部署?)"
log "停止并移除容器 ..."
compose down
log "容器已停止。"
}
restart_services() {
[[ -f "${COMPOSE_FILE}" ]] || err "未找到 ${COMPOSE_FILE},请先执行完整发布"
log "重启服务 ..."
compose restart
log "当前容器状态:"
compose ps
}
# ── 主流程 ───────────────────────────────────────────────────────────────────
main() {
parse_args "$@"
require_root
require_cmd docker
case "${ACTION}" in
deploy)
log "=== claude-ecc 一键发布开始 ==="
init_release_dir
build_bridge_dist
build_images
deploy
log "=== 发布完成 ==="
log "站点: ${ORIGIN}"
log "后端127.0.0.1:9010"
log "前端127.0.0.1:9001"
log "桥接127.0.0.1:9020"
log "日志目录: ${LOG_DIR}"
;;
stop)
stop_services
;;
restart)
init_release_dir
restart_services
;;
esac
}
main "$@"