Files
jianghao d6b8048a01
All checks were successful
test/timeline-frontend/pipeline/head This commit looks good
Jenkins file edit
2025-12-30 13:59:29 +08:00

288 lines
10 KiB
Groovy
Raw Permalink 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
environment {
// 环境变量定义
PROJECT_NAME = 'timeline-frontend'
DOCKER_REGISTRY = 'timeline-registry:5000'
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 {
if (params.GIT_COMMIT) {
checkout scm
sh "git reset --hard ${params.GIT_COMMIT}"
} else {
checkout scm
}
}
echo "当前构建的 Git Commit: ${env.GIT_COMMIT}"
}
}
stage('Build timeline-frontend dist') {
steps {
script {
def workspace = sh(script: "pwd", returnStdout: true).trim()
echo "当前工作空间路径: ${workspace}"
// 确保路径正确
sh "ls -la ${workspace}"
// 修复权限问题
sh "chmod -R 755 ${workspace}"
sh "chown -R jenkins:jenkins ${workspace}"
// 使用 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')
}
// 安装 pnpm
sh 'npm install -g pnpm'
// 检查 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"
}
// 构建项目
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'
}
} else {
error('package.json not found')
}
}
}
}
}
}
stage('Build Docker Image') {
steps {
script {
def imageTag = "${BUILD_NUMBER}-${env.GIT_COMMIT.take(7)}"
env.IMAGE_TAG = imageTag
sh """
docker build -t ${DOCKER_IMAGE}:${imageTag} .
docker tag ${DOCKER_IMAGE}:${imageTag} ${DOCKER_IMAGE}:latest
"""
}
}
}
stage('Push Docker Image') {
steps {
script {
sh "docker push ${DOCKER_IMAGE}:latest"
}
}
}
stage('Deploy to Environment') {
steps {
script {
deployToEnvironmentWithCompose('dev')
}
}
}
}
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
"""
}
}
}
}
// 公共函数定义
def deployToEnvironmentWithCompose(String env) {
script {
// 根据环境设置部署参数,确保容器名称合法
def cleanEnv = env.replaceAll('[^a-zA-Z0-9]', '-')
def containerName = "${PROJECT_NAME}-${cleanEnv}".replaceAll('[^a-zA-Z0-9_.-]', '')
def imageToDeploy = "${DOCKER_IMAGE}:latest"
// 创建 docker-compose.yml 文件,使用 host 网络模式以便访问宿主机上的后端服务
def composeFileContent = """services:
frontend:
image: ${imageToDeploy}
container_name: ${containerName}
restart: unless-stopped
ports:
- "${getPortForEnvironment(env)}:80"
network_mode: "host"
"""
// 写入 docker-compose.yml 文件
writeFile file: 'docker-compose.yml', text: composeFileContent
// 强制停止并删除现有容器无论是否由compose创建
sh """
docker stop ${containerName} || true
docker rm ${containerName} || true
"""
// 启动新服务
sh """
docker compose -f docker-compose.yml up -d
"""
// 验证部署
sh """
echo "等待服务启动..."
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':
return '80'
default:
return '80'
}
}
// 生成docker-compose文件内容的函数
def getComposeFileContent() {
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"
"""
}