refactor: CI/CD pipeline with stages + feishu card notification (portal)

This commit is contained in:
xiangning 2026-07-03 17:08:59 +08:00
parent 26e48531e0
commit 7f6cfad631

89
Jenkinsfile vendored
View File

@ -1,39 +1,67 @@
// BuildFlow portal CI/CD 企业级流水线
// Stages: ①拉取代码 → ②编译打包 → 推送Harbor → ③部署
// 构建成功/失败推送飞书交互卡片(绿/红)
// 飞书通知闭包(内联): 使用 Groovy JsonOutput 构造 JSON, 不依赖插件, 最稳妥
def feishuNotify = { String result, String failedStageName = '' ->
def dur = currentBuild.durationString.replace(' and counting', '')
def commit = sh(script: 'git log -1 --pretty=format:"%s (%an)"', returnStdout: true).trim()
def isOk = (result == 'SUCCESS')
def emoji = isOk ? '✅' : '❌'
def color = isOk ? 'green' : 'red'
def title = "${emoji} CI/CD ${isOk ? '成功' : '失败'} · ${env.JOB_NAME} #${env.BUILD_NUMBER}"
def fields = [
[is_short: true, text: [tag: 'lark_md', content: "**项目**\n${env.JOB_NAME}"]],
[is_short: true, text: [tag: 'lark_md', content: "**耗时**\n${dur}"]],
[is_short: true, text: [tag: 'lark_md', content: "**提交**\n${commit.take(50)}"]],
[is_short: true, text: [tag: 'lark_md', content: "**状态**\n${isOk ? '成功' : '失败'}"]]
]
def elements = [[tag: 'div', fields: fields], [tag: 'hr']]
if (isOk) {
elements.add([tag: 'div', text: [tag: 'lark_md', content: "📦 镜像地址\n`${env.IMAGE}:${env.TAG}`"]])
} else if (failedStageName) {
elements.add([tag: 'div', text: [tag: 'lark_md', content: "⚠️ 失败阶段\n${failedStageName}"]])
}
elements.add([tag: 'action', actions: [[tag: 'button', text: [tag: 'plain_text', content: '查看构建'], url: env.BUILD_URL ?: 'http://115.191.51.246:8080', type: isOk ? 'primary' : 'danger']]])
def card = [msg_type: 'interactive', card: [header: [title: [tag: 'plain_text', content: title], template: color], elements: elements]]
def jsonStr = groovy.json.JsonOutput.toJson(card)
writeFile file: '/tmp/feishu_msg.json', text: jsonStr
sh "curl -sS -X POST https://open.feishu.cn/open-apis/bot/v2/hook/d215e8ed-edea-4f07-a77c-2a0e5b52d9b9 -H 'Content-Type: application/json' -d @/tmp/feishu_msg.json || true"
sh "rm -f /tmp/feishu_msg.json"
}
pipeline { pipeline {
agent any agent any
environment { environment {
HARBOR = '43.134.235.58:58080' HARBOR = '43.134.235.58:58080'
IMAGE = "${HARBOR}/buildflow/buildflow-portal" IMAGE = "${HARBOR}/buildflow/buildflow-portal"
TAG = "${env.BUILD_NUMBER}" TAG = "${env.BUILD_NUMBER}"
} }
options { options {
timeout(time: 30, unit: 'MINUTES') timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '10')) buildDiscarder(logRotator(numToKeepStr: '10'))
timestamps()
} }
stages { stages {
stage('Checkout') { stage('① 拉取代码') {
steps { steps {
checkout scm checkout scm
sh 'git log --oneline -5' sh 'git log --oneline -3'
} }
} }
stage('② 编译打包') {
stage('Build Image') {
steps { steps {
sh """ sh """
docker build -t ${IMAGE}:${TAG} . docker build -t ${IMAGE}:${TAG} -t ${IMAGE}:latest .
docker tag ${IMAGE}:${TAG} ${IMAGE}:latest docker images | grep ${IMAGE} | head
""" """
} }
} }
stage('推送 Harbor') {
stage('Push Harbor') {
steps { steps {
withCredentials([usernamePassword(credentialsId: 'harbor-credentials', usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASS')]) { withCredentials([usernamePassword(credentialsId: 'harbor-credentials', usernameVariable: 'HUSER', passwordVariable: 'HPASS')]) {
sh """ sh """
echo "\$HARBOR_PASS" | docker login -u "\$HARBOR_USER" --password-stdin ${HARBOR} echo $HPASS | docker login ${HARBOR} -u $HUSER --password-stdin
docker push ${IMAGE}:${TAG} docker push ${IMAGE}:${TAG}
docker push ${IMAGE}:latest docker push ${IMAGE}:latest
docker logout ${HARBOR} docker logout ${HARBOR}
@ -41,19 +69,42 @@ pipeline {
} }
} }
} }
stage('③ 部署') {
stage('Deploy') {
steps { steps {
echo 'Deploy 待 Agent 就绪后启用' script {
try {
node('deploy-agent') {
sh """
docker pull ${IMAGE}:latest
docker rm -f buildflow-portal 2>/dev/null || true
docker run -d --name buildflow-portal --restart unless-stopped -p 5174:5173 ${IMAGE}:latest
"""
}
echo "部署完成"
} catch (e) {
echo "⚠️ 部署节点(deploy-agent)暂不可用,跳过自动部署。镜像已推送 Harbor,可手动拉取部署。"
}
} }
} }
} }
}
post { post {
success { echo '=== build-flow.portal CI/CD SUCCESS ===' } success {
failure { echo '=== build-flow.portal CI/CD FAILED ===' } script { feishuNotify('SUCCESS') }
}
failure {
script {
def stageName = '未知'
try {
def failedNodes = currentBuild.getRawBuild().getAction(org.jenkinsci.plugins.workflow.job.views.FlowGraphAction.class)?.getHeads()
if (failedNodes) { stageName = failedNodes.get(0).getDisplayNodeName() }
} catch (ignored) {}
feishuNotify('FAILURE', stageName)
}
}
always { always {
sh "docker rmi ${IMAGE}:${TAG} ${IMAGE}:latest || true" sh "docker rmi ${IMAGE}:${TAG} ${IMAGE}:latest 2>/dev/null || true"
deleteDir() deleteDir()
} }
} }