24d5b7445f
- install.sh: - 滚动更新流程新增「选择 Git 访问方式」步骤(HTTPS 登录 / SSH 访问) - SSH 方式支持指定私钥文件路径或粘贴私钥内容(临时文件用后即删) - 仓库地址输入回填当前 remote(同协议时)作为默认值 - 选环境后立即检测工作区变更,提供暂存/放弃/取消处理 - 更新结束后按 HEAD 是否变化决定自动恢复暂存或提示手动恢复 - scripts/update.sh: - 新增 --ssh-key 参数,SSH 协议下通过 GIT_SSH_COMMAND -i 指定私钥 - 新增 --skip-dirty-check 参数,供 install.sh 预检后透传 - scripts/common.sh: - ctms_ask_line 提示语改输出到 stderr,避免被命令替换吞掉 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
577 lines
22 KiB
Bash
Executable File
577 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=scripts/common.sh
|
||
source "$ROOT_DIR/scripts/common.sh"
|
||
|
||
SELECTED_INDEX=0
|
||
SELECTED_ENV=""
|
||
SELECTED_BASE_URL=""
|
||
SELECTED_REPO_URL=""
|
||
SELECTED_SSH_KEY=""
|
||
SELECTED_SSH_KEY_TEMP=0
|
||
STASHED_BEFORE_UPDATE=0
|
||
ENV_STATUS_LINES=""
|
||
DEPLOYED_ENVS=""
|
||
BREADCRUMB="请选择操作"
|
||
OP_LABEL=""
|
||
UI_BOX_WIDTH=76
|
||
|
||
usage() {
|
||
cat <<EOF
|
||
|
||
${CC_PRIMARY}${C_BOLD}═══════════════════════════════════════════════════════════════${C_RESET}
|
||
${CC_PRIMARY}${C_BOLD} CTMS 部署控制台 - 使用帮助${C_RESET}
|
||
${CC_PRIMARY}${C_BOLD}═══════════════════════════════════════════════════════════════${C_RESET}
|
||
|
||
${C_BOLD}【主入口】${C_RESET} ${CC_INFO}./install.sh${C_RESET} ${CC_MUTED}↑/↓ 选择 Enter 确认 Q 退出${C_RESET}
|
||
|
||
${C_BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C_RESET}
|
||
|
||
${C_BOLD}【底层脚本(CI/CD)】${C_RESET}
|
||
|
||
${CC_INFO}bash scripts/install.sh <env> [--base-url <url>] [--yes] [--skip-build] [--skip-migrate]${C_RESET}
|
||
${CC_INFO}bash scripts/update.sh <env> [--repo-url <url>] [--branch <name>] [--yes] [--force]${C_RESET}
|
||
${CC_INFO}bash scripts/uninstall.sh <env>${C_RESET}
|
||
${CC_INFO}bash scripts/status.sh <env>${C_RESET}
|
||
|
||
${CC_MUTED}env: dev(开发)| main(内网测试)| release(生产)${C_RESET}
|
||
${CC_MUTED}update.sh 认证:export GIT_USERNAME=xxx GIT_PASSWORD=yyy${C_RESET}
|
||
|
||
${CC_MUTED}查看脚本帮助:${C_RESET} bash scripts/<script>.sh --help
|
||
|
||
${CC_PRIMARY}${C_BOLD}═══════════════════════════════════════════════════════════════${C_RESET}
|
||
|
||
EOF
|
||
}
|
||
|
||
require_interactive_terminal() {
|
||
[[ -t 0 && -t 1 ]] || ctms_fail "install.sh 需要在交互式终端中运行"
|
||
}
|
||
|
||
banner() {
|
||
printf '\n'
|
||
ctms_box_top "${C_BLUE}${C_BOLD}" "CTMS DEPLOYMENT CONSOLE" "$UI_BOX_WIDTH"
|
||
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "${ICON_BULLET} ${BREADCRUMB}" "${CC_INFO}${C_BOLD}${ICON_BULLET}${C_RESET} ${CC_PRIMARY}${C_BOLD}${BREADCRUMB}${C_RESET}"
|
||
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "↑/↓ 选择 Enter 确认 Q 退出" "${CC_MUTED}↑/↓ 选择 Enter 确认 Q 退出${C_RESET}"
|
||
ctms_box_bottom "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH"
|
||
printf '\n'
|
||
render_status_panel
|
||
}
|
||
|
||
render_status_panel() {
|
||
ctms_box_top "${C_MAGENTA}${C_BOLD}" "环境状态" "$UI_BOX_WIDTH"
|
||
printf '%b\n' "$ENV_STATUS_LINES" | while IFS= read -r line; do
|
||
ctms_box_line "${C_MAGENTA}${C_BOLD}" "$UI_BOX_WIDTH" "$line" "$line"
|
||
done
|
||
ctms_box_bottom "${C_MAGENTA}${C_BOLD}" "$UI_BOX_WIDTH"
|
||
printf '\n'
|
||
}
|
||
|
||
build_env_status() {
|
||
local output=""
|
||
local detected=0
|
||
local docker_error=""
|
||
DEPLOYED_ENVS=""
|
||
|
||
if ! command -v docker >/dev/null 2>&1; then
|
||
ENV_STATUS_LINES=" ${C_YELLOW}Docker 未安装或 docker 命令不可用${C_RESET}"
|
||
return
|
||
fi
|
||
|
||
if ! docker ps >/dev/null 2>&1; then
|
||
docker_error="$(docker ps 2>&1 >/dev/null || true)"
|
||
ENV_STATUS_LINES=" ${C_YELLOW}Docker 未运行或当前用户无权限访问 Docker${C_RESET}"
|
||
[[ -n "$docker_error" ]] && ENV_STATUS_LINES="${ENV_STATUS_LINES}\n ${C_DIM}${docker_error}${C_RESET}"
|
||
return
|
||
fi
|
||
|
||
local env project rows total running stopped status_label
|
||
for env in dev main release; do
|
||
project="$(ctms_project_name "$env")"
|
||
rows="$(docker ps -a \
|
||
--filter "label=com.docker.compose.project=$project" \
|
||
--format '{{.Names}}|{{.State}}|{{.Status}}' 2>/dev/null || true)"
|
||
[[ -n "$rows" ]] || continue
|
||
|
||
detected=1
|
||
DEPLOYED_ENVS="${DEPLOYED_ENVS}${env} "
|
||
total="$(printf '%s\n' "$rows" | awk 'NF { count++ } END { print count + 0 }')"
|
||
running="$(printf '%s\n' "$rows" | awk -F'|' '$2 == "running" { count++ } END { print count + 0 }')"
|
||
stopped=$(( total - running ))
|
||
|
||
if [[ "$running" -eq "$total" && "$total" -gt 0 ]]; then
|
||
status_label="${CC_SUCCESS}${ICON_RUNNING} 运行中${C_RESET}"
|
||
elif [[ "$running" -gt 0 ]]; then
|
||
status_label="${CC_WARN}${ICON_RUNNING} 部分运行${C_RESET}"
|
||
else
|
||
status_label="${CC_DANGER}${ICON_STOPPED} 已停止${C_RESET}"
|
||
fi
|
||
|
||
output="${output} ${CC_INFO}${C_BOLD}${env}${C_RESET} ${CC_MUTED}(${project})${C_RESET} ${status_label} ${CC_MUTED}${running}/${total} 运行"
|
||
[[ "$stopped" -gt 0 ]] && output="${output},${stopped} 已停止"
|
||
output="${output}${C_RESET}\n"
|
||
output="${output}$(printf '%s\n' "$rows" | awk -F'|' '{ printf " %-18s %-8s %s\n", $1, $2, $3 }')\n"
|
||
done
|
||
|
||
if [[ "$detected" -eq 0 ]]; then
|
||
rows="$(docker ps -a \
|
||
--filter "name=ctms_" \
|
||
--format '{{.Names}}|{{.State}}|{{.Status}}' 2>/dev/null || true)"
|
||
if [[ -n "$rows" ]]; then
|
||
total="$(printf '%s\n' "$rows" | awk 'NF { count++ } END { print count + 0 }')"
|
||
running="$(printf '%s\n' "$rows" | awk -F'|' '$2 == "running" { count++ } END { print count + 0 }')"
|
||
ENV_STATUS_LINES=" ${C_YELLOW}检测到 CTMS 命名容器,但无法从 Compose 标签确认环境${C_RESET}\n"
|
||
ENV_STATUS_LINES="${ENV_STATUS_LINES} ${running}/${total} 运行\n"
|
||
ENV_STATUS_LINES="${ENV_STATUS_LINES}$(printf '%s\n' "$rows" | awk -F'|' '{ printf " - %-18s %-8s %s\n", $1, $2, $3 }')"
|
||
else
|
||
ENV_STATUS_LINES=" ${C_DIM}未检测到 CTMS Compose 容器(ctms_dev / ctms_main / ctms_release)${C_RESET}"
|
||
fi
|
||
else
|
||
ENV_STATUS_LINES="${output%\\n}"
|
||
fi
|
||
}
|
||
|
||
select_menu() {
|
||
local title="$1"
|
||
local default_index="$2"
|
||
shift 2
|
||
local options=("$@")
|
||
local selected="${default_index:-0}"
|
||
local key=""
|
||
|
||
[[ "${#options[@]}" -gt 0 ]] || ctms_fail "菜单没有可选项"
|
||
|
||
trap ctms_restore_cursor EXIT INT TERM
|
||
printf '\033[?25l\033[2J'
|
||
while true; do
|
||
printf '\033[H'
|
||
banner
|
||
ctms_box_top "${C_BLUE}${C_BOLD}" "$title" "$UI_BOX_WIDTH"
|
||
|
||
local i
|
||
for i in "${!options[@]}"; do
|
||
if [[ "$i" -eq "$selected" ]]; then
|
||
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "${ICON_BULLET} ${options[$i]}" "${CC_INFO}${C_BOLD}${ICON_BULLET} ${options[$i]}${C_RESET}"
|
||
else
|
||
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "${ICON_BULLET_OFF} ${options[$i]}" "${CC_MUTED}${ICON_BULLET_OFF} ${options[$i]}${C_RESET}"
|
||
fi
|
||
done
|
||
ctms_box_bottom "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH"
|
||
printf '\033[J'
|
||
|
||
key="$(ctms_read_key)"
|
||
case "$key" in
|
||
"[A") selected=$(( (selected - 1 + ${#options[@]}) % ${#options[@]} )) ;;
|
||
"[B") selected=$(( (selected + 1) % ${#options[@]} )) ;;
|
||
"") ctms_restore_cursor; SELECTED_INDEX="$selected"; return ;;
|
||
q|Q) ctms_restore_cursor; printf '\033[2J\033[H'; exit 0 ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# 读取 .ctms-env 中的 KEY = VALUE 配置
|
||
ctms_config_get() {
|
||
local key="$1" file="$ROOT_DIR/.ctms-env"
|
||
[[ -f "$file" ]] || return 1
|
||
awk -F' *= *' -v k="$key" 'NF==2 && $1==k { print $2; exit }' "$file"
|
||
}
|
||
|
||
pick_env() {
|
||
local env default_index=0
|
||
env="$(ctms_config_get "env" || true)"
|
||
case "$env" in
|
||
main) default_index=1 ;;
|
||
release) default_index=2 ;;
|
||
esac
|
||
# 按显示宽度对齐三列(中文双宽,半角空格凑不齐)
|
||
local n0="dev" n1="main" n2="release"
|
||
if [[ -n "$env" ]]; then
|
||
case "$default_index" in
|
||
0) n0="dev(默认)" ;;
|
||
1) n1="main(默认)" ;;
|
||
2) n2="release(默认)" ;;
|
||
esac
|
||
fi
|
||
local -a opts=(
|
||
"$(ctms_pad_right "$n0" 14)$(ctms_pad_right "本地开发环境" 16)Vite 热加载 / 默认密钥"
|
||
"$(ctms_pad_right "$n1" 14)$(ctms_pad_right "内网测试环境" 16)预发布验证 / 生产模式"
|
||
"$(ctms_pad_right "$n2" 14)$(ctms_pad_right "生产环境" 16)HTTPS / RSA 密钥"
|
||
)
|
||
local menu_title="选择环境"
|
||
[[ -n "$env" ]] && menu_title="选择环境 (已识别: ${env})"
|
||
BREADCRUMB="${OP_LABEL:-操作} › 选择环境"
|
||
select_menu "$menu_title" "$default_index" "${opts[@]}"
|
||
case "$SELECTED_INDEX" in
|
||
0) SELECTED_ENV="dev" ;;
|
||
1) SELECTED_ENV="main" ;;
|
||
2) SELECTED_ENV="release" ;;
|
||
esac
|
||
}
|
||
|
||
# 把环境持久化到 .ctms-env,仅在操作成功完成后调用。
|
||
# 文件不存在则创建(首次安装场景);存在则就地更新 env 行。
|
||
# 若本次收集到健康检查地址(SELECTED_BASE_URL),一并写入/更新 base-url。
|
||
persist_env() {
|
||
local env_to_save="$1"
|
||
local file="$ROOT_DIR/.ctms-env"
|
||
|
||
if [[ ! -f "$file" ]]; then
|
||
{
|
||
printf 'env = %s\n' "$env_to_save"
|
||
[[ -n "${SELECTED_BASE_URL:-}" ]] && printf 'base-url = %s\n' "$SELECTED_BASE_URL"
|
||
} >"$file"
|
||
ctms_info "已创建 .ctms-env,环境记录为: $env_to_save"
|
||
return 0
|
||
fi
|
||
|
||
local current
|
||
current="$(ctms_config_get "env" || true)"
|
||
if [[ "$current" != "$env_to_save" ]]; then
|
||
sed -i.bak "s/^env *=.*/env = $env_to_save/" "$file" && rm -f "${file}.bak"
|
||
ctms_info "已将 .ctms-env 中的环境更新为: $env_to_save"
|
||
fi
|
||
|
||
# 同步 base-url:存在则替换该行,不存在则追加
|
||
if [[ -n "${SELECTED_BASE_URL:-}" ]]; then
|
||
local current_url
|
||
current_url="$(ctms_config_get "base-url" || true)"
|
||
if [[ "$current_url" != "$SELECTED_BASE_URL" ]]; then
|
||
if grep -q '^base-url *=' "$file"; then
|
||
sed -i.bak "s|^base-url *=.*|base-url = $SELECTED_BASE_URL|" "$file" && rm -f "${file}.bak"
|
||
else
|
||
printf 'base-url = %s\n' "$SELECTED_BASE_URL" >>"$file"
|
||
fi
|
||
ctms_info "已将 .ctms-env 中的健康检查地址更新为: $SELECTED_BASE_URL"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# 卸载专用:仅在“实际检测到容器”的环境中选择,避免选到空环境走完确认却什么都没卸。
|
||
# 始终弹出选择菜单(哪怕只有一个环境),作为危险确认前的显式安全闸门。
|
||
# 返回 0 表示已选定 SELECTED_ENV,返回 1 表示无可卸载环境(调用方应跳过卸载)。
|
||
pick_deployed_env() {
|
||
# DEPLOYED_ENVS 由 build_env_status 填充(空格分隔)
|
||
local -a envs=()
|
||
read -r -a envs <<<"${DEPLOYED_ENVS:-}"
|
||
|
||
if [[ "${#envs[@]}" -eq 0 ]]; then
|
||
printf '\n %s%s%s %s未检测到任何已部署的 CTMS 环境,无需卸载。%s\n' \
|
||
"${CC_WARN}${C_BOLD}" "$ICON_WARN" "${C_RESET}" "${CC_MUTED}" "${C_RESET}"
|
||
return 1
|
||
fi
|
||
|
||
# 卸载是破坏性操作:即使只有一个环境也显式选择一次,作为危险确认前的安全闸门。
|
||
local -a opts=()
|
||
local e
|
||
for e in "${envs[@]}"; do
|
||
opts+=("$(ctms_pad_right "$e" 14)$(ctms_pad_right "Compose 项目" 16)$(ctms_project_name "$e")")
|
||
done
|
||
BREADCRUMB="${OP_LABEL:-安全卸载} › 选择环境"
|
||
select_menu "选择要卸载的环境" "" "${opts[@]}"
|
||
SELECTED_ENV="${envs[$SELECTED_INDEX]}"
|
||
return 0
|
||
}
|
||
|
||
collect_install_options() {
|
||
local env="$1"
|
||
SELECTED_BASE_URL="$(ctms_config_get "base-url" || true)"
|
||
|
||
if [[ "$env" == "release" && -z "$SELECTED_BASE_URL" ]]; then
|
||
SELECTED_BASE_URL="$(ctms_ask_line "请输入健康检查地址,例如 https://ctms.example.com")"
|
||
[[ -n "$SELECTED_BASE_URL" ]] || ctms_fail "release 环境必须提供健康检查地址"
|
||
elif [[ "$env" == "main" && -z "$SELECTED_BASE_URL" ]]; then
|
||
SELECTED_BASE_URL="$(ctms_ask_line "请输入健康检查地址" "http://127.0.0.1:8888")"
|
||
fi
|
||
}
|
||
|
||
# 读取当前仓库 origin 的远程地址(供输入仓库地址时作默认值回填)
|
||
ctms_current_remote_url() {
|
||
git -C "$ROOT_DIR" remote get-url origin 2>/dev/null || true
|
||
}
|
||
|
||
# 滚动更新专用:选环境后立即检测工作区变更,脏则就地处理。
|
||
# 返回 0 表示可继续,返回 1 表示用户取消本次更新(调用方应跳过后续步骤)。
|
||
# 处理干净后,update.sh 会以 --skip-dirty-check 跳过重复检测。
|
||
check_workspace_clean() {
|
||
STASHED_BEFORE_UPDATE=0
|
||
cd "$ROOT_DIR" || return 1
|
||
command -v git >/dev/null 2>&1 || return 0 # 无 git 交给 update.sh 报错
|
||
git rev-parse --git-dir >/dev/null 2>&1 || return 0
|
||
|
||
local dirty=0
|
||
git diff-index --quiet HEAD -- 2>/dev/null || dirty=1
|
||
[[ -n "$(git ls-files --others --exclude-standard 2>/dev/null)" ]] && dirty=1
|
||
|
||
if [[ "$dirty" -eq 0 ]]; then
|
||
ctms_ok "工作目录干净"
|
||
return 0
|
||
fi
|
||
|
||
printf '\n %s%s%s 检测到本地有未提交/未跟踪的更改,请选择处理方式:\n' \
|
||
"${CC_WARN}${C_BOLD}" "$ICON_WARN" "${C_RESET}"
|
||
printf ' %s1)%s 暂存更改(git stash)\n' "${C_BOLD}" "${C_RESET}"
|
||
printf ' %s2)%s 放弃更改(git reset --hard)\n' "${C_BOLD}" "${C_RESET}"
|
||
printf ' %s3)%s 取消更新\n' "${C_BOLD}" "${C_RESET}"
|
||
printf ' 请输入选项 [1-3]: '
|
||
|
||
local choice
|
||
read -r choice || choice=""
|
||
case "$choice" in
|
||
1)
|
||
if git stash push -m "auto-stash before update $(date +%Y%m%d-%H%M%S)"; then
|
||
ctms_ok "已暂存本地更改(可用 'git stash pop' 恢复)"
|
||
STASHED_BEFORE_UPDATE=1
|
||
else
|
||
ctms_warn "暂存失败,已取消本次更新"
|
||
return 1
|
||
fi
|
||
;;
|
||
2)
|
||
printf ' 确认输入 %syes%s 放弃所有本地更改: ' "${C_BOLD}" "${C_RESET}"
|
||
local confirm
|
||
read -r confirm || confirm=""
|
||
[[ "$confirm" == "yes" ]] || { ctms_info "已取消本次更新"; return 1; }
|
||
git reset --hard HEAD || { ctms_warn "重置失败,已取消"; return 1; }
|
||
git clean -fd || true
|
||
ctms_ok "已放弃本地更改"
|
||
;;
|
||
*)
|
||
ctms_info "已取消本次更新"
|
||
return 1
|
||
;;
|
||
esac
|
||
return 0
|
||
}
|
||
|
||
# 滚动更新善后:根据更新前后 HEAD 是否变化,决定如何处理之前暂存的本地改动。
|
||
# 仅当 check_workspace_clean 走了「暂存」分支(STASHED_BEFORE_UPDATE=1)时才动作。
|
||
# · HEAD 未变(更新在拉取代码前就失败/中断)→ stash 基线仍匹配,自动 pop,无冲突风险;
|
||
# · HEAD 已变(代码已更新,无论成功失败)→ 不自动 pop,避免与新代码冲突,仅提示手动恢复。
|
||
restore_stash_after_update() {
|
||
[[ "$STASHED_BEFORE_UPDATE" -eq 1 ]] || return 0
|
||
|
||
local head_before="$1" head_after
|
||
head_after="$(git -C "$ROOT_DIR" rev-parse HEAD 2>/dev/null || true)"
|
||
|
||
if [[ -n "$head_before" && "$head_before" == "$head_after" ]]; then
|
||
ctms_info "代码未发生变化,正在自动恢复你暂存的本地改动..."
|
||
if git -C "$ROOT_DIR" stash pop >/dev/null 2>&1; then
|
||
ctms_ok "已自动恢复暂存的本地改动"
|
||
STASHED_BEFORE_UPDATE=0
|
||
return 0
|
||
fi
|
||
ctms_warn "自动恢复失败(可能存在冲突),改动仍保留在 stash 栈中"
|
||
fi
|
||
|
||
printf '\n %s%s%s 你更新前暂存的本地改动仍保留在 stash 栈中。\n' \
|
||
"${CC_WARN}${C_BOLD}" "$ICON_WARN" "${C_RESET}"
|
||
printf ' 恢复请执行: %sgit stash pop%s 查看请执行: %sgit stash list%s\n' \
|
||
"${C_BOLD}" "${C_RESET}" "${C_BOLD}" "${C_RESET}"
|
||
}
|
||
|
||
# SSH 访问专用:收集私钥来源,写入 SELECTED_SSH_KEY(私钥文件路径)。
|
||
# 两种来源:① 输入已有私钥文件路径;② 留空则改为粘贴私钥内容,写入 chmod 600 临时文件
|
||
# (SELECTED_SSH_KEY_TEMP=1 标记,run_update_script 调用后删除)。两者都留空则回退系统默认 SSH 配置。
|
||
collect_ssh_key() {
|
||
local key_path
|
||
key_path="$(ctms_ask_line "请输入 SSH 私钥文件路径(留空则改为粘贴私钥内容)")"
|
||
|
||
# 展开起始的 ~ 为 $HOME
|
||
[[ "$key_path" == "~"* ]] && key_path="${key_path/#\~/$HOME}"
|
||
|
||
if [[ -n "$key_path" ]]; then
|
||
[[ -f "$key_path" ]] || ctms_fail "SSH 私钥文件不存在: $key_path"
|
||
[[ -r "$key_path" ]] || ctms_fail "SSH 私钥文件不可读: $key_path"
|
||
SELECTED_SSH_KEY="$key_path"
|
||
ctms_info "将使用 SSH 私钥文件: $key_path"
|
||
return 0
|
||
fi
|
||
|
||
# 粘贴私钥内容:多行读取,以 Ctrl-D(EOF)结束
|
||
printf '\n %s%s%s 请粘贴 SSH 私钥内容,粘贴完成后按 %sCtrl-D%s 结束(留空则使用系统默认 SSH 配置):\n' \
|
||
"${CC_INFO}${C_BOLD}" "$ICON_PROMPT" "${C_RESET}" "${C_BOLD}" "${C_RESET}"
|
||
local content
|
||
content="$(cat)"
|
||
|
||
if [[ -z "$content" ]]; then
|
||
ctms_info "未提供私钥,将使用系统默认 SSH 配置(~/.ssh/)"
|
||
return 0
|
||
fi
|
||
|
||
local tmp_key
|
||
tmp_key="$(mktemp "/tmp/ctms-ssh-key-$$.XXXXXX")" || ctms_fail "无法创建临时私钥文件"
|
||
chmod 600 "$tmp_key"
|
||
printf '%s\n' "$content" >"$tmp_key"
|
||
SELECTED_SSH_KEY="$tmp_key"
|
||
SELECTED_SSH_KEY_TEMP=1
|
||
ctms_info "私钥内容已写入临时文件(用后即删)"
|
||
}
|
||
|
||
# 滚动更新专用:选择 Git 访问方式(HTTPS 登录 / SSH 密钥),并手动输入对应格式的仓库地址。
|
||
# 选好的地址写入 SELECTED_REPO_URL,由 run_update_script 透传给 update.sh --repo-url。
|
||
# update.sh 会按 URL 协议自动区分认证:https:// 走账号/Token,git@/ssh:// 走 SSH 密钥。
|
||
pick_access_method() {
|
||
SELECTED_REPO_URL=""
|
||
SELECTED_SSH_KEY=""
|
||
SELECTED_SSH_KEY_TEMP=0
|
||
local -a opts=(
|
||
"$(ctms_pad_right "HTTPS 登录" 16)$(ctms_pad_right "账号 / Token" 18)https:// 仓库地址"
|
||
"$(ctms_pad_right "SSH 访问" 16)$(ctms_pad_right "本机 SSH 密钥" 18)git@ 或 ssh:// 仓库地址"
|
||
)
|
||
BREADCRUMB="${OP_LABEL:-滚动更新} › ${SELECTED_ENV} › 选择访问方式"
|
||
select_menu "选择 Git 访问方式" "" "${opts[@]}"
|
||
|
||
# 当前 remote 地址:与所选协议匹配时作为默认值回填,留空即沿用
|
||
local current_url
|
||
current_url="$(ctms_current_remote_url)"
|
||
|
||
local url default=""
|
||
case "$SELECTED_INDEX" in
|
||
0)
|
||
[[ "$current_url" =~ ^https?:// ]] && default="$current_url"
|
||
url="$(ctms_ask_line "请输入仓库地址" "$default")"
|
||
[[ -n "$url" ]] || ctms_fail "仓库地址不能为空"
|
||
[[ "$url" =~ ^https?:// ]] || ctms_fail "HTTPS 方式仓库地址需以 http:// 或 https:// 开头"
|
||
;;
|
||
1)
|
||
[[ "$current_url" =~ ^(git@|ssh://) ]] && default="$current_url"
|
||
url="$(ctms_ask_line "请输入仓库地址" "$default")"
|
||
[[ -n "$url" ]] || ctms_fail "仓库地址不能为空"
|
||
[[ "$url" =~ ^(git@|ssh://) ]] || ctms_fail "SSH 方式仓库地址需以 git@ 或 ssh:// 开头"
|
||
collect_ssh_key
|
||
;;
|
||
esac
|
||
SELECTED_REPO_URL="$url"
|
||
}
|
||
|
||
run_script() {
|
||
local script="$1"
|
||
shift
|
||
[[ -x "$ROOT_DIR/scripts/$script" ]] || ctms_fail "脚本不可执行: $ROOT_DIR/scripts/$script"
|
||
"$ROOT_DIR/scripts/$script" "$@"
|
||
}
|
||
|
||
run_install_like_script() {
|
||
local script="$1"
|
||
local env="$2"
|
||
|
||
if [[ -n "$SELECTED_BASE_URL" ]]; then
|
||
run_script "$script" "$env" --base-url "$SELECTED_BASE_URL"
|
||
else
|
||
run_script "$script" "$env"
|
||
fi
|
||
}
|
||
|
||
# 滚动更新专用:在 install 透传基础上追加 --repo-url / --ssh-key,决定 update.sh 使用的访问方式。
|
||
run_update_script() {
|
||
local env="$1"
|
||
# 工作区脏检查已在 check_workspace_clean 处理过,让 update.sh 跳过重复检测
|
||
local -a args=("$env" --skip-dirty-check)
|
||
[[ -n "$SELECTED_REPO_URL" ]] && args+=(--repo-url "$SELECTED_REPO_URL")
|
||
[[ -n "$SELECTED_SSH_KEY" ]] && args+=(--ssh-key "$SELECTED_SSH_KEY")
|
||
[[ -n "$SELECTED_BASE_URL" ]] && args+=(--base-url "$SELECTED_BASE_URL")
|
||
|
||
local rc=0
|
||
run_script "update.sh" "${args[@]}" || rc=$?
|
||
|
||
# 清理本次粘贴生成的临时私钥
|
||
if [[ "$SELECTED_SSH_KEY_TEMP" -eq 1 && -n "$SELECTED_SSH_KEY" ]]; then
|
||
rm -f "$SELECTED_SSH_KEY" 2>/dev/null || true
|
||
fi
|
||
return "$rc"
|
||
}
|
||
|
||
# 操作完成后暂停,等用户确认再返回菜单
|
||
pause_return() {
|
||
printf '\n %s%s%s 按 Enter 返回主菜单…' "${CC_INFO}${C_BOLD}" "$ICON_PROMPT" "${C_RESET}"
|
||
read -r _ || true
|
||
}
|
||
|
||
interactive_main() {
|
||
BREADCRUMB="请选择操作"
|
||
select_menu "选择操作" "" \
|
||
"安装部署 配置环境、构建容器、迁移数据库、健康检查" \
|
||
"滚动更新 重建服务、执行迁移、复用安装健康检查" \
|
||
"安全卸载 停止并移除容器,保留 .env 与数据文件" \
|
||
"资源状态 查看 CPU、内存、网络与磁盘 I/O 快照" \
|
||
"使用帮助 查看入口和底层脚本说明" \
|
||
"退出控制台"
|
||
|
||
case "$SELECTED_INDEX" in
|
||
0)
|
||
OP_LABEL="安装部署"
|
||
pick_env
|
||
collect_install_options "$SELECTED_ENV"
|
||
BREADCRUMB="${OP_LABEL} › ${SELECTED_ENV} › 执行中"
|
||
if run_install_like_script "install.sh" "$SELECTED_ENV"; then
|
||
persist_env "$SELECTED_ENV"
|
||
fi
|
||
pause_return
|
||
;;
|
||
1)
|
||
OP_LABEL="滚动更新"
|
||
pick_env
|
||
if check_workspace_clean; then
|
||
pick_access_method
|
||
collect_install_options "$SELECTED_ENV"
|
||
BREADCRUMB="${OP_LABEL} › ${SELECTED_ENV} › 执行中"
|
||
# 记录更新前 HEAD,用于结束后判断代码是否被改动(决定 stash 能否安全自动恢复)
|
||
local head_before_update
|
||
head_before_update="$(git -C "$ROOT_DIR" rev-parse HEAD 2>/dev/null || true)"
|
||
if run_update_script "$SELECTED_ENV"; then
|
||
persist_env "$SELECTED_ENV"
|
||
fi
|
||
restore_stash_after_update "$head_before_update"
|
||
fi
|
||
pause_return
|
||
;;
|
||
2)
|
||
OP_LABEL="安全卸载"
|
||
if pick_deployed_env; then
|
||
BREADCRUMB="${OP_LABEL} › ${SELECTED_ENV} › 执行中"
|
||
run_script "uninstall.sh" "$SELECTED_ENV"
|
||
fi
|
||
pause_return
|
||
;;
|
||
3)
|
||
OP_LABEL="资源状态"
|
||
# 只读快照,无需选环境:直接用 .ctms-env 识别的环境(无配置回退 dev)
|
||
local env_for_status
|
||
env_for_status="$(ctms_config_get "env" || true)"
|
||
env_for_status="${env_for_status:-dev}"
|
||
BREADCRUMB="${OP_LABEL} › ${env_for_status}"
|
||
# status.sh 自身是交互循环(q/Enter 退出并清屏),正常退出直接回菜单;
|
||
# 仅在出错时暂停,避免错误信息被菜单重绘一闪而过。
|
||
run_script "status.sh" "$env_for_status" || pause_return
|
||
;;
|
||
4)
|
||
BREADCRUMB="使用帮助"
|
||
usage
|
||
pause_return
|
||
;;
|
||
5)
|
||
return 1
|
||
;;
|
||
esac
|
||
return 0
|
||
}
|
||
|
||
main() {
|
||
if [[ $# -gt 0 ]]; then
|
||
usage
|
||
ctms_fail "install.sh 不接受参数,请直接执行 ./install.sh"
|
||
fi
|
||
|
||
require_interactive_terminal
|
||
while true; do
|
||
build_env_status
|
||
interactive_main || break
|
||
done
|
||
printf '\033[2J\033[H'
|
||
exit 0
|
||
}
|
||
|
||
main "$@"
|