2026-09-18 18:50:49 +08:00
|
|
|
|
def sendProductionWeComNotification(String title, String status, String color, String detail) {
|
2026-09-18 18:02:31 +08:00
|
|
|
|
withCredentials([string(credentialsId: 'wecom-prod-webhook', variable: 'WECOM_WEBHOOK')]) {
|
2026-09-18 18:50:49 +08:00
|
|
|
|
String sourceVersion = env.SOURCE_TAG ?: env.GIT_COMMIT ?: '未解析'
|
|
|
|
|
|
String duration = currentBuild.durationString?.replace(' and counting', '') ?: '—'
|
|
|
|
|
|
String content = """## ${title}
|
|
|
|
|
|
> 应用:客户工作台
|
|
|
|
|
|
> Job:${env.JOB_NAME}
|
2026-09-18 18:02:31 +08:00
|
|
|
|
> 构建:[#${env.BUILD_NUMBER}](${env.BUILD_URL})
|
|
|
|
|
|
> 提交:${sourceVersion}
|
|
|
|
|
|
> 状态:<font color="${color}">${status}</font>
|
2026-09-18 18:50:49 +08:00
|
|
|
|
${detail}
|
|
|
|
|
|
> 耗时:${duration}
|
2026-09-18 18:02:31 +08:00
|
|
|
|
> 时间:${new Date().format('yyyy-MM-dd HH:mm:ss', TimeZone.getTimeZone('Asia/Shanghai'))}
|
|
|
|
|
|
"""
|
|
|
|
|
|
String payloadFile = 'wecom-production-notification.json'
|
|
|
|
|
|
writeFile(
|
|
|
|
|
|
file: payloadFile,
|
|
|
|
|
|
text: groovy.json.JsonOutput.toJson([
|
|
|
|
|
|
msgtype: 'markdown',
|
|
|
|
|
|
markdown: [content: content]
|
|
|
|
|
|
]),
|
|
|
|
|
|
encoding: 'UTF-8'
|
|
|
|
|
|
)
|
|
|
|
|
|
sh '''
|
|
|
|
|
|
set -eu
|
|
|
|
|
|
payload_file='wecom-production-notification.json'
|
|
|
|
|
|
trap 'rm -f "$payload_file"' EXIT HUP INT TERM
|
|
|
|
|
|
response="$(curl --fail --silent --show-error \
|
|
|
|
|
|
-H 'Content-Type: application/json' \
|
|
|
|
|
|
--data-binary @"$payload_file" \
|
|
|
|
|
|
"$WECOM_WEBHOOK")"
|
|
|
|
|
|
printf '%s\n' "$response"
|
|
|
|
|
|
printf '%s' "$response" | grep -Eq '"errcode"[[:space:]]*:[[:space:]]*0([[:space:]]*[,}])'
|
|
|
|
|
|
'''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 21:52:18 +08:00
|
|
|
|
pipeline {
|
2026-08-24 14:40:50 +08:00
|
|
|
|
agent {
|
2026-09-17 11:49:01 +08:00
|
|
|
|
label "${env.JOB_BASE_NAME?.endsWith('-prod') ? 'java-builder' : (env.JOB_BASE_NAME?.endsWith('-dev') ? 'java-dev' : 'java-test')}"
|
2026-08-24 14:40:50 +08:00
|
|
|
|
}
|
2026-07-30 21:52:18 +08:00
|
|
|
|
|
2026-08-14 17:45:43 +08:00
|
|
|
|
environment {
|
2026-08-24 14:40:50 +08:00
|
|
|
|
DEVELOPMENT_COMPOSE_FILE = 'docker-compose.development.yml'
|
2026-08-24 14:44:38 +08:00
|
|
|
|
DEVELOPMENT_COMPOSE_PROJECT = 'client-web'
|
2026-08-24 14:40:50 +08:00
|
|
|
|
REGISTRY_HOST = '172.31.198.7:10082'
|
|
|
|
|
|
REGISTRY_CREDENTIALS = 'gitea-registry-token'
|
|
|
|
|
|
NODE_BASE_IMAGE = '172.31.198.7:10082/java-project/node:22-alpine'
|
|
|
|
|
|
NGINX_BASE_IMAGE = '172.31.198.7:10082/java-project/nginx:1.27-alpine'
|
2026-09-17 11:49:01 +08:00
|
|
|
|
PRODUCTION_IMAGE_REPOSITORY = 'git.buggoglobal.com/html/client-web'
|
2026-08-14 17:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 21:52:18 +08:00
|
|
|
|
options {
|
|
|
|
|
|
timestamps()
|
|
|
|
|
|
disableConcurrentBuilds()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stages {
|
|
|
|
|
|
stage('确认提交版本') {
|
|
|
|
|
|
steps {
|
2026-08-14 17:45:43 +08:00
|
|
|
|
script {
|
|
|
|
|
|
env.IS_DEVELOPMENT = env.JOB_BASE_NAME?.endsWith('-dev') ? 'true' : 'false'
|
2026-09-17 11:49:01 +08:00
|
|
|
|
env.IS_PRODUCTION = env.JOB_BASE_NAME?.endsWith('-prod') ? 'true' : 'false'
|
|
|
|
|
|
if (env.IS_PRODUCTION == 'true') {
|
|
|
|
|
|
sh '''
|
|
|
|
|
|
set -eu
|
|
|
|
|
|
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
|
|
|
|
|
test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" || {
|
|
|
|
|
|
echo "生产部署只允许 origin/main 当前提交;请先合并代码到 main" >&2
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
}
|
|
|
|
|
|
'''
|
|
|
|
|
|
}
|
2026-08-14 17:45:43 +08:00
|
|
|
|
env.SOURCE_TAG = sh(script: 'git rev-parse --short=12 HEAD', returnStdout: true).trim()
|
|
|
|
|
|
}
|
2026-07-30 21:52:18 +08:00
|
|
|
|
sh 'git log -1 --oneline'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stage('构建并部署测试前端') {
|
2026-09-17 11:49:01 +08:00
|
|
|
|
when { expression { env.IS_DEVELOPMENT != 'true' && env.IS_PRODUCTION != 'true' } }
|
2026-07-30 21:52:18 +08:00
|
|
|
|
steps {
|
2026-08-24 14:40:50 +08:00
|
|
|
|
withCredentials([usernamePassword(credentialsId: env.REGISTRY_CREDENTIALS, usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD')]) {
|
|
|
|
|
|
sh '''
|
|
|
|
|
|
set -eu
|
|
|
|
|
|
export DOCKER_CONFIG="${WORKSPACE}@tmp/docker-auth"
|
|
|
|
|
|
rm -rf "$DOCKER_CONFIG"
|
|
|
|
|
|
mkdir -p "$DOCKER_CONFIG"
|
|
|
|
|
|
cleanup_registry() {
|
|
|
|
|
|
docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true
|
|
|
|
|
|
rm -rf "$DOCKER_CONFIG"
|
|
|
|
|
|
}
|
|
|
|
|
|
trap cleanup_registry EXIT HUP INT TERM
|
|
|
|
|
|
printf %s "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "$REGISTRY_HOST"
|
|
|
|
|
|
docker compose -f docker-compose.test.yml build --pull=false
|
|
|
|
|
|
docker compose -f docker-compose.test.yml up -d --remove-orphans
|
|
|
|
|
|
'''
|
|
|
|
|
|
}
|
2026-07-30 21:52:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-17 11:49:01 +08:00
|
|
|
|
stage('构建并发布生产镜像') {
|
|
|
|
|
|
when { expression { env.IS_PRODUCTION == 'true' } }
|
|
|
|
|
|
steps {
|
|
|
|
|
|
withCredentials([usernamePassword(credentialsId: env.REGISTRY_CREDENTIALS, usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD')]) {
|
|
|
|
|
|
sh '''
|
|
|
|
|
|
set -eu
|
|
|
|
|
|
export DOCKER_CONFIG="${WORKSPACE}@tmp/docker-auth"
|
|
|
|
|
|
rm -rf "$DOCKER_CONFIG"
|
|
|
|
|
|
mkdir -p "$DOCKER_CONFIG"
|
|
|
|
|
|
cleanup_registry() {
|
|
|
|
|
|
docker logout git.buggoglobal.com >/dev/null 2>&1 || true
|
|
|
|
|
|
rm -rf "$DOCKER_CONFIG"
|
|
|
|
|
|
}
|
|
|
|
|
|
trap cleanup_registry EXIT HUP INT TERM
|
|
|
|
|
|
printf %s "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin git.buggoglobal.com
|
|
|
|
|
|
image="$PRODUCTION_IMAGE_REPOSITORY:$SOURCE_TAG"
|
|
|
|
|
|
docker build \
|
|
|
|
|
|
--build-arg NODE_IMAGE="$NODE_BASE_IMAGE" \
|
|
|
|
|
|
--build-arg NGINX_IMAGE="$NGINX_BASE_IMAGE" \
|
|
|
|
|
|
--build-arg VITE_ADMIN_WEB_ORIGIN=https://admin.buggoglobal.com \
|
|
|
|
|
|
-t "$image" .
|
|
|
|
|
|
docker push "$image"
|
|
|
|
|
|
'''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stage('部署生产前端') {
|
|
|
|
|
|
when { expression { env.IS_PRODUCTION == 'true' } }
|
|
|
|
|
|
steps {
|
|
|
|
|
|
lock(resource: 'firstleg-production-client-web-deploy', inversePrecedence: true) {
|
|
|
|
|
|
withCredentials([
|
|
|
|
|
|
usernamePassword(credentialsId: 'gitea-production-pull-token', usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD'),
|
|
|
|
|
|
sshUserPrivateKey(credentialsId: 'firstleg-production-ssh', keyFileVariable: 'TARGET_SSH_KEY', usernameVariable: 'TARGET_SSH_USER'),
|
|
|
|
|
|
string(credentialsId: 'firstleg-production-host', variable: 'TARGET_SSH_HOST'),
|
|
|
|
|
|
file(credentialsId: 'firstleg-production-known-hosts', variable: 'TARGET_SSH_KNOWN_HOSTS')
|
|
|
|
|
|
]) {
|
|
|
|
|
|
withEnv([
|
|
|
|
|
|
'TARGET_DEPLOY_DIR=/data/firstleg/deploy/frontend',
|
|
|
|
|
|
'TARGET_SSH_TARGET=' + env.TARGET_SSH_USER + '@' + env.TARGET_SSH_HOST
|
|
|
|
|
|
]) {
|
|
|
|
|
|
sh "sh scripts/deploy-production.sh client-web '${env.PRODUCTION_IMAGE_REPOSITORY}' '${env.SOURCE_TAG}' docker-compose.production.yml"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-14 17:45:43 +08:00
|
|
|
|
stage('构建并部署开发前端') {
|
|
|
|
|
|
when { expression { env.IS_DEVELOPMENT == 'true' } }
|
|
|
|
|
|
steps {
|
2026-08-24 14:40:50 +08:00
|
|
|
|
withCredentials([usernamePassword(credentialsId: env.REGISTRY_CREDENTIALS, usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASSWORD')]) {
|
|
|
|
|
|
sh '''
|
|
|
|
|
|
set -eu
|
|
|
|
|
|
export DOCKER_CONFIG="${WORKSPACE}@tmp/docker-auth"
|
|
|
|
|
|
rm -rf "$DOCKER_CONFIG"
|
|
|
|
|
|
mkdir -p "$DOCKER_CONFIG"
|
|
|
|
|
|
cleanup_registry() {
|
|
|
|
|
|
docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true
|
|
|
|
|
|
rm -rf "$DOCKER_CONFIG"
|
|
|
|
|
|
}
|
|
|
|
|
|
trap cleanup_registry EXIT HUP INT TERM
|
|
|
|
|
|
printf %s "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "$REGISTRY_HOST"
|
|
|
|
|
|
image="client-web-development:${SOURCE_TAG}"
|
2026-08-14 17:45:43 +08:00
|
|
|
|
|
2026-08-24 14:40:50 +08:00
|
|
|
|
docker build \
|
|
|
|
|
|
--build-arg NODE_IMAGE="$NODE_BASE_IMAGE" \
|
|
|
|
|
|
--build-arg NGINX_IMAGE="$NGINX_BASE_IMAGE" \
|
|
|
|
|
|
--build-arg VITE_ADMIN_WEB_ORIGIN=http://8.163.49.32:9001 \
|
|
|
|
|
|
-t "$image" .
|
|
|
|
|
|
export IMAGE_TAG="$SOURCE_TAG"
|
2026-08-24 14:44:38 +08:00
|
|
|
|
docker compose -p "$DEVELOPMENT_COMPOSE_PROJECT" -f "$DEVELOPMENT_COMPOSE_FILE" up -d --remove-orphans --pull never
|
|
|
|
|
|
docker compose -p "$DEVELOPMENT_COMPOSE_PROJECT" -f "$DEVELOPMENT_COMPOSE_FILE" ps
|
|
|
|
|
|
docker compose -p "$DEVELOPMENT_COMPOSE_PROJECT" -f "$DEVELOPMENT_COMPOSE_FILE" exec -T client-web wget -qO- http://127.0.0.1/healthz
|
2026-08-24 14:40:50 +08:00
|
|
|
|
'''
|
|
|
|
|
|
}
|
2026-08-14 17:45:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 21:52:18 +08:00
|
|
|
|
stage('部署检查') {
|
2026-09-17 11:49:01 +08:00
|
|
|
|
when { expression { env.IS_DEVELOPMENT != 'true' && env.IS_PRODUCTION != 'true' } }
|
2026-07-30 21:52:18 +08:00
|
|
|
|
steps {
|
|
|
|
|
|
sh '''
|
|
|
|
|
|
set -eu
|
|
|
|
|
|
docker compose -f docker-compose.test.yml ps
|
|
|
|
|
|
docker compose -f docker-compose.test.yml exec -T client-web wget -qO- http://127.0.0.1/healthz
|
|
|
|
|
|
'''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
post {
|
2026-09-18 18:02:31 +08:00
|
|
|
|
success {
|
|
|
|
|
|
script {
|
|
|
|
|
|
if (env.JOB_BASE_NAME?.endsWith('-prod')) {
|
|
|
|
|
|
try {
|
2026-09-18 18:50:49 +08:00
|
|
|
|
sendProductionWeComNotification(
|
|
|
|
|
|
'✅ 生产部署成功',
|
|
|
|
|
|
'成功',
|
|
|
|
|
|
'info',
|
|
|
|
|
|
'> 结果:Client 生产前端已完成发布和部署流程'
|
|
|
|
|
|
)
|
2026-09-18 18:02:31 +08:00
|
|
|
|
} catch (Exception error) {
|
|
|
|
|
|
echo "企业微信通知失败,不影响原构建结果:${error.message}"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-30 21:52:18 +08:00
|
|
|
|
failure {
|
2026-08-14 17:45:43 +08:00
|
|
|
|
sh '''
|
2026-09-17 11:49:01 +08:00
|
|
|
|
if [ "${IS_PRODUCTION:-false}" = "true" ]; then
|
|
|
|
|
|
echo "生产部署失败,请查看上方远程部署日志"
|
|
|
|
|
|
elif [ "${IS_DEVELOPMENT:-false}" = "true" ]; then
|
2026-08-24 14:40:50 +08:00
|
|
|
|
docker logs --tail=100 client-web-development || true
|
2026-08-14 17:45:43 +08:00
|
|
|
|
else
|
|
|
|
|
|
docker compose -f docker-compose.test.yml logs --tail=100 || true
|
|
|
|
|
|
fi
|
|
|
|
|
|
'''
|
2026-09-18 18:02:31 +08:00
|
|
|
|
script {
|
|
|
|
|
|
if (env.JOB_BASE_NAME?.endsWith('-prod')) {
|
|
|
|
|
|
try {
|
2026-09-18 18:50:49 +08:00
|
|
|
|
sendProductionWeComNotification(
|
|
|
|
|
|
'❌ 生产发布失败',
|
|
|
|
|
|
'失败',
|
|
|
|
|
|
'warning',
|
|
|
|
|
|
'> 说明:请打开 Jenkins 构建日志查看失败阶段'
|
|
|
|
|
|
)
|
2026-09-18 18:02:31 +08:00
|
|
|
|
} catch (Exception error) {
|
|
|
|
|
|
echo "企业微信通知失败,不影响原构建结果:${error.message}"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
aborted {
|
|
|
|
|
|
script {
|
|
|
|
|
|
if (env.JOB_BASE_NAME?.endsWith('-prod')) {
|
|
|
|
|
|
try {
|
2026-09-18 18:50:49 +08:00
|
|
|
|
sendProductionWeComNotification(
|
|
|
|
|
|
'⏹ 生产部署已取消',
|
|
|
|
|
|
'已取消',
|
|
|
|
|
|
'comment',
|
|
|
|
|
|
'> 说明:生产部署流程被人工取消或中止'
|
|
|
|
|
|
)
|
2026-09-18 18:02:31 +08:00
|
|
|
|
} catch (Exception error) {
|
|
|
|
|
|
echo "企业微信通知失败,不影响原构建结果:${error.message}"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-30 21:52:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|