xykj-plus/claude-ecc/release.sh
2026-06-13 13:48:51 +08:00

321 lines
12 KiB
Bash
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
# 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 "$@"