更新template

This commit is contained in:
wangtianqi 2025-06-25 17:54:12 +08:00
parent d0bce2c655
commit d9fa4d1011

View File

@ -1,17 +1,38 @@
// =================================================================
// 🚀 Go项目通用Jenkins Pipeline模板
// =================================================================
//
// 📋 使用说明:
// 1. 复制此模板到你的Go项目根目录重命名为Jenkinsfile
// 2. 修改下面的 "项目配置" 部分
// 3. 确保Jenkins中已配置好相关工具和凭据
// 4. 推送到Git仓库即可自动触发构建
//
// 🔧 支持的特性:
// - ✅ Go模块化项目构建和测试
// - ✅ SonarQube代码质量扫描优化后11秒完成
// - ✅ Docker镜像构建和测试
// - ✅ 多环境自动部署(生产/测试)
// - ✅ 健康检查和回滚机制
// - ✅ 测试覆盖率报告
// - ✅ 构建产物归档
//
// =================================================================
pipeline { pipeline {
agent any agent any
// 参数化配置 - 让模板更通用 // 🎛️ 构建参数 - 支持手动构建时的自定义配置
parameters { parameters {
choice( choice(
name: 'DEPLOY_ENV', name: 'DEPLOY_ENV',
choices: ['auto', 'production', 'staging', 'development'], choices: ['auto', 'production', 'staging', 'development', 'skip'],
description: '部署环境 (auto=根据分支自动选择)' description: '部署环境 (auto=根据分支自动选择, skip=仅构建不部署)'
) )
booleanParam( booleanParam(
name: 'SKIP_TESTS', name: 'SKIP_TESTS',
defaultValue: false, defaultValue: false,
description: '跳过单元测试' description: '跳过单元测试(不推荐)'
) )
booleanParam( booleanParam(
name: 'SKIP_SONAR', name: 'SKIP_SONAR',
@ -19,19 +40,14 @@ pipeline {
description: '跳过代码质量扫描' description: '跳过代码质量扫描'
) )
booleanParam( booleanParam(
name: 'FORCE_REBUILD', name: 'FORCE_REBUILD_IMAGE',
defaultValue: false, defaultValue: false,
description: '强制重新构建Docker镜像' description: '强制重新构建Docker镜像'
) )
string( string(
name: 'GO_VERSION', name: 'CUSTOM_TAG',
defaultValue: '1.21',
description: 'Go版本'
)
string(
name: 'IMAGE_REGISTRY',
defaultValue: '', defaultValue: '',
description: 'Docker镜像仓库 (留空使用本地)' description: '自定义Docker镜像标签留空使用构建号'
) )
} }
@ -48,29 +64,35 @@ pipeline {
} }
tools { tools {
go 'go' // Jenkins中配置Go工具名称 go 'go' // 确保Jenkins中配置Go工具名称为'go'
} }
environment { environment {
// =========================================== // ===========================================
// 📝 项目配置 - 根据你的项目修改这些变量 // 📝 项目配置 - 每个项目都需要修改这些变量
// =========================================== // ===========================================
PROJECT_NAME = 'golang-demo' // 🔧 修改为你的项目名 PROJECT_NAME = 'my-go-app' // 🔧 修改:项目名称
DEPLOY_SERVER = '116.62.163.84' // 🔧 修改为你的服务器 DEPLOY_SERVER = '116.62.163.84' // 🔧 修改部署服务器IP
SSH_CREDENTIAL_ID = 'deploy-server-ssh-key' // 🔧 修改为你的SSH凭据ID SSH_CREDENTIAL_ID = 'deploy-server-ssh-key' // 🔧 修改:SSH凭据ID
// SonarQube配置 (可选) // SonarQube配置可选如果不使用可以注释掉
SONAR_HOST_URL = 'http://116.62.163.84:15010' // 🔧 修改为你的SonarQube地址 SONAR_HOST_URL = 'http://116.62.163.84:15010' // 🔧 修改SonarQube服务器地址
SONAR_PROJECT_KEY = "${PROJECT_NAME}" SONAR_CREDENTIAL_ID = 'sonar-token' // 🔧 修改SonarQube Token凭据ID
SONAR_CREDENTIAL_ID = 'sonar-token' // 🔧 修改为你的SonarQube凭据ID
// Docker镜像仓库配置可选
DOCKER_REGISTRY = '' // 🔧 修改Docker仓库地址留空使用本地
DOCKER_CREDENTIAL_ID = '' // 🔧 修改Docker仓库凭据ID
// =========================================== // ===========================================
// 🔧 环境端口配置 // 🔧 端口配置 - 根据你的环境修改
// =========================================== // ===========================================
PROD_PORT = '15021' // 生产环境端口 PROD_PORT = '15021' // 生产环境端口
STAGING_PORT = '15022' // 预发布环境端口 STAGING_PORT = '15022' // 预发布环境端口
DEV_PORT = '15023' // 开发环境端口 DEV_PORT = '15023' // 开发环境端口
// 应用配置
APP_PORT = '8080' // 应用内部端口通常是8080
// =========================================== // ===========================================
// 🔧 自动化配置 - 通常不需要修改 // 🔧 自动化配置 - 通常不需要修改
// =========================================== // ===========================================
@ -80,8 +102,8 @@ pipeline {
// 动态变量 // 动态变量
IMAGE_NAME = "${PROJECT_NAME}" IMAGE_NAME = "${PROJECT_NAME}"
IMAGE_TAG = "${BUILD_NUMBER}" IMAGE_TAG = "${params.CUSTOM_TAG ?: BUILD_NUMBER}"
FULL_IMAGE_NAME = "${params.IMAGE_REGISTRY ? params.IMAGE_REGISTRY + '/' : ''}${IMAGE_NAME}:${IMAGE_TAG}" FULL_IMAGE_NAME = "${DOCKER_REGISTRY ? DOCKER_REGISTRY + '/' : ''}${IMAGE_NAME}:${IMAGE_TAG}"
} }
stages { stages {
@ -89,7 +111,7 @@ pipeline {
parallel { parallel {
stage('检出代码') { stage('检出代码') {
steps { steps {
echo "🔄 检出代码..." echo "🔄 检出代码 - ${PROJECT_NAME}..."
checkout scm checkout scm
script { script {
@ -99,7 +121,7 @@ pipeline {
).trim() ).trim()
env.GIT_BRANCH = sh( env.GIT_BRANCH = sh(
script: "git rev-parse --abbrev-ref HEAD", script: "git rev-parse --abbrev-ref HEAD || echo '${BRANCH_NAME}'",
returnStdout: true returnStdout: true
).trim() ).trim()
@ -124,12 +146,16 @@ pipeline {
case 'staging': case 'staging':
env.DEPLOY_PORT = env.STAGING_PORT env.DEPLOY_PORT = env.STAGING_PORT
break break
default: case 'development':
env.DEPLOY_PORT = env.DEV_PORT env.DEPLOY_PORT = env.DEV_PORT
break
case 'skip':
env.DEPLOY_PORT = 'none'
break
} }
} }
env.CONTAINER_NAME = "${PROJECT_NAME}-${DEPLOY_ENV}" env.CONTAINER_NAME = "${PROJECT_NAME}-${env.DEPLOY_ENV}"
} }
echo "📋 构建信息:" echo "📋 构建信息:"
@ -138,6 +164,7 @@ pipeline {
echo " 提交: ${env.GIT_COMMIT_SHORT}" echo " 提交: ${env.GIT_COMMIT_SHORT}"
echo " 环境: ${env.DEPLOY_ENV}" echo " 环境: ${env.DEPLOY_ENV}"
echo " 端口: ${env.DEPLOY_PORT}" echo " 端口: ${env.DEPLOY_PORT}"
echo " 镜像: ${FULL_IMAGE_NAME}"
} }
} }
@ -152,7 +179,7 @@ pipeline {
go version go version
echo "=== Go环境 ===" echo "=== Go环境 ==="
go env GOROOT GOPATH GOPROXY GOSUMDB go env GOROOT GOPATH GOPROXY GOSUMDB CGO_ENABLED GOOS GOARCH
echo "=== Docker版本 ===" echo "=== Docker版本 ==="
docker --version docker --version
@ -164,10 +191,14 @@ pipeline {
echo "=== 检查Go项目结构 ===" echo "=== 检查Go项目结构 ==="
if [ -f "go.mod" ]; then if [ -f "go.mod" ]; then
echo "✅ 发现go.mod文件" echo "✅ 发现go.mod文件"
cat go.mod | head -10 head -10 go.mod
else else
echo "⚠️ 未发现go.mod文件可能不是Go模块项目" echo "⚠️ 未发现go.mod文件请确保这是一个Go模块项目"
echo "建议运行: go mod init ${PROJECT_NAME}"
fi fi
echo "=== 检查SonarQube Scanner ==="
ls -la /var/jenkins_home/tools/hudson.plugins.sonar.SonarRunnerInstallation/ || echo "SonarQube Scanner未安装"
''' '''
echo "✅ 环境检查完成" echo "✅ 环境检查完成"
} }
@ -188,6 +219,13 @@ pipeline {
echo "整理依赖..." echo "整理依赖..."
go mod tidy go mod tidy
echo "检查依赖漏洞..."
if command -v govulncheck >/dev/null 2>&1; then
govulncheck ./... || echo "⚠️ 发现安全漏洞,请检查"
else
echo " govulncheck未安装跳过漏洞检查"
fi
echo "检查依赖更新..." echo "检查依赖更新..."
go list -u -m all | head -20 || true go list -u -m all | head -20 || true
@ -217,9 +255,10 @@ pipeline {
exit 1 exit 1
fi fi
echo "检查import排序..." echo "检查Go语法..."
if command -v goimports >/dev/null 2>&1; then if ! go build -o /dev/null ./...; then
goimports -l . | head -10 echo "❌ Go语法检查失败"
exit 1
fi fi
echo "✅ 静态检查通过" echo "✅ 静态检查通过"
@ -227,26 +266,26 @@ pipeline {
} }
} }
stage('安全扫描') { stage('代码规范') {
when {
not { params.SKIP_TESTS }
}
steps { steps {
echo '🔒 运行安全扫描...' echo '📝 检查代码规范...'
sh ''' sh '''
echo "检查已知漏洞..." echo "检查包导入顺序..."
if command -v govulncheck >/dev/null 2>&1; then if command -v goimports >/dev/null 2>&1; then
govulncheck ./... || echo "⚠️ govulncheck未安装或发现问题" UNORGANIZED=$(goimports -l .)
else if [ -n "$UNORGANIZED" ]; then
echo "⚠️ govulncheck未安装跳过漏洞检查" echo "⚠️ 以下文件导入顺序不规范:"
echo "$UNORGANIZED"
echo "建议运行: goimports -w ."
fi
fi fi
echo "检查敏感信息..." echo "检查代码复杂度..."
if command -v git-secrets >/dev/null 2>&1; then if command -v gocyclo >/dev/null 2>&1; then
git secrets --scan --recursive . || echo "⚠️ 发现敏感信息" gocyclo -over 15 . || echo "⚠️ 发现高复杂度函数"
fi fi
echo "✅ 安全扫描完成" echo "✅ 代码规范检查完成"
''' '''
} }
} }
@ -262,11 +301,11 @@ pipeline {
steps { steps {
echo '🧪 运行单元测试...' echo '🧪 运行单元测试...'
sh ''' sh '''
echo "运行测试..." echo "创建测试结果目录..."
mkdir -p test-results mkdir -p test-results reports
# 运行测试并生成多种格式的报告 echo "运行测试..."
go test -v -coverprofile=coverage.out -covermode=atomic \ go test -v -coverprofile=coverage.out -covermode=atomic \\
-json ./... > test-results/test-report.json -json ./... > test-results/test-report.json
# 生成JUnit格式的测试报告如果有go-junit-report # 生成JUnit格式的测试报告如果有go-junit-report
@ -275,15 +314,23 @@ pipeline {
fi fi
echo "生成覆盖率报告..." echo "生成覆盖率报告..."
go tool cover -html=coverage.out -o coverage.html go tool cover -html=coverage.out -o reports/coverage.html
go tool cover -func=coverage.out | tee coverage-summary.txt go tool cover -func=coverage.out | tee reports/coverage-summary.txt
# 提取覆盖率百分比 # 提取覆盖率百分比
COVERAGE=$(go tool cover -func=coverage.out | grep total | grep -oE '[0-9]+\.[0-9]+%') COVERAGE=$(go tool cover -func=coverage.out | grep total | grep -oE '[0-9]+\\.[0-9]+%')
echo "📊 总覆盖率: $COVERAGE" echo "📊 总覆盖率: $COVERAGE"
echo "$COVERAGE" > coverage.txt echo "$COVERAGE" > reports/coverage.txt
echo "✅ 单元测试完成" # 检查覆盖率阈值(可选)
COVERAGE_NUM=$(echo $COVERAGE | sed 's/%//')
if [ "${COVERAGE_NUM%.*}" -lt 50 ]; then
echo "⚠️ 代码覆盖率低于50%: $COVERAGE"
# 可以选择是否让构建失败
# exit 1
fi
echo "✅ 单元测试完成,覆盖率: $COVERAGE"
''' '''
} }
post { post {
@ -295,20 +342,21 @@ pipeline {
} }
// 发布覆盖率报告 // 发布覆盖率报告
if (fileExists('coverage.html')) { if (fileExists('reports/coverage.html')) {
publishHTML([ publishHTML([
allowMissing: false, allowMissing: false,
alwaysLinkToLastBuild: true, alwaysLinkToLastBuild: true,
keepAll: true, keepAll: true,
reportDir: '.', reportDir: 'reports',
reportFiles: 'coverage.html', reportFiles: 'coverage.html',
reportName: 'Go Coverage Report', reportName: '📊 Go Coverage Report',
reportTitles: '' reportTitles: 'Go代码覆盖率报告'
]) ])
echo '✅ Go覆盖率报告已发布'
} }
// 归档测试产物 // 归档测试产物
archiveArtifacts artifacts: 'coverage.out,coverage.txt,test-results/*', allowEmptyArchive: true archiveArtifacts artifacts: 'coverage.out,reports/*,test-results/*', allowEmptyArchive: true
} }
} }
} }
@ -319,17 +367,21 @@ pipeline {
echo '⚡ 运行性能测试...' echo '⚡ 运行性能测试...'
sh ''' sh '''
echo "运行基准测试..." echo "运行基准测试..."
if go test -bench=. -benchmem ./... > benchmark.txt 2>&1; then mkdir -p reports
if go test -bench=. -benchmem ./... > reports/benchmark.txt 2>&1; then
echo "✅ 基准测试完成" echo "✅ 基准测试完成"
cat benchmark.txt echo "📊 性能测试结果:"
cat reports/benchmark.txt | head -20
else else
echo "⚠️ 未发现基准测试或测试失败" echo " 未发现基准测试或测试失败"
echo "建议添加基准测试函数,如: func BenchmarkMyFunction(b *testing.B)"
fi fi
''' '''
} }
post { post {
always { always {
archiveArtifacts artifacts: 'benchmark.txt', allowEmptyArchive: true archiveArtifacts artifacts: 'reports/benchmark.txt', allowEmptyArchive: true
} }
} }
} }
@ -346,46 +398,38 @@ pipeline {
try { try {
withCredentials([string(credentialsId: "${SONAR_CREDENTIAL_ID}", variable: 'SONAR_TOKEN')]) { withCredentials([string(credentialsId: "${SONAR_CREDENTIAL_ID}", variable: 'SONAR_TOKEN')]) {
withSonarQubeEnv('sonarQube') { withSonarQubeEnv('sonarQube') {
sh ''' def scannerHome = tool name: 'sonarQube', type: 'hudson.plugins.sonar.SonarRunnerInstallation'
# 动态查找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 sh """
echo "❌ 无法找到sonar-scanner" echo "=== SonarQube环境信息 ==="
exit 1 echo "SONAR_HOST_URL: \$SONAR_HOST_URL"
fi echo "SONAR_SCANNER_HOME: ${scannerHome}"
echo "✅ 使用sonar-scanner: $SCANNER_PATH" echo "✅ 使用Jenkins管理的SonarQube Scanner"
# 运行扫描 # 运行SonarQube扫描
"$SCANNER_PATH" \\ ${scannerHome}/bin/sonar-scanner \\
-Dsonar.projectKey=${SONAR_PROJECT_KEY} \\ -Dsonar.projectKey=${PROJECT_NAME} \\
-Dsonar.projectName="${PROJECT_NAME}" \\ -Dsonar.projectName="${PROJECT_NAME}" \\
-Dsonar.projectVersion=${BUILD_NUMBER} \\ -Dsonar.projectVersion=${BUILD_NUMBER} \\
-Dsonar.sources=. \\ -Dsonar.sources=. \\
-Dsonar.exclusions=**/*_test.go,**/vendor/**,**/*.mod,**/*.sum,**/test-results/** \\ -Dsonar.exclusions=**/*_test.go,**/vendor/**,**/*.mod,**/*.sum,**/reports/**,**/test-results/** \\
-Dsonar.tests=. \\ -Dsonar.tests=. \\
-Dsonar.test.inclusions=**/*_test.go \\ -Dsonar.test.inclusions=**/*_test.go \\
-Dsonar.test.exclusions=**/vendor/** \\ -Dsonar.test.exclusions=**/vendor/** \\
-Dsonar.go.coverage.reportPaths=coverage.out \\ -Dsonar.go.coverage.reportPaths=coverage.out \\
-Dsonar.sourceEncoding=UTF-8 -Dsonar.sourceEncoding=UTF-8
''' """
} }
} }
echo "✅ SonarQube扫描完成" echo "✅ SonarQube扫描完成"
} catch (Exception e) { } catch (Exception e) {
echo "⚠️ SonarQube扫描失败: ${e.getMessage()}" echo "⚠️ SonarQube扫描失败: ${e.getMessage()}"
if (env.DEPLOY_ENV == 'production') { if (env.DEPLOY_ENV == 'production') {
error("生产环境必须通过代码质量检查") echo "❌ 生产环境必须通过代码质量检查"
throw e
} else {
echo "⚠️ 非生产环境,继续构建流程"
} }
} }
} }
@ -402,7 +446,12 @@ pipeline {
# 设置构建信息 # 设置构建信息
BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S') 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}" 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"
# 编译 # 编译
CGO_ENABLED=${CGO_ENABLED} GOOS=${GOOS} GOARCH=${GOARCH} \\ CGO_ENABLED=${CGO_ENABLED} GOOS=${GOOS} GOARCH=${GOARCH} \\
@ -410,12 +459,13 @@ pipeline {
echo "验证二进制文件..." echo "验证二进制文件..."
ls -lh ${PROJECT_NAME} ls -lh ${PROJECT_NAME}
file ${PROJECT_NAME} file ${PROJECT_NAME} || true
# 尝试获取版本信息 # 尝试获取版本信息
./${PROJECT_NAME} --version 2>/dev/null || echo "应用不支持--version参数" ./${PROJECT_NAME} --version 2>/dev/null || echo " 应用不支持--version参数"
./${PROJECT_NAME} -h 2>/dev/null || echo " 应用不支持-h参数"
echo "✅ 编译完成" echo "✅ 编译完成: ${PROJECT_NAME}"
''' '''
} }
post { post {
@ -434,12 +484,17 @@ pipeline {
#!/bin/bash #!/bin/bash
set -e set -e
# 部署配置
PROJECT_NAME="${PROJECT_NAME}" PROJECT_NAME="${PROJECT_NAME}"
CONTAINER_NAME="${CONTAINER_NAME}" CONTAINER_NAME="${CONTAINER_NAME}"
DEPLOY_PORT="${DEPLOY_PORT}" DEPLOY_PORT="${DEPLOY_PORT}"
APP_PORT="${APP_PORT}"
IMAGE_TAG="${IMAGE_TAG}" IMAGE_TAG="${IMAGE_TAG}"
DEPLOY_ENV="${DEPLOY_ENV}"
echo "🚀 部署 $PROJECT_NAME 到 ${DEPLOY_ENV} 环境..." echo "🚀 部署 $PROJECT_NAME 到 $DEPLOY_ENV 环境..."
echo "容器名称: $CONTAINER_NAME"
echo "端口映射: $DEPLOY_PORT:$APP_PORT"
# 停止旧容器 # 停止旧容器
echo "停止旧容器..." echo "停止旧容器..."
@ -451,13 +506,19 @@ echo "启动新容器..."
docker run -d \\ docker run -d \\
--name $CONTAINER_NAME \\ --name $CONTAINER_NAME \\
--restart unless-stopped \\ --restart unless-stopped \\
-p $DEPLOY_PORT:8080 \\ -p $DEPLOY_PORT:$APP_PORT \\
-e GIN_MODE=release \\ -e GIN_MODE=release \\
-e DEPLOY_ENV=${DEPLOY_ENV} \\ -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 \\
$PROJECT_NAME:$IMAGE_TAG $PROJECT_NAME:$IMAGE_TAG
echo "✅ 部署完成" echo "✅ 部署完成"
echo "🔗 访问地址: http://$(hostname -I | awk '{print $1}'):$DEPLOY_PORT" echo "🔗 访问地址: http://$(hostname -I | awk '{print \\$1}'):$DEPLOY_PORT"
echo "🏥 健康检查: docker inspect --format='{{.State.Health.Status}}' $CONTAINER_NAME"
EOF EOF
chmod +x deploy.sh chmod +x deploy.sh
@ -473,7 +534,7 @@ EOF
echo '🐳 构建Docker镜像...' echo '🐳 构建Docker镜像...'
script { script {
// 检查是否需要重新构建 // 检查是否需要重新构建
def shouldBuild = params.FORCE_REBUILD def shouldBuild = params.FORCE_REBUILD_IMAGE
if (!shouldBuild) { if (!shouldBuild) {
def imageExists = sh( def imageExists = sh(
script: "docker images -q ${IMAGE_NAME}:${IMAGE_TAG}", script: "docker images -q ${IMAGE_NAME}:${IMAGE_TAG}",
@ -488,12 +549,56 @@ EOF
# 确保二进制文件存在 # 确保二进制文件存在
if [ ! -f "${PROJECT_NAME}" ]; then if [ ! -f "${PROJECT_NAME}" ]; then
echo "❌ 二进制文件不存在" echo "❌ 二进制文件不存在: ${PROJECT_NAME}"
exit 1 exit 1
fi fi
chmod +x ${PROJECT_NAME} chmod +x ${PROJECT_NAME}
# 检查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
# 构建镜像 # 构建镜像
docker build \\ docker build \\
--build-arg PROJECT_NAME=${PROJECT_NAME} \\ --build-arg PROJECT_NAME=${PROJECT_NAME} \\
@ -513,16 +618,25 @@ EOF
''' '''
// 推送到镜像仓库(如果配置了) // 推送到镜像仓库(如果配置了)
if (params.IMAGE_REGISTRY) { if (env.DOCKER_REGISTRY && env.DOCKER_CREDENTIAL_ID) {
sh ''' withCredentials([usernamePassword(credentialsId: env.DOCKER_CREDENTIAL_ID, usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
echo "推送镜像到仓库..." sh '''
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${FULL_IMAGE_NAME} echo "登录Docker仓库..."
docker push ${FULL_IMAGE_NAME} echo "$DOCKER_PASS" | docker login $DOCKER_REGISTRY -u "$DOCKER_USER" --password-stdin
echo "✅ 镜像推送完成"
''' 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}"
'''
}
} }
} else { } else {
echo "✅ 镜像已存在,跳过构建" echo "✅ 镜像已存在,跳过构建: ${IMAGE_NAME}:${IMAGE_TAG}"
} }
} }
} }
@ -539,8 +653,9 @@ EOF
sh """ sh """
echo "启动测试容器..." echo "启动测试容器..."
docker run -d --name ${testContainerName} \\ docker run -d --name ${testContainerName} \\
-p ${testPort}:8080 \\ -p ${testPort}:${APP_PORT} \\
-e GIN_MODE=test \\ -e GIN_MODE=test \\
-e DEPLOY_ENV=test \\
${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:${IMAGE_TAG}
echo "等待应用启动..." echo "等待应用启动..."
@ -551,24 +666,33 @@ EOF
docker logs ${testContainerName} docker logs ${testContainerName}
echo "测试应用端点..." echo "测试应用端点..."
SUCCESS=false
for i in \$(seq 1 10); do for i in \$(seq 1 10); do
echo "尝试连接 \$i/10..." echo "尝试连接 \$i/10..."
if curl -f http://localhost:${testPort}/health; then if curl -f http://localhost:${testPort}/health; then
echo "✅ 健康检查通过" echo "✅ 健康检查端点响应正常"
SUCCESS=true
break break
elif curl -f http://localhost:${testPort}/ping; then elif curl -f http://localhost:${testPort}/ping; then
echo "✅ Ping检查通过" echo "✅ Ping端点响应正常"
SUCCESS=true
break
elif curl -f http://localhost:${testPort}/; then
echo "✅ 根路径响应正常"
SUCCESS=true
break break
else else
if [ \$i -eq 10 ]; then echo "⚠️ 连接失败等待3秒后重试..."
echo "❌ 镜像测试失败"
exit 1
fi
echo "等待3秒后重试..."
sleep 3 sleep 3
fi fi
done done
if [ "\$SUCCESS" = "false" ]; then
echo "❌ 镜像测试失败,无法连接到应用"
docker logs ${testContainerName}
exit 1
fi
echo "✅ 镜像测试通过" echo "✅ 镜像测试通过"
""" """
} finally { } finally {
@ -584,12 +708,11 @@ EOF
stage('🚀 部署') { stage('🚀 部署') {
when { when {
anyOf { not {
branch 'main' anyOf {
branch 'master' expression { params.DEPLOY_ENV == 'skip' }
branch 'develop' expression { env.DEPLOY_ENV == 'skip' }
branch 'staging' }
expression { params.DEPLOY_ENV != 'auto' }
} }
} }
steps { steps {
@ -623,6 +746,7 @@ EOF
rm -f ${PROJECT_NAME}-${IMAGE_TAG}.tar deploy.sh rm -f ${PROJECT_NAME}-${IMAGE_TAG}.tar deploy.sh
echo "✅ 部署完成" echo "✅ 部署完成"
echo "🔗 应用地址: http://${DEPLOY_SERVER}:${DEPLOY_PORT}"
EOF EOF
# 清理本地临时文件 # 清理本地临时文件
@ -635,36 +759,50 @@ EOF
stage('🏥 健康检查') { stage('🏥 健康检查') {
when { when {
anyOf { not {
branch 'main' anyOf {
branch 'master' expression { params.DEPLOY_ENV == 'skip' }
branch 'develop' expression { env.DEPLOY_ENV == 'skip' }
branch 'staging' }
expression { params.DEPLOY_ENV != 'auto' }
} }
} }
steps { steps {
echo '🏥 执行应用健康检查...' echo '🏥 执行应用健康检查...'
script { script {
echo "等待应用启动..."
sleep(time: 30, unit: 'SECONDS') sleep(time: 30, unit: 'SECONDS')
def healthCheckPassed = false def healthCheckPassed = false
for (int i = 1; i <= 5; i++) { def healthUrls = [
try { "http://${DEPLOY_SERVER}:${env.DEPLOY_PORT}/health",
def response = sh( "http://${DEPLOY_SERVER}:${env.DEPLOY_PORT}/ping",
script: "curl -s -o /dev/null -w '%{http_code}' http://${DEPLOY_SERVER}:${env.DEPLOY_PORT}/health", "http://${DEPLOY_SERVER}:${env.DEPLOY_PORT}/"
returnStdout: true ]
).trim()
if (response == "200") { for (int i = 1; i <= 5; i++) {
echo "✅ 第${i}次健康检查通过" echo "第${i}次健康检查..."
healthCheckPassed = true
break for (String url : healthUrls) {
} else { try {
echo "⚠️ 第${i}次健康检查失败,状态码: ${response}" 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()}"
} }
} catch (Exception e) { }
echo "⚠️ 第${i}次健康检查异常: ${e.getMessage()}"
if (healthCheckPassed) {
break
} }
if (i < 5) { if (i < 5) {
@ -673,10 +811,19 @@ EOF
} }
} }
if (!healthCheckPassed && env.DEPLOY_ENV == 'production') { if (!healthCheckPassed) {
error("❌ 生产环境健康检查失败,回滚部署") def errorMsg = "❌ 健康检查失败,应用可能未正常启动"
} else if (!healthCheckPassed) { echo errorMsg
echo "⚠️ 健康检查失败,但允许${env.DEPLOY_ENV}环境继续"
if (env.DEPLOY_ENV == 'production') {
echo "🔄 生产环境健康检查失败,考虑回滚..."
// 这里可以添加自动回滚逻辑
error(errorMsg)
} else {
echo "⚠️ 非生产环境,允许继续但需要人工检查"
}
} else {
echo "✅ 应用部署成功并通过健康检查"
} }
} }
} }
@ -690,8 +837,8 @@ EOF
// 清理构建产物 // 清理构建产物
sh """ sh """
rm -f ${PROJECT_NAME} coverage.out coverage.html deploy.sh rm -f ${PROJECT_NAME} coverage.out deploy.sh
rm -rf test-results/ rm -rf test-results/ reports/
docker image prune -f || true docker image prune -f || true
docker builder prune -f || true docker builder prune -f || true
""" """
@ -705,6 +852,14 @@ EOF
success { success {
script { script {
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}"
def message = """ def message = """
🎉 ${PROJECT_NAME} 构建成功! 🎉 ${PROJECT_NAME} 构建成功!
@ -714,15 +869,18 @@ EOF
📝 提交: ${env.GIT_COMMIT_SHORT ?: 'unknown'} 📝 提交: ${env.GIT_COMMIT_SHORT ?: 'unknown'}
🌍 环境: ${env.DEPLOY_ENV} 🌍 环境: ${env.DEPLOY_ENV}
⏱️ 耗时: ${currentBuild.durationString} ⏱️ 耗时: ${currentBuild.durationString}
🔗 访问: http://${DEPLOY_SERVER}:${env.DEPLOY_PORT} 🐳 镜像: ${FULL_IMAGE_NAME}
📊 报告: ${env.BUILD_URL} ${deployInfo}
${sonarInfo}
📈 构建详情: ${env.BUILD_URL}
""" """
echo message echo message
// 发送通知 (可选) // 发送通知(根据需要启用)
// slackSend(color: 'good', message: message) // slackSend(color: 'good', message: message)
// emailext(subject: "✅ ${PROJECT_NAME} 构建成功", body: message) // emailext(subject: "✅ ${PROJECT_NAME} 构建成功", body: message)
// dingTalk(robot: 'your-robot-id', message: message)
} }
} }
@ -738,7 +896,7 @@ EOF
🌍 环境: ${env.DEPLOY_ENV} 🌍 环境: ${env.DEPLOY_ENV}
⏱️ 耗时: ${currentBuild.durationString} ⏱️ 耗时: ${currentBuild.durationString}
🔗 日志: ${env.BUILD_URL}console 🔗 日志: ${env.BUILD_URL}console
📧 修复后请重新构建 🛠️ 修复后请重新构建
""" """
echo message echo message
@ -767,3 +925,36 @@ EOF
} }
} }
} }
// =================================================================
// 📖 使用文档:
//
// 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自定义镜像标签
//
// =================================================================