From 0c7d44d795fc2033dc9ac3ceab28768bf3c16283 Mon Sep 17 00:00:00 2001 From: Milky0217 Date: Sun, 19 Apr 2026 20:55:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20=E6=94=B9=E8=BF=9B=E5=A4=87?= =?UTF-8?q?=E4=BB=BD=E5=91=BD=E5=90=8D=E5=92=8C=E6=B8=85=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E9=98=B2=E6=AD=A2=E7=89=88=E6=9C=AC=E9=87=8D?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:同一版本多次备份时,备份文件仅时间戳不同,内容完全相同, 导致清理后仍保留多个相同版本的备份,失去回滚多样性。 解决方案: 1. 备份文件名加入 Git hash + MD5:{timestamp}.{gitHash}.{md5} 2. 清理逻辑按 MD5 去重:相同版本的多个备份只保留最早一个 3. 手动回滚支持选择特定版本 影响: - 回滚时可明确知道恢复到哪个版本(Git hash + MD5 可验证) - 清理不再基于简单时间戳,而是基于版本内容 - 保留了真正的版本多样性 --- deploy.sh | 123 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 102 insertions(+), 21 deletions(-) diff --git a/deploy.sh b/deploy.sh index 478daca..31bda25 100755 --- a/deploy.sh +++ b/deploy.sh @@ -295,20 +295,44 @@ status() { echo "" } -# 回滚(手动触发) +# 回滚(手动触发)- 支持选择特定版本 rollback() { - log_step "查找最新备份..." + log_step "查找可用备份..." local latest_backup if [ "$DRY_RUN" = true ]; then - latest_backup="${REMOTE_DIR}/${PROJECT_NAME}.backup.20240101_120000" + latest_backup="${REMOTE_DIR}/${PROJECT_NAME}.backup.20240101_120000.unknown.12345678" log_dry "latest_backup=${latest_backup}" else - latest_backup=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ - "ls -t ${REMOTE_DIR}/${PROJECT_NAME}.backup.* 2>/dev/null | head -1" || echo "") + # 列出所有备份,让用户选择 + local backups + backups=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "ls -t ${REMOTE_DIR}/${PROJECT_NAME}.backup.* 2>/dev/null" || echo "") + if [ -z "$backups" ]; then + log_error "没有找到可用的备份" + return 1 + fi + + echo "" + echo -e "${BLUE}可用备份列表:${NC}" + echo "$backups" | head -10 | nl -v 0 + echo "" + + if [ "$SKIP_CONFIRM" = false ]; then + local count + count=$(echo "$backups" | wc -l) + read -p "选择要回滚的备份 [0-$(($count > 10 ? 9 : $count - 1)),默认 0 最新]: " -n 1 -r + echo + if [[ ! $REPLY =~ ^[0-9]$ ]]; then + REPLY=0 + fi + latest_backup=$(echo "$backups" | sed -n "$((REPLY + 1))p" || echo "") + else + latest_backup=$(echo "$backups" | head -1) + fi if [ -z "$latest_backup" ]; then - log_error "没有找到可用的备份" + log_error "无效的备份选择" return 1 fi fi @@ -336,7 +360,7 @@ rollback() { if [ "$DRY_RUN" = true ]; then log_dry "ssh ${REMOTE_USER}@${REMOTE_HOST} 'cp ${latest_backup} ${REMOTE_DIR}/${PROJECT_NAME}'" else - ssh "${REMOTE_USER}@${REMOTE_HOST}" "cp ${latest_backup} ${REMOTE_DIR}/${PROJECT_NAME}" + ssh "${REMOTE_USER}@${REMOTE_HOST}" "cp '${latest_backup}' '${REMOTE_DIR}/${PROJECT_NAME}'" fi log_step "启动服务..." @@ -395,35 +419,80 @@ rollback_on_failure() { send_webhook "failure" "部署测试失败,已自动回滚到: $(basename $backup_path)" } -# 清理旧备份,只保留最近 N 个 +# 清理旧备份,只保留最近 N 个(按内容去重,相同版本只保留最新的) cleanup_old_backups() { local keep_count="${1:-5}" if [ "$DRY_RUN" = true ]; then - log_dry "保留最近 ${keep_count} 个备份,删除其余" + log_dry "保留最近 ${keep_count} 个备份(相同版本只保留最新)" return 0 fi - local backup_count - backup_count=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ - "ls -t ${REMOTE_DIR}/${PROJECT_NAME}.backup.* 2>/dev/null | wc -l" || echo "0") + # 获取所有备份文件(按时间倒序) + local backups + backups=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ + "ls -t ${REMOTE_DIR}/${PROJECT_NAME}.backup.* 2>/dev/null" || echo "") - if [ "$backup_count" -le "$keep_count" ]; then + if [ -z "$backups" ]; then return 0 fi - local to_delete - to_delete=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" \ - "ls -t ${REMOTE_DIR}/${PROJECT_NAME}.backup.* 2>/dev/null | tail -n +$((keep_count + 1))" || echo "") + # 提取所有唯一版本的 MD5(从备份文件名中提取,格式:.backup.timestamp.gitHash.md5) + # 只保留每个 MD5 最早的备份(即该版本的第一次备份) + local seen_md5s="" + local to_delete="" + local kept_count=0 + + while IFS= read -r backup; do + if [ -z "$backup" ]; then + continue + fi + + # 从文件名提取 MD5(最后一段) + local md5 + md5=$(basename "$backup" | rev | cut -d'.' -f2 | rev || echo "") + + if [ -z "$md5" ]; then + # 旧格式备份,直接删除 + to_delete="${to_delete}${backup}"$'\n' + continue + fi + + if echo "$seen_md5s" | grep -q "^${md5}$"; then + # 已见过该版本,标记删除 + to_delete="${to_delete}${backup}"$'\n' + else + # 新版本,保留 + seen_md5s="${seen_md5s}${md5}"$'\n' + kept_count=$((kept_count + 1)) + fi + done <<< "$backups" + + # 如果保留的数量超过 keep_count,则从最早的开始删除 + if [ "$kept_count" -gt "$keep_count" ]; then + log_step "保留版本数超过 ${keep_count},按时间清理多余版本..." + + # 重新处理,删除超出数量的旧备份(基于时间顺序) + local count=0 + while IFS= read -r backup; do + if [ -z "$backup" ]; then + continue + fi + count=$((count + 1)) + if [ "$count" -gt "$keep_count" ]; then + to_delete="${to_delete}${backup}"$'\n' + fi + done <<< "$backups" + fi if [ -z "$to_delete" ]; then return 0 fi - log_step "清理旧备份(保留 ${keep_count} 个)..." - echo "$to_delete" | while read -r file; do + log_step "清理旧备份(保留 ${keep_count} 个不同版本)..." + echo "$to_delete" | while IFS= read -r file; do if [ -n "$file" ]; then - ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -f '${file}'" 2>/dev/null && echo " 已删除: ${file}" || true + ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -f '${file}'" 2>/dev/null && echo " 已删除: $(basename "$file")" || true fi done } @@ -500,10 +569,22 @@ backup_old_version() { local binary_path="./target/release/${PROJECT_NAME}" local timestamp timestamp=$(date +%Y%m%d_%H%M%S) - BACKUP_PATH="${REMOTE_DIR}/${PROJECT_NAME}.backup.${timestamp}" + + # 获取当前运行的二进制文件的 MD5,用于区分不同版本 + local current_md5="" + if [ "$DRY_RUN" = false ]; then + current_md5=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" "md5sum '${REMOTE_DIR}/${PROJECT_NAME}' 2>/dev/null | cut -d' ' -f1" || echo "") + fi + + # 获取 Git commit hash 用于版本标识 + local git_hash + git_hash=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") + + # 组合备份文件名:时间戳 + Git hash + MD5 前8位 + BACKUP_PATH="${REMOTE_DIR}/${PROJECT_NAME}.backup.${timestamp}.${git_hash}.${current_md5:0:8}" if [ "$DRY_RUN" = true ]; then - log_dry "ssh ${REMOTE_USER}@${REMOTE_HOST} 'if [ -f ${REMOTE_DIR}/${PROJECT_NAME} ]; then cp ${REMOTE_DIR}/${PROJECT_NAME} ${BACKUP_PATH}; echo \"\${BACKUP_PATH}\" > ${REMOTE_DIR}/.last_deployed; fi'" + log_dry "ssh ${REMOTE_USER}@${REMOTE_HOST} 'if [ -f ${REMOTE_DIR}/${PROJECT_NAME} ]; then cp ${REMOTE_DIR}/${PROJECT_NAME} '${BACKUP_PATH}'; echo \"\${BACKUP_PATH}\" > ${REMOTE_DIR}/.last_deployed; fi'" return 0 fi