From 7f6cfad63195c413dce0ae07a4c1c900a4c17870 Mon Sep 17 00:00:00 2001 From: xiangning Date: Fri, 3 Jul 2026 17:08:59 +0800 Subject: [PATCH] refactor: CI/CD pipeline with stages + feishu card notification (portal) --- Jenkinsfile | 91 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 71 insertions(+), 20 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 0b2d186..43b38dc 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -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 { agent any - environment { HARBOR = '43.134.235.58:58080' IMAGE = "${HARBOR}/buildflow/buildflow-portal" TAG = "${env.BUILD_NUMBER}" } - options { timeout(time: 30, unit: 'MINUTES') buildDiscarder(logRotator(numToKeepStr: '10')) + timestamps() } - stages { - stage('Checkout') { + stage('① 拉取代码') { steps { checkout scm - sh 'git log --oneline -5' + sh 'git log --oneline -3' } } - - stage('Build Image') { + stage('② 编译打包') { steps { sh """ - docker build -t ${IMAGE}:${TAG} . - docker tag ${IMAGE}:${TAG} ${IMAGE}:latest + docker build -t ${IMAGE}:${TAG} -t ${IMAGE}:latest . + docker images | grep ${IMAGE} | head """ } } - - stage('Push Harbor') { + stage('推送 Harbor') { steps { - withCredentials([usernamePassword(credentialsId: 'harbor-credentials', usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASS')]) { + withCredentials([usernamePassword(credentialsId: 'harbor-credentials', usernameVariable: 'HUSER', passwordVariable: 'HPASS')]) { 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}:latest docker logout ${HARBOR} @@ -41,19 +69,42 @@ pipeline { } } } - - stage('Deploy') { + stage('③ 部署') { 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 { - success { echo '=== build-flow.portal CI/CD SUCCESS ===' } - failure { echo '=== build-flow.portal CI/CD FAILED ===' } + success { + 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 { - sh "docker rmi ${IMAGE}:${TAG} ${IMAGE}:latest || true" + sh "docker rmi ${IMAGE}:${TAG} ${IMAGE}:latest 2>/dev/null || true" deleteDir() } }