diff --git a/CLAUDE.md b/CLAUDE.md index ce9fda0..e27650c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,6 +14,13 @@ npm run build # Build production files to dist/ npm run build:clean # Clean then build npm run preview # Serve dist/ on port 3000 (npx serve) +# Deploy (统一发布脚本) +./scripts/deploy.sh build # 构建静态产物 +./scripts/deploy.sh deploy # 构建并发布到生产服务器 +./scripts/deploy.sh deploy --skip-build # 使用现有 dist/ 直接发布 +./scripts/deploy.sh rollback # 回滚到最近一次远程备份 +./scripts/deploy.sh status # 查看生产环境发布状态 + # Linting & Type Checking npm run lint # ESLint (configured in config/lint/.eslintrc.json) npm run type-check # tsc --noEmit diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 16bf75a..6a6d1ca 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,5 +1,7 @@ # Novalon网站部署文档 +> ⚠️ **本文档部分内容为历史 Docker Compose 部署记录。当前唯一发布入口为 `scripts/deploy.sh`(远程静态发布 dist/ 到生产 Nginx),详细说明见 [docs/deployment.md](docs/deployment.md)。** + ## 项目信息 - **项目名称**: Novalon官网 @@ -94,13 +96,13 @@ - 提供Let's Encrypt配置指导 - 设置证书文件权限 -### 6. deploy.sh -- **作用**: 自动化部署脚本 +### 6. scripts/deploy.sh +- **作用**: 统一发布脚本(当前唯一发布入口) - **功能**: - - 上传部署文件到服务器 - - 配置SSL证书 - - 启动Docker容器 - - 检查容器状态和日志 + - `build`:构建静态产物 + - `deploy`:发布 `dist/` 到生产服务器(备份 + rsync + 权限 + Nginx 重载 + 验证) + - `rollback`:回滚到最近一次远程备份 + - `status`:查看生产环境发布状态 ## 部署步骤 @@ -155,10 +157,10 @@ git push origin feat-dynamic - nginx.conf - .env.example - setup-ssl.sh -- deploy.sh +- scripts/deploy.sh # 设置脚本执行权限 -chmod +x deploy.sh setup-ssl.sh +chmod +x scripts/deploy.sh setup-ssl.sh ``` **配置详情**: @@ -247,8 +249,8 @@ certbot renew --dry-run **自动化部署**: ```bash -# 执行部署脚本 -./deploy.sh +# 执行统一发布脚本(构建 + 发布) +./scripts/deploy.sh deploy ``` **手动部署**: diff --git a/Jenkinsfile b/Jenkinsfile index d0839d7..cd9834a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -8,7 +8,6 @@ pipeline { SERVER_IP = '139.155.109.62' SERVER_USER = 'root' DEPLOY_ROOT = '/home/novalon/docker-app' - STATIC_DIR = 'novalon-static' NGINX_CONTAINER = 'novalon-nginx-secure' DOMAIN = 'https://novalon.cn' BACKUP_RETENTION_COUNT = 3 @@ -53,7 +52,7 @@ pipeline { steps { echo "==========================================" echo "🚀 Novalon Website CI/CD Pipeline" - echo "🐳 Docker Shell Mode (参考 deploy-dist.sh)" + echo "🐳 Docker Shell Mode (参考 scripts/deploy.sh)" echo "==========================================" sh ''' @@ -286,156 +285,19 @@ pipeline { echo "⚠️ 准备部署到生产环境: ${DOMAIN}" sleep(time: 3, unit: 'SECONDS') - sh """ - set -e - - DIST_DIR='dist' - - echo "==========================================" - echo "📋 步骤 1: 检查 dist 目录" - echo "==========================================" - if [ ! -d "\$DIST_DIR" ]; then - echo "❌ dist 目录不存在,请先构建" - exit 1 - fi - DIST_SIZE=\$(du -sh "\$DIST_DIR" | cut -f1) - echo "✅ dist 目录大小: \$DIST_SIZE" - - echo "" - echo "==========================================" - echo "📋 步骤 2: 验证 SSH 连接" - echo "==========================================" - if ! ssh -o ConnectTimeout=5 ${SERVER_USER}@${SERVER_IP} exit; then - echo "❌ 无法连接到服务器 ${SERVER_IP}" - exit 1 - fi - echo "✅ SSH 连接验证成功" - - echo "" - echo "==========================================" - echo "📋 步骤 3: 备份旧版本" - echo "==========================================" - ssh ${SERVER_USER}@${SERVER_IP} " - set -e - STATIC_PATH='${DEPLOY_ROOT}/${STATIC_DIR}' - TIMESTAMP=\$(date +%Y%m%d_%H%M%S) - BACKUP_PATH='${DEPLOY_ROOT}/${STATIC_DIR}_backup_\${TIMESTAMP}' - - if [ -d \"\$STATIC_PATH\" ]; then - echo \"📦 备份旧版本到: \$BACKUP_PATH\" - cp -r \"\$STATIC_PATH\" \"\$BACKUP_PATH\" - echo \"✅ 备份完成\" - - echo \"🧹 清理旧备份(保留最近 ${BACKUP_RETENTION_COUNT} 个)...\" - cd '${DEPLOY_ROOT}' - ls -dt ${STATIC_DIR}_backup_* 2>/dev/null | tail -n +$((BACKUP_RETENTION_COUNT + 1)) | xargs rm -rf 2>/dev/null || true - echo \"✅ 清理完成\" - else - echo \"ℹ️ 无需备份(目录不存在),首次部署\" - fi - " - - echo "" - echo "==========================================" - echo "📋 步骤 4: 上传 dist 目录" - echo "==========================================" - ssh ${SERVER_USER}@${SERVER_IP} "mkdir -p '${DEPLOY_ROOT}/${STATIC_DIR}'" - - echo "📤 使用 rsync 同步文件..." - rsync -avz --delete "\$DIST_DIR/" ${SERVER_USER}@${SERVER_IP}:${DEPLOY_ROOT}/${STATIC_DIR}/ - - FILE_COUNT=\$(find \$DIST_DIR -type f | wc -l) - echo "✅ 上传完成: \$FILE_COUNT 个文件, 总大小 \$DIST_SIZE" - - echo "" - echo "==========================================" - echo "📋 步骤 5: 设置权限" - echo "==========================================" - ssh ${SERVER_USER}@${SERVER_IP} " - set -e - STATIC_PATH='${DEPLOY_ROOT}/${STATIC_DIR}' - chown -R www-data:www-data \"\$STATIC_PATH\" 2>/dev/null || true - chmod -R 755 \"\$STATIC_PATH\" - echo \"✅ 权限设置完成\" - " - - echo "" - echo "==========================================" - echo "📋 步骤 6: 重载 Nginx" - echo "==========================================" - ssh ${SERVER_USER}@${SERVER_IP} " - set -e - if docker exec ${NGINX_CONTAINER} nginx -t; then - docker exec ${NGINX_CONTAINER} nginx -s reload - echo \"✅ Nginx 重载成功\" - else - echo \"⚠️ Nginx 配置检测失败,尝试重启容器...\" - docker restart ${NGINX_CONTAINER} - sleep 3 - echo \"✅ Nginx 容器已重启\" - fi - " - - echo "" - echo "==========================================" - echo "📋 步骤 7: 验证部署" - echo "==========================================" - sleep 3 - - HTTP_STATUS=\$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 10 ${DOMAIN}) - if [ "\$HTTP_STATUS" = "200" ]; then - echo "✅ 主页访问正常 (HTTP \$HTTP_STATUS)" - else - echo "❌ 主页返回异常状态码: \$HTTP_STATUS" - exit 1 - fi - - echo "" - echo "🔍 检查关键页面..." - for PAGE in "/contact" "/about" "/products" "/services"; do - STATUS=\$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 \${DOMAIN}\$PAGE) - if [ "\$STATUS" = "200" ]; then - echo " ✅ \$PAGE (\$STATUS)" - else - echo " ⚠️ \$PAGE (\$STATUS) - 需要关注" - fi - done - - echo "" - echo "🎉 部署成功!" - echo "📋 访问地址:" - echo " HTTP: http://${SERVER_IP}" - echo " HTTPS: https://novalon.cn" - """ + sh ''' + # 统一发布脚本(单一事实源: scripts/deploy.sh) + ./scripts/deploy.sh deploy --skip-build + ''' } post { failure { echo "❌ 部署失败!正在执行自动回滚..." - script { try { - sh """ - ssh ${SERVER_USER}@${SERVER_IP} ' - set -e - LATEST_BACKUP=\$(ls -dt ${DEPLOY_ROOT}/${STATIC_DIR}_backup_* 2>/dev/null | head -1) - - if [ -n "\$LATEST_BACKUP" ]; then - echo "🔄 回滚到: \$LATEST_BACKUP" - rm -rf "${DEPLOY_ROOT}/${STATIC_DIR}" - cp -r "\$LATEST_BACKUP" "${DEPLOY_ROOT}/${STATIC_DIR}" - - if docker exec ${NGINX_CONTAINER} nginx -t; then - docker exec ${NGINX_CONTAINER} nginx -s reload - else - docker restart ${NGINX_CONTAINER} - fi - echo "✅ 回滚完成" - else - echo "❌ 未找到可用备份!需要手动介入" - exit 1 - fi - ' - """ + sh ''' + ./scripts/deploy.sh rollback + ''' } catch (Exception e) { echo "❌ 自动回滚失败: ${e.getMessage()}" echo "🚨 需要立即手动介入!" diff --git a/README.md b/README.md index de8b0c5..f1afdce 100644 --- a/README.md +++ b/README.md @@ -345,27 +345,60 @@ npm run test:all # type-check + lint + coverage + fast E2E ## 部署 -### 静态部署 +### 统一发布脚本(推荐) -项目构建后生成纯静态文件,可部署到任何静态托管服务: +项目发布统一通过 `scripts/deploy.sh` 完成,包含构建、发布、回滚、状态查看: + +```bash +# 构建静态产物 +./scripts/deploy.sh build + +# 构建并发布到生产服务器(默认命令,等价于 ./scripts/deploy.sh deploy) +./scripts/deploy.sh deploy + +# 使用现有 dist/ 直接发布(跳过本地构建) +./scripts/deploy.sh deploy --skip-build + +# 回滚到最近一次远程备份 +./scripts/deploy.sh rollback + +# 查看生产环境发布状态 +./scripts/deploy.sh status + +# 查看完整帮助 +./scripts/deploy.sh help +``` + +常用选项: + +```bash +./scripts/deploy.sh deploy --skip-build --auto-rollback +./scripts/deploy.sh deploy --server-ip 139.155.109.62 --project-name novalon-website +``` + +也可通过环境变量覆盖默认配置:`SERVER_IP`、`SERVER_USER`、`DEPLOY_ROOT`、`PROJECT_NAME`、`NGINX_CONTAINER`、`DOMAIN`、`BACKUP_RETENTION_COUNT`、`DIST_DIR`。 + +npm 快捷命令:`npm run deploy`、`npm run deploy:skip-build`、`npm run deploy:rollback`、`npm run deploy:status`。 + +### 纯静态构建 + +项目构建后生成纯静态文件,产物位于 `dist/` 目录: ```bash npm run build ``` -构建产物位于 `dist/` 目录,可直接部署到: +`dist/` 可直接部署到: - Nginx - CDN - Vercel - Netlify - GitHub Pages -### Docker 部署 +### CI/CD + +Jenkins Pipeline(`Jenkinsfile`)在部署阶段调用 `./scripts/deploy.sh deploy --skip-build`,部署失败时自动调用 `./scripts/deploy.sh rollback` 回滚。 -```bash -docker build -t novalon-website . -docker run -p 3000:3000 novalon-website -``` ## 文档 diff --git a/deploy-dist.sh b/deploy-dist.sh deleted file mode 100755 index 8d0053e..0000000 --- a/deploy-dist.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/bin/bash - -set -e - -SERVER_IP="139.155.109.62" -SERVER_USER="root" -DEPLOY_ROOT="/home/novalon/docker-app" -PROJECT_NAME="novalon-website" -DIST_DIR="dist" -STATIC_DIR="novalon-static" -NGINX_CONTAINER="novalon-nginx-secure" - -while getopts "i:u:p:h" opt; do - case $opt in - i) SERVER_IP="$OPTARG" ;; - u) SERVER_USER="$OPTARG" ;; - p) PROJECT_NAME="$OPTARG" ;; - h) - echo "用法: $0 [选项]" - echo "选项:" - echo " -i IP地址 服务器IP地址 (默认: 139.155.109.62)" - echo " -u 用户名 SSH用户名 (默认: root)" - echo " -p 项目名 项目名称 (默认: novalon-website)" - echo " -h 显示帮助信息" - exit 0 - ;; - \?) - echo "无效选项: -$OPTARG" >&2 - exit 1 - ;; - esac -done - -LOG_DIR="./logs" -LOG_FILE="$LOG_DIR/deploy_dist_$(date +%Y%m%d_%H%M%S).log" - -mkdir -p "$LOG_DIR" -exec > >(tee -a "$LOG_FILE") 2>&1 - -echo "🚀 开始部署 dist 到服务器 $SERVER_IP" -echo "📁 部署目录: $DEPLOY_ROOT/$STATIC_DIR" -echo "📋 部署日志: $LOG_FILE" -echo "" - -echo "📋 步骤1: 检查 dist 目录..." -if [ ! -d "$DIST_DIR" ]; then - echo "❌ dist 目录不存在,请先运行 pnpm build" - exit 1 -fi - -DIST_SIZE=$(du -sh "$DIST_DIR" | cut -f1) -echo "✅ dist 目录大小: $DIST_SIZE" - -echo "" -echo "📋 步骤2: 验证SSH连接..." -if ! ssh -o ConnectTimeout=5 "$SERVER_USER@$SERVER_IP" exit; then - echo "❌ 无法连接到服务器 $SERVER_IP" - exit 1 -fi -echo "✅ SSH连接验证成功" - -echo "" -echo "📋 步骤3: 备份旧版本..." -ssh "$SERVER_USER@$SERVER_IP" " - set -e - STATIC_PATH='$DEPLOY_ROOT/$STATIC_DIR' - BACKUP_PATH='$DEPLOY_ROOT/${STATIC_DIR}_backup_$(date +%Y%m%d_%H%M%S)' - - if [ -d \"\$STATIC_PATH\" ]; then - echo \"📦 备份到 \$BACKUP_PATH\" - cp -r \"\$STATIC_PATH\" \"\$BACKUP_PATH\" - echo \"✅ 备份完成\" - else - echo \"ℹ️ 无需备份,目录不存在\" - fi -" - -echo "" -echo "📋 步骤4: 上传 dist 目录..." -ssh "$SERVER_USER@$SERVER_IP" "mkdir -p '$DEPLOY_ROOT/$STATIC_DIR'" -rsync -avz --delete "$DIST_DIR/" "$SERVER_USER@$SERVER_IP:$DEPLOY_ROOT/$STATIC_DIR/" -echo "✅ dist 目录已上传" - -echo "" -echo "📋 步骤5: 设置文件权限..." -ssh "$SERVER_USER@$SERVER_IP" " - set -e - STATIC_PATH='$DEPLOY_ROOT/$STATIC_DIR' - chown -R www-data:www-data \"\$STATIC_PATH\" 2>/dev/null || true - chmod -R 755 \"\$STATIC_PATH\" - echo \"✅ 权限设置完成\" -" - -echo "" -echo "📋 步骤6: 清除 Nginx 缓存..." -ssh "$SERVER_USER@$SERVER_IP" " - set -e - docker exec $NGINX_CONTAINER nginx -s reload 2>/dev/null || echo \"⚠️ Nginx 重载失败,尝试重启容器\" - echo \"✅ Nginx 缓存已清除\" -" - -echo "" -echo "📋 步骤7: 验证部署..." -if ! ssh "$SERVER_USER@$SERVER_IP" "[ -d '$DEPLOY_ROOT/$STATIC_DIR' ]"; then - echo "❌ 部署目录不存在" - exit 1 -fi - -FILE_COUNT=$(ssh "$SERVER_USER@$SERVER_IP" "find '$DEPLOY_ROOT/$STATIC_DIR' -type f | wc -l") -echo "✅ 部署完成,共 $FILE_COUNT 个文件" - -echo "" -echo "📋 步骤8: 清理旧备份(保留最近3个)..." -ssh "$SERVER_USER@$SERVER_IP" " - set -e - BACKUP_DIR='$DEPLOY_ROOT' - cd \"\$BACKUP_DIR\" - ls -dt ${STATIC_DIR}_backup_* 2>/dev/null | tail -n +4 | xargs rm -rf 2>/dev/null || true - echo \"✅ 清理完成\" -" - -echo "" -echo "📋 步骤9: 验证生产环境..." -LOGO_FONT=$(curl -s "https://novalon.cn/logo.svg" | grep -o "font-family: [^;]*" | head -1) -if echo "$LOGO_FONT" | grep -q "aoyagiReisho"; then - echo "✅ 生产环境已更新: $LOGO_FONT" -else - echo "⚠️ 生产环境可能未更新,请手动验证" -fi - -echo "" -echo "🎉 部署成功!" -echo "" -echo "📋 访问地址:" -echo " HTTP: http://$SERVER_IP" -echo " HTTPS: https://$SERVER_IP" -echo " 域名: https://novalon.cn" -echo "" -echo "📋 后续步骤:" -echo " 1. 验证网站可访问性" -echo " 2. 检查静态资源加载" -echo " 3. 测试网站主要功能" diff --git a/deploy.sh b/deploy.sh deleted file mode 100755 index e1abd9e..0000000 --- a/deploy.sh +++ /dev/null @@ -1,201 +0,0 @@ -#!/bin/bash - -set -e - -SERVER_IP="139.155.109.62" -SERVER_USER="root" -DEPLOY_ROOT="/home/novalon/docker-app" -PROJECT_NAME="novalon-website" -PROJECT_DIR="$DEPLOY_ROOT/$PROJECT_NAME" -CONTAINER_NAME="novalon-website" -NGINX_CONTAINER_NAME="novalon-nginx" -VERSION="1.0.0" - -while getopts "i:u:p:c:v:h" opt; do - case $opt in - i) SERVER_IP="$OPTARG" ;; - u) SERVER_USER="$OPTARG" ;; - p) PROJECT_NAME="$OPTARG" ;; - c) CONTAINER_NAME="$OPTARG" ;; - v) VERSION="$OPTARG" ;; - h) - echo "用法: $0 [选项]" - echo "选项:" - echo " -i IP地址 服务器IP地址 (默认: 139.155.109.62)" - echo " -u 用户名 SSH用户名 (默认: root)" - echo " -p 项目名 项目名称 (默认: novalon-website)" - echo " -c 容器名 容器名称 (默认: novalon-website)" - echo " -v 版本号 版本号 (默认: 1.0.0)" - echo " -h 显示帮助信息" - exit 0 - ;; - \?) - echo "无效选项: -$OPTARG" >&2 - exit 1 - ;; - esac -done - -PROJECT_DIR="$DEPLOY_ROOT/$PROJECT_NAME" - -LOG_DIR="./logs" -LOG_FILE="$LOG_DIR/deploy_$(date +%Y%m%d_%H%M%S).log" - -mkdir -p "$LOG_DIR" -exec > >(tee -a "$LOG_FILE") 2>&1 - -echo "🚀 开始部署Novalon网站到服务器 $SERVER_IP" -echo "📁 部署根目录: $DEPLOY_ROOT" -echo "📁 项目目录: $PROJECT_DIR" -echo "🐳 容器名称: $CONTAINER_NAME" -echo "📦 版本号: $VERSION" -echo "📋 部署日志: $LOG_FILE" -echo "" - -echo "📋 步骤0: 部署前检查..." - -for file in docker-compose.yml Dockerfile nginx-static.conf .env.example setup-ssl.sh; do - if [ ! -f "$file" ]; then - echo "❌ 缺少必要文件: $file" - exit 1 - fi -done - -if ! command -v docker-compose &> /dev/null; then - echo "❌ 本地docker-compose不可用" - exit 1 -fi - -echo "✅ 部署前检查通过" - -echo "" -echo "📋 步骤1: 验证SSH连接..." -if ! ssh -o ConnectTimeout=5 "$SERVER_USER@$SERVER_IP" exit; then - echo "❌ 无法连接到服务器 $SERVER_IP" - exit 1 -fi - -if ! ssh "$SERVER_USER@$SERVER_IP" "docker --version"; then - echo "❌ 服务器上Docker不可用" - exit 1 -fi - -echo "✅ SSH连接验证成功" - -echo "" -echo "📋 步骤2: 上传部署文件..." -ssh "$SERVER_USER@$SERVER_IP" "mkdir -p '$PROJECT_DIR'" -scp -r docker-compose.yml Dockerfile nginx-static.conf .env.example setup-ssl.sh "$SERVER_USER@$SERVER_IP:$PROJECT_DIR/" -echo "✅ 部署文件已上传" - -echo "" -echo "📋 步骤3: 在服务器上执行部署..." -ssh "$SERVER_USER@$SERVER_IP" << ENDSSH -cd '$PROJECT_DIR' - -echo "🔒 配置SSL证书..." -chmod +x setup-ssl.sh -./setup-ssl.sh - -echo "📋 检查环境变量文件..." -if [ ! -f .env ]; then - echo "📝 创建.env文件..." - cp .env.example .env - echo "⚠️ 请编辑.env文件,填入正确的环境变量" - echo "⚠️ 可选配置: NEXT_PUBLIC_GA_ID" -fi - -echo "🐳 启动Docker容器..." -docker-compose down -docker-compose build --no-cache -docker-compose up -d - -echo "📋 等待服务启动..." -timeout=60 -elapsed=0 -check_interval=3 - -while [ $elapsed -lt $timeout ]; do - if docker inspect --format='{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null | grep -q "running"; then - if curl -f -s -o /dev/null "http://localhost:80" --max-time 5 2>/dev/null; then - echo "✅ 服务已启动并响应正常" - break - else - echo "⏳ 容器运行中,等待服务响应..." - fi - else - echo "⏳ 等待容器启动..." - fi - sleep $check_interval - elapsed=$((elapsed + check_interval)) -done - -if [ $elapsed -ge $timeout ]; then - echo "❌ 服务启动超时" - echo "📋 当前容器状态:" - docker ps -a | grep "$CONTAINER_NAME" - echo "📋 容器日志:" - docker logs "$CONTAINER_NAME" --tail 20 - exit 1 -fi - -echo "📋 检查容器状态..." -docker ps | grep "$CONTAINER_NAME" - -echo "📋 检查容器日志..." -if docker logs "$CONTAINER_NAME" --tail 50 2>/dev/null; then - echo "✅ 容器日志检查完成" -else - echo "⚠️ 容器日志为空或无法访问" -fi - -echo "📋 配置SSL证书自动续期..." -if ! crontab -l | grep -q "certbot renew"; then - if ! (crontab -l 2>/dev/null; echo "0 0,12 * * * certbot renew --quiet --post-hook 'docker restart $NGINX_CONTAINER_NAME'") | crontab -; then - echo "❌ SSL证书自动续期任务配置失败" - exit 1 - fi - echo "✅ SSL证书自动续期任务已配置" -else - echo "✅ SSL证书自动续期任务已存在" -fi - -echo "✅ 部署完成!" -ENDSSH - -echo "" -echo "📋 步骤4: 部署后验证..." - -if ! ssh "$SERVER_USER@$SERVER_IP" "docker ps | grep -q '$CONTAINER_NAME'"; then - echo "❌ 容器未运行" - exit 1 -fi - -if ! curl -f -s -o /dev/null "http://$SERVER_IP" --max-time 10; then - echo "⚠️ HTTP服务响应异常" -else - echo "✅ HTTP服务正常" -fi - -if ! curl -f -s -o /dev/null "https://$SERVER_IP" --max-time 10; then - echo "⚠️ HTTPS服务响应异常" -else - echo "✅ HTTPS服务正常" -fi - -echo "✅ 部署后验证通过" - -echo "" -echo "🎉 部署脚本执行完成!" -echo "📋 访问地址:" -echo " HTTP: http://$SERVER_IP" -echo " HTTPS: https://$SERVER_IP" -echo " 域名: https://novalon.cn" -echo "" -echo "📋 后续步骤:" -echo " 1. 验证网站可访问性" -echo " 2. 检查容器运行状态: docker ps" -echo " 3. 查看容器日志: docker logs $CONTAINER_NAME" -echo " 4. 验证HTTPS配置" -echo " 5. 测试网站主要功能" -echo " 6. 检查SSL证书自动续期: crontab -l" \ No newline at end of file diff --git a/docs/CDN_CONFIGURATION.md b/docs/CDN_CONFIGURATION.md index df0fd71..6d6ba37 100644 --- a/docs/CDN_CONFIGURATION.md +++ b/docs/CDN_CONFIGURATION.md @@ -134,10 +134,7 @@ export COS_REGION="ap-chengdu" ``` 3. 运行部署脚本: -```bash -chmod +x scripts/deploy-cdn.sh -./scripts/deploy-cdn.sh -``` + > ⚠️ `scripts/deploy-cdn.sh` 尚未实现;当前统一发布脚本 `scripts/deploy.sh` 只负责构建与服务器静态发布,不包含 COS 上传。COS 上传请先使用下方手动方式,或后续按 `coscmd` 命令封装独立脚本。 #### 方法2: 手动上传 diff --git a/docs/PRODUCTION_DEPLOYMENT.md b/docs/PRODUCTION_DEPLOYMENT.md index 3e91135..add1f97 100644 --- a/docs/PRODUCTION_DEPLOYMENT.md +++ b/docs/PRODUCTION_DEPLOYMENT.md @@ -49,8 +49,8 @@ cp .env.production .env.local ### 2. 运行部署脚本 ```bash -chmod +x scripts/deploy-production.sh -./scripts/deploy-production.sh +# 统一发布脚本(构建 + 发布) +./scripts/deploy.sh deploy ``` ### 3. 验证部署 diff --git a/docs/PRODUCTION_DEPLOYMENT_LIGHTWEIGHT.md b/docs/PRODUCTION_DEPLOYMENT_LIGHTWEIGHT.md index 1f5c9ee..fa59310 100644 --- a/docs/PRODUCTION_DEPLOYMENT_LIGHTWEIGHT.md +++ b/docs/PRODUCTION_DEPLOYMENT_LIGHTWEIGHT.md @@ -493,8 +493,8 @@ chmod +x scripts/setup-lightweight-monitoring.sh ```bash # 运行生产部署脚本 -chmod +x scripts/deploy-production.sh -./scripts/deploy-production.sh +# 统一发布脚本(构建 + 发布) +./scripts/deploy.sh deploy ``` 这个脚本会: diff --git a/docs/deployment.md b/docs/deployment.md index 23bf910..16deb2c 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -85,35 +85,32 @@ - `ssl/` -> `/etc/nginx/ssl`(只读) - `logs/` -> `/var/log/nginx` - `../certbot` -> `/var/www/certbot`(ACME 验证) - - `../novalon-static` -> `/var/www/novalon`(只读,静态文件) + - `../novalon-website/dist` -> `/var/www/novalon`(只读,Novalon 主站静态文件) + - `../novalon-static` -> `/var/www/novavis-landing`(只读,Novavis 落地页) - 使用外部网络 `novalon-network` --- -### 三、部署脚本(2 个) +### 三、部署脚本(1 个统一入口) -#### 9. [deploy.sh](computer:///sessions/69e889f08f8c93c2514713f1/workspace/deploy.sh) -**用途**: 完整部署脚本 -- SSH 到远程服务器执行 Docker 构建+部署。 +#### 9. [scripts/deploy.sh](../scripts/deploy.sh) +**用途**: 统一发布脚本(唯一发布入口),负责构建、发布、回滚与状态查看。 **关键内容**: -- 默认服务器 `139.155.109.62`(root 用户),部署路径 `/home/novalon/docker-app/novalon-website` -- 容器名 `novalon-website`,Nginx 容器名 `novalon-nginx` -- 流程: 部署前检查 -> SSH 验证 -> SCP 上传文件 -> 服务器端 SSL 配置 -> `docker-compose build --no-cache` -> `docker-compose up -d` -> 健康检查 -> SSL 自动续期 cron 配置 -- 支持命令行参数: `-i`(IP)、`-u`(用户)、`-p`(项目名)、`-c`(容器名)、`-v`(版本号) - -#### 10. [deploy-dist.sh](computer:///sessions/69e889f08f8c93c2514713f1/workspace/deploy-dist.sh) -**用途**: 轻量部署脚本 -- 仅上传预构建的 `dist/` 目录到服务器。 - -**关键内容**: -- 同一服务器 `139.155.109.62`,部署到 `/home/novalon/docker-app/novalon-website/dist` -- 流程: 检查 dist 目录 -> SSH 验证 -> 备份旧版本 -> rsync 上传 -> 设置文件权限 -> 验证 -> 清理旧备份(保留最近 3 个) -- 适用于已有 Nginx 容器运行、仅需更新静态文件的场景 +- 默认服务器 `139.155.109.62`(root 用户),远程目标目录 `/home/novalon/docker-app/novalon-website/dist`(生产 Nginx 实际挂载目录) +- 命令: + - `build`:构建静态产物 + - `deploy`:构建 + 远程发布(备份旧版本 -> rsync 上传 -> 设置权限 -> 重载 Nginx -> 验证页面) + - `rollback`:回滚到最近一次远程备份 + - `status`:查看生产环境发布状态 +- 支持 `--skip-build`、`--auto-rollback`、`--dry-run` 及服务器/域名/备份保留数等参数 +- 支持环境变量覆盖:`SERVER_IP`、`SERVER_USER`、`DEPLOY_ROOT`、`PROJECT_NAME`、`NGINX_CONTAINER`、`DOMAIN`、`BACKUP_RETENTION_COUNT`、`DIST_DIR` --- ### 四、引用 "novalon-nginx" / "novalon-website" 的其他文件(29 个) -除上述 10 个核心文件外,以下文件也引用了这两个关键词,按类别归纳: +除上述 9 个核心文件外,以下文件也引用了这两个关键词,按类别归纳: | 类别 | 文件路径 | |------|----------| diff --git a/docs/deployment/rollback-procedure-v2.md b/docs/deployment/rollback-procedure-v2.md index 74e1171..3104848 100644 --- a/docs/deployment/rollback-procedure-v2.md +++ b/docs/deployment/rollback-procedure-v2.md @@ -18,16 +18,16 @@ │ novalon-website │ │ - 构建命令: npm run build:clean │ │ - 输出目录: dist/ │ -│ - 部署命令: ./deploy-dist.sh │ +│ - 部署命令: ./scripts/deploy.sh deploy │ └──────────────────────┬──────────────────────────────┘ │ rsync + SSH (root@139.155.109.62) ▼ ┌─────────────────────────────────────────────────────┐ │ 生产服务器 (139.155.109.62) │ │ └── /home/novalon/docker-app/ │ -│ ├── novalon-static/ ← 当前版本 │ -│ ├── novalon-static_backup_* ← 自动备份 │ -│ └── docker-compose.yml │ +│ ├── novalon-website/dist/ ← 当前版本 │ +│ ├── novalon-website/dist_backup_* ← 自动备份 │ +│ └── novalon-nginx/ │ │ └── novalon-nginx-secure ← Nginx 容器 │ └──────────────────────┬──────────────────────────────┘ │ 反向代理 @@ -226,7 +226,7 @@ else fi # 4.3 部署到生产环境 -./deploy-dist.sh +./scripts/deploy.sh deploy --skip-build # 脚本会自动执行: # 1. 检查 dist 目录 @@ -551,7 +551,7 @@ git reset --hard HEAD ║ 2. git checkout ║ ║ # 切换到稳定版本 ║ ║ ║ -║ 3. ./deploy-dist.sh ║ +║ 3. ./scripts/deploy.sh deploy --skip-build ║ ║ # 重新构建并部署 ║ ║ ║ ║ 验证命令: ║ @@ -598,7 +598,7 @@ git reset --hard HEAD ## 📚 **相关文档链接** -- **部署脚本**: [deploy-dist.sh](./deploy-dist.sh) +- **部署脚本**: [scripts/deploy.sh](../../scripts/deploy.sh) - **Jenkins Pipeline**: [Jenkinsfile](./Jenkinsfile) - **Sentry 监控指南**: [docs/sentry-setup-guide.md](./docs/sentry-setup-guide.md) - **技术栈降级指南**: [scripts/downgrade-stack.sh](./scripts/downgrade-stack.sh) diff --git a/docs/ga4-error-tracking-integration.md b/docs/ga4-error-tracking-integration.md index df22396..e2000ef 100644 --- a/docs/ga4-error-tracking-integration.md +++ b/docs/ga4-error-tracking-integration.md @@ -136,8 +136,8 @@ NEXT_PUBLIC_GA_MEASUREMENT_ID=G-LGTLCR15KM ```bash npm run build:clean -# 然后执行部署脚本 -./deploy-dist.sh +# 然后执行统一发布脚本(跳过重复构建) +./scripts/deploy.sh deploy --skip-build ``` ### 3. 监控错误报告 diff --git a/docs/sentry-setup-guide.md b/docs/sentry-setup-guide.md index 1629e47..a65d612 100644 --- a/docs/sentry-setup-guide.md +++ b/docs/sentry-setup-guide.md @@ -245,8 +245,8 @@ SENTRY_DSN=你的真实SENTRY_DSN ### 7.2 重新构建和部署 ```bash -# 回到本地开发机 -./deploy-dist.sh +# 回到本地开发机,执行统一发布脚本 +./scripts/deploy.sh deploy ``` Jenkins Pipeline 会自动: diff --git a/package.json b/package.json index f711392..4840864 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,11 @@ "lighthouse:upload": "lhci upload", "lighthouse:desktop": "lhci autorun --settings.preset=desktop", "lighthouse:mobile": "lhci autorun --settings.preset=mobile", - "prepare": "husky" + "prepare": "husky", + "deploy": "bash scripts/deploy.sh deploy", + "deploy:skip-build": "bash scripts/deploy.sh deploy --skip-build", + "deploy:rollback": "bash scripts/deploy.sh rollback", + "deploy:status": "bash scripts/deploy.sh status" }, "dependencies": { "@aws-sdk/client-s3": "^3.1089.0", diff --git a/scripts/deploy-static.sh b/scripts/deploy-static.sh deleted file mode 100755 index 9eea7e2..0000000 --- a/scripts/deploy-static.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -# Novalon 静态站点部署脚本 -# 用法: bash scripts/deploy-static.sh [环境] -# 环境参数: production (默认) - -set -euo pipefail - -ENV="${1:-production}" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -BUILD_DIR="$PROJECT_DIR/dist" -DEPLOY_DIR="/var/www/novalon" -BACKUP_DIR="/var/www/novalon-backup" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) - -echo "=========================================" -echo " Novalon 静态站点部署" -echo " 环境: $ENV" -echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')" -echo "=========================================" - -# 步骤 1: 构建 -echo "" -echo "[1/5] 构建静态站点..." -cd "$PROJECT_DIR" -npm run build - -if [ ! -d "$BUILD_DIR" ]; then - echo "❌ 构建失败:dist 目录不存在" - exit 1 -fi - -echo "✅ 构建完成,产物大小:$(du -sh "$BUILD_DIR" | cut -f1)" - -# 步骤 2: 备份当前版本 -echo "" -echo "[2/5] 备份当前版本..." -if [ -d "$DEPLOY_DIR" ]; then - mkdir -p "$BACKUP_DIR" - cp -r "$DEPLOY_DIR" "$BACKUP_DIR/novalon-backup-$TIMESTAMP" - # 只保留最近 3 个备份 - ls -t "$BACKUP_DIR"/ | tail -n +4 | xargs -I {} rm -rf "$BACKUP_DIR/{}" - echo "✅ 备份完成: $BACKUP_DIR/novalon-backup-$TIMESTAMP" -else - echo "⚠️ 首次部署,无需备份" - mkdir -p "$DEPLOY_DIR" -fi - -# 步骤 3: 部署新版本 -echo "" -echo "[3/5] 部署新版本..." -# 清空目标目录(保留 .well-known) -if [ -d "$DEPLOY_DIR/.well-known" ]; then - mv "$DEPLOY_DIR/.well-known" /tmp/well-known-backup -fi -rm -rf "$DEPLOY_DIR"/* -cp -r "$BUILD_DIR"/* "$DEPLOY_DIR/" -if [ -d /tmp/well-known-backup ]; then - mv /tmp/well-known-backup "$DEPLOY_DIR/.well-known" -fi -echo "✅ 文件部署完成" - -# 步骤 4: 设置权限 -echo "" -echo "[4/5] 设置文件权限..." -chmod -R 755 "$DEPLOY_DIR" -echo "✅ 权限设置完成" - -# 步骤 5: 重载 Nginx -echo "" -echo "[5/5] 重载 Nginx..." -if nginx -t 2>/dev/null; then - nginx -s reload - echo "✅ Nginx 重载成功" -else - echo "⚠️ Nginx 配置检查失败,跳过重载(请手动检查)" -fi - -echo "" -echo "=========================================" -echo " ✅ 部署完成!" -echo " 站点地址: https://www.novalon.cn" -echo " 部署目录: $DEPLOY_DIR" -echo "=========================================" diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..92f453a --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,405 @@ +#!/usr/bin/env bash +# ============================================================ +# Novalon 统一发布脚本 +# +# 用法: +# ./scripts/deploy.sh [命令] [选项] +# +# 命令: +# build 构建静态产物 (npm run build / npm run build:clean) +# deploy 构建并发布到生产服务器 (默认命令) +# rollback 回滚到最近一次远程备份 +# status 查看生产环境发布状态 +# help 显示帮助 +# +# 选项: +# --skip-build 跳过本地构建,直接使用现有 dist/ +# --auto-rollback 部署/验证失败时自动回滚到最近备份 +# --clean 构建前清理 .next 与 dist +# --server-ip IP 服务器地址 (默认 139.155.109.62) +# --server-user USER SSH 用户 (默认 root) +# --deploy-root PATH 服务器部署根目录 (默认 /home/novalon/docker-app) +# --project-name NAME 服务器项目目录名 (默认 novalon-website) +# --nginx-container C Nginx 容器名 (默认 novalon-nginx-secure) +# --domain URL 验证域名 (默认 https://novalon.cn) +# --backup-retention N 远程备份保留份数 (默认 3) +# --dist-dir DIR 本地构建产物目录 (默认 dist) +# --dry-run 只打印远程命令,不实际执行 +# -h, --help 显示帮助 +# +# 环境变量覆盖: +# SERVER_IP SERVER_USER DEPLOY_ROOT PROJECT_NAME NGINX_CONTAINER +# DOMAIN BACKUP_RETENTION_COUNT DIST_DIR +# ============================================================ + +set -euo pipefail + +# ---------- 默认配置 ---------- +SERVER_IP="${SERVER_IP:-139.155.109.62}" +SERVER_USER="${SERVER_USER:-root}" +DEPLOY_ROOT="${DEPLOY_ROOT:-/home/novalon/docker-app}" +PROJECT_NAME="${PROJECT_NAME:-novalon-website}" +NGINX_CONTAINER="${NGINX_CONTAINER:-novalon-nginx-secure}" +DOMAIN="${DOMAIN:-https://novalon.cn}" +BACKUP_RETENTION="${BACKUP_RETENTION_COUNT:-${BACKUP_RETENTION:-3}}" +DIST_DIR="${DIST_DIR:-dist}" + +REMOTE_PROJECT_DIR="$DEPLOY_ROOT/$PROJECT_NAME" +REMOTE_DIST_DIR="$REMOTE_PROJECT_DIR/dist" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +LOG_DIR="$PROJECT_ROOT/logs" +LOG_FILE="$LOG_DIR/deploy_$(date +%Y%m%d_%H%M%S).log" + +# ---------- 运行时状态 ---------- +COMMAND="${1:-deploy}" +SKIP_BUILD=0 +AUTO_ROLLBACK=0 +CLEAN_BUILD=0 +DRY_RUN=0 +DEPLOY_STARTED=0 + +# ---------- 工具函数 ---------- +usage() { + awk ' + NR == 1 { next } + /^# =+$/ { c++; if (c == 2) exit } + { line = $0; sub(/^# ?/, "", line); print line } + ' "$0" +} + +log() { + echo -e "$@" +} + +require_local_cmd() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + log "❌ 缺少本地命令: $cmd" + exit 1 + fi +} + +run_remote() { + # 用法: run_remote <远程命令字符串> + # 远程命令会在目标服务器的 bash 中执行 + local remote_cmd="$1" + if [[ "$DRY_RUN" == "1" ]]; then + log "🔍 [dry-run] ssh $SERVER_USER@$SERVER_IP \"$remote_cmd\"" + return 0 + fi + ssh -o ConnectTimeout=8 -o BatchMode=yes "$SERVER_USER@$SERVER_IP" "$remote_cmd" +} + +verify_remote() { + log "" + log "==========================================" + log "📋 步骤 7: 验证生产环境" + log "==========================================" + + local pages=("/" "/about" "/contact" "/products" "/services") + local failed=0 + + for page in "${pages[@]}"; do + local url="${DOMAIN}${page}" + local status + status=$(curl -fsS -o /dev/null -w '%{http_code}' -L --connect-timeout 8 --max-time 15 "$url" 2>/dev/null || echo "000") + if [[ "$status" == "200" ]]; then + log " ✅ $page ($status)" + else + log " ❌ $page ($status)" + failed=1 + fi + done + + if [[ "$failed" == "1" ]]; then + log "❌ 生产环境验证失败" + return 1 + fi + log "✅ 生产环境验证通过" +} + +# ---------- 命令实现 ---------- +cmd_build() { + log "==========================================" + log "📋 构建静态站点" + log "==========================================" + cd "$PROJECT_ROOT" + if [[ "$CLEAN_BUILD" == "1" ]]; then + npm run build:clean + else + npm run build + fi + + if [[ ! -d "$DIST_DIR" ]]; then + log "❌ 构建失败:$DIST_DIR 目录不存在" + exit 1 + fi + log "✅ 构建完成,产物大小: $(du -sh "$DIST_DIR" | cut -f1)" +} + +cmd_deploy() { + log "==========================================" + log "🚀 Novalon 统一发布" + log " 目标: $SERVER_USER@$SERVER_IP:$REMOTE_DIST_DIR" + log " 域名: $DOMAIN" + log "==========================================" + + if [[ "$SKIP_BUILD" != "1" ]]; then + cmd_build + elif [[ ! -d "$DIST_DIR" ]]; then + log "❌ $DIST_DIR 目录不存在,请先构建或去掉 --skip-build" + exit 1 + fi + + require_local_cmd ssh + require_local_cmd rsync + require_local_cmd curl + + # 步骤 1: 检查本地产物 + log "" + log "📋 步骤 1: 检查构建产物" + DIST_SIZE=$(du -sh "$DIST_DIR" | cut -f1) + log "✅ $DIST_DIR 目录存在,大小: $DIST_SIZE" + + # 步骤 2: 验证 SSH 连接 + log "" + log "📋 步骤 2: 验证 SSH 连接" + if ! run_remote "exit"; then + log "❌ 无法连接到服务器 $SERVER_IP" + exit 1 + fi + log "✅ SSH 连接验证成功" + + # 步骤 3: 远程备份旧版本 + log "" + log "📋 步骤 3: 备份服务器上的旧版本" + run_remote " + set -e + PROJECT_DIR='$REMOTE_PROJECT_DIR' + DIST_PATH='$REMOTE_DIST_DIR' + if [ -d \"\$DIST_PATH\" ]; then + TIMESTAMP=\$(date +%Y%m%d_%H%M%S) + BACKUP_PATH=\"\$PROJECT_DIR/dist_backup_\$TIMESTAMP\" + cp -r \"\$DIST_PATH\" \"\$BACKUP_PATH\" + echo \"✅ 备份完成: \$BACKUP_PATH\" + else + echo \"ℹ️ 首次部署,无需备份\" + fi + " + + DEPLOY_STARTED=1 + + # 步骤 4: 清理旧备份(保留最近 N 份) + log "" + log "📋 步骤 4: 清理旧备份(保留最近 ${BACKUP_RETENTION} 份)" + run_remote " + set -e + PROJECT_DIR='$REMOTE_PROJECT_DIR' + cd \"\$PROJECT_DIR\" || exit 0 + ls -dt ${REMOTE_PROJECT_DIR}/dist_backup_* 2>/dev/null | tail -n +$((BACKUP_RETENTION + 1)) | xargs rm -rf 2>/dev/null || true + echo \"✅ 清理完成\" + " + + # 步骤 5: 上传 dist 目录 + log "" + log "📋 步骤 5: 上传 dist 目录" + if [[ "$DRY_RUN" == "1" ]]; then + log "🔍 [dry-run] rsync -avz --delete \"$DIST_DIR/\" $SERVER_USER@$SERVER_IP:$REMOTE_DIST_DIR/" + else + ssh "$SERVER_USER@$SERVER_IP" "mkdir -p '$REMOTE_DIST_DIR'" + rsync -avz --delete "$DIST_DIR/" "$SERVER_USER@$SERVER_IP:$REMOTE_DIST_DIR/" + fi + log "✅ dist 目录已上传" + + # 步骤 6: 设置权限并重载 Nginx + log "" + log "📋 步骤 6: 设置权限并重载 Nginx" + run_remote " + set -e + DIST_PATH='$REMOTE_DIST_DIR' + chown -R www-data:www-data \"\$DIST_PATH\" 2>/dev/null || true + chmod -R 755 \"\$DIST_PATH\" + echo \"✅ 权限设置完成\" + " + run_remote " + set -e + if docker exec $NGINX_CONTAINER nginx -t 2>/dev/null; then + docker exec $NGINX_CONTAINER nginx -s reload + echo \"✅ Nginx 重载成功\" + else + echo \"⚠️ Nginx 配置检测失败,重启容器...\" + docker restart $NGINX_CONTAINER + sleep 3 + echo \"✅ Nginx 容器已重启\" + fi + " + + # 步骤 7: 验证生产环境 + if ! verify_remote; then + if [[ "$AUTO_ROLLBACK" == "1" ]]; then + log "" + log "🔄 自动回滚到最近备份..." + cmd_rollback + fi + exit 1 + fi + + log "" + log "🎉 部署成功!" + log "📋 访问地址: $DOMAIN" +} + +cmd_rollback() { + log "==========================================" + log "🔄 回滚到最近一次远程备份" + log "==========================================" + + require_local_cmd ssh + require_local_cmd curl + + if ! run_remote "exit"; then + log "❌ 无法连接到服务器 $SERVER_IP" + exit 1 + fi + + log "📋 查找最近备份..." + LATEST_BACKUP=$(run_remote " + ls -dt ${REMOTE_PROJECT_DIR}/dist_backup_* 2>/dev/null | head -1 + " | tr -d '\r') + + if [[ -z "$LATEST_BACKUP" ]]; then + log "❌ 未找到任何备份,无法回滚" + exit 1 + fi + log "✅ 最近备份: $LATEST_BACKUP" + + run_remote " + set -e + PROJECT_DIR='$REMOTE_PROJECT_DIR' + DIST_PATH='$REMOTE_DIST_DIR' + BACKUP_PATH='$LATEST_BACKUP' + rm -rf \"\$DIST_PATH\" + cp -r \"\$BACKUP_PATH\" \"\$DIST_PATH\" + chown -R www-data:www-data \"\$DIST_PATH\" 2>/dev/null || true + chmod -R 755 \"\$DIST_PATH\" + echo \"✅ 已从备份恢复: \$BACKUP_PATH\" + " + + run_remote " + set -e + if docker exec $NGINX_CONTAINER nginx -t 2>/dev/null; then + docker exec $NGINX_CONTAINER nginx -s reload + echo \"✅ Nginx 重载成功\" + else + docker restart $NGINX_CONTAINER + sleep 3 + echo \"✅ Nginx 容器已重启\" + fi + " + + if verify_remote; then + log "" + log "🎉 回滚成功!" + else + log "❌ 回滚后验证失败,请手动介入" + exit 1 + fi +} + +cmd_status() { + log "==========================================" + log "📋 生产环境发布状态" + log "==========================================" + require_local_cmd ssh + + if ! run_remote "exit"; then + log "❌ 无法连接到服务器 $SERVER_IP" + exit 1 + fi + + run_remote " + echo \"--- Nginx 容器 ---\" + docker ps --filter name=$NGINX_CONTAINER --format '{{.Names}} {{.Status}}' || echo '❌ 容器未运行' + echo + echo \"--- 远程 dist 目录 ---\" + ls -ld '$REMOTE_DIST_DIR' 2>/dev/null || echo '❌ 目录不存在' + echo \"文件数: \$(find '$REMOTE_DIST_DIR' -type f 2>/dev/null | wc -l)\" + echo + echo \"--- 最近修改 ---\" + find '$REMOTE_DIST_DIR' -maxdepth 1 -type f -printf '%T@ %f\n' 2>/dev/null | sort -rn | head -5 + echo + echo \"--- 可用备份 ---\" + ls -dt ${REMOTE_PROJECT_DIR}/dist_backup_* 2>/dev/null | head -${BACKUP_RETENTION} || echo '无备份' + " +} + +on_error() { + local rc=$? + if [[ "$COMMAND" == "deploy" && "$AUTO_ROLLBACK" == "1" && "$DEPLOY_STARTED" == "1" ]]; then + log "" + log "❌ 部署失败(exit=$rc),执行自动回滚..." + cmd_rollback >&2 || true + fi + exit "$rc" +} +trap on_error ERR + +# ---------- 参数解析 ---------- +case "$COMMAND" in + build|deploy|rollback|status|help|-h|--help) ;; + *) + log "❌ 未知命令: $COMMAND" + usage + exit 1 + ;; +esac + +if [[ "$COMMAND" == "help" || "$COMMAND" == "-h" || "$COMMAND" == "--help" ]]; then + usage + exit 0 +fi + +shift || true + +while [[ $# -gt 0 ]]; do + case "$1" in + --skip-build) SKIP_BUILD=1; shift ;; + --auto-rollback) AUTO_ROLLBACK=1; shift ;; + --clean) CLEAN_BUILD=1; shift ;; + --dry-run) DRY_RUN=1; shift ;; + --server-ip) SERVER_IP="$2"; shift 2 ;; + --server-user) SERVER_USER="$2"; shift 2 ;; + --deploy-root) DEPLOY_ROOT="$2"; shift 2 ;; + --project-name) PROJECT_NAME="$2"; shift 2 ;; + --nginx-container) NGINX_CONTAINER="$2"; shift 2 ;; + --domain) DOMAIN="$2"; shift 2 ;; + --backup-retention) BACKUP_RETENTION="$2"; shift 2 ;; + --dist-dir) DIST_DIR="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) + log "❌ 未知参数: $1" + usage + exit 1 + ;; + esac +done + +REMOTE_PROJECT_DIR="$DEPLOY_ROOT/$PROJECT_NAME" +REMOTE_DIST_DIR="$REMOTE_PROJECT_DIR/dist" + +# ---------- 日志 ---------- +mkdir -p "$LOG_DIR" +exec > >(tee -a "$LOG_FILE") 2>&1 + +log "📋 部署日志: $LOG_FILE" +log "" + +# ---------- 执行 ---------- +case "$COMMAND" in + build) cmd_build ;; + deploy) cmd_deploy ;; + rollback) cmd_rollback ;; + status) cmd_status ;; +esac diff --git a/scripts/deployment/deploy-production.sh b/scripts/deployment/deploy-production.sh deleted file mode 100644 index 242f76a..0000000 --- a/scripts/deployment/deploy-production.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -set -e - -echo "🚀 开始部署到生产环境..." - -export NODE_ENV=production - -if [ ! -d "node_modules" ]; then - echo "📦 安装依赖..." - npm ci --production=false -fi - -echo "🔨 构建静态网站..." -npm run build - -if [ -d "dist_backup" ]; then - rm -rf dist_backup -fi -if [ -d "dist" ]; then - echo "💾 备份当前版本..." - mv dist dist_backup -fi - -echo "✅ 构建完成!" -echo "📊 静态文件位于 dist/ 目录" -echo "🌐 可部署到 Nginx、CDN 或任何静态托管服务" diff --git a/scripts/setup-cicd/cicd.config b/scripts/setup-cicd/cicd.config index deeb1dd..d1b7809 100644 --- a/scripts/setup-cicd/cicd.config +++ b/scripts/setup-cicd/cicd.config @@ -20,7 +20,8 @@ JENKINS_API_TOKEN="" SERVER_IP="139.155.109.62" SERVER_USER="root" DEPLOY_ROOT="/home/novalon/docker-app" -STATIC_DIR="novalon-static" +PROJECT_NAME="novalon-website" +DIST_DIR="dist" NGINX_CONTAINER="novalon-nginx-secure" DOMAIN="https://novalon.cn" diff --git a/scripts/setup-cicd/setup-cicd.sh b/scripts/setup-cicd/setup-cicd.sh index b4be1d7..bca2dad 100755 --- a/scripts/setup-cicd/setup-cicd.sh +++ b/scripts/setup-cicd/setup-cicd.sh @@ -5,7 +5,7 @@ set -e echo "╔════════════════════════════════════════════╗" echo "║ 🚀 Novalon Website CI/CD 自动配置工具 ║" echo "║ Version: 1.0.0 ║" -echo "║ Mode: Docker + Shell (参考 deploy-dist.sh) ║" +echo "║ Mode: Docker + Shell (参考 scripts/deploy.sh) ║" echo "╚════════════════════════════════════════════╝" echo ""