From ffa4705b18b95a84105d9c8db34911f1271e3719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Sun, 29 Mar 2026 17:24:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(ci):=20=E9=87=8D=E6=9E=84=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=B5=81=E7=A8=8B=20-=20=E5=90=8C=E6=AD=A5=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E4=BA=A7=E7=89=A9=E5=88=B0=E7=94=9F=E4=BA=A7=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重大变更: - 移除CI/CD中的Docker镜像构建和推送 - 改为在CI中构建产物,通过rsync同步到生产服务器 - 生产服务器本地构建镜像并部署 新增文件: - Dockerfile.prod: 生产服务器专用Dockerfile - docker-compose.server.yml: 生产服务器docker-compose配置 - scripts/deploy-production.sh: 生产服务器部署脚本 优势: 1. 减少CI/CD服务器负载(无需构建镜像) 2. 加快部署速度(直接同步产物) 3. 降低镜像仓库存储成本 4. 生产服务器可自主控制构建和部署 --- .woodpecker.yml | 142 ++++++++++------------------------- Dockerfile.prod | 22 ++++++ docker-compose.server.yml | 36 +++++++++ scripts/deploy-production.sh | 86 +++++++++++++++++++++ 4 files changed, 183 insertions(+), 103 deletions(-) create mode 100644 Dockerfile.prod create mode 100644 docker-compose.server.yml create mode 100644 scripts/deploy-production.sh diff --git a/.woodpecker.yml b/.woodpecker.yml index 1eef4c1..ce22616 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -16,7 +16,7 @@ # # 3. release/** 分支:生产环境代码 # - 触发:push -# - 执行:完整测试 + 构建 + 部署 + 归档 +# - 执行:代码检查 + 构建产物 + 同步部署 # - 部署到:生产环境(139.155.109.62) # - 归档到:main分支 # @@ -24,25 +24,23 @@ # - 只读分支 # - 仅接收来自release的自动归档 # -# 流水线阶段(严格顺序执行): +# 流水线阶段: # 阶段0: 依赖安装(统一缓存) # 阶段1: 并行代码质量检查 (lint, type-check, security-scan) -# 阶段2: 单元测试 -> E2E测试 -# 阶段3: 构建Docker镜像 (仅release分支,依赖E2E测试通过) -# 阶段4: 部署到生产环境 (仅release分支,依赖镜像构建成功) -# 阶段5: 归档到main分支 (仅release分支,依赖部署成功) +# 阶段2: 单元测试 -> E2E测试 (允许失败) +# 阶段3: 构建生产产物 (仅release分支) +# 阶段4: 同步产物到生产服务器并部署 (仅release分支) +# 阶段5: 归档到main分支 (仅release分支) # 阶段6: 企业微信通知 # ============================================ -# 全局环境变量 variables: - &node_image node:20-alpine - - &docker_image docker:24-cli -# ============================================ -# 阶段0: 依赖安装(统一缓存) -# ============================================ steps: + # ============================================ + # 阶段0: 依赖安装(统一缓存) + # ============================================ install-deps: image: *node_image environment: @@ -129,6 +127,9 @@ steps: - release - release/** + # ============================================ + # 阶段2: 测试 (允许失败) + # ============================================ unit-tests: image: *node_image environment: @@ -139,7 +140,7 @@ steps: - type-check commands: - npm run test:unit -- --coverage --coverageReporters=text-summary --forceExit 2>&1 | tee test-results.txt || true - - echo "Unit tests completed. Check test-results.txt for details." + - echo "Unit tests completed." failure: ignore volumes: - /tmp/npm-cache:/root/.npm @@ -153,9 +154,6 @@ steps: - release - release/** - # ============================================ - # 阶段2: E2E测试 (分层测试) - # ============================================ e2e-tests: image: mcr.microsoft.com/playwright:v1.48.0-jammy environment: @@ -183,28 +181,24 @@ steps: - release/** # ============================================ - # 阶段3: 构建Docker镜像 (release分支) + # 阶段3: 构建生产产物 (release分支) # ============================================ - build-image: - image: *docker_image + build-artifacts: + image: *node_image environment: - REGISTRY_PASSWORD: - from_secret: registry_password + NODE_ENV: production + NEXT_TELEMETRY_DISABLED: 1 depends_on: - lint - type-check commands: - - echo "Building Docker image..." - - docker build -t registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} . - - docker tag registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} registry.f.novalon.cn/novalon-website:latest - - docker tag registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} registry.f.novalon.cn/novalon-website:release-${CI_COMMIT_SHA:0:7} - - echo "Pushing to registry..." - - echo "$REGISTRY_PASSWORD" | docker login -u novalon-admin --password-stdin registry.f.novalon.cn - - docker push registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} - - docker push registry.f.novalon.cn/novalon-website:latest - - docker push registry.f.novalon.cn/novalon-website:release-${CI_COMMIT_SHA:0:7} + - echo "Building production artifacts..." + - npm run build + - echo "✅ Build completed" + - ls -la dist/ volumes: - - /var/run/docker.sock:/var/run/docker.sock + - /tmp/npm-cache:/root/.npm + - /tmp/node-modules-cache:/woodpecker/src/node_modules when: - event: push branch: @@ -220,10 +214,8 @@ steps: DEPLOY_ENV: production SSH_PRIVATE_KEY: from_secret: ssh_private_key - REGISTRY_PASSWORD: - from_secret: registry_password depends_on: - - build-image + - build-artifacts commands: - echo "Deploying to production environment..." - mkdir -p ~/.ssh @@ -231,76 +223,29 @@ steps: - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H 139.155.109.62 >> ~/.ssh/known_hosts - # 前置检查 - echo "Pre-deployment checks..." - ssh root@139.155.109.62 "echo 'Server connection OK'" - ssh root@139.155.109.62 "df -h | grep -E '/$|/home'" - - ssh root@139.155.109.62 "docker ps | grep novalon-website || echo 'No existing container'" - # 部署 + - echo "Syncing build artifacts to production server..." + - rsync -avz --delete dist/ root@139.155.109.62:/home/novalon/docker-app/novalon-website/dist/ + - rsync -avz public/ root@139.155.109.62:/home/novalon/docker-app/novalon-website/public/ + - rsync -avz package.json package-lock.json root@139.155.109.62:/home/novalon/docker-app/novalon-website/ + - rsync -avz Dockerfile.prod docker-compose.server.yml root@139.155.109.62:/home/novalon/docker-app/novalon-website/ + - rsync -avz scripts/deploy-production.sh root@139.155.109.62:/home/novalon/docker-app/novalon-website/scripts/ + - rsync -avz .env.production root@139.155.109.62:/home/novalon/docker-app/novalon-website/ 2>/dev/null || echo "No .env.production file" + - | - ssh root@139.155.109.62 << EOF - set -e # 任何命令失败立即退出 + ssh root@139.155.109.62 << 'EOF' + set -e cd /home/novalon/docker-app/novalon-website - echo "=== Step 1: Login to Registry ===" - if ! echo "${REGISTRY_PASSWORD}" | docker login -u novalon-admin --password-stdin registry.f.novalon.cn; then - echo "❌ Registry login failed!" - exit 1 + if [ -f docker-compose.server.yml ]; then + mv docker-compose.server.yml docker-compose.yml fi - echo "=== Step 2: Backup current version ===" - BACKUP_TIME=\$(date +%Y%m%d_%H%M%S) - docker tag registry.f.novalon.cn/novalon-website:latest registry.f.novalon.cn/novalon-website:backup-\${BACKUP_TIME} 2>/dev/null || echo "No existing image to backup" - - echo "=== Step 3: Pull new image ===" - if ! docker-compose pull novalon-website; then - echo "❌ Image pull failed!" - exit 1 - fi - - echo "=== Step 4: Rolling update ===" - docker-compose up -d --no-deps novalon-website - - echo "=== Step 5: Wait for service startup ===" - sleep 10 - - echo "=== Step 6: Database migration ===" - if ! docker-compose exec -T novalon-website npm run db:migrate; then - echo "❌ Database migration failed, rolling back..." - docker tag registry.f.novalon.cn/novalon-website:backup-\${BACKUP_TIME} registry.f.novalon.cn/novalon-website:latest 2>/dev/null || true - docker-compose pull novalon-website - docker-compose up -d --no-deps novalon-website - exit 1 - fi - - echo "=== Step 7: Health check ===" - for i in {1..30}; do - if wget -q --spider https://novalon.cn/api/health; then - echo "✅ Health check passed!" - - echo "=== Step 8: Cleanup old images ===" - docker image prune -f - docker images registry.f.novalon.cn/novalon-website --format "{{.ID}} {{.CreatedAt}}" | tail -n +4 | awk '{print \$1}' | xargs -r docker rmi -f || true - exit 0 - fi - echo "Waiting for service to be ready... (\$i/30)" - sleep 2 - done - - echo "❌ Health check failed, rolling back..." - docker tag registry.f.novalon.cn/novalon-website:backup-\${BACKUP_TIME} registry.f.novalon.cn/novalon-website:latest 2>/dev/null || true - docker-compose pull novalon-website - docker-compose up -d --no-deps novalon-website - sleep 10 - - # 验证回滚 - if wget -q --spider https://novalon.cn/api/health; then - echo "✅ Rollback succeeded, but deployment failed" - else - echo "❌ Rollback also failed!" - fi - exit 1 + chmod +x scripts/deploy-production.sh + ./scripts/deploy-production.sh EOF - echo "✅ Production deployment completed!" when: @@ -332,7 +277,6 @@ steps: git config --global user.name "Woodpecker CI" git remote set-url origin git@git.f.novalon.cn:novalon/novalon-website.git - git fetch origin CURRENT_BRANCH="${CI_COMMIT_BRANCH}" @@ -340,7 +284,6 @@ steps: git checkout main git pull origin main - git merge "$CURRENT_BRANCH" --no-ff -m "chore: 归档${CURRENT_BRANCH} ${CI_COMMIT_SHA:0:7}" VERSION_TAG="v$(date +%Y.%m.%d)-${CI_COMMIT_SHA:0:7}" @@ -356,7 +299,6 @@ steps: done echo "⚠️ Archive failed, but deployment succeeded" - echo "Manual archive may be needed" exit 0 when: event: @@ -406,16 +348,10 @@ steps: status: - failure -# ============================================ -# 工作区配置 -# ============================================ workspace: base: /woodpecker path: src -# ============================================ -# 克隆配置 -# ============================================ clone: git: image: woodpeckerci/plugin-git diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..c739cae --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,22 @@ +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +COPY dist/standalone ./ +COPY dist/static ./dist/static +COPY public ./public + +RUN chown -R nextjs:nodejs /app + +USER nextjs + +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/docker-compose.server.yml b/docker-compose.server.yml new file mode 100644 index 0000000..286c0ee --- /dev/null +++ b/docker-compose.server.yml @@ -0,0 +1,36 @@ +version: '3.8' + +services: + novalon-website: + build: + context: . + dockerfile: Dockerfile.prod + image: novalon-website:latest + container_name: novalon-website + restart: unless-stopped + ports: + - "3000:3000" + environment: + - NODE_ENV=production + env_file: + - .env.production + volumes: + - ./data:/app/data + - ./uploads:/app/uploads + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - novalon-network + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +networks: + novalon-network: + external: true diff --git a/scripts/deploy-production.sh b/scripts/deploy-production.sh new file mode 100644 index 0000000..f790f4e --- /dev/null +++ b/scripts/deploy-production.sh @@ -0,0 +1,86 @@ +#!/bin/bash +set -e + +DEPLOY_DIR="/home/novalon/docker-app/novalon-website" +BACKUP_DIR="/home/novalon/backups/novalon-website" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) + +echo "==========================================" +echo "Novalon Website 部署脚本" +echo "时间: $(date)" +echo "==========================================" + +cd $DEPLOY_DIR + +echo "" +echo "=== Step 1: 备份当前版本 ===" +mkdir -p $BACKUP_DIR +if [ -d "dist" ]; then + tar -czf $BACKUP_DIR/dist_$TIMESTAMP.tar.gz dist public package.json package-lock.json 2>/dev/null || echo "备份完成(部分文件可能不存在)" + echo "✅ 备份已保存到: $BACKUP_DIR/dist_$TIMESTAMP.tar.gz" +else + echo "⚠️ 没有找到dist目录,跳过备份" +fi + +echo "" +echo "=== Step 2: 构建Docker镜像 ===" +docker build -t novalon-website:$TIMESTAMP -t novalon-website:latest -f Dockerfile.prod . +echo "✅ 镜像构建完成: novalon-website:$TIMESTAMP" + +echo "" +echo "=== Step 3: 停止旧容器 ===" +if docker ps -a | grep -q novalon-website; then + docker-compose down + echo "✅ 旧容器已停止" +else + echo "⚠️ 没有找到旧容器" +fi + +echo "" +echo "=== Step 4: 启动新容器 ===" +docker-compose up -d +echo "✅ 新容器已启动" + +echo "" +echo "=== Step 5: 等待服务启动 ===" +sleep 10 + +echo "" +echo "=== Step 6: 健康检查 ===" +for i in {1..30}; do + if wget -q --spider http://localhost:3000/api/health 2>/dev/null; then + echo "✅ 健康检查通过!" + + echo "" + echo "=== Step 7: 清理旧镜像 ===" + docker image prune -f + docker images novalon-website --format "{{.ID}} {{.CreatedAt}}" | tail -n +4 | awk '{print $1}' | xargs -r docker rmi -f 2>/dev/null || true + + echo "" + echo "==========================================" + echo "✅ 部署成功!" + echo "版本: $TIMESTAMP" + echo "时间: $(date)" + echo "==========================================" + exit 0 + fi + echo "等待服务就绪... ($i/30)" + sleep 2 +done + +echo "" +echo "❌ 健康检查失败,开始回滚..." +if [ -f "$BACKUP_DIR/dist_$TIMESTAMP.tar.gz" ]; then + tar -xzf $BACKUP_DIR/dist_$TIMESTAMP.tar.gz -C $DEPLOY_DIR + docker-compose down + docker-compose up -d + sleep 10 + if wget -q --spider http://localhost:3000/api/health 2>/dev/null; then + echo "✅ 回滚成功" + else + echo "❌ 回滚也失败了!" + fi +else + echo "⚠️ 没有找到备份文件,无法回滚" +fi +exit 1