8840c4398a
- Downgrade Next.js 16→14.2, React 19→18.3, Tailwind 4→3.4 - Add comprehensive GA4 error monitoring system - Create Jenkins CI/CD pipeline with quality gates - Fix build issues: ESLint, SWC conflict, config format - Add documentation for deployment and error tracking
389 lines
14 KiB
Groovy
389 lines
14 KiB
Groovy
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 被中止!"
|
|
}
|
|
}
|
|
}
|