Files
timeline-frontend/Jenkinsfile

331 lines
12 KiB
Plaintext
Raw Normal View History

2025-12-26 21:02:15 +08:00
pipeline {
2025-12-26 21:18:32 +08:00
agent any
2025-12-26 21:02:15 +08:00
environment {
// 环境变量定义
PROJECT_NAME = 'timeline-frontend'
2025-12-26 21:18:32 +08:00
DOCKER_REGISTRY = 'timeline-registry:5000'
2025-12-26 21:02:15 +08:00
DOCKER_IMAGE = "${DOCKER_REGISTRY}/${PROJECT_NAME}"
}
parameters {
// 构建参数
choice(
name: 'DEPLOY_TARGET',
choices: ['dev', 'staging', 'prod'],
description: '选择部署环境'
)
string(
name: 'GIT_COMMIT',
defaultValue: '',
description: '指定 Git Commit SHA 进行构建(留空则使用最新提交)'
)
}
stages {
stage('Checkout') {
steps {
script {
2025-12-26 21:14:40 +08:00
if (params.GIT_COMMIT) {
checkout scm
sh "git reset --hard ${params.GIT_COMMIT}"
} else {
checkout scm
}
2025-12-26 21:02:15 +08:00
}
2025-12-26 21:14:40 +08:00
echo "当前构建的 Git Commit: ${env.GIT_COMMIT}"
2025-12-26 21:02:15 +08:00
}
}
2025-12-26 21:18:32 +08:00
2025-12-26 21:08:00 +08:00
stage('Build timeline-frontend dist') {
2025-12-26 21:02:15 +08:00
steps {
script {
2025-12-29 15:13:23 +08:00
def workspace = sh(script: "pwd", returnStdout: true).trim()
2025-12-29 14:39:16 +08:00
echo "当前工作空间路径: ${workspace}"
2025-12-29 14:36:47 +08:00
// 确保路径正确
2025-12-29 14:39:16 +08:00
sh "ls -la ${workspace}"
2025-12-29 14:36:47 +08:00
2025-12-29 15:47:36 +08:00
// 修复权限问题
sh "chmod -R 755 ${workspace}"
sh "chown -R jenkins:jenkins ${workspace}"
2025-12-29 15:26:05 +08:00
2025-12-29 15:47:36 +08:00
// 使用 Jenkins Node.js 插件,并确保在工作空间目录中执行
dir(workspace) {
nodejs('NodeJS-18') {
// 检查是否存在 package.json 和 pnpm-lock.yaml
if (fileExists('package.json')) {
echo "package.json found"
if (fileExists('pnpm-lock.yaml')) {
echo "pnpm-lock.yaml found"
} else {
error('pnpm-lock.yaml not found')
}
2025-12-29 15:41:20 +08:00
2025-12-29 15:47:36 +08:00
// 安装 pnpm
sh 'npm install -g pnpm'
2025-12-29 15:41:20 +08:00
2025-12-30 11:31:41 +08:00
// 检查 package.json 或 pnpm-lock.yaml 是否自上次构建以来发生了更改
def shouldInstall = false
// 检查是否有上次构建的哈希记录
def packageJsonHash = ''
def pnpmLockHash = ''
if (fileExists('.last_build_hashes')) {
def lastHashes = readFile('.last_build_hashes').split('\n')
def lastPackageJsonHash = ''
def lastPnpmLockHash = ''
lastHashes.each { line ->
if (line.startsWith('packageJson=')) {
lastPackageJsonHash = line.split('=')[1].trim()
} else if (line.startsWith('pnpmLock=')) {
lastPnpmLockHash = line.split('=')[1].trim()
}
}
// 计算当前文件哈希
packageJsonHash = sh(script: 'cat package.json | md5sum | cut -d" " -f1', returnStdout: true).trim()
pnpmLockHash = sh(script: 'cat pnpm-lock.yaml | md5sum | cut -d" " -f1', returnStdout: true).trim()
// 如果任一文件哈希与上次不同,则需要安装
if (packageJsonHash != lastPackageJsonHash || pnpmLockHash != lastPnpmLockHash) {
shouldInstall = true
echo "Detected changes in package management files, running pnpm install"
} else {
echo "No changes detected in package management files, skipping pnpm install"
}
} else {
// 第一次构建,需要安装
shouldInstall = true
packageJsonHash = sh(script: 'cat package.json | md5sum | cut -d" " -f1', returnStdout: true).trim()
pnpmLockHash = sh(script: 'cat pnpm-lock.yaml | md5sum | cut -d" " -f1', returnStdout: true).trim()
echo "First build, running pnpm install"
}
if (shouldInstall) {
// 使用 pnpm 安装依赖,由于锁文件兼容性问题,不使用 --frozen-lockfile
sh 'pnpm install --no-frozen-lockfile'
// 保存当前哈希值供下次比较
writeFile file: '.last_build_hashes', text: "packageJson=${packageJsonHash}\npnpmLock=${pnpmLockHash}"
} else {
echo "Skipping pnpm install due to no changes in package management files"
}
2025-12-29 15:41:20 +08:00
2025-12-29 15:47:36 +08:00
// 构建项目
sh 'pnpm run build'
// 检查 dist 目录是否存在
if (!fileExists('dist')) {
error('ERROR: dist directory does not exist after build')
} else {
echo 'Build completed successfully, dist directory exists'
}
2025-12-29 15:41:20 +08:00
} else {
2025-12-29 15:47:36 +08:00
error('package.json not found')
2025-12-29 15:41:20 +08:00
}
}
}
2025-12-29 15:24:13 +08:00
}
2025-12-29 14:55:43 +08:00
}
2025-12-26 21:08:00 +08:00
}
2025-12-26 21:02:15 +08:00
2025-12-26 21:08:00 +08:00
stage('Build Docker Image') {
steps {
script {
2025-12-26 21:02:15 +08:00
def imageTag = "${BUILD_NUMBER}-${env.GIT_COMMIT.take(7)}"
env.IMAGE_TAG = imageTag
2025-12-26 21:18:32 +08:00
2025-12-26 21:02:15 +08:00
sh """
2025-12-29 16:28:02 +08:00
docker build -t ${DOCKER_IMAGE}:${imageTag} .
docker tag ${DOCKER_IMAGE}:${imageTag} ${DOCKER_IMAGE}:latest
2025-12-26 21:02:15 +08:00
"""
}
}
}
stage('Push Docker Image') {
steps {
script {
2025-12-29 16:17:06 +08:00
2025-12-29 16:08:27 +08:00
sh "docker push ${DOCKER_IMAGE}:latest"
2025-12-26 21:02:15 +08:00
}
}
}
stage('Deploy to Environment') {
2025-12-30 11:47:21 +08:00
steps {
script {
// 创建或更新docker-compose文件
def composeContent = getComposeFileContent()
writeFile file: 'docker-compose.yml', text: composeContent
2025-12-26 21:02:15 +08:00
2025-12-30 11:47:21 +08:00
// 拉取最新镜像
sh 'docker compose pull'
2025-12-26 21:02:15 +08:00
2025-12-30 11:47:21 +08:00
// 停止旧容器
sh 'docker compose down || true'
// 启动新容器
sh 'docker compose up -d'
echo "所有服务已部署完成"
2025-12-26 21:02:15 +08:00
}
2025-12-30 11:47:21 +08:00
}
2025-12-26 21:02:15 +08:00
}
}
post {
always {
// 清理构建产物
cleanWs()
}
success {
script {
def slackMessage = """
*✅ 构建成功*
项目: ${PROJECT_NAME}
构建: ${BUILD_NUMBER}
分支: ${env.BRANCH_NAME}
提交: ${env.GIT_COMMIT}
镜像: ${DOCKER_IMAGE}:${env.IMAGE_TAG}
"""
// 发送成功通知(如果配置了 Slack 通知)
// slackSend(channel: '#builds', color: 'good', message: slackMessage)
}
}
failure {
script {
def slackMessage = """
*❌ 构建失败*
项目: ${PROJECT_NAME}
构建: ${BUILD_NUMBER}
分支: ${env.BRANCH_NAME}
提交: ${env.GIT_COMMIT}
错误: ${currentBuild.description ?: '构建失败'}
"""
// 发送失败通知(如果配置了 Slack 通知)
// slackSend(channel: '#builds', color: 'danger', message: slackMessage)
}
}
cleanup {
// 清理 Docker 镜像
script {
sh """
docker rmi -f ${DOCKER_IMAGE}:${env.IMAGE_TAG} || true
docker rmi -f ${DOCKER_IMAGE}:latest || true
"""
}
}
}
}
// 公共函数定义
2025-12-30 12:41:12 +08:00
def deployToEnvironmentWithCompose(String env) {
2025-12-26 21:02:15 +08:00
script {
// 根据环境设置部署参数
def containerName = "${PROJECT_NAME}-${env}"
2025-12-29 16:17:06 +08:00
def imageToDeploy = "${DOCKER_IMAGE}:latest"
2025-12-26 21:02:15 +08:00
2025-12-30 12:41:12 +08:00
// 读取原始 nginx 配置
def nginxConfContent = readFile('nginx.conf')
// 根据部署环境替换后端服务地址
switch(env) {
case 'dev':
2025-12-30 13:26:46 +08:00
// 开发环境使用特定的后端服务地址
2025-12-30 12:41:12 +08:00
nginxConfContent = nginxConfContent.replaceAll('host.docker.internal:33333', 'dev-backend-service:33333')
break
case 'staging':
2025-12-30 13:26:46 +08:00
// 预发布环境使用特定的后端服务地址
2025-12-30 12:41:12 +08:00
nginxConfContent = nginxConfContent.replaceAll('host.docker.internal:33333', 'staging-backend-service:33333')
break
case 'prod':
2025-12-30 13:26:46 +08:00
// 生产环境使用特定的后端服务地址
2025-12-30 12:41:12 +08:00
nginxConfContent = nginxConfContent.replaceAll('host.docker.internal:33333', 'prod-backend-service:33333')
break
default:
// 默认使用生产环境后端服务
nginxConfContent = nginxConfContent.replaceAll('host.docker.internal:33333', 'prod-backend-service:33333')
}
// 写入环境特定的 nginx 配置
writeFile file: "nginx-${env}.conf", text: nginxConfContent
2025-12-30 13:26:46 +08:00
// 创建 docker-compose.yml 文件,使用卷挂载覆盖容器内的 nginx 配置
def composeFileContent = """services:
frontend:
image: ${imageToDeploy}
container_name: ${containerName}
restart: unless-stopped
ports:
- "${getPortForEnvironment(env)}:80"
extra_hosts:
- "host.docker.internal:host-gateway" # 允许访问宿主机
networks:
- app-network
networks:
app-network:
driver: bridge
"""
2025-12-30 12:41:12 +08:00
// 写入 docker-compose.yml 文件
writeFile file: 'docker-compose.yml', text: composeFileContent
// 停止现有服务
2025-12-26 21:02:15 +08:00
sh """
2025-12-30 12:41:12 +08:00
docker-compose -f docker-compose.yml down || true
2025-12-26 21:02:15 +08:00
"""
2025-12-30 12:41:12 +08:00
// 启动新服务
2025-12-26 21:02:15 +08:00
sh """
2025-12-30 12:41:12 +08:00
docker-compose -f docker-compose.yml up -d
2025-12-26 21:02:15 +08:00
"""
// 验证部署
sh """
2025-12-30 12:41:12 +08:00
echo "等待服务启动..."
2025-12-26 21:02:15 +08:00
sleep 10
docker ps | grep ${containerName}
docker logs ${containerName}
"""
}
}
// 获取环境对应端口的辅助函数
def getPortForEnvironment(String env) {
switch(env) {
case 'dev':
return '3001'
case 'staging':
return '3002'
case 'prod':
2025-12-26 21:18:32 +08:00
return '80'
2025-12-26 21:02:15 +08:00
default:
2025-12-26 21:18:32 +08:00
return '80'
2025-12-26 21:02:15 +08:00
}
}
2025-12-30 11:47:21 +08:00
// 生成docker-compose文件内容的函数
2025-12-30 12:41:12 +08:00
def getComposeFileContent() {
2025-12-30 11:47:21 +08:00
def imageToDeploy = "${DOCKER_IMAGE}:latest"
def containerName = "${PROJECT_NAME}-${env}"
return """
version: '3.8'
services:
timeline-story-service:
image: ${imageToDeploy}
container_name: ${containerName}
ports:
- "3000:80"
extra_hosts:
- "host.docker.internal:host-gateway"
"""
}