622 lines
21 KiB
Bash
Executable File
622 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=scripts/common.sh
|
||
source "$SCRIPT_DIR/common.sh"
|
||
|
||
# ── 全局变量 ─────────────────────────────────
|
||
TARGET_ENV=""
|
||
REPO_URL=""
|
||
GIT_BRANCH=""
|
||
SSH_KEY=""
|
||
ASSUME_YES=0
|
||
SKIP_BACKUP=0
|
||
SKIP_DIRTY_CHECK=0
|
||
FORCE_PULL=0
|
||
BOOTSTRAP_GIT_REPO=0
|
||
INSTALL_ARGS=()
|
||
|
||
# 项目身份验证标识文件(至少需要其中 2 个)
|
||
REQUIRED_PROJECT_FILES=(
|
||
"scripts/install.sh"
|
||
"docker-compose.yaml"
|
||
"backend/app/main.py"
|
||
"README.md"
|
||
)
|
||
|
||
# ── Banner ───────────────────────────────────
|
||
_banner() {
|
||
local b="${CC_PRIMARY}${C_BOLD}"
|
||
local a="${CC_INFO}"
|
||
local r="${C_RESET}"
|
||
printf '\n'
|
||
printf '%s╔══════════════════════════════════════════════╗%s\n' "$b" "$r"
|
||
printf '%s║%s ██╗ ██╗██████╗ ██████╗ █████╗ ████████╗ %s║%s\n' "$b" "$a" "${CC_PRIMARY}" "$r"
|
||
printf '%s║%s ██║ ██║██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝ %s║%s\n' "$b" "$a" "${CC_PRIMARY}" "$r"
|
||
printf '%s║%s ██║ ██║██████╔╝██║ ██║███████║ ██║ %s║%s\n' "$b" "$a" "${CC_PRIMARY}" "$r"
|
||
printf '%s║%s ██║ ██║██╔═══╝ ██║ ██║██╔══██║ ██║ %s║%s\n' "$b" "$a" "${CC_PRIMARY}" "$r"
|
||
printf '%s║%s ╚██████╔╝██║ ██████╔╝██║ ██║ ██║ %s║%s\n' "$b" "$a" "${CC_PRIMARY}" "$r"
|
||
printf '%s║%s ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ %s║%s\n' "$b" "$a" "${CC_PRIMARY}" "$r"
|
||
printf '%s║%s 临床试验管理系统 · 在线更新程序 %s║%s\n' "$b" "${CC_MUTED}" "${CC_PRIMARY}" "$r"
|
||
printf '%s╚══════════════════════════════════════════════╝%s\n' "$b" "$r"
|
||
printf '\n'
|
||
}
|
||
|
||
# ── 帮助 ─────────────────────────────────────
|
||
usage() {
|
||
cat <<EOF
|
||
${C_BOLD}用法:${C_RESET}
|
||
bash scripts/update.sh ${C_CYAN}<环境>${C_RESET} [选项]
|
||
|
||
${C_BOLD}环境:${C_RESET}
|
||
${C_CYAN}dev${C_RESET} 本地开发环境
|
||
${C_CYAN}main${C_RESET} 内网测试 / 预发布环境
|
||
${C_CYAN}release${C_RESET} 生产环境
|
||
|
||
${C_BOLD}选项:${C_RESET}
|
||
${C_WHITE}--repo-url <url>${C_RESET} Git 仓库地址(HTTPS 或 SSH)
|
||
不指定时会提示输入或使用当前远程地址
|
||
${C_WHITE}--branch <name>${C_RESET} 指定拉取的分支(默认为当前分支)
|
||
${C_WHITE}--ssh-key <path>${C_RESET} SSH 私钥文件路径(仅 SSH 仓库地址时生效)
|
||
不指定时使用本机默认 SSH 配置(~/.ssh/)
|
||
${C_WHITE}--yes${C_RESET} 跳过所有交互确认,适合 CI/CD 自动化执行
|
||
${C_WHITE}--skip-backup${C_RESET} 跳过更新前自动备份 tag(不推荐)
|
||
${C_WHITE}--skip-dirty-check${C_RESET} 跳过工作目录变更检测(由 install.sh 预检后透传)
|
||
${C_WHITE}--force${C_RESET} 强制覆盖本地更改(git reset --hard)
|
||
${C_WHITE}-h, --help${C_RESET} 显示此帮助信息
|
||
|
||
${C_BOLD}认证方式:${C_RESET}
|
||
${C_WHITE}1. SSH 密钥${C_RESET} 使用 SSH URL(如已配置密钥,自动使用)
|
||
${C_WHITE}2. Personal Token${C_RESET} 推荐使用 GitHub/Gitea Personal Access Token
|
||
${C_WHITE}3. 用户名密码${C_RESET} HTTPS 基础认证(不推荐,安全性较低)
|
||
|
||
可通过环境变量提供凭证(适合自动化):
|
||
GIT_USERNAME Git 用户名
|
||
GIT_PASSWORD Git 密码或 Personal Access Token
|
||
|
||
${C_BOLD}示例:${C_RESET}
|
||
# 交互式更新(会提示输入仓库地址)
|
||
bash scripts/update.sh dev
|
||
|
||
# 指定仓库地址更新
|
||
bash scripts/update.sh main --repo-url https://github.com/user/ctms-dev.git
|
||
|
||
# 全自动模式(从环境变量读取凭证)
|
||
export GIT_USERNAME="user"
|
||
export GIT_PASSWORD="ghp_xxxxxxxxxxxxx"
|
||
bash scripts/update.sh release --yes --repo-url https://github.com/user/ctms-dev.git
|
||
|
||
# 强制覆盖本地更改
|
||
bash scripts/update.sh dev --force
|
||
|
||
${C_BOLD}说明:${C_RESET}
|
||
更新流程:检查工作目录 → 验证仓库身份 → 拉取代码 → 调用安装脚本部署
|
||
默认会在拉取前创建备份 tag(格式:pre-update-YYYYMMDD-HHMMSS)
|
||
EOF
|
||
}
|
||
|
||
# ── 参数解析 ─────────────────────────────────
|
||
parse_args() {
|
||
if [[ $# -eq 0 ]]; then
|
||
usage
|
||
exit 1
|
||
fi
|
||
|
||
TARGET_ENV="$1"
|
||
shift
|
||
|
||
case "$TARGET_ENV" in
|
||
dev|main|release) ;;
|
||
-h|--help) usage; exit 0 ;;
|
||
*)
|
||
usage
|
||
ctms_fail "未知环境 \"$TARGET_ENV\",有效值为 dev、main 或 release"
|
||
;;
|
||
esac
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--repo-url)
|
||
[[ $# -ge 2 ]] || ctms_fail "--repo-url 需要提供一个 URL 参数"
|
||
REPO_URL="$2"
|
||
shift 2
|
||
;;
|
||
--branch)
|
||
[[ $# -ge 2 ]] || ctms_fail "--branch 需要提供分支名"
|
||
GIT_BRANCH="$2"
|
||
shift 2
|
||
;;
|
||
--ssh-key)
|
||
[[ $# -ge 2 ]] || ctms_fail "--ssh-key 需要提供私钥文件路径"
|
||
SSH_KEY="$2"
|
||
shift 2
|
||
;;
|
||
--yes) ASSUME_YES=1; shift ;;
|
||
--skip-backup) SKIP_BACKUP=1; shift ;;
|
||
--skip-dirty-check) SKIP_DIRTY_CHECK=1; shift ;;
|
||
--force) FORCE_PULL=1; shift ;;
|
||
-h|--help) usage; exit 0 ;;
|
||
--skip-build|--skip-migrate|--verbose|--base-url)
|
||
# 透传给 install.sh 的参数
|
||
INSTALL_ARGS+=("$1")
|
||
if [[ "$1" == "--base-url" && $# -ge 2 ]]; then
|
||
INSTALL_ARGS+=("$2")
|
||
shift
|
||
fi
|
||
shift
|
||
;;
|
||
*)
|
||
usage
|
||
ctms_fail "未知参数: $1"
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# ── Git 工具函数 ─────────────────────────────
|
||
check_git_installed() {
|
||
command -v git >/dev/null 2>&1 || ctms_fail "未检测到 git 命令,请先安装 Git"
|
||
}
|
||
|
||
detect_git_repository() {
|
||
if git rev-parse --git-dir >/dev/null 2>&1 && git rev-parse --verify HEAD >/dev/null 2>&1; then
|
||
BOOTSTRAP_GIT_REPO=0
|
||
else
|
||
BOOTSTRAP_GIT_REPO=1
|
||
fi
|
||
}
|
||
|
||
initialize_git_repository() {
|
||
[[ "$BOOTSTRAP_GIT_REPO" -eq 1 ]] || return 0
|
||
|
||
ctms_step "接管现有部署目录"
|
||
if [[ ! -d "$CTMS_ROOT_DIR/.git" ]]; then
|
||
git init "$CTMS_ROOT_DIR" >/dev/null 2>&1 || ctms_fail "初始化 Git 仓库失败"
|
||
fi
|
||
ctms_ok "已初始化 Git 仓库,准备同步远程代码"
|
||
}
|
||
|
||
get_current_branch() {
|
||
git branch --show-current 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "HEAD"
|
||
}
|
||
|
||
get_current_remote_url() {
|
||
local branch="${1:-$(get_current_branch)}"
|
||
local remote
|
||
remote="$(git config "branch.$branch.remote" 2>/dev/null || echo "origin")"
|
||
git config "remote.$remote.url" 2>/dev/null || echo ""
|
||
}
|
||
|
||
has_uncommitted_changes() {
|
||
! git diff-index --quiet HEAD -- 2>/dev/null
|
||
}
|
||
|
||
has_untracked_files() {
|
||
[[ -n "$(git ls-files --others --exclude-standard 2>/dev/null)" ]]
|
||
}
|
||
|
||
# ── 项目身份验证 ─────────────────────────────
|
||
verify_project_identity() {
|
||
ctms_step "验证项目身份"
|
||
|
||
local found=0
|
||
local file
|
||
for file in "${REQUIRED_PROJECT_FILES[@]}"; do
|
||
if [[ -f "$CTMS_ROOT_DIR/$file" ]]; then
|
||
found=$((found + 1))
|
||
fi
|
||
done
|
||
|
||
if [[ $found -lt 2 ]]; then
|
||
ctms_fail "项目身份验证失败:未检测到足够的 CTMS 项目特征文件\n" \
|
||
" 需要至少 2 个特征文件,当前仅找到 $found 个\n" \
|
||
" 请确认仓库地址是否正确"
|
||
fi
|
||
|
||
# 额外检查 README 内容
|
||
if [[ -f "$CTMS_ROOT_DIR/README.md" ]]; then
|
||
if ! grep -q "CTMS" "$CTMS_ROOT_DIR/README.md" 2>/dev/null; then
|
||
ctms_warn "README.md 不包含 CTMS 关键字,可能拉取了错误的仓库"
|
||
fi
|
||
fi
|
||
|
||
ctms_ok "项目身份验证通过(检测到 $found 个特征文件)"
|
||
}
|
||
|
||
# ── 工作目录状态检查 ─────────────────────────
|
||
check_working_directory() {
|
||
if [[ "$SKIP_DIRTY_CHECK" -eq 1 ]]; then
|
||
return 0
|
||
fi
|
||
|
||
ctms_step "检查工作目录状态"
|
||
|
||
if [[ "$FORCE_PULL" -eq 1 ]]; then
|
||
ctms_warn "已启用 --force,将强制覆盖本地更改"
|
||
return 0
|
||
fi
|
||
|
||
local has_changes=0
|
||
if has_uncommitted_changes; then
|
||
ctms_warn "检测到未提交的更改"
|
||
has_changes=1
|
||
fi
|
||
|
||
if has_untracked_files; then
|
||
ctms_warn "检测到未跟踪的文件"
|
||
has_changes=1
|
||
fi
|
||
|
||
if [[ $has_changes -eq 0 ]]; then
|
||
ctms_ok "工作目录干净"
|
||
return 0
|
||
fi
|
||
|
||
if [[ "$ASSUME_YES" -eq 1 ]]; then
|
||
ctms_fail "检测到未提交的更改且已启用 --yes 模式\n" \
|
||
" 请先提交或使用 --force 参数强制覆盖"
|
||
fi
|
||
|
||
printf '\n %s%s%s 选择处理方式:%s\n' "${CC_WARN}${C_BOLD}" "$ICON_PROMPT" "${C_RESET}" "${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)
|
||
ctms_info "暂存本地更改..."
|
||
git stash push -m "auto-stash before update $(date +%Y%m%d-%H%M%S)" || ctms_fail "暂存失败"
|
||
ctms_ok "已暂存本地更改(可使用 'git stash pop' 恢复)"
|
||
;;
|
||
2)
|
||
ctms_warn "即将放弃所有本地更改..."
|
||
printf ' 确认输入 %syes%s 继续: ' "${C_BOLD}" "${C_RESET}"
|
||
local confirm
|
||
read -r confirm || confirm=""
|
||
[[ "$confirm" == "yes" ]] || ctms_fail "用户取消操作"
|
||
git reset --hard HEAD || ctms_fail "重置失败"
|
||
git clean -fd || true
|
||
ctms_ok "已放弃本地更改"
|
||
;;
|
||
3|*)
|
||
ctms_fail "用户取消更新"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ── 仓库地址解析 ─────────────────────────────
|
||
resolve_repo_url() {
|
||
ctms_step "解析仓库地址"
|
||
|
||
# 如果未指定,尝试从当前 remote 获取
|
||
if [[ -z "$REPO_URL" ]]; then
|
||
REPO_URL="$(get_current_remote_url)"
|
||
if [[ -n "$REPO_URL" ]]; then
|
||
ctms_info "检测到当前远程地址: $REPO_URL"
|
||
fi
|
||
fi
|
||
|
||
# 如果仍为空且非交互模式,报错
|
||
if [[ -z "$REPO_URL" && "$ASSUME_YES" -eq 1 ]]; then
|
||
ctms_fail "未指定仓库地址且无法从当前 remote 获取\n" \
|
||
" 请使用 --repo-url 参数指定"
|
||
fi
|
||
|
||
# 交互式输入
|
||
if [[ -z "$REPO_URL" ]]; then
|
||
printf ' %s▸%s 请输入 Git 仓库地址: ' "${C_YELLOW}${C_BOLD}" "${C_RESET}"
|
||
read -r REPO_URL || true
|
||
fi
|
||
|
||
[[ -n "$REPO_URL" ]] || ctms_fail "仓库地址不能为空"
|
||
|
||
# 验证 URL 格式
|
||
if [[ ! "$REPO_URL" =~ ^(https?://|git@|ssh://) ]]; then
|
||
ctms_fail "仓库地址格式不正确\n" \
|
||
" 支持格式:https://..., git@..., ssh://..."
|
||
fi
|
||
|
||
ctms_ok "仓库地址: $REPO_URL"
|
||
}
|
||
|
||
# ── 远程引用选择 ─────────────────────────────
|
||
pick_remote_ref() {
|
||
# --branch 已指定则跳过
|
||
if [[ -n "$GIT_BRANCH" ]]; then
|
||
ctms_info "指定引用: $GIT_BRANCH"
|
||
return
|
||
fi
|
||
|
||
# --yes 模式无法交互,fallback 到当前分支
|
||
if [[ "$ASSUME_YES" -eq 1 ]]; then
|
||
GIT_BRANCH="$(get_current_branch)"
|
||
ctms_info "自动模式,使用当前分支: $GIT_BRANCH"
|
||
return
|
||
fi
|
||
|
||
ctms_step "选择远程引用"
|
||
|
||
local raw
|
||
raw="$(git ls-remote --refs "$REPO_URL" 2>/dev/null)" || {
|
||
GIT_BRANCH="$(ctms_ask_line "无法列出远程引用,请手动输入分支/标签/SHA" "$(get_current_branch)")"
|
||
return
|
||
}
|
||
|
||
local -a names types
|
||
local sha ref name
|
||
|
||
while IFS=$'\t' read -r sha ref; do
|
||
name="${ref#refs/heads/}"
|
||
names+=("$name"); types+=("branch")
|
||
done < <(printf '%s\n' "$raw" | grep $'\trefs/heads/')
|
||
|
||
while IFS=$'\t' read -r sha ref; do
|
||
name="${ref#refs/tags/}"
|
||
names+=("$name"); types+=("tag")
|
||
done < <(printf '%s\n' "$raw" | grep $'\trefs/tags/' | sort -rt/ -k3 | head -10)
|
||
|
||
local manual_idx=$(( ${#names[@]} + 1 ))
|
||
local choice i
|
||
|
||
while true; do
|
||
printf '\n'
|
||
for i in "${!names[@]}"; do
|
||
printf ' %s%2d)%s %s%-8s%s %s\n' \
|
||
"${C_BOLD}" $((i+1)) "${C_RESET}" \
|
||
"${CC_MUTED}" "${types[$i]}" "${C_RESET}" "${names[$i]}"
|
||
done
|
||
printf ' %s%2d)%s %s手动输入 SHA / 分支名 / 标签名%s\n\n' \
|
||
"${C_BOLD}" "$manual_idx" "${C_RESET}" "${CC_MUTED}" "${C_RESET}"
|
||
|
||
printf ' %s▸%s 请选择 (1-%d): ' "${C_YELLOW}${C_BOLD}" "${C_RESET}" "$manual_idx"
|
||
read -r choice || choice=""
|
||
|
||
if [[ "$choice" == "$manual_idx" || -z "$choice" ]]; then
|
||
GIT_BRANCH="$(ctms_ask_line "请输入分支名、标签或 SHA")"
|
||
break
|
||
elif [[ "$choice" =~ ^[0-9]+$ && "$choice" -ge 1 && "$choice" -le "${#names[@]}" ]]; then
|
||
GIT_BRANCH="${names[$((choice-1))]}"
|
||
ctms_ok "已选择 ${types[$((choice-1))]}: $GIT_BRANCH"
|
||
break
|
||
else
|
||
ctms_warn "无效选择,请重新输入"
|
||
fi
|
||
done
|
||
}
|
||
|
||
# ── 认证处理 ─────────────────────────────────
|
||
setup_git_credentials() {
|
||
# SSH 协议不需要额外配置
|
||
if [[ "$REPO_URL" =~ ^(git@|ssh://) ]]; then
|
||
if [[ -n "$SSH_KEY" ]]; then
|
||
[[ -f "$SSH_KEY" ]] || ctms_fail "指定的 SSH 私钥文件不存在: $SSH_KEY"
|
||
# 通过 GIT_SSH_COMMAND 指定私钥;IdentitiesOnly 确保只用这把钥匙
|
||
export GIT_SSH_COMMAND="ssh -i '$SSH_KEY' -o IdentitiesOnly=yes"
|
||
ctms_info "使用 SSH 协议,已指定私钥: $SSH_KEY"
|
||
else
|
||
ctms_info "使用 SSH 协议,使用本机默认 SSH 配置"
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
ctms_step "配置 Git 认证"
|
||
|
||
# 从环境变量读取凭证
|
||
local username="${GIT_USERNAME:-}"
|
||
local password="${GIT_PASSWORD:-}"
|
||
|
||
# 如果环境变量未设置且非自动模式,交互式输入
|
||
if [[ -z "$username" && "$ASSUME_YES" -eq 0 ]]; then
|
||
printf ' %s▸%s Git 用户名: ' "${C_YELLOW}${C_BOLD}" "${C_RESET}"
|
||
read -r username || username=""
|
||
fi
|
||
|
||
if [[ -z "$password" && "$ASSUME_YES" -eq 0 ]]; then
|
||
printf ' %s▸%s Git 密码/Token (输入不回显): ' "${C_YELLOW}${C_BOLD}" "${C_RESET}"
|
||
read -rs password || password=""
|
||
printf '\n'
|
||
fi
|
||
|
||
# 如果凭证为空,尝试使用系统凭证存储
|
||
if [[ -z "$username" || -z "$password" ]]; then
|
||
ctms_info "未提供凭证,将尝试使用系统凭证存储"
|
||
return 0
|
||
fi
|
||
|
||
# 配置临时凭证助手(仅本次会话有效)
|
||
export GIT_ASKPASS_USERNAME="$username"
|
||
export GIT_ASKPASS_PASSWORD="$password"
|
||
|
||
# 创建临时凭证助手脚本
|
||
local askpass_script="/tmp/git-askpass-$$.sh"
|
||
cat > "$askpass_script" <<'ASKPASS'
|
||
#!/bin/bash
|
||
case "$1" in
|
||
*Username*) echo "$GIT_ASKPASS_USERNAME" ;;
|
||
*Password*) echo "$GIT_ASKPASS_PASSWORD" ;;
|
||
esac
|
||
ASKPASS
|
||
chmod +x "$askpass_script"
|
||
export GIT_ASKPASS="$askpass_script"
|
||
|
||
ctms_ok "认证配置完成"
|
||
}
|
||
|
||
cleanup_git_credentials() {
|
||
if [[ -n "${GIT_ASKPASS:-}" && -f "$GIT_ASKPASS" ]]; then
|
||
rm -f "$GIT_ASKPASS" 2>/dev/null || true
|
||
fi
|
||
unset GIT_ASKPASS GIT_ASKPASS_USERNAME GIT_ASKPASS_PASSWORD
|
||
unset GIT_SSH_COMMAND
|
||
}
|
||
|
||
# ── 备份当前版本 ─────────────────────────────
|
||
create_backup_tag() {
|
||
if [[ "$SKIP_BACKUP" -eq 1 ]]; then
|
||
ctms_info "已跳过备份(--skip-backup)"
|
||
return 0
|
||
fi
|
||
|
||
ctms_step "创建更新前备份"
|
||
|
||
local backup_tag="pre-update-$(date +%Y%m%d-%H%M%S)"
|
||
if git tag "$backup_tag" 2>/dev/null; then
|
||
ctms_ok "已创建备份 tag: $backup_tag"
|
||
ctms_info "如需回滚,执行: git reset --hard $backup_tag"
|
||
else
|
||
ctms_warn "备份 tag 创建失败(可能当前无提交)"
|
||
fi
|
||
}
|
||
|
||
# ── 拉取代码 ─────────────────────────────────
|
||
fetch_and_pull() {
|
||
ctms_step "拉取远程代码"
|
||
|
||
local remote="origin"
|
||
|
||
# 更新或添加 remote
|
||
if git remote | grep -q "^${remote}$"; then
|
||
git remote set-url "$remote" "$REPO_URL" || ctms_fail "更新远程地址失败"
|
||
else
|
||
git remote add "$remote" "$REPO_URL" || ctms_fail "添加远程仓库失败"
|
||
fi
|
||
|
||
# Fetch(含 tags)
|
||
ctms_info "正在从远程仓库获取更新..."
|
||
if ! git fetch "$remote" --tags 2>&1 | while IFS= read -r line; do
|
||
printf ' %s\n' "$line"
|
||
done; then
|
||
cleanup_git_credentials
|
||
ctms_fail "拉取失败,请检查网络连接和仓库权限"
|
||
fi
|
||
|
||
# 支持分支、标签、SHA — 统一 reset --hard
|
||
if git rev-parse --verify "$remote/$GIT_BRANCH" >/dev/null 2>&1; then
|
||
ctms_info "切换到远程分支: $GIT_BRANCH"
|
||
git reset --hard "$remote/$GIT_BRANCH" || ctms_fail "重置失败"
|
||
elif git rev-parse --verify "$GIT_BRANCH" >/dev/null 2>&1; then
|
||
ctms_info "切换到引用: $GIT_BRANCH"
|
||
git reset --hard "$GIT_BRANCH" || ctms_fail "重置失败"
|
||
else
|
||
cleanup_git_credentials
|
||
ctms_fail "无法解析引用 '$GIT_BRANCH',请确认分支/标签/SHA 正确"
|
||
fi
|
||
|
||
cleanup_git_credentials
|
||
ctms_ok "代码拉取完成"
|
||
}
|
||
|
||
# ── 更新确认 ─────────────────────────────────
|
||
confirm_update() {
|
||
if [[ "$ASSUME_YES" -eq 1 ]]; then
|
||
ctms_info "已启用 --yes,跳过确认"
|
||
return 0
|
||
fi
|
||
|
||
ctms_step "确认更新参数"
|
||
|
||
local w=66
|
||
local row
|
||
row() {
|
||
local label="$1" value="$2" vcolor="${3:-$CC_TEXT}"
|
||
local padded
|
||
padded="$(ctms_pad_right "$label" 14)"
|
||
ctms_box_line "${CC_PRIMARY}${C_BOLD}" "$w" \
|
||
"${padded}${value}" \
|
||
"${CC_MUTED}${padded}${C_RESET}${vcolor}${value}${C_RESET}"
|
||
}
|
||
|
||
printf '\n'
|
||
ctms_box_top "${CC_PRIMARY}${C_BOLD}" "更新参数预览" "$w"
|
||
row "目标环境" "$TARGET_ENV" "${CC_INFO}${C_BOLD}"
|
||
row "仓库地址" "$REPO_URL" "${CC_INFO}"
|
||
row "目标引用" "$GIT_BRANCH" "${CC_TEXT}"
|
||
row "本地更改" "$([[ "$FORCE_PULL" -eq 1 ]] && echo "强制跳过检查" || echo "检测并确认")" "${CC_TEXT}"
|
||
row "备份策略" "$([[ "$SKIP_BACKUP" -eq 1 ]] && echo "跳过" || echo "自动创建 tag")" "${CC_TEXT}"
|
||
ctms_box_bottom "${CC_PRIMARY}${C_BOLD}" "$w"
|
||
|
||
printf '\n %s%s%s 确认以上参数并继续更新?输入 %syes%s 继续: ' \
|
||
"${CC_WARN}${C_BOLD}" "$ICON_PROMPT" "${C_RESET}" "${C_BOLD}" "${C_RESET}"
|
||
local answer
|
||
read -r answer || answer=""
|
||
[[ "$answer" == "yes" ]] || ctms_fail "用户取消更新"
|
||
}
|
||
|
||
# ── 调用安装脚本 ─────────────────────────────
|
||
run_install_script() {
|
||
ctms_step "执行部署更新"
|
||
|
||
local install_script="$CTMS_ROOT_DIR/scripts/install.sh"
|
||
[[ -x "$install_script" ]] || ctms_fail "安装脚本不可执行: $install_script"
|
||
|
||
ctms_info "调用安装脚本完成容器更新..."
|
||
"$install_script" "$TARGET_ENV" "${INSTALL_ARGS[@]}"
|
||
}
|
||
|
||
# ── 完成提示 ─────────────────────────────────
|
||
show_success() {
|
||
local w=66
|
||
local current_commit
|
||
current_commit="$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")"
|
||
|
||
printf '\n'
|
||
ctms_box_top "${CC_SUCCESS}${C_BOLD}" "$ICON_OK CTMS 更新成功" "$w"
|
||
ctms_box_line "${CC_SUCCESS}${C_BOLD}" "$w" \
|
||
"$(ctms_pad_right "当前版本" 14)${current_commit}" \
|
||
"${CC_MUTED}$(ctms_pad_right "当前版本" 14)${C_RESET}${CC_INFO}${current_commit}${C_RESET}"
|
||
ctms_box_line "${CC_SUCCESS}${C_BOLD}" "$w" \
|
||
"$(ctms_pad_right "环境" 14)${TARGET_ENV}" \
|
||
"${CC_MUTED}$(ctms_pad_right "环境" 14)${C_RESET}${CC_TEXT}${TARGET_ENV}${C_RESET}"
|
||
ctms_box_bottom "${CC_SUCCESS}${C_BOLD}" "$w"
|
||
printf '\n'
|
||
}
|
||
|
||
# ── 入口 ─────────────────────────────────────
|
||
main() {
|
||
_banner
|
||
parse_args "$@"
|
||
cd "$CTMS_ROOT_DIR"
|
||
|
||
check_git_installed
|
||
detect_git_repository
|
||
if [[ "$BOOTSTRAP_GIT_REPO" -eq 1 ]]; then
|
||
ctms_warn "部署目录缺少可用的 Git 历史,将从指定仓库接管现有部署"
|
||
# 必须先确认这是 CTMS 目录,避免在任意目录中初始化并覆盖文件。
|
||
verify_project_identity
|
||
else
|
||
check_working_directory
|
||
fi
|
||
resolve_repo_url
|
||
setup_git_credentials
|
||
pick_remote_ref
|
||
confirm_update
|
||
|
||
# 先验证当前项目身份
|
||
verify_project_identity
|
||
initialize_git_repository
|
||
|
||
# 创建备份
|
||
create_backup_tag
|
||
|
||
# 拉取代码
|
||
fetch_and_pull
|
||
|
||
# 拉取后再次验证(防止拉错仓库)
|
||
verify_project_identity
|
||
|
||
# 调用安装脚本完成部署
|
||
run_install_script
|
||
|
||
show_success
|
||
}
|
||
|
||
# 捕获退出信号,清理凭证
|
||
trap cleanup_git_credentials EXIT
|
||
|
||
main "$@"
|