diff --git a/.env.example b/.env.example index fa0b0e9..fd98ba2 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,7 @@ # 当前部署版本号,格式:YYYY.MM.DD-private.N # 用于镜像标签匹配和审计追溯 -PRIVATE_VERSION=2026.05.21-private.2 +PRIVATE_VERSION=2026.07.28-private.1 # 安装目录的绝对路径(deploy.sh 自动写入,用于容器内一键更新功能) PRIVATE_DEPLOY_ROOT=/opt/xuqm-private @@ -38,8 +38,8 @@ MANAGEMENT_HEALTH_MAIL_ENABLED=false # ============================================================================= # # 可选值(用逗号分隔): -# base 必选,包含:tenant-service / file-service / tenant-web / -# ops-web / docs-site / nginx +# base 必选,包含:tenant-service / file-service / tenant-web / nginx; +# 文档站由 tenant-web 镜像在 /docs/ 同步提供,不存在独立容器 # infra-mysql 托管 MySQL 容器(MYSQL_MODE=managed 时必选) # infra-redis 托管 Redis 容器(REDIS_MODE=managed 时必选) # im IM HTTP 和 WebSocket 服务 diff --git a/.gitignore b/.gitignore index 81367c8..7a5afd6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,13 +6,8 @@ data/redis/* data/uploads/* data/update/* data/backups/* -!data/mysql/.gitkeep -!data/redis/.gitkeep -!data/uploads/.gitkeep -!data/update/.gitkeep -!data/backups/.gitkeep +data/sourcemaps/* dist/ *.tar.gz *.bak .DS_Store - diff --git a/CHANGELOG.md b/CHANGELOG.md index 618474e..3c6e5c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # 更新日志 +## 2026.07.28-private.1 + +- 私有化升级继承完整服务 profile,自动移除已废弃的 license profile,并在存在 + BugCollect 定义时追加而不是覆盖原 profile,避免升级只拉取单一服务。 +- 旧部署缺少 JWT 和应用商店监测密钥时各生成一次部署独享密钥,后续升级保持原值; + 不再使用可公开推导的 JWT 默认值。 +- 全量部署移除镜像仓库、MySQL 和 Redis 的可用默认密码;交互模式安全输入, + 非交互模式必须显式注入只读仓库凭据,托管数据库密码自动随机生成。 +- 删除未被入口调用的重复 configure/install/upgrade 实现、占位目录文件和不存在的 + docs-site 独立镜像定义;文档站继续由 tenant-web 在 `/docs/` 提供。 + ## 2026.06.18-private.1 ### 修复 diff --git a/README.md b/README.md index 594d499..30c3926 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ curl -fsSL https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy/raw/branch/mai ``` 交互式向导依次完成:依赖检测 → 配置生成 → 镜像拉取 → 容器启动 → 租户初始化 → 全量验证。 +首次执行时会要求输入私有镜像仓库及只读拉取凭据;凭据只写入部署机器上被 Git +忽略的配置,不会进入部署仓库、业务镜像或普通文档。自动化部署通过 +`REGISTRY`、`REGISTRY_USER`、`REGISTRY_PASSWORD` 环境变量传入。 ### 升级已有部署 @@ -37,7 +40,8 @@ bash scripts/reset.sh | `install.sh` | 全新服务器首次部署;或彻底清除数据重装 | | `upgrade.sh` | 保留数据,更新部署脚本和镜像(推荐日常升级方式) | | `scripts/reset.sh` | 保留数据,容器状态异常时快速重建恢复 | -| `scripts/update.sh` | 在安装目录内执行的局部升级(由 upgrade.sh 调用) | +| `scripts/deploy.sh` | 顶层安装器下载仓库后进入的唯一全量部署实现 | +| `scripts/update.sh` | 顶层升级器调用的唯一原地升级实现 | | `scripts/verify.sh` | 随时重新运行全量验证 | | `scripts/backup.sh` | 备份数据 | | `scripts/restore.sh` | 恢复备份 | diff --git a/VERSION b/VERSION index e826ca6..01b56d2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2026.05.21-private.2 +2026.07.28-private.1 diff --git a/data/backups/.gitkeep b/data/backups/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/data/backups/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/data/mysql/.gitkeep b/data/mysql/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/data/mysql/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/data/redis/.gitkeep b/data/redis/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/data/redis/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/data/update/.gitkeep b/data/update/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/data/update/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/data/uploads/.gitkeep b/data/uploads/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/data/uploads/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docker-compose.yml b/docker-compose.yml index db73b3a..ee41fd5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -188,7 +188,7 @@ services: SPRING_DATA_REDIS_PORT: "${REDIS_PORT:-6379}" SPRING_DATA_REDIS_DATABASE: "${REDIS_DATABASE:-0}" SERVER_PORT: "9006" - JWT_SECRET: "${JWT_SECRET:-xuqm-bugcollect-jwt-secret}" + JWT_SECRET: "${XUQM_JWT_SECRET:?XUQM_JWT_SECRET must be configured in config/secrets.env}" SPRING_FLYWAY_IGNORE_MIGRATION_PATTERNS: "*:Missing" SYMBOLICATOR_URL: "http://symbolicator:3000" deploy: diff --git a/image-manifest.json b/image-manifest.json index b9462e3..749b6e3 100644 --- a/image-manifest.json +++ b/image-manifest.json @@ -1,12 +1,11 @@ { "schemaVersion": 1, - "privateVersion": "2026.05.21-private.2", + "privateVersion": "2026.07.28-private.1", "registry": "registry.example.com/xuqm", "images": [ {"name": "tenant-service", "required": true}, {"name": "file-service", "required": true}, {"name": "tenant-web", "required": true}, - {"name": "docs-site", "required": true}, {"name": "im-service", "required": false, "profile": "im"}, {"name": "push-service", "required": false, "profile": "push"}, {"name": "update-service", "required": false, "profile": "update"} diff --git a/install.sh b/install.sh index e830205..5413f30 100755 --- a/install.sh +++ b/install.sh @@ -238,6 +238,11 @@ fi mkdir -p "$INSTALL_DIR" tar -xzf "$TMP_PKG" -C "$INSTALL_DIR" --strip-components=1 --exclude='*/data' chmod +x "$INSTALL_DIR/scripts/"*.sh "$INSTALL_DIR/install.sh" 2>/dev/null || true +# tar 解压不会删除新版已移除的旧文件,必须显式清理已废弃的平行入口和占位文件。 +rm -f "$INSTALL_DIR/scripts/configure.sh" \ + "$INSTALL_DIR/scripts/install.sh" \ + "$INSTALL_DIR/scripts/upgrade.sh" +find "$INSTALL_DIR/data" "$INSTALL_DIR/logs" -type f -name '.gitkeep' -delete 2>/dev/null || true ok "部署包解压完成: $INSTALL_DIR" # 恢复关键配置文件(确保现有配置不被模板覆盖) diff --git a/logs/.gitkeep b/logs/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/logs/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/scripts/configure.sh b/scripts/configure.sh deleted file mode 100755 index 774dca9..0000000 --- a/scripts/configure.sh +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -. "$ROOT_DIR/scripts/lib.sh" - -# Non-interactive mode: skip prompts if all required vars are already set -NON_INTERACTIVE="${NON_INTERACTIVE:-false}" -[ -t 0 ] || NON_INTERACTIVE=true - -audit "configure" "STARTED" "rendering initial config" -progress "configure" "STARTED" "rendering initial config" - -# Initialize from template if missing -if [ ! -f "$ROOT_DIR/.env" ]; then - cp "$ROOT_DIR/.env.example" "$ROOT_DIR/.env" -fi -ensure_secret_file -load_env - -ask() { - local prompt="$1" - local current="${2:-}" - local default="${3:-}" - [ "$NON_INTERACTIVE" = "true" ] && { printf '%s' "${current:-$default}"; return; } - local display_default="" - [ -n "$current" ] && display_default=" [current: $current]" - [ -z "$current" ] && [ -n "$default" ] && display_default=" [default: $default]" - printf '%s%s: ' "$prompt" "$display_default" >&2 - read -r value - printf '%s' "${value:-${current:-$default}}" -} - -ask_secret() { - local prompt="$1" - [ "$NON_INTERACTIVE" = "true" ] && { printf 'change-me'; return; } - printf '%s (leave blank to generate): ' "$prompt" >&2 - read -rs value - printf '\n' >&2 - if [ -z "$value" ]; then - value="$(random_secret)" - printf ' → generated strong password\n' >&2 - fi - printf '%s' "$value" -} - -printf '\n=== XuqmGroup Private Deployment Configuration ===\n\n' - -# Domains -CONSOLE_DOMAIN="$(ask "Console domain (e.g. https://console.customer.com)" "${CONSOLE_DOMAIN:-}" "")" -OPS_DOMAIN="$(ask "Ops domain (e.g. https://ops.customer.com)" "${OPS_DOMAIN:-}" "")" -DOCS_DOMAIN="$(ask "Docs domain (e.g. https://docs.customer.com)" "${DOCS_DOMAIN:-}" "")" -FILE_DOMAIN="$(ask "File domain (e.g. https://file.customer.com)" "${FILE_DOMAIN:-}" "")" - -# Optional service domains (only ask if enabling) -ENABLE_IM="${ENABLE_IM:-false}" -ENABLE_PUSH="${ENABLE_PUSH:-false}" -ENABLE_UPDATE="${ENABLE_UPDATE:-false}" - -if [ "$NON_INTERACTIVE" != "true" ]; then - printf '\nWhich optional services to enable now? (space-separated, e.g. "im update")\n' - printf 'Available: im push update\nCurrent: %s\n' "$COMPOSE_PROFILES" - printf 'Enter services (blank to keep current): ' - read -r OPT_SVCS -fi - -if printf '%s' "${OPT_SVCS:-}" | grep -q 'im'; then ENABLE_IM=true; IM_DOMAIN="$(ask "IM domain (e.g. https://im.customer.com)" "${IM_DOMAIN:-}" "")"; fi -if printf '%s' "${OPT_SVCS:-}" | grep -q 'push'; then ENABLE_PUSH=true; PUSH_DOMAIN="$(ask "Push domain" "${PUSH_DOMAIN:-}" "")"; fi -if printf '%s' "${OPT_SVCS:-}" | grep -q 'update'; then ENABLE_UPDATE=true; UPDATE_DOMAIN="$(ask "Update domain" "${UPDATE_DOMAIN:-}" "")"; fi - -# MySQL mode -printf '\nMySQL mode (external=you provide connection / managed=deploy script installs MySQL)\n' -MYSQL_MODE="$(ask "MySQL mode" "${MYSQL_MODE:-external}" "external")" -if [ "$MYSQL_MODE" = "external" ]; then - MYSQL_HOST="$(ask "MySQL host" "${MYSQL_HOST:-127.0.0.1}" "127.0.0.1")" - MYSQL_PORT="$(ask "MySQL port" "${MYSQL_PORT:-3306}" "3306")" - MYSQL_DATABASE="$(ask "MySQL database" "${MYSQL_DATABASE:-xuqm_private}" "xuqm_private")" - MYSQL_USERNAME="$(ask "MySQL username" "${MYSQL_USERNAME:-xuqm}" "xuqm")" - MYSQL_PASSWORD="$(ask_secret "MySQL password")" -else - MYSQL_HOST="mysql" - MYSQL_PORT="3306" - MYSQL_DATABASE="$(ask "MySQL database" "${MYSQL_DATABASE:-xuqm_private}" "xuqm_private")" - MYSQL_USERNAME="$(ask "MySQL username" "${MYSQL_USERNAME:-xuqm}" "xuqm")" - MYSQL_PASSWORD="$(ensure_env_value "$ROOT_DIR/config/secrets.env" "MYSQL_PASSWORD" "${MYSQL_PASSWORD:-}" "$(random_secret)")" - MYSQL_ROOT_PASSWORD="$(ensure_env_value "$ROOT_DIR/config/secrets.env" "MYSQL_ROOT_PASSWORD" "${MYSQL_ROOT_PASSWORD:-}" "$(random_secret)")" -fi - -# Redis mode -printf '\nRedis mode (external=you provide connection / managed=deploy script installs Redis)\n' -REDIS_MODE="$(ask "Redis mode" "${REDIS_MODE:-external}" "external")" -if [ "$REDIS_MODE" = "external" ]; then - REDIS_HOST="$(ask "Redis host" "${REDIS_HOST:-127.0.0.1}" "127.0.0.1")" - REDIS_PORT="$(ask "Redis port" "${REDIS_PORT:-6379}" "6379")" - REDIS_PASSWORD="$(ask_secret "Redis password")" -else - REDIS_HOST="redis" - REDIS_PORT="6379" - REDIS_PASSWORD="$(ensure_env_value "$ROOT_DIR/config/secrets.env" "REDIS_PASSWORD" "${REDIS_PASSWORD:-}" "$(random_secret)")" -fi - -# Bootstrap tenant -printf '\nBootstrap admin account:\n' -TENANT_BOOTSTRAP_EMAIL="$(ask "Admin email" "${TENANT_BOOTSTRAP_EMAIL:-admin@customer.com}" "admin@customer.com")" -TENANT_BOOTSTRAP_APP_KEY="$(ask "App key" "${TENANT_BOOTSTRAP_APP_KEY:-ak_private_default}" "ak_private_default")" - -# Registry -REGISTRY="$(ask "Docker registry" "${REGISTRY:-registry.example.com/xuqm}" "registry.example.com/xuqm")" -IMAGE_TAG="$(ask "Image tag" "${IMAGE_TAG:-$(cat "$ROOT_DIR/VERSION" | tr -d '[:space:]')}" "")" - -# Write .env (compose variable substitution — used by ${VAR} in docker-compose.yml) -set_env_value "$ROOT_DIR/.env" "CONSOLE_DOMAIN" "$CONSOLE_DOMAIN" -set_env_value "$ROOT_DIR/.env" "OPS_DOMAIN" "$OPS_DOMAIN" -set_env_value "$ROOT_DIR/.env" "DOCS_DOMAIN" "$DOCS_DOMAIN" -set_env_value "$ROOT_DIR/.env" "FILE_DOMAIN" "$FILE_DOMAIN" -[ -n "${IM_DOMAIN:-}" ] && set_env_value "$ROOT_DIR/.env" "IM_DOMAIN" "$IM_DOMAIN" -[ -n "${PUSH_DOMAIN:-}" ] && set_env_value "$ROOT_DIR/.env" "PUSH_DOMAIN" "$PUSH_DOMAIN" -[ -n "${UPDATE_DOMAIN:-}" ] && set_env_value "$ROOT_DIR/.env" "UPDATE_DOMAIN" "$UPDATE_DOMAIN" - -set_env_value "$ROOT_DIR/.env" "ENABLE_IM" "$ENABLE_IM" -set_env_value "$ROOT_DIR/.env" "ENABLE_PUSH" "$ENABLE_PUSH" -set_env_value "$ROOT_DIR/.env" "ENABLE_UPDATE" "$ENABLE_UPDATE" - -set_env_value "$ROOT_DIR/.env" "MYSQL_MODE" "$MYSQL_MODE" -set_env_value "$ROOT_DIR/.env" "MYSQL_HOST" "$MYSQL_HOST" -set_env_value "$ROOT_DIR/.env" "MYSQL_PORT" "$MYSQL_PORT" -set_env_value "$ROOT_DIR/.env" "MYSQL_DATABASE" "$MYSQL_DATABASE" -set_env_value "$ROOT_DIR/.env" "MYSQL_USERNAME" "$MYSQL_USERNAME" -set_env_value "$ROOT_DIR/.env" "REDIS_MODE" "$REDIS_MODE" -set_env_value "$ROOT_DIR/.env" "REDIS_HOST" "$REDIS_HOST" -set_env_value "$ROOT_DIR/.env" "REDIS_PORT" "$REDIS_PORT" -set_env_value "$ROOT_DIR/.env" "REGISTRY" "$REGISTRY" -set_env_value "$ROOT_DIR/.env" "IMAGE_TAG" "$IMAGE_TAG" -set_env_value "$ROOT_DIR/.env" "TENANT_BOOTSTRAP_EMAIL" "$TENANT_BOOTSTRAP_EMAIL" -set_env_value "$ROOT_DIR/.env" "TENANT_BOOTSTRAP_APP_KEY" "$TENANT_BOOTSTRAP_APP_KEY" - -# Sync infra settings into xuqm.env so containers receive correct host/port values -# via env_file:, consistent with the SPRING_DATASOURCE_* overrides in docker-compose.yml. -XUQM_ENV="$ROOT_DIR/config/xuqm.env" -set_env_value "$XUQM_ENV" "MYSQL_HOST" "$MYSQL_HOST" -set_env_value "$XUQM_ENV" "MYSQL_PORT" "$MYSQL_PORT" -set_env_value "$XUQM_ENV" "MYSQL_DATABASE" "$MYSQL_DATABASE" -set_env_value "$XUQM_ENV" "MYSQL_USERNAME" "$MYSQL_USERNAME" -set_env_value "$XUQM_ENV" "REDIS_HOST" "$REDIS_HOST" -set_env_value "$XUQM_ENV" "REDIS_PORT" "$REDIS_PORT" -set_env_value "$XUQM_ENV" "CONSOLE_DOMAIN" "$CONSOLE_DOMAIN" -[ -n "${FILE_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "FILE_DOMAIN" "$FILE_DOMAIN" -[ -n "${IM_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "IM_DOMAIN" "$IM_DOMAIN" -[ -n "${PUSH_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "PUSH_DOMAIN" "$PUSH_DOMAIN" -[ -n "${UPDATE_DOMAIN:-}" ] && set_env_value "$XUQM_ENV" "UPDATE_DOMAIN" "$UPDATE_DOMAIN" -set_env_value "$XUQM_ENV" "ENABLE_IM" "$ENABLE_IM" -set_env_value "$XUQM_ENV" "ENABLE_PUSH" "$ENABLE_PUSH" -set_env_value "$XUQM_ENV" "ENABLE_UPDATE" "$ENABLE_UPDATE" - -# Write secrets.env -set_env_value "$ROOT_DIR/config/secrets.env" "MYSQL_PASSWORD" "$MYSQL_PASSWORD" -[ -n "${MYSQL_ROOT_PASSWORD:-}" ] && \ - set_env_value "$ROOT_DIR/config/secrets.env" "MYSQL_ROOT_PASSWORD" "$MYSQL_ROOT_PASSWORD" -set_env_value "$ROOT_DIR/config/secrets.env" "REDIS_PASSWORD" "$REDIS_PASSWORD" -set_env_value "$ROOT_DIR/config/secrets.env" "SPRING_DATASOURCE_PASSWORD" "$MYSQL_PASSWORD" -set_env_value "$ROOT_DIR/config/secrets.env" "SPRING_DATA_REDIS_PASSWORD" "$REDIS_PASSWORD" -ensure_env_value "$ROOT_DIR/config/secrets.env" "XUQM_JWT_SECRET" \ - "${XUQM_JWT_SECRET:-}" "$(random_secret)$(random_secret)" >/dev/null -ensure_env_value "$ROOT_DIR/config/secrets.env" "SDK_INTERNAL_TOKEN" \ - "${SDK_INTERNAL_TOKEN:-}" "$(random_secret)" >/dev/null -ensure_env_value "$ROOT_DIR/config/secrets.env" "XUQM_STORE_SECRET_MASTER_KEY" \ - "${XUQM_STORE_SECRET_MASTER_KEY:-}" "$(random_aes256_base64)" >/dev/null -set_env_value "$ROOT_DIR/config/secrets.env" "XUQM_STORE_SECRET_KEY_VERSION" \ - "${XUQM_STORE_SECRET_KEY_VERSION:-v1}" -chmod 600 "$ROOT_DIR/config/secrets.env" - -"$ROOT_DIR/scripts/render-config.sh" - -audit "configure" "DONE" "config ready" -progress "configure" "DONE" "config ready" -printf '\nConfiguration complete. Review .env and config/secrets.env before deployment.\n' diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 3158112..ab436fd 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -23,8 +23,8 @@ # 覆盖默认值(可通过环境变量传入): # DEPLOY_HOST 目标机器 IP / 主机名(默认 localhost) # 部署向导会提示配置域名与 HTTPS -# REGISTRY_PASSWORD ACR 密码(默认 xuqinmin1022) -# MYSQL_ROOT_PASSWORD、MYSQL_PASSWORD、REDIS_PASSWORD 同理 +# REGISTRY / REGISTRY_USER / REGISTRY_PASSWORD 私有镜像仓库与只读拉取凭据 +# MYSQL_ROOT_PASSWORD、MYSQL_PASSWORD、REDIS_PASSWORD 可显式传入;未传时随机生成 set -euo pipefail @@ -33,11 +33,11 @@ set -euo pipefail # --------------------------------------------------------------------------- -# 镜像仓库 -REGISTRY="crpi-n44qjpuucgjt8e8c.cn-beijing.personal.cr.aliyuncs.com/xuqmgroup" -REGISTRY_HOST="crpi-n44qjpuucgjt8e8c.cn-beijing.personal.cr.aliyuncs.com" -REGISTRY_USER="xuqinmin12" -REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-xuqinmin1022}" +# 镜像仓库。客户交付包不得内置可用凭据;仓库地址与只读拉取账号由交付环境提供。 +REGISTRY="${REGISTRY:-registry.example.com/xuqm}" +REGISTRY_HOST="${REGISTRY%%/*}" +REGISTRY_USER="${REGISTRY_USER:-}" +REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-}" IMAGE_TAG="latest" # 部署主机(用于健康检查 HTTP 请求,直接打服务端口) @@ -46,13 +46,13 @@ CONSOLE_BASE="http://${DEPLOY_HOST}" _WS_SCHEME="ws" # MySQL(managed 模式,由 Docker 容器托管) -MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-XuqmRoot@2026}" -MYSQL_PASSWORD="${MYSQL_PASSWORD:-XuqmMysql@2026}" +MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-}" +MYSQL_PASSWORD="${MYSQL_PASSWORD:-}" MYSQL_DATABASE="xuqm_private" MYSQL_USERNAME="xuqm" # Redis(managed 模式) -REDIS_PASSWORD="${REDIS_PASSWORD:-XuqmRedis@2026}" +REDIS_PASSWORD="${REDIS_PASSWORD:-}" # 公有化平台地址(迁移时调用 export API) PUBLIC_PLATFORM_URL="https://dev.xuqinmin.com" @@ -61,6 +61,7 @@ PUBLIC_PLATFORM_URL="https://dev.xuqinmin.com" # 内部变量 # --------------------------------------------------------------------------- ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PRIVATE_VERSION="$(tr -d '[:space:]' < "$ROOT_DIR/VERSION")" STEP=0 TOTAL_STEPS=7 @@ -74,6 +75,14 @@ warn() { printf ' \033[33m⚠\033[0m %s\n' "$*"; } fail() { printf '\n\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; } step() { STEP=$((STEP+1)); log "$*"; } +random_secret() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -base64 32 | tr -d '\n' + else + dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d '\n' + fi +} + container_running() { docker ps --filter "name=$1" --filter "status=running" --format '{{.Names}}' 2>/dev/null | grep -q "$1" } @@ -86,6 +95,29 @@ container_running() { cd "$ROOT_DIR" +if [ "${XUQM_NONINTERACTIVE:-0}" != "1" ]; then + if [ "$REGISTRY" = "registry.example.com/xuqm" ]; then + read -rp " 私有镜像仓库(不含协议): " REGISTRY + REGISTRY_HOST="${REGISTRY%%/*}" + fi + [ -n "$REGISTRY_USER" ] || read -rp " 镜像仓库只读用户名: " REGISTRY_USER + if [ -z "$REGISTRY_PASSWORD" ]; then + read -rsp " 镜像仓库只读密码/令牌: " REGISTRY_PASSWORD + printf '\n' + fi +fi + +[ -n "$REGISTRY" ] && [ "$REGISTRY" != "registry.example.com/xuqm" ] || \ + fail "请指定私有镜像仓库地址" +[ -n "$REGISTRY_USER" ] && [ -n "$REGISTRY_PASSWORD" ] || \ + fail "请提供只读镜像拉取凭据" + +# 托管数据库和 Redis 的密码只在首次部署进程中随机生成,随后写入被 Git 忽略的 +# config/secrets.env;源码、镜像和普通文档中不保留任何可用默认密码。 +[ -n "$MYSQL_ROOT_PASSWORD" ] || MYSQL_ROOT_PASSWORD="$(random_secret)" +[ -n "$MYSQL_PASSWORD" ] || MYSQL_PASSWORD="$(random_secret)" +[ -n "$REDIS_PASSWORD" ] || REDIS_PASSWORD="$(random_secret)" + printf '\n\033[1;35m══════════════════════════════════════════════════\033[0m\n' printf '\033[1;35m XuqmGroup 私有化一键部署脚本(全量部署)\033[0m\n' printf '\033[1;35m══════════════════════════════════════════════════\033[0m\n' @@ -326,7 +358,7 @@ cat > "$ROOT_DIR/.env" < "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" < "$ROOT_DIR/config/docs/docs-runtime.json" </dev/null | head -1 || true)" if [ -n "$STATE" ]; then @@ -39,8 +39,8 @@ for svc in tenant-service file-service tenant-web nginx; do fi done -# Optional service containers (docs-site included here — warn only if present but unhealthy) -for svc in docs-site im-service push-service update-service; do +# 可选服务容器。 +for svc in im-service push-service update-service; do STATE="$(compose ps -q "$svc" 2>/dev/null | head -1 || true)" if [ -n "$STATE" ]; then STATUS="$(docker inspect --format='{{.State.Status}}' "$STATE" 2>/dev/null || echo 'unknown')" diff --git a/scripts/install.sh b/scripts/install.sh deleted file mode 100755 index ccb48d4..0000000 --- a/scripts/install.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -. "$ROOT_DIR/scripts/lib.sh" -load_env - -if [ ! -f "$ROOT_DIR/.env" ]; then - cp "$ROOT_DIR/.env.example" "$ROOT_DIR/.env" -fi -ensure_secret_file -load_env - -PROFILE="${COMPOSE_PROFILES:-base}" -OFFLINE_BUNDLE="" -SKIP_LOGIN=false - -usage() { - cat <<'EOF' -Usage: ./scripts/install.sh [options] - -Options: - --profile Docker Compose profiles, for example: base,im,push,update - --mysql-mode external or managed - --redis-mode external or managed - --offline-bundle Load images from an offline bundle before deployment - --skip-registry-login Skip docker login (use when already logged in or in offline mode) - -h, --help Show help -EOF -} - -while [ "$#" -gt 0 ]; do - case "$1" in - --skip-registry-login) - SKIP_LOGIN=true - shift - ;; - --profile) - PROFILE="${2:-}" - shift 2 - ;; - --profile=*) - PROFILE="${1#--profile=}" - shift - ;; - --mysql-mode) - MYSQL_MODE="${2:-}" - set_env_value "$ROOT_DIR/.env" "MYSQL_MODE" "$MYSQL_MODE" - set_env_value "$ROOT_DIR/config/infra.env" "MYSQL_MODE" "$MYSQL_MODE" - shift 2 - ;; - --mysql-mode=*) - MYSQL_MODE="${1#--mysql-mode=}" - set_env_value "$ROOT_DIR/.env" "MYSQL_MODE" "$MYSQL_MODE" - set_env_value "$ROOT_DIR/config/infra.env" "MYSQL_MODE" "$MYSQL_MODE" - shift - ;; - --redis-mode) - REDIS_MODE="${2:-}" - set_env_value "$ROOT_DIR/.env" "REDIS_MODE" "$REDIS_MODE" - set_env_value "$ROOT_DIR/config/infra.env" "REDIS_MODE" "$REDIS_MODE" - shift 2 - ;; - --redis-mode=*) - REDIS_MODE="${1#--redis-mode=}" - set_env_value "$ROOT_DIR/.env" "REDIS_MODE" "$REDIS_MODE" - set_env_value "$ROOT_DIR/config/infra.env" "REDIS_MODE" "$REDIS_MODE" - shift - ;; - --offline-bundle) - OFFLINE_BUNDLE="${2:-}" - shift 2 - ;; - --offline-bundle=*) - OFFLINE_BUNDLE="${1#--offline-bundle=}" - shift - ;; - -h|--help) - usage - exit 0 - ;; - *) - fail_json "XUQM_PRIVATE_4002" "unknown install option: $1" "install" - ;; - esac -done - -[ -n "$PROFILE" ] || fail_json "XUQM_PRIVATE_4003" "profile cannot be empty" "install" -set_env_value "$ROOT_DIR/.env" "COMPOSE_PROFILES" "$PROFILE" - -audit "install" "STARTED" "profile=$PROFILE" -progress "install" "STARTED" "profile=$PROFILE" - -require_cmd docker - -if [ -n "$OFFLINE_BUNDLE" ]; then - [ -f "$OFFLINE_BUNDLE" ] || fail_json "XUQM_PRIVATE_4004" "offline bundle not found: $OFFLINE_BUNDLE" "install" - docker load -i "$OFFLINE_BUNDLE" - SKIP_LOGIN=true -fi - -# Login to image registry (skip for offline bundles or when already authenticated) -if [ "$SKIP_LOGIN" = "false" ] && [ -n "${REGISTRY:-}" ]; then - REGISTRY_HOST="$(printf '%s' "$REGISTRY" | cut -d/ -f1)" - REGISTRY_USER="${REGISTRY_USER:-}" - REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-}" - if [ -n "$REGISTRY_USER" ] && [ -n "$REGISTRY_PASSWORD" ]; then - printf '%s' "$REGISTRY_PASSWORD" | \ - docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin \ - || fail_json "XUQM_PRIVATE_4005" "docker login failed for $REGISTRY_HOST" "install" - audit "install" "REGISTRY_LOGIN_OK" "host=$REGISTRY_HOST" - else - audit "install" "REGISTRY_LOGIN_SKIPPED" "REGISTRY_USER/REGISTRY_PASSWORD not set; assuming pre-authenticated" - fi -fi - -if [ "${MYSQL_MODE:-external}" = "managed" ]; then - "$ROOT_DIR/scripts/install-mysql.sh" -fi - -if [ "${REDIS_MODE:-external}" = "managed" ]; then - "$ROOT_DIR/scripts/install-redis.sh" -fi - -"$ROOT_DIR/scripts/render-config.sh" - -COMPOSE_PROFILES="$PROFILE" compose up -d -"$ROOT_DIR/scripts/healthcheck.sh" - -audit "install" "DONE" "profile=$PROFILE" -progress "install" "DONE" "profile=$PROFILE" diff --git a/scripts/render-config.sh b/scripts/render-config.sh index 58b4312..e397ce7 100755 --- a/scripts/render-config.sh +++ b/scripts/render-config.sh @@ -4,6 +4,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" . "$ROOT_DIR/scripts/lib.sh" load_env +PRIVATE_VERSION="${PRIVATE_VERSION:-$(tr -d '[:space:]' < "$ROOT_DIR/VERSION")}" strip_protocol() { local value="$1" @@ -31,7 +32,7 @@ mkdir -p "$ROOT_DIR/config/docs" "$ROOT_DIR/config/sdk" cat > "$ROOT_DIR/config/docs/docs-runtime.json" < "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" < "$ROOT_DIR/.deploy-state/current.json" </de fi [ "$_patched" -eq 1 ] && ok "secrets.env 已补齐 SPRING_DATASOURCE_PASSWORD / SPRING_DATA_REDIS_PASSWORD" +# 旧私有化部署可能依赖各服务源码中的 JWT 默认值。新版本不允许继续使用公开默认 +# 密钥,因此升级时生成一次部署独享密钥并持久化;后续升级必须原值保留。 +if ! grep -q "^XUQM_JWT_SECRET=." "$ROOT_DIR/config/secrets.env" 2>/dev/null; then + if command -v openssl >/dev/null 2>&1; then + _jwt_secret="$(openssl rand -base64 48 | tr -d '\n')" + else + _jwt_secret="$(dd if=/dev/urandom bs=48 count=1 2>/dev/null | base64 | tr -d '\n')" + fi + printf 'XUQM_JWT_SECRET=%s\n' "$_jwt_secret" >> "$ROOT_DIR/config/secrets.env" + chmod 600 "$ROOT_DIR/config/secrets.env" + ok "secrets.env 已生成部署独享 JWT 密钥(现有登录会话需重新登录)" +fi + # 商店监测凭据使用独立的 AES-256-GCM 主密钥。升级旧部署时只生成一次, # 后续更新必须保留原值,否则已有商店凭据将无法解密。 if ! grep -q "^XUQM_STORE_SECRET_MASTER_KEY=." "$ROOT_DIR/config/secrets.env" 2>/dev/null; then @@ -153,14 +172,26 @@ ok "NGINX_BIND=${_NGINX_BIND}" [ -n "$_CONSOLE_DOMAIN" ] || fail "config/xuqm.env 中 CONSOLE_DOMAIN 未设置,请手动补充后重试" -# 若 docker-compose.yml 包含 bugcollect-service 定义,激活 bugcollect profile, -# 确保 --remove-orphans 不把该服务的容器当孤儿停掉 -_ACTIVE_PROFILES="" +# 继承现有部署的全部 profile,删除已经废弃的 license;若 compose 包含 +# bugcollect-service,则补充 bugcollect。禁止只启用 bugcollect 后误把 base/update +# 等服务排除在拉取与编排计划之外。 +_ACTIVE_PROFILES="$( + printf '%s' "${COMPOSE_PROFILES:-base}" | + tr ',' '\n' | + awk 'NF && $0 != "license" && !seen[$0]++ { values[++n]=$0 } + END { for (i=1; i<=n; i++) printf "%s%s", (i>1 ? "," : ""), values[i] }' +)" +[ -n "$_ACTIVE_PROFILES" ] || _ACTIVE_PROFILES="base" if grep -q 'bugcollect-service:' "$ROOT_DIR/docker-compose.yml" 2>/dev/null; then - _ACTIVE_PROFILES="bugcollect" - ok "docker-compose.yml 包含 bugcollect-service → 激活 bugcollect profile" + case ",${_ACTIVE_PROFILES}," in + *,bugcollect,*) ;; + *) _ACTIVE_PROFILES="${_ACTIVE_PROFILES},bugcollect" ;; + esac + ok "docker-compose.yml 包含 bugcollect-service → 保留现有 profile 并激活 bugcollect" fi +_set_env "$ROOT_DIR/.env" "COMPOSE_PROFILES" "$_ACTIVE_PROFILES" export COMPOSE_PROFILES="${_ACTIVE_PROFILES}" +ok "升级使用 profiles=${_ACTIVE_PROFILES}" # --------------------------------------------------------------------------- # Step 1 — 检测 CONSOLE_DOMAIN 是否为裸 IP,提示更正 @@ -270,25 +301,10 @@ fi [ "$_FIXED" -eq 0 ] && ok "SDK 配置无需修复" # --------------------------------------------------------------------------- -# Step 3 — 同步 SDK 初始化配置文件 +# Step 3 — 通过唯一渲染器同步 SDK、文档和部署状态配置 # --------------------------------------------------------------------------- -if [ -f "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" ]; then - python3 - "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" \ - "$_CONSOLE_DOMAIN" "$_SDK_IM_WS_URL" <<'PY' -import json, sys -path, console, ws = sys.argv[1], sys.argv[2], sys.argv[3] -try: - d = json.load(open(path)) - d['controlBaseUrl'] = console - d['fileBaseUrl'] = console - d['imApiBaseUrl'] = console - d['imWsUrl'] = ws - json.dump(d, open(path, 'w'), ensure_ascii=False, indent=2) -except Exception as e: - print(f'warning: {e}', file=sys.stderr) -PY - ok "config/sdk/xuqm-private-sdk.json 已同步" -fi +bash "$ROOT_DIR/scripts/render-config.sh" +ok "SDK、文档和部署状态配置已由统一渲染器同步" # --------------------------------------------------------------------------- # Step 4 — 检测并修复宿主机 nginx WebSocket 代理头 diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh deleted file mode 100755 index 50f07ae..0000000 --- a/scripts/upgrade.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -. "$ROOT_DIR/scripts/lib.sh" -load_env - -TARGET_VERSION="${1:-}" -[ -n "$TARGET_VERSION" ] || fail_json "XUQM_PRIVATE_4020" "usage: upgrade.sh " "upgrade" - -CURRENT_VERSION="$(cat "$ROOT_DIR/VERSION" | tr -d '[:space:]')" -audit "upgrade" "STARTED" "from=$CURRENT_VERSION to=$TARGET_VERSION" -progress "upgrade" "STARTED" "from=$CURRENT_VERSION to=$TARGET_VERSION" - -printf 'Upgrading from %s to %s\n' "$CURRENT_VERSION" "$TARGET_VERSION" - -# Pre-upgrade backup -"$ROOT_DIR/scripts/backup.sh" -audit "upgrade" "BACKUP_DONE" "" - -# Validate target version exists in manifest (if present) -MANIFEST="$ROOT_DIR/image-manifest.json" -if command -v python3 >/dev/null 2>&1 && [ -f "$MANIFEST" ]; then - MANIFEST_VER="$(python3 -c "import json,sys; d=json.load(open('$MANIFEST')); print(d.get('privateVersion',''))" 2>/dev/null || true)" - if [ -n "$MANIFEST_VER" ] && [ "$MANIFEST_VER" != "$TARGET_VERSION" ]; then - printf 'WARNING: image-manifest.json declares version %s but upgrading to %s\n' "$MANIFEST_VER" "$TARGET_VERSION" - printf 'Update image-manifest.json before proceeding (y/N): ' - read -r CONFIRM - [ "$CONFIRM" = "y" ] || fail_json "XUQM_PRIVATE_4021" "upgrade aborted: version mismatch in image-manifest.json" "upgrade" - fi -fi - -# Save old image tag for rollback -OLD_TAG="${IMAGE_TAG:-$CURRENT_VERSION}" -set_env_value "$ROOT_DIR/.deploy-state/current.json.bak" "IMAGE_TAG" "$OLD_TAG" 2>/dev/null || true -printf '%s\n' "$OLD_TAG" > "$ROOT_DIR/.deploy-state/previous-image-tag.txt" - -# Update image tag -set_env_value "$ROOT_DIR/.env" "IMAGE_TAG" "$TARGET_VERSION" -set_env_value "$ROOT_DIR/.env" "PRIVATE_VERSION" "$TARGET_VERSION" -load_env - -# Pull new images for active profiles -PROFILES="${COMPOSE_PROFILES:-base}" -audit "upgrade" "PULL_STARTED" "profiles=$PROFILES" -COMPOSE_PROFILES="$PROFILES" compose pull -audit "upgrade" "PULL_DONE" "" - -# Render new configs -"$ROOT_DIR/scripts/render-config.sh" - -# Rolling restart — stop then start services -COMPOSE_PROFILES="$PROFILES" compose up -d --remove-orphans - -# Health check — rollback automatically on failure -if ! "$ROOT_DIR/scripts/healthcheck.sh"; then - printf 'Health check failed after upgrade. Rolling back...\n' - audit "upgrade" "HEALTHCHECK_FAILED" "rolling back to $OLD_TAG" - set_env_value "$ROOT_DIR/.env" "IMAGE_TAG" "$OLD_TAG" - set_env_value "$ROOT_DIR/.env" "PRIVATE_VERSION" "$OLD_TAG" - load_env - COMPOSE_PROFILES="$PROFILES" compose up -d --remove-orphans - audit "upgrade" "ROLLED_BACK" "restored to $OLD_TAG" - fail_json "XUQM_PRIVATE_4022" "upgrade failed; automatically rolled back to $OLD_TAG" "upgrade" -fi - -# Update VERSION file -printf '%s\n' "$TARGET_VERSION" > "$ROOT_DIR/VERSION" -audit "upgrade" "DONE" "from=$CURRENT_VERSION to=$TARGET_VERSION" -progress "upgrade" "DONE" "from=$CURRENT_VERSION to=$TARGET_VERSION" -printf 'Upgrade complete: %s → %s\n' "$CURRENT_VERSION" "$TARGET_VERSION" diff --git a/templates/env.external.tpl b/templates/env.external.tpl index 3a6f40d..9a67f41 100644 --- a/templates/env.external.tpl +++ b/templates/env.external.tpl @@ -1,6 +1,6 @@ -PRIVATE_VERSION=2026.05.18-private.1 +PRIVATE_VERSION=2026.07.28-private.1 REGISTRY=registry.example.com/xuqm -IMAGE_TAG=2026.05.18-private.1 +IMAGE_TAG=latest COMPOSE_PROFILES=base MYSQL_MODE=external diff --git a/templates/env.managed.tpl b/templates/env.managed.tpl index 96f1342..25d468e 100644 --- a/templates/env.managed.tpl +++ b/templates/env.managed.tpl @@ -1,6 +1,6 @@ -PRIVATE_VERSION=2026.05.18-private.1 +PRIVATE_VERSION=2026.07.28-private.1 REGISTRY=registry.example.com/xuqm -IMAGE_TAG=2026.05.18-private.1 +IMAGE_TAG=latest COMPOSE_PROFILES=base MYSQL_MODE=managed diff --git a/upgrade.sh b/upgrade.sh index 4796110..5558769 100644 --- a/upgrade.sh +++ b/upgrade.sh @@ -111,6 +111,11 @@ tar -xzf "$TMP_PKG" -C "$INSTALL_DIR" --strip-components=1 --exclude='*/data' chmod +x "$INSTALL_DIR/scripts/"*.sh \ "$INSTALL_DIR/install.sh" \ "$INSTALL_DIR/upgrade.sh" 2>/dev/null || true +# tar 解压只覆盖同名文件,不会删除新版已经移除的历史入口。 +rm -f "$INSTALL_DIR/scripts/configure.sh" \ + "$INSTALL_DIR/scripts/install.sh" \ + "$INSTALL_DIR/scripts/upgrade.sh" +find "$INSTALL_DIR/data" "$INSTALL_DIR/logs" -type f -name '.gitkeep' -delete 2>/dev/null || true ok "脚本和配置模板已更新" for _cf in .env config/secrets.env config/xuqm.env; do