// ================================================================
// Groovy Script-Level 辅助函数
// ================================================================
/**
* 根据 Git 引用推断目标环境
*/
def inferEnvironment(String gitRef) {
if (gitRef.startsWith('refs/tags/')) {
def tag = gitRef.replace('refs/tags/', '')
if (tag.toLowerCase().contains('prod') || tag.toLowerCase().contains('release')) {
return 'prod'
}
return 'test'
}
def branch = gitRef.replace('refs/heads/', '')
if (branch == 'master' || branch == 'main') { return 'prod' }
if (branch.startsWith('release/')) { return 'test' }
return 'dev'
}
/**
* 解析需要构建的模块列表
*/
def resolveModulesToBuild(buildScope, selectedModules) {
def ALL_SERVICES = [
'gateway', 'auth', 'system'
]
def CORE_SERVICES = ['gateway', 'auth', 'system']
switch (buildScope) {
case 'all': return ALL_SERVICES.join(',')
case 'core-only': return CORE_SERVICES.join(',')
case 'select-modules': return selectedModules?.trim() ?: ''
default: return ALL_SERVICES.join(',')
}
}
/**
* 将模块简称转换为 Maven -pl 参数路径
*/
def moduleToMavenPath(String module) {
def mapping = [
'gateway': 'gateway',
'auth': 'auth',
'system': 'system'
]
return mapping[module] ?: module
}
/**
* 获取模块 jar 输出目录 (相对于项目根目录)
*/
def getJarOutputDir(String module) {
return moduleToMavenPath(module)
}
/**
* 判断是否为可独立部署的服务模块 (非 library)
*/
def isDeployableService(String module) {
return !(module in ['common-core'])
}
/**
* 模块简称 → Docker 镜像名
*/
def moduleToImageName(String module) {
return "jenkins-${module}"
}
/**
* 获取服务端口
*/
def getServicePort(String module) {
def ports = [
'gateway': '9001',
'auth': '9002',
'system': '9003'
]
return ports[module] ?: '8080'
}
/**
* 获取健康检查 URL (本地)
*/
def getHealthUrl(String module) {
def port = getServicePort(module)
def contextPaths = [
'gateway': '/',
'auth': '/auth',
'system': '/system'
]
def context = contextPaths[module] ?: ''
if (context == '/') { context = '' }
return "http://127.0.0.1:${port}${context}/actuator/health"
}
/**
* 生成通用 Dockerfile
*/
def generateGenericDockerfile(String module, String mavenProfile) {
return """\
|FROM eclipse-temurin:8-jre-jammy
|
|WORKDIR /app
|
|COPY ./target/*.jar ./app.jar
|
|ENV JAVA_ENV="-Xms512m -Xmx1024m -Dspring.profiles.active=${mavenProfile}"
|ENV MODULE_NAME="${module}"
|
|RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \\
| && echo "Asia/Shanghai" > /etc/timezone
|
|EXPOSE ${getServicePort(module)}
|
|ENTRYPOINT ["sh", "-c", "java \$JAVA_ENV -jar ./app.jar"]
|""".stripMargin()
}
/**
* 生成 docker-compose.yml 内容
*/
def generateDockerComposeFile(String modules, String profile, String baseDir,
String nacosServerAddr, String nacosNs, String discoveryIp) {
def serviceDefs = []
def deployModules = modules.tokenize(',')
deployModules.each { module ->
def m = module.trim()
if (!isDeployableService(m)) { return }
def imageName = moduleToImageName(m)
def port = getServicePort(m)
serviceDefs << """\
jenkins-${m}:
image: ${imageName}
container_name: jenkins-${m}
restart: unless-stopped
ports:
- "${port}:${port}"
environment:
- SPRING_PROFILES_ACTIVE=${profile}
- SERVER_PORT=${port}
- NACOS_SERVER_ADDR=${nacosServerAddr}
- SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR=${nacosServerAddr}
- SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR=${nacosServerAddr}
- SPRING_CLOUD_NACOS_DISCOVERY_NAMESPACE=${nacosNs}
- SPRING_CLOUD_NACOS_CONFIG_NAMESPACE=${nacosNs}
- SPRING_CLOUD_NACOS_DISCOVERY_IP=${discoveryIp}
volumes:
- ${baseDir}/${m}/logs:/app/logs
- /etc/localtime:/etc/localtime:ro
"""
}
return """\
version: '3.8'
services:
${serviceDefs.join('\n')}
"""
}
// ================================================================
// Pipeline 主流程
// ================================================================
pipeline {
agent any
// ======================== 触发器 ========================
triggers {
gitlab(
triggerOnPush: true,
triggerOnMergeRequest: true,
triggerOnAcceptedMergeRequest: true,
triggerOnClosedMergeRequest: false,
triggerOnApprovedMergeRequest: false,
triggerOnNoteRequest: false,
branchFilterType: 'NameBasedFilter',
includeBranchesSpec: 'develop',
excludeBranchesSpec: '',
secretToken: 'a6b2cb5e8b1feac013a85469dd7609c3'
)
}
// ======================== 环境变量 ========================
environment {
PROJECT_NAME = 'jenkins'
PROJECT_VERSION = '3.6.5'
// Nacos 配置中心
NACOS_SERVER_ADDR = '127.0.0.1:8848'
NACOS_NAMESPACE = 'docker'
DISCOVERY_IP = '127.0.0.1'
// 本地部署目录
DEPLOY_BASE_DIR = '/opt/jenkins'
}
// ======================== 构建参数 ========================
parameters {
// ------ Git ------
string(
name: 'GIT_URL',
defaultValue: '',
description: 'GitLab 仓库地址'
)
string(
name: 'GIT_BRANCH',
defaultValue: 'develop',
description: 'Git 分支名称 (e.g. master, develop, feature/xxx)'
)
string(
name: 'GIT_TAG',
defaultValue: '',
description: 'Git Tag (若填写则优先使用 Tag, 忽略分支)'
)
// ------ Maven 构建 ------
choice(
name: 'MAVEN_PROFILE',
choices: ['dev', 'prod', 'local'],
description: 'Maven / Spring 激活的 Profile'
)
booleanParam(
name: 'SKIP_TESTS',
defaultValue: true,
description: '是否跳过单元测试'
)
// ------ 模块选择 ------
choice(
name: 'BUILD_SCOPE',
choices: ['all', 'core-only', 'select-modules'],
description: '''构建范围:
all --- 全量构建所有模块
core-only --- 仅构建核心模块 (gateway, auth, system)
select-modules --- 手动指定要构建的业务模块'''
)
choice(
name: 'SELECTED_MODULES',
choices: [
'',
'gateway',
'auth',
'system'
],
description: 'BUILD_SCOPE=select-modules 时选择要构建的模块'
)
// ------ Docker 镜像 ------
booleanParam(
name: 'BUILD_DOCKER',
defaultValue: true,
description: '是否构建 Docker 镜像'
)
string(
name: 'DOCKER_IMAGE_TAG',
defaultValue: '',
description: 'Docker 镜像 Tag (留空则自动生成: <分支名>-<构建号>)'
)
// ------ 部署 ------
booleanParam(
name: 'DO_DEPLOY',
defaultValue: true,
description: '是否在本地启动/重启 Docker 容器'
)
string(
name: 'DEPLOY_MODULES',
defaultValue: '',
description: '待部署模块, 逗号分隔; 留空则根据 BUILD_SCOPE 自动推断'
)
}
stages {
// ==================== Stage 0: 初始化 ====================
stage('0. Initialize') {
steps {
script {
env.START_TIME = "${System.currentTimeMillis()}"
// Git 引用
if (params.GIT_TAG?.trim()) {
env.GIT_REF = "refs/tags/${params.GIT_TAG.trim()}"
} else {
env.GIT_REF = "refs/heads/${params.GIT_BRANCH.trim()}"
}
// 目标环境
env.TARGET_ENV = inferEnvironment(env.GIT_REF)
echo "目标环境: ${env.TARGET_ENV}"
// Docker 镜像 Tag
if (!params.DOCKER_IMAGE_TAG?.trim()) {
def safeBranch = env.GIT_BRANCH.replaceAll('/','-')
env.DOCKER_TAG = "${safeBranch}-${env.BUILD_NUMBER}"
} else {
env.DOCKER_TAG = params.DOCKER_IMAGE_TAG.trim()
}
// 构建模块列表
env.MODULES_TO_BUILD = resolveModulesToBuild(params.BUILD_SCOPE, params.SELECTED_MODULES)
echo "待构建模块: ${env.MODULES_TO_BUILD}"
// 部署模块列表
if (params.DO_DEPLOY) {
env.MODULES_TO_DEPLOY = params.DEPLOY_MODULES?.trim()
? params.DEPLOY_MODULES.trim()
: env.MODULES_TO_BUILD
} else {
env.MODULES_TO_DEPLOY = ''
}
echo "待部署模块: ${env.MODULES_TO_DEPLOY ?: '(无)'}"
// 校验
if (params.BUILD_SCOPE == 'select-modules' && !params.SELECTED_MODULES?.trim()) {
error("BUILD_SCOPE=select-modules 时必须填写 SELECTED_MODULES 参数!")
}
if (params.DO_DEPLOY && !params.BUILD_DOCKER) {
error("部署前必须先构建 Docker 镜像, 请启用 BUILD_DOCKER!")
}
}
}
}
// ==================== Stage 1: 代码拉取 ====================
stage('1. Checkout') {
steps {
script {
echo "正在拉取代码: ${env.GIT_REF}"
def gitUrl = params.GIT_URL?.trim() ?: env.GIT_URL?.trim()
if (!gitUrl) {
try {
gitUrl = scm.userRemoteConfigs?.url?.first()
} catch (e) { /* 非 Multibranch */ }
}
if (!gitUrl) {
error("请提供 GIT_URL 参数或在 Jenkins Job 中配置 Pipeline script from SCM")
}
checkout([
$class: 'GitSCM',
branches: [[name: env.GIT_REF]],
userRemoteConfigs: [[
url: gitUrl,
credentialsId: '27437595-afee-41b5-b7f0-d9e7a4781ca5'
]],
extensions: [
[$class: 'CleanBeforeCheckout'],
[$class: 'CloneOption', noTags: false, reference: '', shallow: false, depth: 0]
]
])
env.GIT_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
env.GIT_COMMIT_SHORT = env.GIT_COMMIT.take(7)
env.GIT_AUTHOR = sh(script: 'git log -1 --pretty=format:"%an"', returnStdout: true).trim()
env.GIT_MESSAGE = sh(script: 'git log -1 --pretty=format:"%s"', returnStdout: true).trim()
echo """
======== 代码信息 ========
分支/Tag : ${env.GIT_REF}
Commit : ${env.GIT_COMMIT}
作者 : ${env.GIT_AUTHOR}
提交信息 : ${env.GIT_MESSAGE}
==========================
""".stripIndent()
}
}
}
// ==================== Stage 2: 单元测试 (SKIP_TESTS=false 时才执行) ====================
stage('2. Test') {
when {
expression { !params.SKIP_TESTS }
}
steps {
script {
def mvnHome = tool name: 'maven3', type: 'maven'
def javaHome = tool name: 'jdk8', type: 'jdk'
withEnv(["JAVA_HOME=${javaHome}", "PATH+MAVEN=${mvnHome}/bin"]) {
echo '===== 单元测试 ====='
try {
sh "mvn test -P${params.MAVEN_PROFILE} -T 1C"
} catch (e) {
echo "WARNING: 单元测试存在失败, 请检查测试报告"
}
}
}
}
post {
always {
junit(
allowEmptyResults: true,
testResults: '**/target/surefire-reports/*.xml'
)
}
}
}
// ==================== Stage 3: Maven 打包 ====================
stage('3. Package') {
steps {
script {
def mvnHome = tool name: 'maven3', type: 'maven'
def javaHome = tool name: 'jdk8', type: 'jdk'
withEnv(["JAVA_HOME=${javaHome}", "PATH+MAVEN=${mvnHome}/bin"]) {
echo '===== Maven Package ====='
def modulesToPackage = env.MODULES_TO_BUILD
def packageCmd
if (modulesToPackage == 'ALL') {
packageCmd = "mvn clean package -Dmaven.test.skip=true -P${params.MAVEN_PROFILE}"
} else {
def moduleArgs = modulesToPackage.tokenize(',').collect { m ->
moduleToMavenPath(m.trim())
}.join(',')
packageCmd = "mvn clean package -pl ${moduleArgs} -am -Dmaven.test.skip=true -P${params.MAVEN_PROFILE}"
}
sh(script: packageCmd)
echo '===== 构建产物 ====='
modulesToPackage.tokenize(',').each { m ->
def jarDir = getJarOutputDir(m.trim())
sh(
script: "ls -lh ${jarDir}/target/*.jar 2>/dev/null || echo ' (library 模块 --- 无独立 jar)'",
returnStdout: true
)
}
}
}
}
post {
success {
archiveArtifacts(
artifacts: 'jenkins-gateway/target/*.jar, jenkins-auth/target/*.jar, jenkins-modules/jenkins-system/target/*.jar',
fingerprint: true,
onlyIfSuccessful: true
)
}
}
}
// ==================== Stage 4: Docker 镜像构建 ====================
stage('4. Docker Build') {
when {
expression { params.BUILD_DOCKER }
}
steps {
script {
echo '===== Docker 镜像构建 ====='
def buildableModules = env.MODULES_TO_BUILD.tokenize(',').findAll { m ->
isDeployableService(m.trim())
}
if (buildableModules.isEmpty()) {
echo '没有需要构建 Docker 镜像的服务模块'
return
}
def dockerBuildSteps = [:]
buildableModules.each { module ->
def m = module.trim()
def imageName = moduleToImageName(m)
def contextDir = getJarOutputDir(m)
def profile = params.MAVEN_PROFILE
def version = env.PROJECT_VERSION
def commitShort = env.GIT_COMMIT_SHORT ?: 'unknown'
dockerBuildSteps[m] = {
echo ">>> 构建镜像: ${imageName}"
// 删除旧镜像
sh "docker rmi -f ${imageName} 2>/dev/null || true"
// 生成 .dockerignore, 只传 jar 文件给 Docker daemon
writeFile(
file: "${contextDir}/.dockerignore",
text: "*\n!target/*.jar"
)
// 统一使用自动生成的 Dockerfile
writeFile(
file: "${contextDir}/Dockerfile",
text: generateGenericDockerfile(m, profile)
)
sh """
docker build \
--build-arg GIT_COMMIT="${commitShort}" \
--build-arg MODULE_NAME="${m}" \
--build-arg APP_VERSION="${version}" \
-t ${imageName} \
${contextDir}
""".stripIndent()
echo " 镜像构建完成: ${imageName}"
}
}
dockerBuildSteps.each { name, step -> step.call() }
}
}
}
// ==================== Stage 5: 本地 Docker 部署 ====================
stage('5. Deploy') {
when {
expression { params.DO_DEPLOY }
}
stages {
stage('5.1 Stop Old Containers') {
steps {
script {
echo '===== 停止旧容器 (docker-compose down) ====='
def baseDir = env.DEPLOY_BASE_DIR
def nacosServerAddr = env.NACOS_SERVER_ADDR
def nacosNs = env.NACOS_NAMESPACE
def discoveryIp = env.DISCOVERY_IP
def profile = params.MAVEN_PROFILE
def deployModules = env.MODULES_TO_DEPLOY.tokenize(',')
// 生成 docker-compose.yml
def composeContent = generateDockerComposeFile(
env.MODULES_TO_DEPLOY, profile, baseDir,
nacosServerAddr, nacosNs, discoveryIp
)
def composeFile = 'docker-compose.yml'
sh "mkdir -p ${baseDir}"
writeFile(file: composeFile, text: composeContent)
// 停止并移除容器
def hasCompose = sh(
script: 'command -v docker-compose >/dev/null 2>&1 && echo yes || echo no',
returnStdout: true
).trim()
def hasComposeV2 = sh(
script: 'docker compose version >/dev/null 2>&1 && echo yes || echo no',
returnStdout: true
).trim()
if (hasCompose == 'yes') {
sh "docker-compose -f ${composeFile} down"
} else if (hasComposeV2 == 'yes') {
sh "docker compose -f ${composeFile} down"
} else {
echo 'WARNING: docker-compose 不可用, 回退到 docker 命令停止容器'
deployModules.each { module ->
def m = module.trim()
if (!isDeployableService(m)) { return }
sh """
docker stop jenkins-${m} 2>/dev/null || true
docker rm jenkins-${m} 2>/dev/null || true
echo " 已停止 jenkins-${m}"
""".stripIndent()
}
}
}
}
}
stage('5.2 Start New Containers') {
steps {
script {
echo '===== 启动新容器 (docker-compose up) ====='
def baseDir = env.DEPLOY_BASE_DIR
def nacosServerAddr = env.NACOS_SERVER_ADDR
def nacosNs = env.NACOS_NAMESPACE
def discoveryIp = env.DISCOVERY_IP
def profile = params.MAVEN_PROFILE
// 生成 docker-compose.yml
def composeContent = generateDockerComposeFile(
env.MODULES_TO_DEPLOY, profile, baseDir,
nacosServerAddr, nacosNs, discoveryIp
)
def composeFile = 'docker-compose.yml'
writeFile(file: composeFile, text: composeContent)
// 确保各模块日志目录存在
def deployModules = env.MODULES_TO_DEPLOY.tokenize(',')
deployModules.each { module ->
def m = module.trim()
if (isDeployableService(m)) {
sh "mkdir -p ${baseDir}/${m}/logs"
}
}
// 检测 docker-compose 可用性
def hasCompose = sh(
script: 'command -v docker-compose >/dev/null 2>&1 && echo yes || echo no',
returnStdout: true
).trim()
def hasComposeV2 = sh(
script: 'docker compose version >/dev/null 2>&1 && echo yes || echo no',
returnStdout: true
).trim()
if (hasCompose == 'yes') {
sh "docker-compose -f ${composeFile} up -d"
} else if (hasComposeV2 == 'yes') {
sh "docker compose -f ${composeFile} up -d"
} else {
echo 'WARNING: docker-compose 不可用, 回退到 docker run 启动容器'
deployModules.each { module ->
def m = module.trim()
if (!isDeployableService(m)) { return }
def imageName = moduleToImageName(m)
def port = getServicePort(m)
echo "--- 启动 ${m} (端口 ${port}) ---"
sh """
docker run -d \\
--name jenkins-${m} \\
--restart unless-stopped \\
-p ${port}:${port} \\
-e SPRING_PROFILES_ACTIVE=${profile} \\
-e SERVER_PORT=${port} \\
-e NACOS_SERVER_ADDR=${nacosServerAddr} \\
-e SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR=${nacosServerAddr} \\
-e SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR=${nacosServerAddr} \\
-e SPRING_CLOUD_NACOS_DISCOVERY_NAMESPACE=${nacosNs} \\
-e SPRING_CLOUD_NACOS_CONFIG_NAMESPACE=${nacosNs} \\
-e SPRING_CLOUD_NACOS_DISCOVERY_IP=${discoveryIp} \\
-v ${baseDir}/${m}/logs:/app/logs \\
-v /etc/localtime:/etc/localtime:ro \\
${imageName}
""".stripIndent()
}
}
echo ' 所有容器已启动'
// 展示运行中的容器
if (hasCompose == 'yes') {
sh "docker-compose -f ${composeFile} ps || true"
} else if (hasComposeV2 == 'yes') {
sh "docker compose -f ${composeFile} ps || true"
} else {
sh 'docker ps --filter "name=jenkins-" --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" || true'
}
}
}
}
}
}
}
// ======================== 构建后处理 ========================
post {
always {
script {
def startMs = (env.START_TIME ?: System.currentTimeMillis().toString()) as long
def totalSecs = (System.currentTimeMillis() - startMs) / 1000L as long
def minutes = totalSecs / 60L as long
def seconds = totalSecs % 60L as long
def durationStr = "${minutes} 分 ${seconds} 秒"
env.BUILD_DURATION = durationStr
echo """
======== 构建总结 ========
分支 : ${env.GIT_REF}
环境 : ${env.TARGET_ENV}
镜像 Tag : ${env.DOCKER_TAG}
耗时 : ${durationStr}
结果 : ${currentBuild.result ?: 'SUCCESS'}
==========================
""".stripIndent()
// 查看当前运行的容器
sh 'echo "当前运行容器:"; docker ps --filter "name=jenkins-" --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" || true'
}
cleanWs(
cleanWhenAborted: true,
cleanWhenFailure: true,
cleanWhenNotBuilt: true,
cleanWhenSuccess: true,
deleteDirs: true,
disableDeferredWipeout: true
)
}
success {
echo '✓ 流水线执行成功'
}
failure {
echo '✗ 流水线执行失败, 请检查日志'
}
unstable {
echo '⚠ 流水线执行不稳定, 部分阶段未通过'
}
}
}