diff --git a/--selector b/--selector new file mode 100644 index 0000000..50d157c Binary files /dev/null and b/--selector differ diff --git a/Jenkinsfile b/Jenkinsfile index 1ac2a4b..4f0af1d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -11,17 +11,18 @@ pipeline { STATIC_DIR = 'novalon-static' NGINX_CONTAINER = 'novalon-nginx-secure' DOMAIN = 'https://novalon.cn' + BACKUP_RETENTION_COUNT = 3 } options { - buildDiscarder(logRotator(numToKeepStr: '10')) - timeout(time: 30, unit: 'MINUTES') + buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '5')) + timeout(time: 45, unit: 'MINUTES') timestamps() ansiColor('xterm') + disableConcurrentBuilds() } triggers { - // Gitea Webhook 触发 GenericTrigger( genericVariables: [ [key: 'ref', value: '$.ref'], @@ -30,16 +31,13 @@ pipeline { [key: 'commit_message', value: '$.commits[0].message'] ], token: '${PROJECT_NAME}-ci-token', - causeString: 'Triggered by $ref on $repository', + causeString: 'Triggered by $ref on $repository (Commit: $commit_sha)', printContributedVariables: true, printPostContent: true, silentResponse: false, - regexpFilterText: '$ref$repository', - regexpFilterExpression: '^(refs/heads/main|refs/heads/develop).*$' + regexpFilterText: '$ref', + regexpFilterExpression: '^(refs/heads/main|refs/heads/develop)$' ) - - // 每天凌晨 3 点定时检查依赖安全漏洞 - pollSCM('H 3 * * *') } parameters { @@ -51,161 +49,149 @@ pipeline { } stages { - stage('🔧 环境准备') { + stage('🔧 环境检测与准备') { steps { - echo "📦 Node.js 版本: ${NODE_VERSION}" - echo "🌐 目标域名: ${DOMAIN}" - echo "🖥️ 生产服务器: ${SERVER_USER}@${SERVER_IP}" + echo "==========================================" + echo "🚀 Novalon Website CI/CD Pipeline" + echo "🐳 Docker Shell Mode (参考 deploy-dist.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' - sh 'npm --version' + sh 'node --version && npm --version' } } } stage('📥 安装依赖') { steps { + checkout scm + nodejs(nodeJSInstallationName: "${NODE_VERSION}") { sh ''' - rm -rf node_modules package-lock.json - npm ci --registry=${NPM_REGISTRY} --prefer-offline + 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}' ''' } } - post { - failure { - echo "❌ 依赖安装失败!请检查 npm registry 是否可用。" - } - } } stage('🔍 代码质量检查') { parallel { - stage('ESLint 检查') { + stage('ESLint') { steps { nodejs(nodeJSInstallationName: "${NODE_VERSION}") { - sh 'npm run lint' + sh 'npm run lint || exit 1' } } } - - stage('TypeScript 类型检查') { + stage('TypeScript') { steps { nodejs(nodeJSInstallationName: "${NODE_VERSION}") { - sh 'npm run type-check' + sh 'npm run type-check || exit 1' } } } } - post { - failure { - echo "❌ 代码质量检查未通过!" - echo "💡 提示:请运行 npm run lint 和 npm run type-check 在本地修复问题" - } - } } - stage('🧪 单元测试 + 覆盖率检查') { + stage('🧪 单元测试') { steps { nodejs(nodeJSInstallationName: "${NODE_VERSION}") { sh ''' export CI=true - npm run test:coverage:check + npm run test:coverage:check || exit 1 ''' } } post { always { publishHTML(target: [ - allowMissing: false, - alwaysLinkToLastBuild: true, - keepAll: true, + allowMissing: true, reportDir: 'coverage', reportFiles: 'index.html', - reportName: 'Coverage Report (Jest)' + reportName: 'Coverage Report' ]) } - success { - echo "✅ 单元测试通过!覆盖率 ≥ 80%" - } - failure { - echo "❌ 单元测试失败或覆盖率不足 80%!" - echo "💡 请查看 Coverage Report 了解详情" - } } } - stage('🏗️ 生产构建') { - when { - anyOf { - branch 'main' - branch 'develop' - } - } + stage('🏗️ 构建 dist') { steps { nodejs(nodeJSInstallationName: "${NODE_VERSION}") { sh ''' + set -e + + echo "🧹 清理旧构建..." rm -rf .next dist + + echo "🔨 执行 Next.js 构建..." npm run build:clean - ''' - } - sh ''' - if [ -d "dist" ]; then + + 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" - else - echo "❌ 构建失败:dist 目录不存在" - exit 1 - fi - ''' + echo "📊 文件数: $FILE_COUNT, 大小: $DIST_SIZE" + ''' + } } 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 了解哪些用例未通过" - } } } @@ -217,124 +203,167 @@ pipeline { } } steps { - echo "⚠️ 即将部署到生产环境:${DOMAIN}" - sleep(time: 5, unit: 'SECONDS') + echo "⚠️ 准备部署到生产环境: ${DOMAIN}" + sleep(time: 3, 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 + 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" - HTTP_STATUS=\$(curl -s -o /dev/null -w '%{http_code}' ${DOMAIN}) + 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" + echo "✅ 主页访问正常 (HTTP \$HTTP_STATUS)" else - echo "❌ 部署验证失败!HTTP 状态码: \$HTTP_STATUS" + echo "❌ 主页返回异常状态码: \$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" + 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 "🎉 部署完成!访问地址: ${DOMAIN}" + echo "🎉 部署成功!" + echo "📋 访问地址:" + echo " HTTP: http://${SERVER_IP}" + echo " HTTPS: https://novalon.cn" """ } 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 - ' - """ + 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 + ' + """ + } catch (Exception e) { + echo "❌ 自动回滚失败: ${e.getMessage()}" + echo "🚨 需要立即手动介入!" + } } } } } - - stage('🧹 清理工作空间') { - steps { - cleanWs(cleanWhenNotBuilt: false, - deleteDirs: true, - notFailBuild: true) - } - } } post { @@ -342,34 +371,18 @@ pipeline { 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""" - ) - } +╔════════════════════════════════════════════╗ +║ 📊 Jenkins Pipeline 报告 ║ +╠════════════════════════════════════════════╣ +║ 项目: ${env.JOB_NAME} +║ 构建号: #${env.BUILD_NUMBER} +║ 结果: ${result} +║ 耗时: ${duration} +║ 详情: ${env.BUILD_URL} +╚════════════════════════════════════════════╝ +""" } } @@ -378,11 +391,7 @@ ${env.BUILD_URL}console""" } failure { - echo "❌ Pipeline 执行失败!请检查上述错误信息。" - } - - aborted { - echo "⚠️ Pipeline 被中止!" + echo "❌ Pipeline 执行失败!请查看日志。" } } } diff --git a/jenkins-logged-in.png b/jenkins-logged-in.png new file mode 100644 index 0000000..ad72a89 Binary files /dev/null and b/jenkins-logged-in.png differ diff --git a/jenkins-login.png b/jenkins-login.png new file mode 100644 index 0000000..d3a87c5 Binary files /dev/null and b/jenkins-login.png differ diff --git a/scripts/setup-cicd/README.md b/scripts/setup-cicd/README.md new file mode 100644 index 0000000..fdc3bc1 --- /dev/null +++ b/scripts/setup-cicd/README.md @@ -0,0 +1,373 @@ +# Novalon Website CI/CD 自动化配置工具 + +## 📋 概述 + +一套完整的自动化脚本,用于快速配置 **Jenkins + Gitea** 的 CI/CD 流水线,实现: + +- ✅ **一键配置**:自动完成所有 Jenkins 和 Gitea 的配置 +- ✅ **Docker 适配**:专为 Docker 环境优化(Shell 模式) +- ✅ **免密部署**:自动挂载 SSH 密钥,实现生产服务器免密登录 +- ✅ **Webhook 集成**:自动配置 Gitea Webhook 触发构建 +- ✅ **完整验证**:自动检测所有配置是否正确 + +## 🎯 适用场景 + +- **首次部署**:全新环境的一键初始化 +- **环境重建**:需要重置或迁移时 +- **故障排查**:验证当前配置状态 +- **日常维护**:更新插件、重新配置 Webhook + +## 📁 文件结构 + +``` +scripts/setup-cicd/ +├── cicd.sh # 一键启动器(入口脚本) +├── setup-cicd.sh # 主配置流程 +├── cicd.config # 配置文件(可自定义修改) +└── lib/ + ├── utils.sh # 工具函数库 + ├── jenkins-setup.sh # Jenkins 配置逻辑 + ├── gitea-setup.sh # Gitea Webhook 配置 + └── verify.sh # 验证和测试 +``` + +## 🚀 快速开始 + +### 前置条件 + +1. **Docker 已安装并运行** +2. **SSH 密钥已生成**(用于免密登录生产服务器): + ```bash + ssh-keygen -t rsa -b 4096 -C "your-email@example.com" + ``` +3. **生产服务器的 `authorized_keys` 包含你的公钥**: + ```bash + ssh-copy-id root@139.155.109.62 + ``` + +### 使用方法 + +#### 1️⃣ 完整安装(首次使用) + +```bash +# 进入项目目录 +cd /Users/zhangxiang/Codes/Novalon/novalon-website + +# 赋予执行权限 +chmod +x scripts/setup-cicd/*.sh scripts/setup-cicd/lib/*.sh + +# 执行完整安装(会重建 Jenkins 容器) +./scripts/setup-cicd/cicd.sh --full +``` + +**执行过程**: +``` +╔════════════════════════════════════════════╗ +║ 🚀 Novalon Website CI/CD 自动配置工具 ║ +╚════════════════════════════════════════════╝ + +📋 模式: 完整安装(包含 Docker 容器重建) + +════════════════════════════════════════════ + 检查前置条件 +════════════════════════════════════════════ + +✅ docker 已安装 +✅ curl 已安装 +✅ jq 已安装 +✅ 前置条件检查通过 + +════════════════════════════════════════════ + 配置 Jenkins Docker 容器 +════════════════════════════════════════════ + +✅ Jenkins 容器已启动 +容器名称: jenkins +访问地址: http://ci.f.novalon.cn:8080 + +...(后续步骤自动完成) + +════════════════════════════════════════════ + ✅ 自动化配置完成! +════════════════════════════════════════════ + +🌐 Jenkins: http://ci.f.novalon.cn:8080 +🔗 Gitea: https://git.f.novalon.cn +🚀 任务名: novalon-website-ci-cd +``` + +--- + +#### 2️⃣ 快速配置(跳过容器重建) + +如果 Jenkins 容器已经在运行,只是需要重新配置: + +```bash +./scripts/setup-cicd/cicd.sh +``` + +这会跳过容器创建,直接进行工具安装、凭据配置、任务创建和 Webhook 设置。 + +--- + +#### 3️⃣ 仅配置 Jenkins + +```bash +./scripts/setup-cicd/cicd.sh --jenkins +``` + +适用于: +- 安装新的 Jenkins 插件 +- 更新全局工具配置 +- 重新创建 Pipeline 任务 + +--- + +#### 4️⃣ 仅配置 Gitea Webhook + +```bash +./scripts/setup-cicd/cicd.sh --webhook +``` + +适用于: +- Webhook 丢失或损坏 +- 更新 Webhook URL +- 重新测试触发机制 + +--- + +#### 5️⃣ 验证当前配置 + +```bash +./scripts/setup-cicd/cicd.sh --verify +``` + +输出示例: +``` +════════════════════════════════════════════ + 验证结果汇总 +════════════════════════════════════════════ + + 总计: 7 项检查 + 通过: 7 项 + + 🎉 所有配置已就绪!可以开始使用 CI/CD 流水线了。 + + 下一步操作: + 1. 访问 http://ci.f.novalon.cn:8080/job/novalon-website-ci-cd/ + 2. 点击 'Build Now' 进行首次构建 + 3. 推送代码到 main 分支测试自动触发 + 4. 勾选 DEPLOY_TO_PRODUCTION 参数测试部署 +``` + +--- + +#### 6️⃣ 清理所有配置 + +```bash +./scripts/setup-cicd/cicd.sh --clean +``` + +⚠️ **警告**:此操作会删除 Jenkins 容器和所有数据! + +--- + +## ⚙️ 自定义配置 + +编辑 `cicd.config` 文件可以修改以下参数: + +```bash +# Jenkins 配置 +JENKINS_URL="http://ci.f.novalon.cn:8080" +JENKINS_CONTAINER_NAME="jenkins" +JENKINS_IMAGE="jenkins/jenkins:lts-jdk17" + +# Gitea 配置 +GITEA_URL="https://git.f.novalon.cn" +GITEA_USERNAME="zhangxiang@novalon.cn" +GITEA_PASSWORD="qerqek-cavxaC-5jupri" + +# 生产服务器配置 +SERVER_IP="139.155.109.62" +SERVER_USER="root" +DEPLOY_ROOT="/home/novalon/docker-app" + +# 项目配置 +REPO_OWNER="novalon" +REPO_NAME="novalon-website" +JOB_NAME="novalon-website-ci-cd" +``` + +## 🔧 故障排查 + +### 问题 1:SSH 连接失败 + +**症状**:验证阶段显示 "❌ SSH 免密登录: 失败" + +**解决方案**: +```bash +# 1. 在宿主机上测试 SSH 连接 +ssh root@139.155.109.62 + +# 2. 如果失败,复制公钥到服务器 +ssh-copy-id root@139.155.109.62 + +# 3. 重新运行配置脚本 +./scripts/setup-cicd/cicd.sh --jenkins +``` + +--- + +### 问题 2:Jenkins 容器启动失败 + +**症状**:`docker run` 报错或容器无法访问 + +**解决方案**: +```bash +# 查看容器日志 +docker logs jenkins + +# 检查端口占用 +lsof -i :8080 + +# 清理后重试 +docker stop jenkins && docker rm jenkins +./scripts/setup-cicd/cicd.sh --full +``` + +--- + +### 问题 3:Webhook 未触发构建 + +**症状**:推送代码后 Jenkins 无反应 + +**解决方案**: +```bash +# 1. 验证 Webhook 是否存在 +./scripts/setup-cicd/cicd.sh --verify + +# 2. 手动测试 Webhook +curl -X POST "http://ci.f.novalon.cn:8080/generic-webhook-trigger/invoke?token=novalon-website-ci-token" \ + -H "Content-Type: application/json" \ + -d '{"ref":"refs/heads/main"}' + +# 3. 如果不通,检查网络连通性 +docker exec gitea curl -I http://jenkins:8080 +``` + +--- + +### 问题 4:插件安装失败 + +**症状**:Jenkins 插件未正确加载 + +**解决方案**: +```bash +# 重启 Jenkins 使插件生效 +docker restart jenkins + +# 等待就绪后重新运行 +sleep 30 +./scripts/setup-cicd/cicd.sh --jenkins +``` + +--- + +## 📊 工作流程图 + +``` +┌─────────────────────┐ +│ 开发者推送代码 │ +│ git push origin main│ +└─────────┬───────────┘ + │ + ▼ +┌─────────────────────┐ ┌──────────────────┐ +│ Gitea Webhook │────▶│ Jenkins Trigger │ +│ (Push Event) │ │ (Generic Trigger) │ +└─────────────────────┘ └────────┬─────────┘ + │ + ┌────────────────┼────────────────┐ + ▼ ▼ ▼ + ┌───────────┐ ┌─────────────┐ ┌──────────────┐ + │ 环境准备 │ │ 依赖安装 │ │ 代码质量检查 │ + │ (Node.js) │ │ (npm ci) │ │(ESLint+TS) │ + └───────────┘ └─────────────┘ └──────────────┘ + │ │ │ + └────────────────┼────────────────┘ + ▼ + ┌──────────────┐ + │ 单元测试 │ + │ (覆盖率≥80%) │ + └──────┬───────┘ + ▼ + ┌──────────────┐ + │ 构建 dist │ + │ (next build) │ + └──────┬───────┘ + ▼ + ┌──────────────────────────────┐ + │ DEPLOY_TO_PRODUCTION=true ? │ + └──────────────┬───────────────┘ + Yes ↘ ↘ No + ┌──────────┐ ┌──────────┐ + │ rsync 部署│ │ 构建完成 │ + │ Nginx 重载│ │ (仅构建) │ + │ 验证页面 │ └──────────┘ + └─────┬────┘ + ▼ + ┌──────────────┐ + │ 发送通知 │ + │ (钉钉/邮件) │ + └──────────────┘ +``` + +--- + +## 🎯 最佳实践建议 + +### 1. 定期运行验证 + +```bash +# 添加到 crontab,每周五上午 10 点自动验证 +0 10 * * 5 cd /path/to/project && ./scripts/setup-cicd/cicd.sh --verify >> /var/log/cicd-check.log 2>&1 +``` + +### 2. 版本控制配置文件 + +将 `cicd.config` 提交到 Git(注意移除敏感信息或使用环境变量): + +```bash +git add scripts/setup-cicd/ +git commit -m "chore: add automated CI/CD setup scripts" +git push origin main +``` + +### 3. 团队协作 + +新成员入职时只需运行: + +```bash +git clone +cd novalon-website +./scripts/setup-cicd/cicd.sh --full +``` + +即可在 5 分钟内完成完整的开发环境搭建! + +--- + +## 📞 技术支持 + +如遇到问题: + +1. 运行 `./scripts/setup-cicd/cicd.sh --verify` 查看详细诊断 +2. 检查日志:`docker logs jenkins` +3. 查看 Gitea Webhook 的 Recent Deliveries +4. 访问 Jenkins 系统日志:Manage Jenkins → System Log + +--- + +**版本**: 1.0.0 +**最后更新**: 2026-05-12 +**适用环境**: Docker + Jenkins 2.400+ + Gitea 1.19+ diff --git a/scripts/setup-cicd/cicd.config b/scripts/setup-cicd/cicd.config new file mode 100644 index 0000000..deeb1dd --- /dev/null +++ b/scripts/setup-cicd/cicd.config @@ -0,0 +1,36 @@ +#!/bin/bash + +JENKINS_URL="http://ci.f.novalon.cn:8080" +JENKINS_HOST="ci.f.novalon.cn" +JENKINS_PORT=8080 +JENKINS_CONTAINER_NAME="jenkins" +JENKINS_IMAGE="jenkins/jenkins:lts-jdk17" + +GITEA_URL="https://git.f.novalon.cn" +GITEA_HOST="git.f.novalon.cn" + +GITEA_USERNAME="zhangxiang@novalon.cn" +GITEA_PASSWORD="qerqek-cavxaC-5jupri" +GITEA_TOKEN="" + +JENKINS_ADMIN_USER="zhangxiang" +JENKINS_ADMIN_PASSWORD="Vaptyc-jadpe1-fywmaj" +JENKINS_API_TOKEN="" + +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" + +REPO_OWNER="novalon" +REPO_NAME="novalon-website" +JOB_NAME="novalon-website-ci-cd" +WEBHOOK_TOKEN="novalon-website-ci-token" + +NODE_VERSION="18" +NPM_REGISTRY="https://registry.npmmirror.com" +BACKUP_RETENTION_COUNT=3 + +DOCKER_NETWORK="novalon-network" diff --git a/scripts/setup-cicd/cicd.sh b/scripts/setup-cicd/cicd.sh new file mode 100755 index 0000000..c5aa0e2 --- /dev/null +++ b/scripts/setup-cicd/cicd.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "╔════════════════════════════════════════════╗" +echo "║ 🚀 Novalon CI/CD 一键配置启动器 ║" +echo "╚════════════════════════════════════════════╝" +echo "" + +case "$1" in + --full|-f) + echo "📋 模式: 完整安装(重建容器 + 全部配置)" + exec bash "${SCRIPT_DIR}/setup-cicd.sh" --full + ;; + --jenkins|-j) + echo "📋 模式: 仅配置 Jenkins(工具 + 凭据 + 插件)" + exec bash "${SCRIPT_DIR}/setup-cicd.sh" --jenkins + ;; + --webhook|-w) + echo "📋 模式: 仅配置 Gitea Webhook" + exec bash "${SCRIPT_DIR}/setup-cicd.sh" --webhook + ;; + --verify|-v) + echo "📋 模式: 验证当前配置" + source "${SCRIPT_DIR}/lib/utils.sh" + source "${SCRIPT_DIR}/lib/verify.sh" + load_config_or_defaults + run_all_verifications + exit 0 + ;; + --clean|-c) + echo "📋 模式: 清理所有配置" + read -p "⚠️ 确定要删除 Jenkins 容器和所有配置吗?(y/N): " confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + docker stop jenkins 2>/dev/null || true + docker rm jenkins 2>/dev/null || true + docker volume rm jenkins_data 2>/dev/null || true + echo "✅ 清理完成" + fi + exit 0 + ;; + --help|-h|*) + echo "用法: $0 [选项]" + echo "" + echo "选项:" + echo " --full, -f 完整安装(重建 Docker 容器 + 安装工具 + 配置凭据 + 创建任务 + 配置 Webhook)" + echo " --jenkins, -j 仅配置 Jenkins(安装工具、插件、凭据,创建 Pipeline 任务)" + echo " --webhook, -w 仅配置 Gitea Webhook" + echo " --verify, -v 验证当前配置是否正确" + echo " --clean, -c 清理 Jenkins 容器和数据" + echo " --help, -h 显示帮助信息" + echo "" + echo "示例:" + echo " $0 --full # 首次使用或需要完全重置时" + echo " $0 # 快速配置(不重建容器)" + echo " $0 --verify # 检查所有配置是否正常" + echo "" + exit 0 + ;; +esac diff --git a/scripts/setup-cicd/setup-cicd.sh b/scripts/setup-cicd/setup-cicd.sh new file mode 100755 index 0000000..b4be1d7 --- /dev/null +++ b/scripts/setup-cicd/setup-cicd.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +set -e + +echo "╔════════════════════════════════════════════╗" +echo "║ 🚀 Novalon Website CI/CD 自动配置工具 ║" +echo "║ Version: 1.0.0 ║" +echo "║ Mode: Docker + Shell (参考 deploy-dist.sh) ║" +echo "╚════════════════════════════════════════════╝" +echo "" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +CONFIG_FILE="$SCRIPT_DIR/cicd.config" + +source "$SCRIPT_DIR/lib/utils.sh" +source "$SCRIPT_DIR/lib/jenkins-setup.sh" +source "$SCRIPT_DIR/lib/gitea-setup.sh" +source "$SCRIPT_DIR/lib/verify.sh" + +load_config_or_defaults + +show_banner "开始自动化配置流程" + +check_prerequisites || exit 1 + +if [ "$1" == "--full" ] || [ "$1" == "-f" ]; then + echo "📋 模式: 完整安装(包含 Docker 容器重建)" + setup_jenkins_container + install_jenkins_tools +elif [ "$1" == "--jenkins" ] || [ "$1" == "-j" ]; then + echo "📋 模式: 仅配置 Jenkins" + install_jenkins_tools +elif [ "$1" == "--webhook" ] || [ "$1" == "-w" ]; then + echo "📋 模式: 仅配置 Gitea Webhook" +else + echo "📋 模式: 快速配置(跳过容器重建)" +fi + +wait_for_jenkins_ready + +setup_jenkins_credentials +setup_jenkins_global_tools +install_required_plugins +create_pipeline_job +setup_gitea_webhook + +echo "" +show_banner "配置完成!开始验证..." + +run_all_verifications + +echo "" +echo "╔════════════════════════════════════════════╗" +echo "║ ✅ 自动化配置完成! ║" +echo "╠════════════════════════════════════════════╣" +echo "║ ║" +echo "║ 🌐 Jenkins: ${JENKINS_URL} ║" +echo "║ 🔗 Gitea: ${GITEA_URL} ║" +echo "║ 🚀 任务名: ${JOB_NAME} ║" +echo "║ ║" +echo "║ 下一步操作: ║" +echo "║ 1. 访问 Jenkins 并点击 'Build Now' ║" +echo "║ 2. 推送代码到 main 分支测试 Webhook ║" +echo "║ 3. 勾选 DEPLOY_TO_PRODUCTION 测试部署 ║" +echo "║ ║" +echo "╚════════════════════════════════════════════╝"