Some checks failed
test/timeline-frontend/pipeline/head There was a failure building this commit
287 lines
10 KiB
Groovy
287 lines
10 KiB
Groovy
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
|
|
|
|
// 停止现有服务
|
|
sh """
|
|
docker compose -f docker-compose.yml down || 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"
|
|
"""
|
|
}
|