Files
novalon-website/Jenkinsfile
T
zhangxiang af57504e8e refactor(deploy): 统一发布脚本为单一入口 scripts/deploy.sh
- 新增 scripts/deploy.sh,支持 build/deploy/rollback/status 子命令
- 删除 deploy.sh、deploy-dist.sh、scripts/deploy-static.sh、scripts/deployment/deploy-production.sh
- Jenkinsfile 部署/回滚改为调用统一脚本,移除内联部署逻辑与 STATIC_DIR
- 同步 README、DEPLOYMENT、docs、CLAUDE.md、setup-cicd 与 package.json 脚本
2026-08-17 18:40:51 +08:00

383 lines
14 KiB
Groovy
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'
NGINX_CONTAINER = 'novalon-nginx-secure'
DOMAIN = 'https://novalon.cn'
BACKUP_RETENTION_COUNT = 3
}
options {
buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '5'))
timeout(time: 45, unit: 'MINUTES')
timestamps()
ansiColor('xterm')
disableConcurrentBuilds()
}
triggers {
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 (Commit: $commit_sha)',
printContributedVariables: true,
printPostContent: true,
silentResponse: false,
regexpFilterText: '$ref',
regexpFilterExpression: '^(refs/heads/main|refs/heads/develop)$'
)
}
parameters {
booleanParam(
defaultValue: false,
description: '是否部署到生产环境(仅在 main 分支且测试通过后可用)',
name: 'DEPLOY_TO_PRODUCTION'
)
}
stages {
stage('🔧 环境检测与准备') {
steps {
echo "=========================================="
echo "🚀 Novalon Website CI/CD Pipeline"
echo "🐳 Docker Shell Mode (参考 scripts/deploy.sh)"
echo "=========================================="
sh '''
echo "🔍 检测环境依赖..."
echo ""
echo "--- 系统信息 ---"
uname -a
whoami
pwd
echo ""
echo "--- 工具版本检查 ---"
node --version 2>/dev/null || echo "❌ Node.js 未安装"
npm --version 2>/dev/null || echo "❌ npm 未安装"
git --version || echo "❌ Git 未安装"
ssh -V || echo "❌ SSH 未安装"
rsync --version | head -1 || echo "❌ rsync 未安装"
curl --version | head -1 || echo "❌ curl 未安装"
echo ""
echo "--- SSH 连接测试 ---"
if ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 ${SERVER_USER}@${SERVER_IP} "hostname && whoami"; then
echo "✅ SSH 连接成功"
else
echo "❌ SSH 连接失败!请检查:"
echo " 1. SSH 密钥是否已挂载到 Jenkins 容器"
echo " 2. 生产服务器的 authorized_keys 是否包含公钥"
exit 1
fi
echo ""
echo "--- npm registry 测试 ---"
npm config get registry || npm config set registry ${NPM_REGISTRY}
'''
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh 'node --version && npm --version'
}
}
}
stage('📥 安装依赖') {
steps {
checkout scm
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh '''
echo "📦 安装项目依赖..."
if [ -f "package-lock.json" ]; then
rm -rf node_modules package-lock.json
npm ci --registry=${NPM_REGISTRY} --prefer-offline --no-audit --no-fund || {
echo "⚠️ npm ci 失败,尝试 npm install..."
npm install --registry=${NPM_REGISTRY} --legacy-peer-deps
}
else
rm -rf node_modules
npm install --registry=${NPM_REGISTRY} --legacy-peer-deps
fi
echo "✅ 依赖安装完成"
du -sh node_modules | awk '{print "📊 大小: " $1}'
'''
}
}
}
stage('🔍 代码质量检查') {
parallel {
stage('ESLint') {
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh 'npm run lint || exit 1'
}
}
}
stage('TypeScript') {
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh 'npm run type-check || exit 1'
}
}
}
}
}
stage('🧪 单元测试') {
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh '''
export CI=true
npm run test:coverage:check || exit 1
'''
}
}
post {
always {
publishHTML(target: [
allowMissing: true,
reportDir: 'coverage',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
}
}
}
// ====== L3: E2E + 用户旅程测试 ======
stage('🌐 E2E 测试') {
when {
branch 'main'
beforeAgent true
}
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh '''
echo "🔨 构建用于 E2E 测试..."
npm run build
echo "🚀 启动预览服务器..."
npx serve dist -l 3000 &
sleep 5
echo "🧪 运行 E2E 快速测试(@smoke + @critical..."
cd e2e && npx playwright test --grep "@smoke|@critical" || echo "⚠️ E2E 测试部分失败,继续执行"
echo "🧪 运行用户旅程测试..."
npx playwright test --grep @journey || echo "⚠️ 用户旅程测试部分失败,继续执行"
'''
}
}
post {
always {
sh 'kill $(lsof -t -i:3000) 2>/dev/null || true'
publishHTML(target: [
allowMissing: true,
reportDir: 'e2e/playwright-report',
reportFiles: 'index.html',
reportName: 'E2E Test Report'
])
}
failure {
archiveArtifacts artifacts: 'e2e/test-results/**/*.png', allowEmptyArchive: true
}
}
}
// ====== L4: 视觉回归测试 ======
stage('👁️ 视觉回归测试') {
when {
branch 'main'
beforeAgent true
}
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh '''
echo "🚀 启动预览服务器..."
npx serve dist -l 3000 &
sleep 5
echo "🧪 运行视觉回归测试..."
cd e2e && npx playwright test visual-regression.spec.ts --project=visual-chromium-desktop || echo "⚠️ 视觉回归测试失败,请检查基线是否需要更新"
'''
}
}
post {
always {
sh 'kill $(lsof -t -i:3000) 2>/dev/null || true'
archiveArtifacts artifacts: 'e2e/test-results/**/*.png', allowEmptyArchive: true
}
}
}
// ====== L5: 安全扫描 ======
stage('🔒 安全扫描') {
when {
branch 'main'
beforeAgent true
}
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh '''
echo "🔒 运行依赖安全审计..."
npm audit --audit-level=high || echo "⚠️ 存在高危依赖漏洞,请检查"
echo "🔒 检查安全响应头..."
npm run test:security:headers -- --url https://novalon.cn || echo "⚠️ 安全头检查未通过,请检查 Nginx 配置"
'''
}
}
}
stage('🏗️ 构建 dist') {
steps {
nodejs(nodeJSInstallationName: "${NODE_VERSION}") {
sh '''
set -e
echo "🧹 清理旧构建..."
rm -rf .next dist
echo "🔨 执行 Next.js 构建..."
npm run build:clean
echo ""
echo "✅ 构建完成!验证产物..."
if [ ! -d "dist" ]; then
echo "❌ dist 目录不存在"
exit 1
fi
FILE_COUNT=$(find dist -type f | wc -l)
DIST_SIZE=$(du -sh dist | cut -f1)
echo "📊 文件数: $FILE_COUNT, 大小: $DIST_SIZE"
'''
}
}
post {
success {
archiveArtifacts artifacts: 'dist/**', fingerprint: true
}
}
}
stage('🚀 部署到生产环境') {
when {
allOf {
branch 'main'
expression { return params.DEPLOY_TO_PRODUCTION == true }
}
}
steps {
echo "⚠️ 准备部署到生产环境: ${DOMAIN}"
sleep(time: 3, unit: 'SECONDS')
sh '''
# 统一发布脚本(单一事实源: scripts/deploy.sh
./scripts/deploy.sh deploy --skip-build
'''
}
post {
failure {
echo "❌ 部署失败!正在执行自动回滚..."
script {
try {
sh '''
./scripts/deploy.sh rollback
'''
} catch (Exception e) {
echo "❌ 自动回滚失败: ${e.getMessage()}"
echo "🚨 需要立即手动介入!"
}
}
}
}
}
}
post {
always {
script {
def result = currentBuild.result ?: 'SUCCESS'
def duration = currentBuild.durationString.replace(' and counting', '')
echo """
╔════════════════════════════════════════════╗
║ 📊 Jenkins Pipeline 报告 ║
╠════════════════════════════════════════════╣
║ 项目: ${env.JOB_NAME}
║ 构建号: #${env.BUILD_NUMBER}
║ 结果: ${result}
║ 耗时: ${duration}
║ 详情: ${env.BUILD_URL}
╚════════════════════════════════════════════╝
"""
}
}
success {
echo "✅ Pipeline 执行成功!"
}
failure {
echo "❌ Pipeline 执行失败!请查看日志。"
script {
// 邮件通知(使用 Jenkins 内置 mail step,无需额外插件)
try {
mail(
to: 'team@novalon.cn',
subject: "[FAILED] ${env.JOB_NAME} - #${env.BUILD_NUMBER}",
body: """
Pipeline 执行失败!
项目: ${env.JOB_NAME}
构建号: #${env.BUILD_NUMBER}
分支: ${env.BRANCH_NAME}
提交: ${env.GIT_COMMIT}
详情: ${env.BUILD_URL}console
日志: ${env.BUILD_URL}
"""
)
echo "📧 邮件通知已发送至 team@novalon.cn"
} catch (Exception e) {
echo "⚠️ 邮件通知发送失败(mail plugin 可能未配置): ${e.getMessage()}"
}
// Webhook 通知(预留,可接入钉钉/企业微信/Gitee Webhook
try {
def webhookUrl = env.WEBHOOK_NOTIFICATION_URL ?: ''
if (webhookUrl) {
sh """
curl -s -X POST '${webhookUrl}' \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "markdown",
"markdown": {
"title": "❌ Pipeline 失败: ${env.JOB_NAME}",
"text": "### ❌ Pipeline 执行失败\\n\\n**项目**: ${env.JOB_NAME}\\n**构建号**: #${env.BUILD_NUMBER}\\n**分支**: ${env.BRANCH_NAME}\\n**详情**: [查看日志](${env.BUILD_URL}console)"
}
}' || true
"""
echo "🔔 Webhook 通知已发送"
}
} catch (Exception e) {
echo "⚠️ Webhook 通知发送失败: ${e.getMessage()}"
}
}
}
}
}