fix(ci): 部署链路重构为拉取式,修复工作流问题
- deploy.sh: 目录级原子切换(current.new/current.old)替代 rm -rf 空窗式替换; 健康检查改为轮询 /api/version HTTP 接口;失败自动回滚上一版本 - 新增 deploy-watch.sh: 服务器本地 systemd timer 轮询 /ci-artifacts 产物指纹, 发现新版本自动部署——CI 与 AI 不再需要 SSH 到服务器执行任何命令 - server-deploy.yml: 部署包补入 web/ 后台静态资源;移除永不生效的前端构建死代码; 通知文案如实改为'构建成功'(部署由服务器侧 watcher 完成) - miniapp-preview.yml: 移除无意义的 --runInBand 测试参数 - 新增 PULL_DEPLOY.md 一次性安装文档(管理员手工执行)
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# 拉取式部署 watcher - 监听 CI 产物目录,发现新版本自动部署
|
||||
# 设计原则:
|
||||
# - CI 只负责构建并把产物写入 /ci-artifacts(runner 与本机同宿主)
|
||||
# - 部署动作只由服务器本地的本脚本触发,任何 AI/CI 均不通过 SSH 操作服务器
|
||||
# 安装方式见 .gitea/docs/PULL_DEPLOY.md(由管理员在服务器上手工执行一次)
|
||||
|
||||
set -u
|
||||
|
||||
ARTIFACT="${ARTIFACT:-/ci-artifacts/server-deploy-latest.tar.gz}"
|
||||
STATE_FILE="${STATE_FILE:-/opt/lunar/production/.last-deployed-sha256}"
|
||||
DEPLOY_SCRIPT="${DEPLOY_SCRIPT:-/opt/lunar/scripts/deploy.sh}"
|
||||
LOG_FILE="${LOG_FILE:-/opt/lunar/deploy-watch.log}"
|
||||
|
||||
log() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
# 产物不存在时静默等待
|
||||
if [ ! -f "$ARTIFACT" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 计算产物指纹
|
||||
CURRENT_SHA="$(sha256sum "$ARTIFACT" | awk '{print $1}')"
|
||||
LAST_SHA=""
|
||||
if [ -f "$STATE_FILE" ]; then
|
||||
LAST_SHA="$(cat "$STATE_FILE")"
|
||||
fi
|
||||
|
||||
# 无变化则跳过
|
||||
if [ "$CURRENT_SHA" = "$LAST_SHA" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "发现新版本产物 sha256=${CURRENT_SHA:0:12},开始部署"
|
||||
|
||||
# 执行部署;成功后才记录指纹(失败时下次轮询会重试)
|
||||
if bash "$DEPLOY_SCRIPT" "$ARTIFACT" >> "$LOG_FILE" 2>&1; then
|
||||
mkdir -p "$(dirname "$STATE_FILE")"
|
||||
printf '%s' "$CURRENT_SHA" > "$STATE_FILE"
|
||||
log "部署成功"
|
||||
else
|
||||
log "部署失败,保留旧指纹,等待下次轮询重试或人工介入"
|
||||
exit 1
|
||||
fi
|
||||
+50
-23
@@ -1,5 +1,6 @@
|
||||
#!/bin/bash
|
||||
# 自动部署脚本 - 部署到 1Panel Docker 容器
|
||||
# 设计原则:拉取式部署,只在服务器本地执行,CI 不通过 SSH 触发
|
||||
# 使用方法: ./deploy.sh <deploy_package_path>
|
||||
# deploy_package_path: 部署包路径(如 /ci-artifacts/server-deploy-latest.tar.gz)
|
||||
|
||||
@@ -9,6 +10,7 @@ DEPLOY_PACKAGE="$1"
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-lunar-server}"
|
||||
DEPLOY_DIR="${DEPLOY_DIR:-/opt/lunar/production}"
|
||||
BACKUP_DIR="${BACKUP_DIR:-/opt/lunar/backups}"
|
||||
HEALTH_URL="${HEALTH_URL:-http://localhost:8080/api/version}"
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
@@ -28,6 +30,18 @@ log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 回滚函数:恢复 current.old 并重启容器
|
||||
rollback() {
|
||||
if [ -d "$DEPLOY_DIR/current.old" ]; then
|
||||
rm -rf "$DEPLOY_DIR/current"
|
||||
mv "$DEPLOY_DIR/current.old" "$DEPLOY_DIR/current"
|
||||
docker restart "$CONTAINER_NAME" || log_error "回滚后容器重启失败,需人工介入"
|
||||
log_warn "已回滚到上一版本"
|
||||
else
|
||||
log_error "无可回滚版本,需人工介入"
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查部署包是否存在
|
||||
if [ ! -f "$DEPLOY_PACKAGE" ]; then
|
||||
log_error "部署包不存在: $DEPLOY_PACKAGE"
|
||||
@@ -52,7 +66,7 @@ if [ -d "$DEPLOY_DIR/current" ]; then
|
||||
tar -czf "$BACKUP_FILE" -C "$DEPLOY_DIR" current/ || log_warn "备份失败,继续部署"
|
||||
fi
|
||||
|
||||
# 解压新版本
|
||||
# 解压新版本到暂存目录(不碰 current,避免部署空窗)
|
||||
log_info "解压部署包..."
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
tar -xzf "$DEPLOY_PACKAGE" -C "$TEMP_DIR"
|
||||
@@ -64,15 +78,23 @@ if [ ! -d "$TEMP_DIR/deploy" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 部署新版本
|
||||
log_info "部署新版本..."
|
||||
rm -rf "$DEPLOY_DIR/current"
|
||||
mv "$TEMP_DIR/deploy" "$DEPLOY_DIR/current"
|
||||
rm -rf "$TEMP_DIR"
|
||||
# 校验二进制存在
|
||||
if [ ! -f "$TEMP_DIR/deploy/bin/server" ]; then
|
||||
log_error "部署包缺少 bin/server 二进制"
|
||||
rm -rf "$TEMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$TEMP_DIR/deploy/bin/server"
|
||||
|
||||
# 设置权限
|
||||
log_info "设置文件权限..."
|
||||
chmod +x "$DEPLOY_DIR/current/bin/server" || log_warn "设置可执行权限失败"
|
||||
# 部署新版本(目录级交换,接近原子)
|
||||
log_info "部署新版本..."
|
||||
mv "$TEMP_DIR/deploy" "$DEPLOY_DIR/current.new"
|
||||
rm -rf "$DEPLOY_DIR/current.old"
|
||||
if [ -d "$DEPLOY_DIR/current" ]; then
|
||||
mv "$DEPLOY_DIR/current" "$DEPLOY_DIR/current.old"
|
||||
fi
|
||||
mv "$DEPLOY_DIR/current.new" "$DEPLOY_DIR/current"
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
# 检查 Docker 容器是否存在
|
||||
if ! docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||
@@ -95,25 +117,30 @@ log_info "重启 Docker 容器: $CONTAINER_NAME"
|
||||
if docker restart "$CONTAINER_NAME"; then
|
||||
log_info "✅ 容器重启成功"
|
||||
else
|
||||
log_error "❌ 容器重启失败"
|
||||
log_error "❌ 容器重启失败,回滚到上一版本"
|
||||
rollback
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 等待容器启动
|
||||
log_info "等待容器启动..."
|
||||
sleep 3
|
||||
# 健康检查:轮询 HTTP 接口确认服务真正可用
|
||||
log_info "执行健康检查: $HEALTH_URL"
|
||||
HEALTHY=0
|
||||
for i in $(seq 1 10); do
|
||||
sleep 2
|
||||
if curl -fsS --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
||||
HEALTHY=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# 检查容器状态
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||
log_info "✅ 容器运行正常"
|
||||
|
||||
# 显示容器日志(最后 20 行)
|
||||
log_info "容器日志(最后 20 行):"
|
||||
docker logs --tail 20 "$CONTAINER_NAME"
|
||||
if [ "$HEALTHY" = "1" ]; then
|
||||
log_info "✅ 健康检查通过"
|
||||
curl -fsS --max-time 5 "$HEALTH_URL" && echo ""
|
||||
rm -rf "$DEPLOY_DIR/current.old"
|
||||
else
|
||||
log_error "❌ 容器未运行"
|
||||
log_error "容器日志:"
|
||||
docker logs --tail 50 "$CONTAINER_NAME"
|
||||
log_error "❌ 健康检查失败,回滚到上一版本"
|
||||
docker logs --tail 50 "$CONTAINER_NAME" || true
|
||||
rollback
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user