pipeline { agent any environment { NODE_VERSION = '18' NPM_REGISTRY = 'https://registry.npmmirror.com' PROJECT_NAME = 'novalon-website' 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' } options { buildDiscarder(logRotator(numToKeepStr: '10')) timeout(time: 30, unit: 'MINUTES') timestamps() ansiColor('xterm') } triggers { // Gitea Webhook 触发 GenericTrigger( genericVariables: [ [key: 'ref', value: '$.ref'], [key: 'repository', value: '$.repository.full_name'], [key: 'commit_sha', value: '$.after'], [key: 'commit_message', value: '$.commits[0].message'] ], token: '${PROJECT_NAME}-ci-token', causeString: 'Triggered by $ref on $repository', printContributedVariables: true, printPostContent: true, silentResponse: false, regexpFilterText: '$ref$repository', regexpFilterExpression: '^(refs/heads/main|refs/heads/develop).*$' ) // 每天凌晨 3 点定时检查依赖安全漏洞 pollSCM('H 3 * * *') } parameters { booleanParam( defaultValue: false, description: '是否部署到生产环境(仅在 main 分支且测试通过后可用)', name: 'DEPLOY_TO_PRODUCTION' ) } stages { stage('🔧 环境准备') { steps { echo "📦 Node.js 版本: ${NODE_VERSION}" echo "🌐 目标域名: ${DOMAIN}" echo "🖥️ 生产服务器: ${SERVER_USER}@${SERVER_IP}" nodejs(nodeJSInstallationName: "${NODE_VERSION}") { sh 'node --version' sh 'npm --version' } } } stage('📥 安装依赖') { steps { nodejs(nodeJSInstallationName: "${NODE_VERSION}") { sh ''' rm -rf node_modules package-lock.json npm ci --registry=${NPM_REGISTRY} --prefer-offline ''' } } post { failure { echo "❌ 依赖安装失败!请检查 npm registry 是否可用。" } } } stage('🔍 代码质量检查') { parallel { stage('ESLint 检查') { steps { nodejs(nodeJSInstallationName: "${NODE_VERSION}") { sh 'npm run lint' } } } stage('TypeScript 类型检查') { steps { nodejs(nodeJSInstallationName: "${NODE_VERSION}") { sh 'npm run type-check' } } } } post { failure { echo "❌ 代码质量检查未通过!" echo "💡 提示:请运行 npm run lint 和 npm run type-check 在本地修复问题" } } } stage('🧪 单元测试 + 覆盖率检查') { steps { nodejs(nodeJSInstallationName: "${NODE_VERSION}") { sh ''' export CI=true npm run test:coverage:check ''' } } post { always { publishHTML(target: [ allowMissing: false, alwaysLinkToLastBuild: true, keepAll: true, reportDir: 'coverage', reportFiles: 'index.html', reportName: 'Coverage Report (Jest)' ]) } success { echo "✅ 单元测试通过!覆盖率 ≥ 80%" } failure { echo "❌ 单元测试失败或覆盖率不足 80%!" echo "💡 请查看 Coverage Report 了解详情" } } } stage('🏗️ 生产构建') { when { anyOf { branch 'main' branch 'develop' } } steps { nodejs(nodeJSInstallationName: "${NODE_VERSION}") { sh ''' rm -rf .next dist npm run build:clean ''' } sh ''' if [ -d "dist" ]; then FILE_COUNT=$(find dist -type f | wc -l) DIST_SIZE=$(du -sh dist | cut -f1) echo "✅ 构建成功!生成了 $FILE_COUNT 个文件,总大小 $DIST_SIZE" else echo "❌ 构建失败:dist 目录不存在" exit 1 fi ''' } post { success { archiveArtifacts artifacts: 'dist/**', fingerprint: true } failure { echo "❌ Next.js 构建失败!请检查构建日志中的错误信息。" } } } stage('🌐 E2E 测试') { when { allOf { anyOf { branch 'main' branch 'develop' } expression { return params.DEPLOY_TO_PRODUCTION == true } } } steps { nodejs(nodeJSInstallationName: "${NODE_VERSION}") { sh ''' cd e2e && npx playwright test --config=playwright.config.ts || exit 1 ''' } } post { always { publishHTML(target: [ allowMissing: false, alwaysLinkToLastBuild: true, keepAll: true, reportDir: 'e2e/playwright-report', reportFiles: 'index.html', reportName: 'E2E Test Report (Playwright)' ]) } success { echo "✅ E2E 测试全部通过!" } failure { echo "❌ E2E 测试失败!" echo "💡 请查看 E2E Test Report 了解哪些用例未通过" } } } stage('🚀 部署到生产环境') { when { allOf { branch 'main' expression { return params.DEPLOY_TO_PRODUCTION == true } } } steps { echo "⚠️ 即将部署到生产环境:${DOMAIN}" sleep(time: 5, unit: 'SECONDS') sshagent(['deploy-server-ssh-key']) { sh """ # 备份旧版本 ssh -o StrictHostKeyChecking=no ${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\" fi ' # 上传新版本 echo "📤 上传 dist 目录到服务器..." ssh -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_IP} \ \"mkdir -p ${DEPLOY_ROOT}/${STATIC_DIR}\" rsync -avz --delete \ dist/ \ ${SERVER_USER}@${SERVER_IP}:${DEPLOY_ROOT}/${STATIC_DIR}/ # 设置权限并重载 Nginx ssh -o StrictHostKeyChecking=no ${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\" docker exec ${NGINX_CONTAINER} nginx -s reload ' """ } // 验证部署成功 sh """ sleep 3 HTTP_STATUS=\$(curl -s -o /dev/null -w '%{http_code}' ${DOMAIN}) if [ "\$HTTP_STATUS" = "200" ]; then echo "✅ 部署验证成功!HTTP 状态码: \$HTTP_STATUS" else echo "❌ 部署验证失败!HTTP 状态码: \$HTTP_STATUS" exit 1 fi # 检查关键页面 for PAGE in "/" "/contact" "/about" "/products"; do STATUS=\$(curl -s -o /dev/null -w '%{http_code}' ${DOMAIN}\$PAGE) if [ "\$STATUS" != "200" ]; then echo "⚠️ 页面 \$PAGE 返回状态码 \$STATUS" fi done echo "" echo "🎉 部署完成!访问地址: ${DOMAIN}" """ } post { success { script { def commitMsg = sh( script: 'git log -1 --pretty=%B', returnStdout: true ).trim() def buildUrl = env.BUILD_URL def buildNum = env.BUILD_NUMBER // 发送部署成功通知(钉钉/企业微信) echo """ 🎉 **Novalon Website 部署成功** - **构建号**: #${buildNum} - **提交信息**: ${commitMsg} - **目标域名**: ${DOMAIN} - **构建详情**: ${buildUrl} """ } } failure { echo "❌ 部署失败!正在执行自动回滚..." // 自动回滚到上一个备份 sshagent(['deploy-server-ssh-key']) { sh """ ssh -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_IP} ' set -e BACKUP_DIR=\"${DEPLOY_ROOT}\" LATEST_BACKUP=\$(ls -dt \${BACKUP_DIR}/${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}\" docker exec ${NGINX_CONTAINER} nginx -s reload echo "✅ 回滚完成" else echo "❌ 未找到可用的备份!需要手动介入" exit 1 fi ' """ } } } } stage('🧹 清理工作空间') { steps { cleanWs(cleanWhenNotBuilt: false, deleteDirs: true, notFailBuild: true) } } } post { always { script { def result = currentBuild.result ?: 'SUCCESS' def duration = currentBuild.durationString.replace(' and counting', '') def color = result == 'SUCCESS' ? 'good' : (result == 'UNSTABLE' ? 'warning' : 'danger') echo """ ==================================== 📊 Jenkins 构建报告 ==================================== - **项目**: ${env.JOB_NAME} - **构建号**: #${env.BUILD_NUMBER} - **结果**: ${result} - **耗时**: ${duration} - **详情**: ${env.BUILD_URL} ==================================== """ // 发送通知(可根据需要集成钉钉、企业微信、邮件等) if (result != 'SUCCESS') { mail ( to: 'dev-team@novalon.cn', subject: "❌ Novalon Website 构建失败: #${env.BUILD_NUMBER}", body: """构建结果: ${result} 请尽快查看构建详情: ${env.BUILD_URL} 构建日志: ${env.BUILD_URL}console""" ) } } } success { echo "✅ Pipeline 执行成功!" } failure { echo "❌ Pipeline 执行失败!请检查上述错误信息。" } aborted { echo "⚠️ Pipeline 被中止!" } } }