fix(deploy): 改进备份命名和清理逻辑,防止版本重复

问题:同一版本多次备份时,备份文件仅时间戳不同,内容完全相同,
导致清理后仍保留多个相同版本的备份,失去回滚多样性。

解决方案:
1. 备份文件名加入 Git hash + MD5:{timestamp}.{gitHash}.{md5}
2. 清理逻辑按 MD5 去重:相同版本的多个备份只保留最早一个
3. 手动回滚支持选择特定版本

影响:
- 回滚时可明确知道恢复到哪个版本(Git hash + MD5 可验证)
- 清理不再基于简单时间戳,而是基于版本内容
- 保留了真正的版本多样性
This commit is contained in:
2026-04-19 20:55:47 +08:00
parent 9974bfbf58
commit 0c7d44d795

123
deploy.sh
View File

@@ -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