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" """ }