Golang_demo/Jenkinsfile.template

769 lines
32 KiB
Plaintext
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
// 参数化配置 - 让模板更通用
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()}"
}
}
}
}
}