321 lines
12 KiB
Bash
321 lines
12 KiB
Bash
#!/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:latest(origin=${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 "$@"
|