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()}" } } } } }