From 1103cd95e02925c737210f376ccdc584dd8bc0fb Mon Sep 17 00:00:00 2001 From: wangtianqi <1350217033@qq.com> Date: Wed, 25 Jun 2025 16:46:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=9D=E8=AF=95=E6=9C=AC=E5=9C=B0=E9=85=8D?= =?UTF-8?q?=E7=BD=AEsonarQube=20scanner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile | 14 +- Jenkinsfile.template | 769 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 774 insertions(+), 9 deletions(-) create mode 100644 Jenkinsfile.template diff --git a/Jenkinsfile b/Jenkinsfile index 5d89d6b..51e2ead 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,6 +9,7 @@ pipeline { tools { go 'go' // 使用Jenkins手动配置的Go工具 + sonarRunnerInstallation 'sonarQube' // 使用Jenkins配置的SonarQube工具 } environment { @@ -157,20 +158,15 @@ pipeline { echo "SONAR_SCANNER_HOME: $SONAR_SCANNER_HOME" echo "PATH: $PATH" - # 查找sonar-scanner工具位置 - SCANNER_PATH="" + # 直接使用配置的sonar-scanner if [ -n "$SONAR_SCANNER_HOME" ] && [ -f "$SONAR_SCANNER_HOME/bin/sonar-scanner" ]; then SCANNER_PATH="$SONAR_SCANNER_HOME/bin/sonar-scanner" - elif [ -f "/var/jenkins_home/tools/hudson.plugins.sonar.SonarRunnerInstallation/sonarQube/bin/sonar-scanner" ]; then - SCANNER_PATH="/var/jenkins_home/tools/hudson.plugins.sonar.SonarRunnerInstallation/sonarQube/bin/sonar-scanner" - elif command -v sonar-scanner >/dev/null 2>&1; then - SCANNER_PATH="sonar-scanner" else - echo "❌ 无法找到sonar-scanner,跳过扫描" - exit 0 + echo "❌ SONAR_SCANNER_HOME未正确配置" + exit 1 fi - echo "✅ 找到sonar-scanner: $SCANNER_PATH" + echo "✅ 使用sonar-scanner: $SCANNER_PATH" # 运行SonarQube扫描 "$SCANNER_PATH" \ diff --git a/Jenkinsfile.template b/Jenkinsfile.template new file mode 100644 index 0000000..8c05b3f --- /dev/null +++ b/Jenkinsfile.template @@ -0,0 +1,769 @@ +pipeline { + agent any + + // 参数化配置 - 让模板更通用 + parameters { + choice( + name: 'DEPLOY_ENV', + choices: ['auto', 'production', 'staging', 'development'], + description: '部署环境 (auto=根据分支自动选择)' + ) + booleanParam( + name: 'SKIP_TESTS', + defaultValue: false, + description: '跳过单元测试' + ) + booleanParam( + name: 'SKIP_SONAR', + defaultValue: false, + description: '跳过代码质量扫描' + ) + booleanParam( + name: 'FORCE_REBUILD', + defaultValue: false, + description: '强制重新构建Docker镜像' + ) + string( + name: 'GO_VERSION', + defaultValue: '1.21', + description: 'Go版本' + ) + string( + name: 'IMAGE_REGISTRY', + defaultValue: '', + description: 'Docker镜像仓库 (留空使用本地)' + ) + } + + options { + buildDiscarder(logRotator( + numToKeepStr: '20', + daysToKeepStr: '30', + artifactNumToKeepStr: '10' + )) + timeout(time: 60, unit: 'MINUTES') + timestamps() + parallelsAlwaysFailFast() + skipDefaultCheckout() + } + + tools { + go 'go' // Jenkins中配置的Go工具名称 + } + + environment { + // =========================================== + // 📝 项目配置 - 根据你的项目修改这些变量 + // =========================================== + PROJECT_NAME = 'golang-demo' // 🔧 修改为你的项目名 + DEPLOY_SERVER = '116.62.163.84' // 🔧 修改为你的服务器 + SSH_CREDENTIAL_ID = 'deploy-server-ssh-key' // 🔧 修改为你的SSH凭据ID + + // SonarQube配置 (可选) + SONAR_HOST_URL = 'http://116.62.163.84:15010' // 🔧 修改为你的SonarQube地址 + SONAR_PROJECT_KEY = "${PROJECT_NAME}" + SONAR_CREDENTIAL_ID = 'sonar-token' // 🔧 修改为你的SonarQube凭据ID + + // =========================================== + // 🔧 环境端口配置 + // =========================================== + PROD_PORT = '15021' // 生产环境端口 + STAGING_PORT = '15022' // 预发布环境端口 + DEV_PORT = '15023' // 开发环境端口 + + // =========================================== + // 🔧 自动化配置 - 通常不需要修改 + // =========================================== + CGO_ENABLED = '0' + GOOS = 'linux' + GOARCH = 'amd64' + + // 动态变量 + IMAGE_NAME = "${PROJECT_NAME}" + IMAGE_TAG = "${BUILD_NUMBER}" + FULL_IMAGE_NAME = "${params.IMAGE_REGISTRY ? params.IMAGE_REGISTRY + '/' : ''}${IMAGE_NAME}:${IMAGE_TAG}" + } + + stages { + stage('🔄 初始化') { + parallel { + stage('检出代码') { + steps { + echo "🔄 检出代码..." + checkout scm + + script { + env.GIT_COMMIT_SHORT = sh( + script: "git rev-parse --short HEAD", + returnStdout: true + ).trim() + + env.GIT_BRANCH = sh( + script: "git rev-parse --abbrev-ref HEAD", + returnStdout: true + ).trim() + + // 根据分支自动确定部署环境 + if (params.DEPLOY_ENV == 'auto') { + if (env.BRANCH_NAME == 'main' || env.BRANCH_NAME == 'master') { + env.DEPLOY_ENV = 'production' + env.DEPLOY_PORT = env.PROD_PORT + } else if (env.BRANCH_NAME == 'staging' || env.BRANCH_NAME == 'release') { + env.DEPLOY_ENV = 'staging' + env.DEPLOY_PORT = env.STAGING_PORT + } else { + env.DEPLOY_ENV = 'development' + env.DEPLOY_PORT = env.DEV_PORT + } + } else { + env.DEPLOY_ENV = params.DEPLOY_ENV + switch(params.DEPLOY_ENV) { + case 'production': + env.DEPLOY_PORT = env.PROD_PORT + break + case 'staging': + env.DEPLOY_PORT = env.STAGING_PORT + break + default: + env.DEPLOY_PORT = env.DEV_PORT + } + } + + env.CONTAINER_NAME = "${PROJECT_NAME}-${DEPLOY_ENV}" + } + + echo "📋 构建信息:" + echo " 项目: ${PROJECT_NAME}" + echo " 分支: ${env.BRANCH_NAME}" + echo " 提交: ${env.GIT_COMMIT_SHORT}" + echo " 环境: ${env.DEPLOY_ENV}" + echo " 端口: ${env.DEPLOY_PORT}" + } + } + + stage('环境检查') { + steps { + echo '🔍 检查构建环境...' + sh ''' + echo "=== 系统信息 ===" + uname -a + + echo "=== Go版本 ===" + go version + + echo "=== Go环境 ===" + go env GOROOT GOPATH GOPROXY GOSUMDB + + echo "=== Docker版本 ===" + docker --version + docker system info --format "{{.Name}}: {{.ServerVersion}}" + + echo "=== 工作目录 ===" + pwd && ls -la + + echo "=== 检查Go项目结构 ===" + if [ -f "go.mod" ]; then + echo "✅ 发现go.mod文件" + cat go.mod | head -10 + else + echo "⚠️ 未发现go.mod文件,可能不是Go模块项目" + fi + ''' + echo "✅ 环境检查完成" + } + } + } + } + + stage('📦 依赖管理') { + steps { + echo '📦 管理Go依赖...' + sh ''' + echo "下载依赖..." + go mod download -x + + echo "验证依赖..." + go mod verify + + echo "整理依赖..." + go mod tidy + + echo "检查依赖更新..." + go list -u -m all | head -20 || true + + echo "✅ 依赖管理完成" + ''' + } + } + + stage('🔍 代码质量') { + parallel { + stage('静态检查') { + steps { + echo '🔍 运行Go静态检查...' + sh ''' + echo "运行go vet..." + go vet ./... || { + echo "❌ go vet 发现问题" + exit 1 + } + + echo "检查代码格式..." + UNFORMATTED=$(gofmt -l .) + if [ -n "$UNFORMATTED" ]; then + echo "❌ 以下文件格式不正确:" + echo "$UNFORMATTED" + echo "请运行: go fmt ./..." + exit 1 + fi + + echo "检查import排序..." + if command -v goimports >/dev/null 2>&1; then + goimports -l . | head -10 + fi + + echo "✅ 静态检查通过" + ''' + } + } + + stage('安全扫描') { + when { + not { params.SKIP_TESTS } + } + steps { + echo '🔒 运行安全扫描...' + sh ''' + echo "检查已知漏洞..." + if command -v govulncheck >/dev/null 2>&1; then + govulncheck ./... || echo "⚠️ govulncheck未安装或发现问题" + else + echo "⚠️ govulncheck未安装,跳过漏洞检查" + fi + + echo "检查敏感信息..." + if command -v git-secrets >/dev/null 2>&1; then + git secrets --scan --recursive . || echo "⚠️ 发现敏感信息" + fi + + echo "✅ 安全扫描完成" + ''' + } + } + } + } + + stage('🧪 测试') { + when { + not { params.SKIP_TESTS } + } + parallel { + stage('单元测试') { + steps { + echo '🧪 运行单元测试...' + sh ''' + echo "运行测试..." + mkdir -p test-results + + # 运行测试并生成多种格式的报告 + go test -v -coverprofile=coverage.out -covermode=atomic \ + -json ./... > test-results/test-report.json + + # 生成JUnit格式的测试报告(如果有go-junit-report) + if command -v go-junit-report >/dev/null 2>&1; then + cat test-results/test-report.json | go-junit-report > test-results/junit.xml + fi + + echo "生成覆盖率报告..." + go tool cover -html=coverage.out -o coverage.html + go tool cover -func=coverage.out | tee coverage-summary.txt + + # 提取覆盖率百分比 + COVERAGE=$(go tool cover -func=coverage.out | grep total | grep -oE '[0-9]+\.[0-9]+%') + echo "📊 总覆盖率: $COVERAGE" + echo "$COVERAGE" > coverage.txt + + echo "✅ 单元测试完成" + ''' + } + post { + always { + script { + // 发布测试报告 + if (fileExists('test-results/junit.xml')) { + publishTestResults testResultsPattern: 'test-results/junit.xml' + } + + // 发布覆盖率报告 + if (fileExists('coverage.html')) { + publishHTML([ + allowMissing: false, + alwaysLinkToLastBuild: true, + keepAll: true, + reportDir: '.', + reportFiles: 'coverage.html', + reportName: 'Go Coverage Report', + reportTitles: '' + ]) + } + + // 归档测试产物 + archiveArtifacts artifacts: 'coverage.out,coverage.txt,test-results/*', allowEmptyArchive: true + } + } + } + } + + stage('性能测试') { + steps { + echo '⚡ 运行性能测试...' + sh ''' + echo "运行基准测试..." + if go test -bench=. -benchmem ./... > benchmark.txt 2>&1; then + echo "✅ 基准测试完成" + cat benchmark.txt + else + echo "⚠️ 未发现基准测试或测试失败" + fi + ''' + } + post { + always { + archiveArtifacts artifacts: 'benchmark.txt', allowEmptyArchive: true + } + } + } + } + } + + stage('📊 代码扫描') { + when { + not { params.SKIP_SONAR } + } + steps { + echo '📊 运行SonarQube代码扫描...' + script { + try { + withCredentials([string(credentialsId: "${SONAR_CREDENTIAL_ID}", variable: 'SONAR_TOKEN')]) { + withSonarQubeEnv('sonarQube') { + sh ''' + # 动态查找sonar-scanner + SCANNER_PATH="" + for path in \ + "$SONAR_SCANNER_HOME/bin/sonar-scanner" \ + "/var/jenkins_home/tools/hudson.plugins.sonar.SonarRunnerInstallation/sonarQube/bin/sonar-scanner" \ + "$(which sonar-scanner 2>/dev/null)"; do + if [ -f "$path" ]; then + SCANNER_PATH="$path" + break + fi + done + + if [ -z "$SCANNER_PATH" ]; then + echo "❌ 无法找到sonar-scanner" + exit 1 + fi + + echo "✅ 使用sonar-scanner: $SCANNER_PATH" + + # 运行扫描 + "$SCANNER_PATH" \\ + -Dsonar.projectKey=${SONAR_PROJECT_KEY} \\ + -Dsonar.projectName="${PROJECT_NAME}" \\ + -Dsonar.projectVersion=${BUILD_NUMBER} \\ + -Dsonar.sources=. \\ + -Dsonar.exclusions=**/*_test.go,**/vendor/**,**/*.mod,**/*.sum,**/test-results/** \\ + -Dsonar.tests=. \\ + -Dsonar.test.inclusions=**/*_test.go \\ + -Dsonar.test.exclusions=**/vendor/** \\ + -Dsonar.go.coverage.reportPaths=coverage.out \\ + -Dsonar.sourceEncoding=UTF-8 + ''' + } + } + echo "✅ SonarQube扫描完成" + } catch (Exception e) { + echo "⚠️ SonarQube扫描失败: ${e.getMessage()}" + if (env.DEPLOY_ENV == 'production') { + error("生产环境必须通过代码质量检查") + } + } + } + } + } + + stage('🔨 构建') { + parallel { + stage('编译应用') { + steps { + echo '🔨 编译Go应用...' + sh ''' + echo "开始编译..." + + # 设置构建信息 + BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S') + LDFLAGS="-w -s -X main.Version=${BUILD_NUMBER} -X main.GitCommit=${GIT_COMMIT_SHORT} -X main.BuildTime=${BUILD_TIME}" + + # 编译 + CGO_ENABLED=${CGO_ENABLED} GOOS=${GOOS} GOARCH=${GOARCH} \\ + go build -ldflags="$LDFLAGS" -o ${PROJECT_NAME} . + + echo "验证二进制文件..." + ls -lh ${PROJECT_NAME} + file ${PROJECT_NAME} + + # 尝试获取版本信息 + ./${PROJECT_NAME} --version 2>/dev/null || echo "应用不支持--version参数" + + echo "✅ 编译完成" + ''' + } + post { + success { + archiveArtifacts artifacts: "${PROJECT_NAME}", fingerprint: true + } + } + } + + stage('准备部署文件') { + steps { + echo '📝 准备部署配置文件...' + sh ''' + # 创建部署脚本 + cat > deploy.sh << 'EOF' +#!/bin/bash +set -e + +PROJECT_NAME="${PROJECT_NAME}" +CONTAINER_NAME="${CONTAINER_NAME}" +DEPLOY_PORT="${DEPLOY_PORT}" +IMAGE_TAG="${IMAGE_TAG}" + +echo "🚀 部署 $PROJECT_NAME 到 ${DEPLOY_ENV} 环境..." + +# 停止旧容器 +echo "停止旧容器..." +docker stop $CONTAINER_NAME 2>/dev/null || true +docker rm $CONTAINER_NAME 2>/dev/null || true + +# 启动新容器 +echo "启动新容器..." +docker run -d \\ + --name $CONTAINER_NAME \\ + --restart unless-stopped \\ + -p $DEPLOY_PORT:8080 \\ + -e GIN_MODE=release \\ + -e DEPLOY_ENV=${DEPLOY_ENV} \\ + $PROJECT_NAME:$IMAGE_TAG + +echo "✅ 部署完成" +echo "🔗 访问地址: http://$(hostname -I | awk '{print $1}'):$DEPLOY_PORT" +EOF + + chmod +x deploy.sh + echo "✅ 部署脚本准备完成" + ''' + } + } + } + } + + stage('🐳 Docker镜像') { + steps { + echo '🐳 构建Docker镜像...' + script { + // 检查是否需要重新构建 + def shouldBuild = params.FORCE_REBUILD + if (!shouldBuild) { + def imageExists = sh( + script: "docker images -q ${IMAGE_NAME}:${IMAGE_TAG}", + returnStdout: true + ).trim() + shouldBuild = imageExists.isEmpty() + } + + if (shouldBuild) { + sh ''' + echo "开始构建Docker镜像..." + + # 确保二进制文件存在 + if [ ! -f "${PROJECT_NAME}" ]; then + echo "❌ 二进制文件不存在" + exit 1 + fi + + chmod +x ${PROJECT_NAME} + + # 构建镜像 + docker build \\ + --build-arg PROJECT_NAME=${PROJECT_NAME} \\ + --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \\ + --build-arg VCS_REF=${GIT_COMMIT_SHORT} \\ + --build-arg VERSION=${BUILD_NUMBER} \\ + -t ${IMAGE_NAME}:${IMAGE_TAG} \\ + -t ${IMAGE_NAME}:latest \\ + . + + echo "镜像构建完成" + docker images ${IMAGE_NAME}:${IMAGE_TAG} + + # 检查镜像大小 + IMAGE_SIZE=$(docker images --format "table {{.Size}}" ${IMAGE_NAME}:${IMAGE_TAG} | tail -n 1) + echo "📦 镜像大小: $IMAGE_SIZE" + ''' + + // 推送到镜像仓库(如果配置了) + if (params.IMAGE_REGISTRY) { + sh ''' + echo "推送镜像到仓库..." + docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${FULL_IMAGE_NAME} + docker push ${FULL_IMAGE_NAME} + echo "✅ 镜像推送完成" + ''' + } + } else { + echo "✅ 镜像已存在,跳过构建" + } + } + } + } + + stage('🧪 镜像测试') { + steps { + echo '🧪 测试Docker镜像...' + script { + def testContainerName = "test-${PROJECT_NAME}-${BUILD_NUMBER}" + def testPort = "808${BUILD_NUMBER % 100}" + + try { + sh """ + echo "启动测试容器..." + docker run -d --name ${testContainerName} \\ + -p ${testPort}:8080 \\ + -e GIN_MODE=test \\ + ${IMAGE_NAME}:${IMAGE_TAG} + + echo "等待应用启动..." + sleep 15 + + echo "检查容器状态..." + docker ps | grep ${testContainerName} + docker logs ${testContainerName} + + echo "测试应用端点..." + for i in \$(seq 1 10); do + echo "尝试连接 \$i/10..." + if curl -f http://localhost:${testPort}/health; then + echo "✅ 健康检查通过" + break + elif curl -f http://localhost:${testPort}/ping; then + echo "✅ Ping检查通过" + break + else + if [ \$i -eq 10 ]; then + echo "❌ 镜像测试失败" + exit 1 + fi + echo "等待3秒后重试..." + sleep 3 + fi + done + + echo "✅ 镜像测试通过" + """ + } finally { + // 清理测试容器 + sh """ + docker stop ${testContainerName} || true + docker rm ${testContainerName} || true + """ + } + } + } + } + + stage('🚀 部署') { + when { + anyOf { + branch 'main' + branch 'master' + branch 'develop' + branch 'staging' + expression { params.DEPLOY_ENV != 'auto' } + } + } + steps { + echo "🚀 部署到${env.DEPLOY_ENV}环境..." + script { + sshagent([env.SSH_CREDENTIAL_ID]) { + sh ''' + echo "📤 传输部署文件..." + + # 保存镜像 + docker save ${IMAGE_NAME}:${IMAGE_TAG} -o ${PROJECT_NAME}-${IMAGE_TAG}.tar + + # 传输文件 + scp -o StrictHostKeyChecking=no \\ + ${PROJECT_NAME}-${IMAGE_TAG}.tar \\ + deploy.sh \\ + root@${DEPLOY_SERVER}:/tmp/ + + # 远程部署 + ssh -o StrictHostKeyChecking=no root@${DEPLOY_SERVER} << EOF + cd /tmp + + echo "🔄 加载Docker镜像..." + docker load -i ${PROJECT_NAME}-${IMAGE_TAG}.tar + + echo "🚀 执行部署..." + chmod +x deploy.sh + ./deploy.sh + + echo "🧹 清理临时文件..." + rm -f ${PROJECT_NAME}-${IMAGE_TAG}.tar deploy.sh + + echo "✅ 部署完成" +EOF + + # 清理本地临时文件 + rm -f ${PROJECT_NAME}-${IMAGE_TAG}.tar + ''' + } + } + } + } + + stage('🏥 健康检查') { + when { + anyOf { + branch 'main' + branch 'master' + branch 'develop' + branch 'staging' + expression { params.DEPLOY_ENV != 'auto' } + } + } + steps { + echo '🏥 执行应用健康检查...' + script { + sleep(time: 30, unit: 'SECONDS') + + def healthCheckPassed = false + for (int i = 1; i <= 5; i++) { + try { + def response = sh( + script: "curl -s -o /dev/null -w '%{http_code}' http://${DEPLOY_SERVER}:${env.DEPLOY_PORT}/health", + returnStdout: true + ).trim() + + if (response == "200") { + echo "✅ 第${i}次健康检查通过" + healthCheckPassed = true + break + } else { + echo "⚠️ 第${i}次健康检查失败,状态码: ${response}" + } + } catch (Exception e) { + echo "⚠️ 第${i}次健康检查异常: ${e.getMessage()}" + } + + if (i < 5) { + echo "等待30秒后重试..." + sleep(time: 30, unit: 'SECONDS') + } + } + + if (!healthCheckPassed && env.DEPLOY_ENV == 'production') { + error("❌ 生产环境健康检查失败,回滚部署") + } else if (!healthCheckPassed) { + echo "⚠️ 健康检查失败,但允许${env.DEPLOY_ENV}环境继续" + } + } + } + } + } + + post { + always { + script { + echo '🧹 执行清理工作...' + + // 清理构建产物 + sh """ + rm -f ${PROJECT_NAME} coverage.out coverage.html deploy.sh + rm -rf test-results/ + docker image prune -f || true + docker builder prune -f || true + """ + + // 发布构建徽章 + def badgeText = "${env.DEPLOY_ENV}: ${currentBuild.currentResult}" + def badgeColor = currentBuild.currentResult == 'SUCCESS' ? 'brightgreen' : 'red' + addBadge(icon: "success.gif", text: badgeText, color: badgeColor) + } + } + + success { + script { + def message = """ +🎉 ${PROJECT_NAME} 构建成功! + +📋 项目: ${env.JOB_NAME} +🔢 构建号: ${env.BUILD_NUMBER} +🌿 分支: ${env.BRANCH_NAME ?: 'unknown'} +📝 提交: ${env.GIT_COMMIT_SHORT ?: 'unknown'} +🌍 环境: ${env.DEPLOY_ENV} +⏱️ 耗时: ${currentBuild.durationString} +🔗 访问: http://${DEPLOY_SERVER}:${env.DEPLOY_PORT} +📊 报告: ${env.BUILD_URL} + """ + + echo message + + // 发送通知 (可选) + // slackSend(color: 'good', message: message) + // emailext(subject: "✅ ${PROJECT_NAME} 构建成功", body: message) + } + } + + failure { + script { + def message = """ +💥 ${PROJECT_NAME} 构建失败! + +📋 项目: ${env.JOB_NAME} +🔢 构建号: ${env.BUILD_NUMBER} +🌿 分支: ${env.BRANCH_NAME ?: 'unknown'} +📝 提交: ${env.GIT_COMMIT_SHORT ?: 'unknown'} +🌍 环境: ${env.DEPLOY_ENV} +⏱️ 耗时: ${currentBuild.durationString} +🔗 日志: ${env.BUILD_URL}console +📧 修复后请重新构建 + """ + + echo message + + // 清理失败的资源 + sh """ + docker stop test-${PROJECT_NAME}-${BUILD_NUMBER} || true + docker rm test-${PROJECT_NAME}-${BUILD_NUMBER} || true + """ + + // 发送失败通知 + // slackSend(color: 'danger', message: message) + // emailext(subject: "❌ ${PROJECT_NAME} 构建失败", body: message) + } + } + + cleanup { + script { + try { + cleanWs(deleteDirs: true) + echo "✅ 工作空间清理完成" + } catch (Exception e) { + echo "⚠️ 清理失败: ${e.getMessage()}" + } + } + } + } +} \ No newline at end of file