2025-06-25 17:54:12 +08:00
|
|
|
|
// =================================================================
|
|
|
|
|
// 🚀 Go项目通用Jenkins Pipeline模板
|
|
|
|
|
// =================================================================
|
|
|
|
|
//
|
|
|
|
|
// 📋 使用说明:
|
|
|
|
|
// 1. 复制此模板到你的Go项目根目录,重命名为Jenkinsfile
|
|
|
|
|
// 2. 修改下面的 "项目配置" 部分
|
|
|
|
|
// 3. 确保Jenkins中已配置好相关工具和凭据
|
|
|
|
|
// 4. 推送到Git仓库即可自动触发构建
|
|
|
|
|
//
|
|
|
|
|
// 🔧 支持的特性:
|
|
|
|
|
// - ✅ Go模块化项目构建和测试
|
|
|
|
|
// - ✅ SonarQube代码质量扫描(优化后11秒完成)
|
|
|
|
|
// - ✅ Docker镜像构建和测试
|
|
|
|
|
// - ✅ 多环境自动部署(生产/测试)
|
|
|
|
|
// - ✅ 健康检查和回滚机制
|
|
|
|
|
// - ✅ 测试覆盖率报告
|
|
|
|
|
// - ✅ 构建产物归档
|
|
|
|
|
//
|
|
|
|
|
// =================================================================
|
|
|
|
|
|
2025-06-25 16:46:58 +08:00
|
|
|
|
pipeline {
|
|
|
|
|
agent any
|
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
// 🎛️ 构建参数 - 支持手动构建时的自定义配置
|
2025-06-25 16:46:58 +08:00
|
|
|
|
parameters {
|
|
|
|
|
choice(
|
|
|
|
|
name: 'DEPLOY_ENV',
|
2025-06-25 17:54:12 +08:00
|
|
|
|
choices: ['auto', 'production', 'staging', 'development', 'skip'],
|
|
|
|
|
description: '部署环境 (auto=根据分支自动选择, skip=仅构建不部署)'
|
2025-06-25 16:46:58 +08:00
|
|
|
|
)
|
|
|
|
|
booleanParam(
|
|
|
|
|
name: 'SKIP_TESTS',
|
|
|
|
|
defaultValue: false,
|
2025-06-25 17:54:12 +08:00
|
|
|
|
description: '跳过单元测试(不推荐)'
|
2025-06-25 16:46:58 +08:00
|
|
|
|
)
|
|
|
|
|
booleanParam(
|
|
|
|
|
name: 'SKIP_SONAR',
|
|
|
|
|
defaultValue: false,
|
|
|
|
|
description: '跳过代码质量扫描'
|
|
|
|
|
)
|
|
|
|
|
booleanParam(
|
2025-06-25 17:54:12 +08:00
|
|
|
|
name: 'FORCE_REBUILD_IMAGE',
|
2025-06-25 16:46:58 +08:00
|
|
|
|
defaultValue: false,
|
|
|
|
|
description: '强制重新构建Docker镜像'
|
|
|
|
|
)
|
|
|
|
|
string(
|
2025-06-25 17:54:12 +08:00
|
|
|
|
name: 'CUSTOM_TAG',
|
2025-06-25 16:46:58 +08:00
|
|
|
|
defaultValue: '',
|
2025-06-25 17:54:12 +08:00
|
|
|
|
description: '自定义Docker镜像标签(留空使用构建号)'
|
2025-06-25 16:46:58 +08:00
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
options {
|
|
|
|
|
buildDiscarder(logRotator(
|
|
|
|
|
numToKeepStr: '20',
|
|
|
|
|
daysToKeepStr: '30',
|
|
|
|
|
artifactNumToKeepStr: '10'
|
|
|
|
|
))
|
|
|
|
|
timeout(time: 60, unit: 'MINUTES')
|
|
|
|
|
timestamps()
|
|
|
|
|
parallelsAlwaysFailFast()
|
|
|
|
|
skipDefaultCheckout()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tools {
|
2025-06-25 17:54:12 +08:00
|
|
|
|
go 'go' // 确保Jenkins中已配置Go工具,名称为'go'
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
environment {
|
|
|
|
|
// ===========================================
|
2025-06-25 17:54:12 +08:00
|
|
|
|
// 📝 项目配置 - 每个项目都需要修改这些变量
|
2025-06-25 16:46:58 +08:00
|
|
|
|
// ===========================================
|
2025-06-25 17:54:12 +08:00
|
|
|
|
PROJECT_NAME = 'my-go-app' // 🔧 修改:项目名称
|
|
|
|
|
DEPLOY_SERVER = '116.62.163.84' // 🔧 修改:部署服务器IP
|
|
|
|
|
SSH_CREDENTIAL_ID = 'deploy-server-ssh-key' // 🔧 修改:SSH凭据ID
|
2025-06-25 16:46:58 +08:00
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
// SonarQube配置(可选,如果不使用可以注释掉)
|
|
|
|
|
SONAR_HOST_URL = 'http://116.62.163.84:15010' // 🔧 修改:SonarQube服务器地址
|
|
|
|
|
SONAR_CREDENTIAL_ID = 'sonar-token' // 🔧 修改:SonarQube Token凭据ID
|
|
|
|
|
|
|
|
|
|
// Docker镜像仓库配置(可选)
|
|
|
|
|
DOCKER_REGISTRY = '' // 🔧 修改:Docker仓库地址(留空使用本地)
|
|
|
|
|
DOCKER_CREDENTIAL_ID = '' // 🔧 修改:Docker仓库凭据ID
|
2025-06-25 16:46:58 +08:00
|
|
|
|
|
|
|
|
|
// ===========================================
|
2025-06-25 17:54:12 +08:00
|
|
|
|
// 🔧 端口配置 - 根据你的环境修改
|
2025-06-25 16:46:58 +08:00
|
|
|
|
// ===========================================
|
|
|
|
|
PROD_PORT = '15021' // 生产环境端口
|
|
|
|
|
STAGING_PORT = '15022' // 预发布环境端口
|
|
|
|
|
DEV_PORT = '15023' // 开发环境端口
|
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
// 应用配置
|
|
|
|
|
APP_PORT = '8080' // 应用内部端口(通常是8080)
|
|
|
|
|
|
2025-06-25 16:46:58 +08:00
|
|
|
|
// ===========================================
|
|
|
|
|
// 🔧 自动化配置 - 通常不需要修改
|
|
|
|
|
// ===========================================
|
|
|
|
|
CGO_ENABLED = '0'
|
|
|
|
|
GOOS = 'linux'
|
|
|
|
|
GOARCH = 'amd64'
|
|
|
|
|
|
|
|
|
|
// 动态变量
|
|
|
|
|
IMAGE_NAME = "${PROJECT_NAME}"
|
2025-06-25 17:54:12 +08:00
|
|
|
|
IMAGE_TAG = "${params.CUSTOM_TAG ?: BUILD_NUMBER}"
|
|
|
|
|
FULL_IMAGE_NAME = "${DOCKER_REGISTRY ? DOCKER_REGISTRY + '/' : ''}${IMAGE_NAME}:${IMAGE_TAG}"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stages {
|
|
|
|
|
stage('🔄 初始化') {
|
|
|
|
|
parallel {
|
|
|
|
|
stage('检出代码') {
|
|
|
|
|
steps {
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "🔄 检出代码 - ${PROJECT_NAME}..."
|
2025-06-25 16:46:58 +08:00
|
|
|
|
checkout scm
|
|
|
|
|
|
|
|
|
|
script {
|
|
|
|
|
env.GIT_COMMIT_SHORT = sh(
|
|
|
|
|
script: "git rev-parse --short HEAD",
|
|
|
|
|
returnStdout: true
|
|
|
|
|
).trim()
|
|
|
|
|
|
|
|
|
|
env.GIT_BRANCH = sh(
|
2025-06-25 17:54:12 +08:00
|
|
|
|
script: "git rev-parse --abbrev-ref HEAD || echo '${BRANCH_NAME}'",
|
2025-06-25 16:46:58 +08:00
|
|
|
|
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
|
2025-06-25 17:54:12 +08:00
|
|
|
|
case 'development':
|
2025-06-25 16:46:58 +08:00
|
|
|
|
env.DEPLOY_PORT = env.DEV_PORT
|
2025-06-25 17:54:12 +08:00
|
|
|
|
break
|
|
|
|
|
case 'skip':
|
|
|
|
|
env.DEPLOY_PORT = 'none'
|
|
|
|
|
break
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
env.CONTAINER_NAME = "${PROJECT_NAME}-${env.DEPLOY_ENV}"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
echo "📋 构建信息:"
|
|
|
|
|
echo " 项目: ${PROJECT_NAME}"
|
|
|
|
|
echo " 分支: ${env.BRANCH_NAME}"
|
|
|
|
|
echo " 提交: ${env.GIT_COMMIT_SHORT}"
|
|
|
|
|
echo " 环境: ${env.DEPLOY_ENV}"
|
|
|
|
|
echo " 端口: ${env.DEPLOY_PORT}"
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo " 镜像: ${FULL_IMAGE_NAME}"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage('环境检查') {
|
|
|
|
|
steps {
|
|
|
|
|
echo '🔍 检查构建环境...'
|
|
|
|
|
sh '''
|
|
|
|
|
echo "=== 系统信息 ==="
|
|
|
|
|
uname -a
|
|
|
|
|
|
|
|
|
|
echo "=== Go版本 ==="
|
|
|
|
|
go version
|
|
|
|
|
|
|
|
|
|
echo "=== Go环境 ==="
|
2025-06-25 17:54:12 +08:00
|
|
|
|
go env GOROOT GOPATH GOPROXY GOSUMDB CGO_ENABLED GOOS GOARCH
|
2025-06-25 16:46:58 +08:00
|
|
|
|
|
|
|
|
|
echo "=== Docker版本 ==="
|
|
|
|
|
docker --version
|
|
|
|
|
docker system info --format "{{.Name}}: {{.ServerVersion}}"
|
|
|
|
|
|
|
|
|
|
echo "=== 工作目录 ==="
|
|
|
|
|
pwd && ls -la
|
|
|
|
|
|
|
|
|
|
echo "=== 检查Go项目结构 ==="
|
|
|
|
|
if [ -f "go.mod" ]; then
|
|
|
|
|
echo "✅ 发现go.mod文件"
|
2025-06-25 17:54:12 +08:00
|
|
|
|
head -10 go.mod
|
2025-06-25 16:46:58 +08:00
|
|
|
|
else
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "⚠️ 未发现go.mod文件,请确保这是一个Go模块项目"
|
|
|
|
|
echo "建议运行: go mod init ${PROJECT_NAME}"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
fi
|
2025-06-25 17:54:12 +08:00
|
|
|
|
|
|
|
|
|
echo "=== 检查SonarQube Scanner ==="
|
|
|
|
|
ls -la /var/jenkins_home/tools/hudson.plugins.sonar.SonarRunnerInstallation/ || echo "SonarQube Scanner未安装"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
'''
|
|
|
|
|
echo "✅ 环境检查完成"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage('📦 依赖管理') {
|
|
|
|
|
steps {
|
|
|
|
|
echo '📦 管理Go依赖...'
|
|
|
|
|
sh '''
|
|
|
|
|
echo "下载依赖..."
|
|
|
|
|
go mod download -x
|
|
|
|
|
|
|
|
|
|
echo "验证依赖..."
|
|
|
|
|
go mod verify
|
|
|
|
|
|
|
|
|
|
echo "整理依赖..."
|
|
|
|
|
go mod tidy
|
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "检查依赖漏洞..."
|
|
|
|
|
if command -v govulncheck >/dev/null 2>&1; then
|
|
|
|
|
govulncheck ./... || echo "⚠️ 发现安全漏洞,请检查"
|
|
|
|
|
else
|
|
|
|
|
echo "ℹ️ govulncheck未安装,跳过漏洞检查"
|
|
|
|
|
fi
|
|
|
|
|
|
2025-06-25 16:46:58 +08:00
|
|
|
|
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
|
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "检查Go语法..."
|
|
|
|
|
if ! go build -o /dev/null ./...; then
|
|
|
|
|
echo "❌ Go语法检查失败"
|
|
|
|
|
exit 1
|
2025-06-25 16:46:58 +08:00
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "✅ 静态检查通过"
|
|
|
|
|
'''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
stage('代码规范') {
|
2025-06-25 16:46:58 +08:00
|
|
|
|
steps {
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo '📝 检查代码规范...'
|
2025-06-25 16:46:58 +08:00
|
|
|
|
sh '''
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "检查包导入顺序..."
|
|
|
|
|
if command -v goimports >/dev/null 2>&1; then
|
|
|
|
|
UNORGANIZED=$(goimports -l .)
|
|
|
|
|
if [ -n "$UNORGANIZED" ]; then
|
|
|
|
|
echo "⚠️ 以下文件导入顺序不规范:"
|
|
|
|
|
echo "$UNORGANIZED"
|
|
|
|
|
echo "建议运行: goimports -w ."
|
|
|
|
|
fi
|
2025-06-25 16:46:58 +08:00
|
|
|
|
fi
|
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "检查代码复杂度..."
|
|
|
|
|
if command -v gocyclo >/dev/null 2>&1; then
|
|
|
|
|
gocyclo -over 15 . || echo "⚠️ 发现高复杂度函数"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
fi
|
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "✅ 代码规范检查完成"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
'''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage('🧪 测试') {
|
|
|
|
|
when {
|
|
|
|
|
not { params.SKIP_TESTS }
|
|
|
|
|
}
|
|
|
|
|
parallel {
|
|
|
|
|
stage('单元测试') {
|
|
|
|
|
steps {
|
|
|
|
|
echo '🧪 运行单元测试...'
|
|
|
|
|
sh '''
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "创建测试结果目录..."
|
|
|
|
|
mkdir -p test-results reports
|
2025-06-25 16:46:58 +08:00
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "运行测试..."
|
|
|
|
|
go test -v -coverprofile=coverage.out -covermode=atomic \\
|
2025-06-25 16:46:58 +08:00
|
|
|
|
-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 "生成覆盖率报告..."
|
2025-06-25 17:54:12 +08:00
|
|
|
|
go tool cover -html=coverage.out -o reports/coverage.html
|
|
|
|
|
go tool cover -func=coverage.out | tee reports/coverage-summary.txt
|
2025-06-25 16:46:58 +08:00
|
|
|
|
|
|
|
|
|
# 提取覆盖率百分比
|
2025-06-25 17:54:12 +08:00
|
|
|
|
COVERAGE=$(go tool cover -func=coverage.out | grep total | grep -oE '[0-9]+\\.[0-9]+%')
|
2025-06-25 16:46:58 +08:00
|
|
|
|
echo "📊 总覆盖率: $COVERAGE"
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "$COVERAGE" > reports/coverage.txt
|
|
|
|
|
|
|
|
|
|
# 检查覆盖率阈值(可选)
|
|
|
|
|
COVERAGE_NUM=$(echo $COVERAGE | sed 's/%//')
|
|
|
|
|
if [ "${COVERAGE_NUM%.*}" -lt 50 ]; then
|
|
|
|
|
echo "⚠️ 代码覆盖率低于50%: $COVERAGE"
|
|
|
|
|
# 可以选择是否让构建失败
|
|
|
|
|
# exit 1
|
|
|
|
|
fi
|
2025-06-25 16:46:58 +08:00
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "✅ 单元测试完成,覆盖率: $COVERAGE"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
'''
|
|
|
|
|
}
|
|
|
|
|
post {
|
|
|
|
|
always {
|
|
|
|
|
script {
|
|
|
|
|
// 发布测试报告
|
|
|
|
|
if (fileExists('test-results/junit.xml')) {
|
|
|
|
|
publishTestResults testResultsPattern: 'test-results/junit.xml'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 发布覆盖率报告
|
2025-06-25 17:54:12 +08:00
|
|
|
|
if (fileExists('reports/coverage.html')) {
|
2025-06-25 16:46:58 +08:00
|
|
|
|
publishHTML([
|
|
|
|
|
allowMissing: false,
|
|
|
|
|
alwaysLinkToLastBuild: true,
|
|
|
|
|
keepAll: true,
|
2025-06-25 17:54:12 +08:00
|
|
|
|
reportDir: 'reports',
|
2025-06-25 16:46:58 +08:00
|
|
|
|
reportFiles: 'coverage.html',
|
2025-06-25 17:54:12 +08:00
|
|
|
|
reportName: '📊 Go Coverage Report',
|
|
|
|
|
reportTitles: 'Go代码覆盖率报告'
|
2025-06-25 16:46:58 +08:00
|
|
|
|
])
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo '✅ Go覆盖率报告已发布'
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 归档测试产物
|
2025-06-25 17:54:12 +08:00
|
|
|
|
archiveArtifacts artifacts: 'coverage.out,reports/*,test-results/*', allowEmptyArchive: true
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage('性能测试') {
|
|
|
|
|
steps {
|
|
|
|
|
echo '⚡ 运行性能测试...'
|
|
|
|
|
sh '''
|
|
|
|
|
echo "运行基准测试..."
|
2025-06-25 17:54:12 +08:00
|
|
|
|
mkdir -p reports
|
|
|
|
|
|
|
|
|
|
if go test -bench=. -benchmem ./... > reports/benchmark.txt 2>&1; then
|
2025-06-25 16:46:58 +08:00
|
|
|
|
echo "✅ 基准测试完成"
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "📊 性能测试结果:"
|
|
|
|
|
cat reports/benchmark.txt | head -20
|
2025-06-25 16:46:58 +08:00
|
|
|
|
else
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "ℹ️ 未发现基准测试或测试失败"
|
|
|
|
|
echo "建议添加基准测试函数,如: func BenchmarkMyFunction(b *testing.B)"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
fi
|
|
|
|
|
'''
|
|
|
|
|
}
|
|
|
|
|
post {
|
|
|
|
|
always {
|
2025-06-25 17:54:12 +08:00
|
|
|
|
archiveArtifacts artifacts: 'reports/benchmark.txt', allowEmptyArchive: true
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage('📊 代码扫描') {
|
|
|
|
|
when {
|
|
|
|
|
not { params.SKIP_SONAR }
|
|
|
|
|
}
|
|
|
|
|
steps {
|
|
|
|
|
echo '📊 运行SonarQube代码扫描...'
|
|
|
|
|
script {
|
|
|
|
|
try {
|
|
|
|
|
withCredentials([string(credentialsId: "${SONAR_CREDENTIAL_ID}", variable: 'SONAR_TOKEN')]) {
|
|
|
|
|
withSonarQubeEnv('sonarQube') {
|
2025-06-25 17:54:12 +08:00
|
|
|
|
def scannerHome = tool name: 'sonarQube', type: 'hudson.plugins.sonar.SonarRunnerInstallation'
|
|
|
|
|
|
|
|
|
|
sh """
|
|
|
|
|
echo "=== SonarQube环境信息 ==="
|
|
|
|
|
echo "SONAR_HOST_URL: \$SONAR_HOST_URL"
|
|
|
|
|
echo "SONAR_SCANNER_HOME: ${scannerHome}"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "✅ 使用Jenkins管理的SonarQube Scanner"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
# 运行SonarQube扫描
|
|
|
|
|
${scannerHome}/bin/sonar-scanner \\
|
|
|
|
|
-Dsonar.projectKey=${PROJECT_NAME} \\
|
2025-06-25 16:46:58 +08:00
|
|
|
|
-Dsonar.projectName="${PROJECT_NAME}" \\
|
|
|
|
|
-Dsonar.projectVersion=${BUILD_NUMBER} \\
|
|
|
|
|
-Dsonar.sources=. \\
|
2025-06-25 17:54:12 +08:00
|
|
|
|
-Dsonar.exclusions=**/*_test.go,**/vendor/**,**/*.mod,**/*.sum,**/reports/**,**/test-results/** \\
|
2025-06-25 16:46:58 +08:00
|
|
|
|
-Dsonar.tests=. \\
|
|
|
|
|
-Dsonar.test.inclusions=**/*_test.go \\
|
|
|
|
|
-Dsonar.test.exclusions=**/vendor/** \\
|
|
|
|
|
-Dsonar.go.coverage.reportPaths=coverage.out \\
|
|
|
|
|
-Dsonar.sourceEncoding=UTF-8
|
2025-06-25 17:54:12 +08:00
|
|
|
|
"""
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
echo "✅ SonarQube扫描完成"
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
echo "⚠️ SonarQube扫描失败: ${e.getMessage()}"
|
|
|
|
|
if (env.DEPLOY_ENV == 'production') {
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "❌ 生产环境必须通过代码质量检查"
|
|
|
|
|
throw e
|
|
|
|
|
} else {
|
|
|
|
|
echo "⚠️ 非生产环境,继续构建流程"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage('🔨 构建') {
|
|
|
|
|
parallel {
|
|
|
|
|
stage('编译应用') {
|
|
|
|
|
steps {
|
|
|
|
|
echo '🔨 编译Go应用...'
|
|
|
|
|
sh '''
|
|
|
|
|
echo "开始编译..."
|
|
|
|
|
|
|
|
|
|
# 设置构建信息
|
|
|
|
|
BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')
|
2025-06-25 17:54:12 +08:00
|
|
|
|
LDFLAGS="-w -s"
|
|
|
|
|
LDFLAGS="$LDFLAGS -X main.Version=${BUILD_NUMBER}"
|
|
|
|
|
LDFLAGS="$LDFLAGS -X main.GitCommit=${GIT_COMMIT_SHORT}"
|
|
|
|
|
LDFLAGS="$LDFLAGS -X main.BuildTime=${BUILD_TIME}"
|
|
|
|
|
|
|
|
|
|
echo "构建标志: $LDFLAGS"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
|
|
|
|
|
# 编译
|
|
|
|
|
CGO_ENABLED=${CGO_ENABLED} GOOS=${GOOS} GOARCH=${GOARCH} \\
|
|
|
|
|
go build -ldflags="$LDFLAGS" -o ${PROJECT_NAME} .
|
|
|
|
|
|
|
|
|
|
echo "验证二进制文件..."
|
|
|
|
|
ls -lh ${PROJECT_NAME}
|
2025-06-25 17:54:12 +08:00
|
|
|
|
file ${PROJECT_NAME} || true
|
2025-06-25 16:46:58 +08:00
|
|
|
|
|
|
|
|
|
# 尝试获取版本信息
|
2025-06-25 17:54:12 +08:00
|
|
|
|
./${PROJECT_NAME} --version 2>/dev/null || echo "ℹ️ 应用不支持--version参数"
|
|
|
|
|
./${PROJECT_NAME} -h 2>/dev/null || echo "ℹ️ 应用不支持-h参数"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "✅ 编译完成: ${PROJECT_NAME}"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
'''
|
|
|
|
|
}
|
|
|
|
|
post {
|
|
|
|
|
success {
|
|
|
|
|
archiveArtifacts artifacts: "${PROJECT_NAME}", fingerprint: true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage('准备部署文件') {
|
|
|
|
|
steps {
|
|
|
|
|
echo '📝 准备部署配置文件...'
|
|
|
|
|
sh '''
|
|
|
|
|
# 创建部署脚本
|
|
|
|
|
cat > deploy.sh << 'EOF'
|
|
|
|
|
#!/bin/bash
|
|
|
|
|
set -e
|
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
# 部署配置
|
2025-06-25 16:46:58 +08:00
|
|
|
|
PROJECT_NAME="${PROJECT_NAME}"
|
|
|
|
|
CONTAINER_NAME="${CONTAINER_NAME}"
|
|
|
|
|
DEPLOY_PORT="${DEPLOY_PORT}"
|
2025-06-25 17:54:12 +08:00
|
|
|
|
APP_PORT="${APP_PORT}"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
IMAGE_TAG="${IMAGE_TAG}"
|
2025-06-25 17:54:12 +08:00
|
|
|
|
DEPLOY_ENV="${DEPLOY_ENV}"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "🚀 部署 $PROJECT_NAME 到 $DEPLOY_ENV 环境..."
|
|
|
|
|
echo "容器名称: $CONTAINER_NAME"
|
|
|
|
|
echo "端口映射: $DEPLOY_PORT:$APP_PORT"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
|
|
|
|
|
# 停止旧容器
|
|
|
|
|
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 \\
|
2025-06-25 17:54:12 +08:00
|
|
|
|
-p $DEPLOY_PORT:$APP_PORT \\
|
2025-06-25 16:46:58 +08:00
|
|
|
|
-e GIN_MODE=release \\
|
2025-06-25 17:54:12 +08:00
|
|
|
|
-e DEPLOY_ENV=$DEPLOY_ENV \\
|
|
|
|
|
-e TZ=Asia/Shanghai \\
|
|
|
|
|
--health-cmd="curl -f http://localhost:$APP_PORT/health || curl -f http://localhost:$APP_PORT/ping || exit 1" \\
|
|
|
|
|
--health-interval=30s \\
|
|
|
|
|
--health-timeout=3s \\
|
|
|
|
|
--health-retries=3 \\
|
2025-06-25 16:46:58 +08:00
|
|
|
|
$PROJECT_NAME:$IMAGE_TAG
|
|
|
|
|
|
|
|
|
|
echo "✅ 部署完成"
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "🔗 访问地址: http://$(hostname -I | awk '{print \\$1}'):$DEPLOY_PORT"
|
|
|
|
|
echo "🏥 健康检查: docker inspect --format='{{.State.Health.Status}}' $CONTAINER_NAME"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
chmod +x deploy.sh
|
|
|
|
|
echo "✅ 部署脚本准备完成"
|
|
|
|
|
'''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage('🐳 Docker镜像') {
|
|
|
|
|
steps {
|
|
|
|
|
echo '🐳 构建Docker镜像...'
|
|
|
|
|
script {
|
|
|
|
|
// 检查是否需要重新构建
|
2025-06-25 17:54:12 +08:00
|
|
|
|
def shouldBuild = params.FORCE_REBUILD_IMAGE
|
2025-06-25 16:46:58 +08:00
|
|
|
|
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
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "❌ 二进制文件不存在: ${PROJECT_NAME}"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
chmod +x ${PROJECT_NAME}
|
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
# 检查Dockerfile
|
|
|
|
|
if [ ! -f "Dockerfile" ]; then
|
|
|
|
|
echo "⚠️ 未找到Dockerfile,创建默认Dockerfile..."
|
|
|
|
|
cat > Dockerfile << 'DOCKERFILE_EOF'
|
|
|
|
|
FROM alpine:latest
|
|
|
|
|
|
|
|
|
|
# 安装必要工具和设置时区
|
|
|
|
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \\
|
|
|
|
|
apk update && \\
|
|
|
|
|
apk add --no-cache curl ca-certificates tzdata && \\
|
|
|
|
|
rm -rf /var/cache/apk/*
|
|
|
|
|
|
|
|
|
|
ENV TZ=Asia/Shanghai
|
|
|
|
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
|
|
|
|
|
|
|
|
|
# 创建非root用户
|
|
|
|
|
RUN addgroup -g 1000 appuser && adduser -u 1000 -G appuser -s /bin/sh -D appuser
|
|
|
|
|
|
|
|
|
|
# 设置工作目录
|
|
|
|
|
WORKDIR /app
|
|
|
|
|
|
|
|
|
|
# 复制应用程序
|
|
|
|
|
COPY ${PROJECT_NAME} .
|
|
|
|
|
|
|
|
|
|
# 设置权限
|
|
|
|
|
RUN mkdir -p /app/logs && \\
|
|
|
|
|
chown -R appuser:appuser /app && \\
|
|
|
|
|
chmod +x /app/${PROJECT_NAME}
|
|
|
|
|
|
|
|
|
|
# 切换到非root用户
|
|
|
|
|
USER appuser
|
|
|
|
|
|
|
|
|
|
# 暴露端口
|
|
|
|
|
EXPOSE ${APP_PORT}
|
|
|
|
|
|
|
|
|
|
# 健康检查
|
|
|
|
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \\
|
|
|
|
|
CMD curl -f http://localhost:${APP_PORT}/health || curl -f http://localhost:${APP_PORT}/ping || exit 1
|
|
|
|
|
|
|
|
|
|
# 启动应用
|
|
|
|
|
ENTRYPOINT ["./${PROJECT_NAME}"]
|
|
|
|
|
DOCKERFILE_EOF
|
|
|
|
|
fi
|
|
|
|
|
|
2025-06-25 16:46:58 +08:00
|
|
|
|
# 构建镜像
|
|
|
|
|
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"
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
// 推送到镜像仓库(如果配置了)
|
2025-06-25 17:54:12 +08:00
|
|
|
|
if (env.DOCKER_REGISTRY && env.DOCKER_CREDENTIAL_ID) {
|
|
|
|
|
withCredentials([usernamePassword(credentialsId: env.DOCKER_CREDENTIAL_ID, usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
|
|
|
|
|
sh '''
|
|
|
|
|
echo "登录Docker仓库..."
|
|
|
|
|
echo "$DOCKER_PASS" | docker login $DOCKER_REGISTRY -u "$DOCKER_USER" --password-stdin
|
|
|
|
|
|
|
|
|
|
echo "推送镜像到仓库..."
|
|
|
|
|
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${FULL_IMAGE_NAME}
|
|
|
|
|
docker push ${FULL_IMAGE_NAME}
|
|
|
|
|
|
|
|
|
|
docker tag ${IMAGE_NAME}:latest ${DOCKER_REGISTRY}/${IMAGE_NAME}:latest
|
|
|
|
|
docker push ${DOCKER_REGISTRY}/${IMAGE_NAME}:latest
|
|
|
|
|
|
|
|
|
|
echo "✅ 镜像推送完成: ${FULL_IMAGE_NAME}"
|
|
|
|
|
'''
|
|
|
|
|
}
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "✅ 镜像已存在,跳过构建: ${IMAGE_NAME}:${IMAGE_TAG}"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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} \\
|
2025-06-25 17:54:12 +08:00
|
|
|
|
-p ${testPort}:${APP_PORT} \\
|
2025-06-25 16:46:58 +08:00
|
|
|
|
-e GIN_MODE=test \\
|
2025-06-25 17:54:12 +08:00
|
|
|
|
-e DEPLOY_ENV=test \\
|
2025-06-25 16:46:58 +08:00
|
|
|
|
${IMAGE_NAME}:${IMAGE_TAG}
|
|
|
|
|
|
|
|
|
|
echo "等待应用启动..."
|
|
|
|
|
sleep 15
|
|
|
|
|
|
|
|
|
|
echo "检查容器状态..."
|
|
|
|
|
docker ps | grep ${testContainerName}
|
|
|
|
|
docker logs ${testContainerName}
|
|
|
|
|
|
|
|
|
|
echo "测试应用端点..."
|
2025-06-25 17:54:12 +08:00
|
|
|
|
SUCCESS=false
|
2025-06-25 16:46:58 +08:00
|
|
|
|
for i in \$(seq 1 10); do
|
|
|
|
|
echo "尝试连接 \$i/10..."
|
|
|
|
|
if curl -f http://localhost:${testPort}/health; then
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "✅ 健康检查端点响应正常"
|
|
|
|
|
SUCCESS=true
|
2025-06-25 16:46:58 +08:00
|
|
|
|
break
|
|
|
|
|
elif curl -f http://localhost:${testPort}/ping; then
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "✅ Ping端点响应正常"
|
|
|
|
|
SUCCESS=true
|
|
|
|
|
break
|
|
|
|
|
elif curl -f http://localhost:${testPort}/; then
|
|
|
|
|
echo "✅ 根路径响应正常"
|
|
|
|
|
SUCCESS=true
|
2025-06-25 16:46:58 +08:00
|
|
|
|
break
|
|
|
|
|
else
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "⚠️ 连接失败,等待3秒后重试..."
|
2025-06-25 16:46:58 +08:00
|
|
|
|
sleep 3
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
if [ "\$SUCCESS" = "false" ]; then
|
|
|
|
|
echo "❌ 镜像测试失败,无法连接到应用"
|
|
|
|
|
docker logs ${testContainerName}
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
2025-06-25 16:46:58 +08:00
|
|
|
|
echo "✅ 镜像测试通过"
|
|
|
|
|
"""
|
|
|
|
|
} finally {
|
|
|
|
|
// 清理测试容器
|
|
|
|
|
sh """
|
|
|
|
|
docker stop ${testContainerName} || true
|
|
|
|
|
docker rm ${testContainerName} || true
|
|
|
|
|
"""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage('🚀 部署') {
|
|
|
|
|
when {
|
2025-06-25 17:54:12 +08:00
|
|
|
|
not {
|
|
|
|
|
anyOf {
|
|
|
|
|
expression { params.DEPLOY_ENV == 'skip' }
|
|
|
|
|
expression { env.DEPLOY_ENV == 'skip' }
|
|
|
|
|
}
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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 "✅ 部署完成"
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "🔗 应用地址: http://${DEPLOY_SERVER}:${DEPLOY_PORT}"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
# 清理本地临时文件
|
|
|
|
|
rm -f ${PROJECT_NAME}-${IMAGE_TAG}.tar
|
|
|
|
|
'''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage('🏥 健康检查') {
|
|
|
|
|
when {
|
2025-06-25 17:54:12 +08:00
|
|
|
|
not {
|
|
|
|
|
anyOf {
|
|
|
|
|
expression { params.DEPLOY_ENV == 'skip' }
|
|
|
|
|
expression { env.DEPLOY_ENV == 'skip' }
|
|
|
|
|
}
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
steps {
|
|
|
|
|
echo '🏥 执行应用健康检查...'
|
|
|
|
|
script {
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "等待应用启动..."
|
2025-06-25 16:46:58 +08:00
|
|
|
|
sleep(time: 30, unit: 'SECONDS')
|
|
|
|
|
|
|
|
|
|
def healthCheckPassed = false
|
2025-06-25 17:54:12 +08:00
|
|
|
|
def healthUrls = [
|
|
|
|
|
"http://${DEPLOY_SERVER}:${env.DEPLOY_PORT}/health",
|
|
|
|
|
"http://${DEPLOY_SERVER}:${env.DEPLOY_PORT}/ping",
|
|
|
|
|
"http://${DEPLOY_SERVER}:${env.DEPLOY_PORT}/"
|
|
|
|
|
]
|
|
|
|
|
|
2025-06-25 16:46:58 +08:00
|
|
|
|
for (int i = 1; i <= 5; i++) {
|
2025-06-25 17:54:12 +08:00
|
|
|
|
echo "第${i}次健康检查..."
|
|
|
|
|
|
|
|
|
|
for (String url : healthUrls) {
|
|
|
|
|
try {
|
|
|
|
|
def response = sh(
|
|
|
|
|
script: "curl -s -o /dev/null -w '%{http_code}' '${url}'",
|
|
|
|
|
returnStdout: true
|
|
|
|
|
).trim()
|
|
|
|
|
|
|
|
|
|
if (response == "200") {
|
|
|
|
|
echo "✅ 健康检查通过: ${url} (${response})"
|
|
|
|
|
healthCheckPassed = true
|
|
|
|
|
break
|
|
|
|
|
} else {
|
|
|
|
|
echo "⚠️ ${url} 响应状态码: ${response}"
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
echo "⚠️ ${url} 检查异常: ${e.getMessage()}"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
2025-06-25 17:54:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (healthCheckPassed) {
|
|
|
|
|
break
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (i < 5) {
|
|
|
|
|
echo "等待30秒后重试..."
|
|
|
|
|
sleep(time: 30, unit: 'SECONDS')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
if (!healthCheckPassed) {
|
|
|
|
|
def errorMsg = "❌ 健康检查失败,应用可能未正常启动"
|
|
|
|
|
echo errorMsg
|
|
|
|
|
|
|
|
|
|
if (env.DEPLOY_ENV == 'production') {
|
|
|
|
|
echo "🔄 生产环境健康检查失败,考虑回滚..."
|
|
|
|
|
// 这里可以添加自动回滚逻辑
|
|
|
|
|
error(errorMsg)
|
|
|
|
|
} else {
|
|
|
|
|
echo "⚠️ 非生产环境,允许继续但需要人工检查"
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
echo "✅ 应用部署成功并通过健康检查"
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
post {
|
|
|
|
|
always {
|
|
|
|
|
script {
|
|
|
|
|
echo '🧹 执行清理工作...'
|
|
|
|
|
|
|
|
|
|
// 清理构建产物
|
|
|
|
|
sh """
|
2025-06-25 17:54:12 +08:00
|
|
|
|
rm -f ${PROJECT_NAME} coverage.out deploy.sh
|
|
|
|
|
rm -rf test-results/ reports/
|
2025-06-25 16:46:58 +08:00
|
|
|
|
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 {
|
2025-06-25 17:54:12 +08:00
|
|
|
|
def deployInfo = env.DEPLOY_ENV != 'skip' ?
|
|
|
|
|
"🔗 访问: http://${DEPLOY_SERVER}:${env.DEPLOY_PORT}" :
|
|
|
|
|
"📦 仅构建,未部署"
|
|
|
|
|
|
|
|
|
|
def sonarInfo = params.SKIP_SONAR ?
|
|
|
|
|
"" :
|
|
|
|
|
"📊 SonarQube: ${SONAR_HOST_URL}/dashboard?id=${PROJECT_NAME}"
|
|
|
|
|
|
2025-06-25 16:46:58 +08:00
|
|
|
|
def message = """
|
|
|
|
|
🎉 ${PROJECT_NAME} 构建成功!
|
|
|
|
|
|
|
|
|
|
📋 项目: ${env.JOB_NAME}
|
|
|
|
|
🔢 构建号: ${env.BUILD_NUMBER}
|
|
|
|
|
🌿 分支: ${env.BRANCH_NAME ?: 'unknown'}
|
|
|
|
|
📝 提交: ${env.GIT_COMMIT_SHORT ?: 'unknown'}
|
|
|
|
|
🌍 环境: ${env.DEPLOY_ENV}
|
|
|
|
|
⏱️ 耗时: ${currentBuild.durationString}
|
2025-06-25 17:54:12 +08:00
|
|
|
|
🐳 镜像: ${FULL_IMAGE_NAME}
|
|
|
|
|
${deployInfo}
|
|
|
|
|
${sonarInfo}
|
|
|
|
|
📈 构建详情: ${env.BUILD_URL}
|
2025-06-25 16:46:58 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
echo message
|
|
|
|
|
|
2025-06-25 17:54:12 +08:00
|
|
|
|
// 发送通知(根据需要启用)
|
2025-06-25 16:46:58 +08:00
|
|
|
|
// slackSend(color: 'good', message: message)
|
|
|
|
|
// emailext(subject: "✅ ${PROJECT_NAME} 构建成功", body: message)
|
2025-06-25 17:54:12 +08:00
|
|
|
|
// dingTalk(robot: 'your-robot-id', message: message)
|
2025-06-25 16:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2025-06-25 17:54:12 +08:00
|
|
|
|
🛠️ 修复后请重新构建
|
2025-06-25 16:46:58 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
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()}"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-25 17:54:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =================================================================
|
|
|
|
|
// 📖 使用文档:
|
|
|
|
|
//
|
|
|
|
|
// 1. 项目配置:
|
|
|
|
|
// - 修改 PROJECT_NAME 为你的项目名称
|
|
|
|
|
// - 修改 DEPLOY_SERVER 为你的服务器IP
|
|
|
|
|
// - 配置端口映射和凭据ID
|
|
|
|
|
//
|
|
|
|
|
// 2. Jenkins要求:
|
|
|
|
|
// - Go工具:名称为'go'
|
|
|
|
|
// - SonarQube Server:名称为'sonarQube'
|
|
|
|
|
// - SonarQube Scanner:名称为'sonarQube'
|
|
|
|
|
// - SSH凭据:配置部署服务器的SSH Key
|
|
|
|
|
//
|
|
|
|
|
// 3. 项目要求:
|
|
|
|
|
// - 必须是Go模块项目(有go.mod文件)
|
|
|
|
|
// - 建议有健康检查端点:/health 或 /ping
|
|
|
|
|
// - 可选:添加基准测试函数
|
|
|
|
|
//
|
|
|
|
|
// 4. 分支策略:
|
|
|
|
|
// - main/master → 生产环境
|
|
|
|
|
// - staging/release → 预发布环境
|
|
|
|
|
// - 其他分支 → 开发环境
|
|
|
|
|
//
|
|
|
|
|
// 5. 手动参数:
|
|
|
|
|
// - DEPLOY_ENV:选择部署环境
|
|
|
|
|
// - SKIP_TESTS:跳过测试(不推荐)
|
|
|
|
|
// - SKIP_SONAR:跳过代码扫描
|
|
|
|
|
// - FORCE_REBUILD_IMAGE:强制重建镜像
|
|
|
|
|
// - CUSTOM_TAG:自定义镜像标签
|
|
|
|
|
//
|
|
|
|
|
// =================================================================
|