From 9a522f4284a79d19aec328c1096aae74622cca85 Mon Sep 17 00:00:00 2001 From: gitadmin Date: Sun, 21 Sep 2025 19:52:30 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AD=BE=E5=85=A5=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 43 + .jenkins/.env.yml | 3 + .jenkins/Jenkinsfile | 3 + LICENSE | 73 + README.md | 227 +++ document/#510.txt | 1628 +++++++++++++++++ document/#675.txt | 633 +++++++ document/Node基础镜像.zip | Bin 0 -> 5451 bytes resources/scripts/docker-cleanup.sh | 163 ++ resources/scripts/health-check.sh | 0 resources/scripts/jenkins-cleanup.sh | 243 +++ resources/scripts/setup-env.sh | 0 resources/templates/build_email.html | 475 +++++ src/com/common/DingTalkService.groovy | 738 ++++++++ src/com/common/EmailService.groovy | 305 +++ src/com/common/NotificationService.groovy | 565 ++++++ src/com/common/SmsService.groovy | 1000 ++++++++++ src/com/common/StageService.groovy | 317 ++++ src/com/constants/ConfigConstants.groovy | 50 + src/com/docker/DeploymentService.groovy | 180 ++ src/com/docker/ResolverService.groovy | 647 +++++++ src/com/docker/stages/BuildService.groovy | 286 +++ src/com/docker/stages/DeployService.groovy | 326 ++++ src/com/jdk/DeploymentService.groovy | 253 +++ src/com/jdk/ResolverService.groovy | 865 +++++++++ src/com/jdk/stages/BuildService.groovy | 216 +++ .../jdk/stages/ContainerBuildService.groovy | 153 ++ src/com/jdk/stages/DeployService.groovy | 581 ++++++ src/com/jdk/stages/HealthCheckService.groovy | 180 ++ src/com/jdk/stages/RollbackService.groovy | 216 +++ src/com/jdk/utils/PipelineUtils.groovy | 1389 ++++++++++++++ src/com/node/DeploymentService.groovy | 149 ++ src/com/node/ResolverService.groovy | 865 +++++++++ src/com/node/stages/BuildService.groovy | 353 ++++ src/com/node/stages/DeployService.groovy | 482 +++++ src/com/utils/CommonUtils.groovy | 26 + src/com/utils/DurationFormatter.groovy | 166 ++ src/com/utils/ServerParser.groovy | 481 +++++ src/com/utils/SshUtils.groovy | 196 ++ vars/pipelineDockerService.groovy | 265 +++ vars/pipelineGolangService.groovy | 0 vars/pipelineJDKService.groovy | 364 ++++ vars/pipelineK8SService.groovy | 0 vars/pipelineNexusService.groovy | 0 vars/pipelineNodeService.groovy | 354 ++++ vars/pipelinePythonService.groovy | 0 46 files changed, 15459 insertions(+) create mode 100644 .gitignore create mode 100644 .jenkins/.env.yml create mode 100644 .jenkins/Jenkinsfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 document/#510.txt create mode 100644 document/#675.txt create mode 100644 document/Node基础镜像.zip create mode 100644 resources/scripts/docker-cleanup.sh create mode 100644 resources/scripts/health-check.sh create mode 100644 resources/scripts/jenkins-cleanup.sh create mode 100644 resources/scripts/setup-env.sh create mode 100644 resources/templates/build_email.html create mode 100644 src/com/common/DingTalkService.groovy create mode 100644 src/com/common/EmailService.groovy create mode 100644 src/com/common/NotificationService.groovy create mode 100644 src/com/common/SmsService.groovy create mode 100644 src/com/common/StageService.groovy create mode 100644 src/com/constants/ConfigConstants.groovy create mode 100644 src/com/docker/DeploymentService.groovy create mode 100644 src/com/docker/ResolverService.groovy create mode 100644 src/com/docker/stages/BuildService.groovy create mode 100644 src/com/docker/stages/DeployService.groovy create mode 100644 src/com/jdk/DeploymentService.groovy create mode 100644 src/com/jdk/ResolverService.groovy create mode 100644 src/com/jdk/stages/BuildService.groovy create mode 100644 src/com/jdk/stages/ContainerBuildService.groovy create mode 100644 src/com/jdk/stages/DeployService.groovy create mode 100644 src/com/jdk/stages/HealthCheckService.groovy create mode 100644 src/com/jdk/stages/RollbackService.groovy create mode 100644 src/com/jdk/utils/PipelineUtils.groovy create mode 100644 src/com/node/DeploymentService.groovy create mode 100644 src/com/node/ResolverService.groovy create mode 100644 src/com/node/stages/BuildService.groovy create mode 100644 src/com/node/stages/DeployService.groovy create mode 100644 src/com/utils/CommonUtils.groovy create mode 100644 src/com/utils/DurationFormatter.groovy create mode 100644 src/com/utils/ServerParser.groovy create mode 100644 src/com/utils/SshUtils.groovy create mode 100644 vars/pipelineDockerService.groovy create mode 100644 vars/pipelineGolangService.groovy create mode 100644 vars/pipelineJDKService.groovy create mode 100644 vars/pipelineK8SService.groovy create mode 100644 vars/pipelineNexusService.groovy create mode 100644 vars/pipelineNodeService.groovy create mode 100644 vars/pipelinePythonService.groovy diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b245fd1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +/logs +.svn +.idea +.git \ No newline at end of file diff --git a/.jenkins/.env.yml b/.jenkins/.env.yml new file mode 100644 index 0000000..957352d --- /dev/null +++ b/.jenkins/.env.yml @@ -0,0 +1,3 @@ +masterProfiles: ['','uat','prod'] +Profiles: ['','dev'] +jobConfigFile: "xiaomayiConfig.yml" diff --git a/.jenkins/Jenkinsfile b/.jenkins/Jenkinsfile new file mode 100644 index 0000000..8c2fd41 --- /dev/null +++ b/.jenkins/Jenkinsfile @@ -0,0 +1,3 @@ +@Library('share-library@master')_ +orgName = 'xiaomayi' +pipelineJDKService(orgName) \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..62b5a4d --- /dev/null +++ b/LICENSE @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright 2025 cicd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..762dcb1 --- /dev/null +++ b/README.md @@ -0,0 +1,227 @@ +## Jenkins自动化DevOps共享库 + +Docker常规命令: + +``` +# 查看docker磁盘使用情况 +docker system df + +# 查看更详细的信息 +docker system df -v + +# 用于清理磁盘,删除关闭的容器、无用的数据卷和网络,以及dangling镜像(即无tag的镜像) +docker system prune +``` + +1、首先在 Jenkins 中正确配置 Shared Library +步骤 1:在 Jenkins 中配置 Global Shared Library + + 进入 Jenkins → Manage Jenkins → Configure System + + 找到 Global Pipeline Libraries 部分 + + 点击 Add 添加新的库配置: + +```groovy +Name: share-library # 库的名称(必须与@Library中的名称匹配) +Default version: master # 默认分支,可以是 master、yunheng 等 +Retrieval method: Modern SCM +Source Code Management: Git +Project Repository: https://github.com/your-username/share-library.git +Credentials: 添加您的 Git 凭据 ID +``` + +步骤 2:检查库的配置详情 + +确保配置正确: + + Name 必须与 @Library('share-library')_ 中的名称完全匹配 + + 如果使用 @yunheng 分支,需要确保该分支在 Git 仓库中存在 + + 凭据必须有访问 Git 仓库的权限 + + +Jenkins安装插件: +1、HTTP Request + +钉钉客服: + +``` +小钉钉客服1号: +https://oapi.dingtalk.com/robot/send?access_token=976ae1a272bc79a1c3bf2eba97dae488eec80b57387d17e1c8352627b963bb5c +SEC0e2d1bed404cc0615885321276766598ce2efefcd0be23bfffe96c6b8f4d0ed6 + +小钉钉客服2号: +https://oapi.dingtalk.com/robot/send?access_token=c7fd2348813bacd0797f141bc579e7f1db31d50e00ebc8af658a769534ac755c +SEC26137d6d5ff8f9ecda917e787ec31c2b3804e7820d93363a1a9f0770f1720f2d +``` + +钉钉消息案例: + +``` +// 基本使用 +def dingtalk = new DingTalkNotifier(this) +dingtalk.sendBuildSuccessMessage("my-project", "main", 120000) + +// 完整参数使用 +def dingtalk = new DingTalkNotifier( + this, // script + false, // atAll + [], // atMobiles + 'markdown', // msgType + 'my-dingtalk-robot', // credentialsId + 3, // retryCount + 2000 // retryInterval +) + +// 运行时更新配置 +dingtalk.updateMsgType('text') +dingtalk.updateAtSettings(true, ['13800138000']) +``` + +新版本使用案例: + +``` +try{ + // 初始化钉钉通知器 + def dingTalk = new com.docker.DingTalkNotifier( + this, + false, + ['13800138000'], + [], + 'markdown', + ) + echo "钉钉新服务:${dingTalk}" + + // 发送构建开始消息 + dingTalk.sendBuildStartMessage( + env.JOB_NAME, + env.GIT_BRANCH ?: '', + '修复bug', + 'developer', + [环境: '测试环境', 版本: 'v1.0.0'] + ) + + // ActionCard 消息示例 + dingTalk.sendMultiActionCard( + "构建完成通知", + "项目构建已完成,请查看详情", + [ + [title: "查看构建详情", actionURL: "${env.BUILD_URL}"], + [title: "下载制品", actionURL: "https://www.dingtalk.com/"], + [title: "查看测试报告", actionURL: "https://www.dingtalk.com/"] + ] + ) + + // FeedCard 消息示例 + dingTalk.sendFeedCardMessage([ + [title: "生产环境部署成功", messageURL: "https://www.dingtalk.com/", picURL: "https://gw.alicdn.com/imgextra/i4/O1CN017tMvJS1jaMdFd8RSv_!!6000000004564-2-tps-640-368.png"], + [title: "测试环境构建完成", messageURL: "https://www.dingtalk.com/", picURL: "https://gw.alicdn.com/imgextra/i4/O1CN017tMvJS1jaMdFd8RSv_!!6000000004564-2-tps-640-368.png"] + ]) +}catch(Exception e) { + echo "⚠️ 钉钉消息发送失败: ${e.message}" +} +``` + +```js +stage('发送各种类型消息') { + steps { + script { + // 1. 文本消息 + dingtalk.sendTextMessage( + "简单文本消息内容", + false, + ['13800138000'], + [] + ) + + // 2. Markdown消息 + dingtalk.sendMarkdownMessage( + "Markdown标题", + "### 这是Markdown内容\n- 列表项1\n- 列表项2\n**加粗文本**", + false, + ['13800138000'], + [] + ) + + // 3. 链接消息 + dingtalk.sendLinkMessage( + "链接标题", + "链接描述内容", + "https://www.example.com", + "https://img.example.com/image.jpg" + ) + + // 4. 独立ActionCard + dingtalk.sendSingleActionCard( + "卡片标题", + "卡片内容描述", + "查看详情", + "https://www.example.com", + '0' // 按钮方向:0-竖直 + ) + + // 5. 多按钮ActionCard + def buttons = [ + [title: "按钮1", actionURL: "https://example.com/btn1"], + [title: "按钮2", actionURL: "https://example.com/btn2"], + [title: "按钮3", actionURL: "https://example.com/btn3"] + ] + dingtalk.sendMultiActionCard( + "多按钮卡片", + "请选择操作:", + buttons, + '1' // 按钮方向:1-横向 + ) + + // 6. FeedCard消息 + def links = [ + [ + title: "新闻标题1", + messageURL: "https://example.com/news1", + picURL: "https://img.example.com/news1.jpg" + ], + [ + title: "新闻标题2", + messageURL: "https://example.com/news2", + picURL: "https://img.example.com/news2.jpg" + ] + ] + dingtalk.sendFeedCardMessage(links) + } + } +} +``` + +消息通知发布案例: +``` +// 在Pipeline中初始化 +def notificationService = new NotificationService(this, params, stageService) + +// 构建成功时发送通知 +notificationService.sendBuildSuccess( + projectName: "my-project", + branch: "master", + duration: currentBuild.duration, + extraInfo: [version: "1.0.0", commit: "abc123"] +) + +// 构建失败时发送通知 +notificationService.sendBuildFailure( + projectName: "my-project", + branch: "feature/branch", + duration: currentBuild.duration, + errorMsg: "单元测试失败", + extraInfo: [stage: "test", errorDetails: "具体错误信息"] +) + +// 部署通知 +notificationService.sendDeploymentNotification( + projectName: "my-project", + environment: "prod", + version: "1.0.0", + status: "success", + deployUrl: "http://deploy.example.com" +) +``` \ No newline at end of file diff --git a/document/#510.txt b/document/#510.txt new file mode 100644 index 0000000..93cd3d9 --- /dev/null +++ b/document/#510.txt @@ -0,0 +1,1628 @@ +Started by user 陈红丽 + +[钉钉插件]全局配置信息,{"proxyConfig":{"type":"HTTP","host":"","port":0},"verbose":true,"noticeOccasions":["SUCCESS","START","ABORTED","FAILURE","UNSTABLE"],"robotConfigs":[{"id":"bdd673c1-c779-4088-82be-b16624c53430","name":"【Jenkins构建消息通知】","webhook":{"value":"https://oapi.dingtalk.com/robot/send?access_token\u003d36238ae800104a95fc3e6e02ff12ccf471a55a47fcb9ad9501a7ddfb82d7a293","iv":[64,-55,-21,38,-93,-43,2,-46,75,-95,-32,-16,77,43,0,-117]},"securityPolicyConfigs":[{"type":"KEY","desc":"关键字","value":{"value":"发布","iv":[25,33,-25,-65,95,33,22,-43,47,93,35,43,-77,-77,-60,-63]}},{"type":"SECRET","desc":"加密","value":{"value":"","iv":[-19,96,-91,94,19,65,-82,-94,42,25,6,-33,-90,120,77,40]}}]}]} + +[钉钉插件]当前任务未配置机器人,已跳过 +Looking up repository yunheng/yunheng-adminui +Querying the current revision of branch test... +Current revision of branch test is 087e95e753ce2b1f5627da639fbaec56c3d93d23 +Obtained .jenkins/Jenkinsfile from 087e95e753ce2b1f5627da639fbaec56c3d93d23 +Loading library share-library@yunheng +Attempting to resolve yunheng from remote references... + > /usr/bin/git --version # timeout=10 + > git --version # 'git version 2.39.2' +using GIT_ASKPASS to set credentials gitee access token + > /usr/bin/git ls-remote -h -- https://gitee.com/lamefox/jenkins-share-library.git # timeout=10 +Found match: refs/heads/yunheng revision ed914f8c9a64514a1caf3747b13a7cd0ddc38222 +The recommended git tool is: NONE +using credential gitee-lamefox + > /usr/bin/git rev-parse --resolve-git-dir /var/jenkins_home/workspace/web-v3-yunheng-adminui_test@libs/6e7b93324666b4dab5c7ec108dfb8a0401b885f9aef36f5f50cddf9142b4168d/.git # timeout=10 +Fetching changes from the remote Git repository + > /usr/bin/git config remote.origin.url https://gitee.com/lamefox/jenkins-share-library.git # timeout=10 +Using shallow fetch with depth 1 +Fetching upstream changes from https://gitee.com/lamefox/jenkins-share-library.git + > /usr/bin/git --version # timeout=10 + > git --version # 'git version 2.39.2' +using GIT_ASKPASS to set credentials gitee access token + > /usr/bin/git fetch --tags --force --progress --depth=1 -- https://gitee.com/lamefox/jenkins-share-library.git +refs/heads/*:refs/remotes/origin/* # timeout=10 +Checking out Revision ed914f8c9a64514a1caf3747b13a7cd0ddc38222 (yunheng) + > /usr/bin/git config core.sparsecheckout # timeout=10 + > /usr/bin/git checkout -f ed914f8c9a64514a1caf3747b13a7cd0ddc38222 # timeout=10 +Commit message: "add println" + > /usr/bin/git rev-list --no-walk ed914f8c9a64514a1caf3747b13a7cd0ddc38222 # timeout=10 +[Gitea] Notifying branch build status: PENDING Build started... +[Gitea] Notified +[Pipeline] Start of Pipeline +[Pipeline] node +Running on Jenkins in /var/jenkins_home/workspace/web-v3-yunheng-adminui_test +[Pipeline] { +[Pipeline] stage +[Pipeline] { (Declarative: Checkout SCM) +[Pipeline] checkout +The recommended git tool is: NONE +using credential 03a74b6e-92de-429b-8d63-22fe15da43c2 + > /usr/bin/git rev-parse --resolve-git-dir /var/jenkins_home/workspace/web-v3-yunheng-adminui_test/.git # timeout=10 +Fetching changes from the remote Git repository + > /usr/bin/git config remote.origin.url https://git.yunhengwang.net/yunheng/yunheng-adminui.git # timeout=10 +Fetching without tags +Fetching upstream changes from https://git.yunhengwang.net/yunheng/yunheng-adminui.git + > /usr/bin/git --version # timeout=10 + > git --version # 'git version 2.39.2' +using GIT_ASKPASS to set credentials git.yunhengwang.com + > /usr/bin/git fetch --no-tags --force --progress -- https://git.yunhengwang.net/yunheng/yunheng-adminui.git +refs/heads/test:refs/remotes/origin/test # timeout=10 +Checking out Revision 087e95e753ce2b1f5627da639fbaec56c3d93d23 (test) + > /usr/bin/git config core.sparsecheckout # timeout=10 + > /usr/bin/git checkout -f 087e95e753ce2b1f5627da639fbaec56c3d93d23 # timeout=10 +Commit message: "Merge remote-tracking branch 'remotes/origin/3.5.4' into test" + > /usr/bin/git rev-list --no-walk a99a7348dbf592391704af708081086a9eb38181 # timeout=10 +[Gitea] Notifying branch build status: PENDING Build started... +[Gitea] Notified +[Pipeline] } +[Pipeline] // stage +[Pipeline] withEnv +[Pipeline] { +[Pipeline] stage +[Pipeline] { (Setup Parameters & ENV) +[Pipeline] node +Running on jiangning-slave02-docker in /var/jenkins_home/workspace/web-v3-yunheng-adminui_test +[Pipeline] { +[Pipeline] checkout +The recommended git tool is: NONE +using credential 03a74b6e-92de-429b-8d63-22fe15da43c2 +Fetching changes from the remote Git repository +Fetching without tags +Checking out Revision 087e95e753ce2b1f5627da639fbaec56c3d93d23 (test) +Commit message: "Merge remote-tracking branch 'remotes/origin/3.5.4' into test" +[Gitea] Notifying branch build status: PENDING Build started... +[Gitea] Notified +[Pipeline] withEnv +[Pipeline] { +[Pipeline] script +[Pipeline] { +[Pipeline] checkout +The recommended git tool is: NONE +using credential 03a74b6e-92de-429b-8d63-22fe15da43c2 +Fetching changes from the remote Git repository + > /usr/bin/git rev-parse --resolve-git-dir /var/jenkins_home/workspace/web-v3-yunheng-adminui_test/.pipelines/.git # timeout=10 + > /usr/bin/git config remote.origin.url https://git.yunhengwang.net/cicd/pipeline-files.git # timeout=10 + > /usr/bin/git rev-parse --resolve-git-dir /var/jenkins_home/workspace/web-v3-yunheng-adminui_test/.git # timeout=10 + > /usr/bin/git config remote.origin.url https://git.yunhengwang.net/yunheng/yunheng-adminui.git # timeout=10 +Fetching upstream changes from https://git.yunhengwang.net/yunheng/yunheng-adminui.git + > /usr/bin/git --version # timeout=10 + > git --version # 'git version 2.20.1' +using GIT_ASKPASS to set credentials git.yunhengwang.com + > /usr/bin/git fetch --no-tags --force --progress -- https://git.yunhengwang.net/yunheng/yunheng-adminui.git +refs/heads/test:refs/remotes/origin/test # timeout=10 + > /usr/bin/git config core.sparsecheckout # timeout=10 + > /usr/bin/git checkout -f 087e95e753ce2b1f5627da639fbaec56c3d93d23 # timeout=10 +Checking out Revision 405ec24f243830a7570aeeebd3cdb5cf774626c1 (refs/remotes/origin/master) +Commit message: "add digitize" +Fetching upstream changes from https://git.yunhengwang.net/cicd/pipeline-files.git + > /usr/bin/git --version # timeout=10 + > git --version # 'git version 2.20.1' +using GIT_ASKPASS to set credentials git.yunhengwang.com + > /usr/bin/git fetch --tags --force --progress -- https://git.yunhengwang.net/cicd/pipeline-files.git +refs/heads/*:refs/remotes/origin/* # timeout=10 + > /usr/bin/git rev-parse refs/remotes/origin/master^{commit} # timeout=10 + > /usr/bin/git config core.sparsecheckout # timeout=10 + > /usr/bin/git checkout -f 405ec24f243830a7570aeeebd3cdb5cf774626c1 # timeout=10 + > /usr/bin/git rev-list --no-walk 405ec24f243830a7570aeeebd3cdb5cf774626c1 # timeout=10 +[Gitea] Notifying branch build status: PENDING Build started... +[Gitea] Notified +[Pipeline] echo +[INFO] envFileName: .jenkins/.env.yml +[Pipeline] readYaml +[Pipeline] readYaml +[Pipeline] readYaml +[Pipeline] readJSON +[Pipeline] readJSON +[Pipeline] echo +[INFO] The branchConfig jenkinsAgent is: jiangning-docker +[Pipeline] echo +[INFO] The branchConfig mavenSet is: nexus-maven-yhzzv3-test +[Pipeline] fileExists +[Pipeline] readYaml +[Pipeline] echo +[INFO] The .env.yml file masterProfiles is: [, uat, prod, newuat, newprod] +[Pipeline] echo +[INFO] The .env.yml file jobConfigFile is: .pipelines/config-file/yunhengv3Config.yml +[Pipeline] echo +[INFO] The .env.yml file npmCompileCMD is: npm config set registry https://mirrors.huaweicloud.com/repository/npm/ \ +&& npm install rimraf@5.0.7 -g \ +&& rimraf node_modules \ +&& rimraf package-lock.json \ +&& rimraf node_modules/.cache \ +&& npm cache clean --force \ +&& npm config set sass_binary_site https://repo.huaweicloud.com/node-sass/node-sass \ +&& npm install -g node-gyp@10.3.1 \ +&& npm install --unsafe-perm --legacy-peer-deps \ +&& npm run build:${APP_PROFILE} + +[Pipeline] echo +[INFO] The .env.yml file jobName is: web-v3-yunheng-adminui +[Pipeline] echo +[INFO] The .env.yml file jenkinsAgent is: jiangning-docker +[Pipeline] echo +[INFO] The Default Registry Info: jnhub.yunhengwang.net/yhzzv3 +[Pipeline] echo +[INFO] The commonConfig registryInfo is: [default:[registry:jnhub.yunhengwang.net, registryCredential:jiangning-hub-credentials, registry_project:yhzzv3], acr:[registry:registry-vpc.cn-hangzhou.aliyuncs.com, registryCredential:aliyun-hub-credentials, registry_project:yhzzv3]] +[Pipeline] echo +[INFO] The commonConfig deployScript is: deploy_env.sh +[Pipeline] echo +[INFO] The commonConfig deployPath is: /data/apps/yhsc +[Pipeline] echo +[INFO] The commonConfig serverInfo is: [dev:[serverIP01:root@192.168.124.125, sshkey:jenkins-ssh-152], test:[serverIP01:root@192.168.124.126, sshkey:jenkins-ssh-152], uat:[serverIP01:[root@172.18.1.7], sshkey:jenkins-ssh-152], prod:[serverIP01:[root@172.18.1.5], sshkey:jenkins-ssh-152], newuat:[serverIP01:root@172.18.1.7, sshkey:jenkins-ssh-152], newprod:[serverIP01:root@172.18.1.5, sshkey:jenkins-ssh-152]] +[Pipeline] echo +[INFO] The commonConfig containerHub is: [dev:[default], test:[default], uat:[acr], prod:[acr]] +[Pipeline] echo +jobConfigJson.serviceConfig: [v3-yhzz-register:[programLang:java, programLangVersion:8, containerName:yhzz-register, pomFilePath:pom.xml, servicePath:yhzz-register, jarName:yhzz-register.jar, port:8848, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, composeFile:yhzz-docker-compose], v3-yhzz-gateway:[programLang:java, programLangVersion:8, containerName:yhzz-gateway, pomFilePath:pom.xml, servicePath:yhzz-gateway, jarName:yhzz-gateway.jar, port:9999, javaOpts:-server -Xms256m -Xmx1536m -Xss1m, deployServerHost:serverIP01, composeFile:yhzz-docker-compose], v3-yhzz-seata-server:[programLang:java, programLangVersion:8, containerName:yhzz-seata-server, pomFilePath:pom.xml, jarName:yhzz-seata-server.jar, servicePath:yhzz-visual/yhzz-seata-server, port:7091, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, composeFile:yhzz-docker-compose], v3-yhzz-auth:[programLang:java, programLangVersion:8, containerName:yhzz-auth, pomFilePath:pom.xml, servicePath:yhzz-auth, jarName:yhzz-auth.jar, port:3000, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, composeFile:yhzz-docker-compose], v3-yhzz-basic-biz:[programLang:java, programLangVersion:8, containerName:yhzz-basic-biz, pomFilePath:pom.xml, servicePath:yhzz-basic/yhzz-basic-biz, jarName:yhzz-basic-biz.jar, port:4000, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-basic-api, composeFile:yhzz-docker-compose], v3-yhzz-admin-biz:[programLang:java, programLangVersion:8, containerName:yhzz-admin-biz, pomFilePath:pom.xml, servicePath:yhzz-admin/yhzz-admin-biz, jarName:yhzz-admin-biz.jar, port:4001, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-admin-api, composeFile:yhzz-docker-compose], v3-yhzz-user-biz:[programLang:java, programLangVersion:8, containerName:yhzz-user-biz, pomFilePath:pom.xml, jarName:yhzz-user-biz.jar, servicePath:yhzz-user/yhzz-user-biz, port:4002, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-user-api, composeFile:yhzz-docker-compose], v3-yhzz-flow-engine-biz:[programLang:java, programLangVersion:8, containerName:yhzz-flow-engine-biz, pomFilePath:pom.xml, jarName:yhzz-flow-engine-biz.jar, servicePath:yhzz-flow/yhzz-flow-engine/yhzz-flow-engine-biz, port:9020, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-flow-engine-api, composeFile:yhzz-docker-compose], v3-yhzz-flow-task-biz:[programLang:java, programLangVersion:8, containerName:yhzz-flow-task-biz, pomFilePath:pom.xml, jarName:yhzz-flow-task-biz.jar, servicePath:yhzz-flow/yhzz-flow-task/yhzz-flow-task-biz, port:9030, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-flow-task-api, composeFile:yhzz-docker-compose], v3-yhzz-order-biz:[programLang:java, programLangVersion:8, containerName:yhzz-order-biz, pomFilePath:pom.xml, jarName:yhzz-order-biz.jar, servicePath:yhzz-order/yhzz-order-biz, port:4003, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-order-api, composeFile:yhzz-docker-compose], v3-yhzz-goods-biz:[programLang:java, programLangVersion:8, containerName:yhzz-goods-biz, pomFilePath:pom.xml, jarName:yhzz-goods-biz.jar, servicePath:yhzz-goods/yhzz-goods-biz, port:4009, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-goods-api, composeFile:yhzz-docker-compose], v3-yhzz-business-biz:[programLang:java, programLangVersion:8, containerName:yhzz-business-biz, pomFilePath:pom.xml, jarName:yhzz-business-biz.jar, servicePath:yhzz-business/yhzz-business-biz, port:4010, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-business-api, composeFile:yhzz-docker-compose], v3-yhzz-web-biz:[programLang:java, programLangVersion:8, containerName:yhzz-web-biz, pomFilePath:pom.xml, jarName:yhzz-web-biz.jar, servicePath:yhzz-web/yhzz-web-biz, port:3008, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-web-api, composeFile:yhzz-docker-compose], v3-yhzz-daemon-quartz:[programLang:java, programLangVersion:8, containerName:yhzz-daemon-quartz, pomFilePath:pom.xml, jarName:yhzz-daemon-quartz.jar, servicePath:yhzz-visual/yhzz-daemon-quartz, port:5007, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, composeFile:yhzz-docker-compose], v3-yhzz-ucenter-biz:[programLang:java, programLangVersion:8, containerName:yhzz-ucenter-biz, pomFilePath:pom.xml, jarName:yhzz-ucenter-biz.jar, servicePath:yhzz-ucenter/yhzz-ucenter-biz, port:4005, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-ucenter-api, composeFile:yhzz-docker-compose], v3-yhzz-yhsc-biz:[programLang:java, programLangVersion:8, containerName:yhzz-yhsc-biz, pomFilePath:pom.xml, jarName:yhzz-yhsc-biz.jar, servicePath:yhzz-yhsc/yhzz-yhsc-biz, port:4012, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-yhsc-api, composeFile:yhzz-docker-compose], v3-yhzz-mobile-biz:[programLang:java, programLangVersion:8, containerName:yhzz-mobile-biz, pomFilePath:pom.xml, jarName:yhzz-mobile-biz.jar, servicePath:yhzz-mobile/yhzz-mobile-biz, port:3009, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-mobile-api, composeFile:yhzz-docker-compose], v3-yhzz-chuang-biz:[programLang:java, programLangVersion:8, containerName:yhzz-chuang-biz, pomFilePath:pom.xml, jarName:yhzz-chuang-biz.jar, servicePath:yhzz-chuang/yhzz-chuang-biz, port:4011, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-chuang-api, composeFile:yhzz-docker-compose], v3-yhzz-technology-biz:[programLang:java, programLangVersion:8, containerName:yhzz-technology-biz, pomFilePath:pom.xml, jarName:yhzz-technology-biz.jar, servicePath:yhzz-technology/yhzz-technology-biz, port:4011, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-technology-api, composeFile:yhzz-docker-compose], v3-yhzz-digitize-biz:[programLang:java, programLangVersion:8, containerName:yhzz-digitize-biz, pomFilePath:pom.xml, jarName:yhzz-digitize-biz.jar, servicePath:yhzz-digitize/yhzz-digitize-biz, port:4014, javaOpts:-server -Xms256m -Xmx4g -Xss1m, deployServerHost:serverIP01, isApi:true, apiProjectName:yhzz-digitize-biz, composeFile:yhzz-docker-compose], web-v3-yunheng-adminui:[programLang:nodejs, programLangVersion:16, deployMethod:static, containerName:web-yunheng-adminui, port:8008, deployServerHost:serverIP01, composeFile:yhzz-docker-compose], web-v3-yhsc-businessui:[programLang:nodejs, programLangVersion:14, deployMethod:static, containerName:web-yhsc-businessui, port:8009, deployServerHost:serverIP01, composeFile:yhzz-docker-compose], web-v3-yunheng-ucenterui:[programLang:nodejs, programLangVersion:14, deployMethod:static, containerName:web-yunheng-ucenterui, port:8010, deployServerHost:serverIP01, composeFile:yhzz-docker-compose], web-v3-yhchuang-webui:[programLang:nodejs, programLangVersion:14, deployMethod:dynamic, containerName:web-yhchuang-webui, port:8101, deployServerHost:serverIP01, composeFile:yhzz-docker-compose], web-v3-yhjs-webui:[programLang:nodejs, programLangVersion:14, deployMethod:dynamic, containerName:web-yhjs-webui, port:8101, deployServerHost:serverIP01, composeFile:yhzz-docker-compose], web-v3-yhsc-webui:[programLang:nodejs, programLangVersion:14, deployMethod:dynamic, containerName:web-yhsc-webui, port:8101, deployServerHost:serverIP01, composeFile:yhzz-docker-compose], web-v3-yhzz-webui:[programLang:nodejs, programLangVersion:14, deployMethod:dynamic, containerName:web-yhzz-webui, port:8101, deployServerHost:serverIP01, composeFile:yhzz-docker-compose], web-v3-yhzz-mui:[programLang:nodejs, programLangVersion:14, deployMethod:static, containerName:web-yhzz-mui, port:8010, deployServerHost:serverIP01, composeFile:yhzz-docker-compose], web-v3-yunheng-digitize:[programLang:nodejs, programLangVersion:18, deployMethod:static, containerName:web-yunheng-digitize, port:8010, deployServerHost:serverIP01, composeFile:yhzz-docker-compose]] +[Pipeline] echo +cicdEnvMap.envConfig.jobName: web-v3-yunheng-adminui +[Pipeline] echo +{"programLang":"nodejs","programLangVersion":16,"deployMethod":"static","containerName":"web-yunheng-adminui","port":"8008","deployServerHost":"serverIP01","composeFile":"yhzz-docker-compose"} +[Pipeline] echo +[INFO] serviceData programLang: nodejs/16 +[Pipeline] echo +[INFO] serviceData jarFile full path: null/target/null +[Pipeline] echo +[INFO] serviceData container info: web-yunheng-adminui/8008 +[Pipeline] echo +[INFO] mvnDeploy is null +[Pipeline] echo +[INFO] compileModule default standard +[Pipeline] echo +[INFO] deployModule default standard +[Pipeline] echo +[INFO] The build image for the null branch is: node:16.20.2-bullseye-dos +[Pipeline] echo +[INFO] The base image for the null branch is: nginx:1.24.0 +[Pipeline] echo +[INFO] The serviceConfig programLang is: nodejs +[Pipeline] echo +[INFO] The serviceConfig programLangVersion is: 16 +[Pipeline] echo +[INFO] The serviceConfig deployMethod is: static +[Pipeline] echo +[INFO] The serviceConfig containerName is: web-yunheng-adminui +[Pipeline] echo +[INFO] The serviceConfig port is: 8008 +[Pipeline] echo +[INFO] The serviceConfig deployServerHost is: serverIP01 +[Pipeline] echo +[INFO] The serviceConfig composeFile is: yhzz-docker-compose +[Pipeline] echo +[INFO] The serviceConfig jarFilePath is: null/target +[Pipeline] echo +[INFO] The serviceConfig mvnDeploy is: isNull +[Pipeline] echo +[INFO] The serviceConfig compileModule is: standard +[Pipeline] echo +[INFO] The serviceConfig deployModule is: standard +[Pipeline] echo +[INFO] The serviceConfig buildImage is: node:16.20.2-bullseye-dos +[Pipeline] echo +[INFO] The serviceConfig baseImage is: nginx:1.24.0 +[Pipeline] echo +[INFO] The serviceConfig deployServerUser is: root +[Pipeline] echo +[INFO] The Maven Set XML for the test branch is: nexus-maven-yhzzv3-test +[Pipeline] properties +[Pipeline] echo +[INFO] the branch profile is: test +[Pipeline] echo +[INFO] deploy Server: root@192.168.124.126 +[Pipeline] } +[Pipeline] // script +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] } +[Pipeline] // node +[Pipeline] } +[Pipeline] // stage +[Pipeline] stage +[Pipeline] { (Compile) +[Pipeline] node +Running on jiangning-slave02-docker in /var/jenkins_home/workspace/web-v3-yunheng-adminui_test +[Pipeline] { +[Pipeline] isUnix +[Pipeline] withEnv +[Pipeline] { +[Pipeline] sh ++ docker inspect -f . node:16.20.2-bullseye-dos +. +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] withDockerContainer +jiangning-slave02-docker seems to be running inside container 1deb1a89c78572d0c50c06179ee9d6136406ac06571d7d8a3d52d796bce78f88 +$ docker run -t -d -u 0:0 -m 8g -v /data/apps/repository/maven/nexus-maven-yhzzv3-test:/root/.m2/repository -v /etc/hosts:/etc/hosts -w /var/jenkins_home/workspace/web-v3-yunheng-adminui_test --volumes-from 1deb1a89c78572d0c50c06179ee9d6136406ac06571d7d8a3d52d796bce78f88 -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** node:16.20.2-bullseye-dos cat +$ docker top bfcbc15ecfee4b137630e6d6289d5f1014a4230040634088941e736975dc9f2b -eo pid,comm +[Pipeline] { +[Pipeline] script +[Pipeline] { +[Pipeline] checkout +The recommended git tool is: NONE +using credential 03a74b6e-92de-429b-8d63-22fe15da43c2 +Warning: JENKINS-30600: special launcher org.jenkinsci.plugins.docker.workflow.WithContainerStep$Decorator$1@6a459627; decorates RemoteLauncher[hudson.remoting.Channel@446660e0:JNLP4-connect connection from nexus.yunhengwang.net/172.18.1.250:46936] will be ignored (a typical symptom is the Git executable not being run inside a designated container) +Fetching changes from the remote Git repository +Checking out Revision 405ec24f243830a7570aeeebd3cdb5cf774626c1 (refs/remotes/origin/master) +Commit message: "add digitize" +[Gitea] Notifying branch build status: PENDING Build started... +[Gitea] Notified +[Pipeline] echo +[INFO] Compiling NodeJS code +[Pipeline] echo +[INFO] standard maven build +[Pipeline] echo +[INFO] npm Compile profile: test +[Pipeline] sh + > /usr/bin/git rev-parse --resolve-git-dir /var/jenkins_home/workspace/web-v3-yunheng-adminui_test/.pipelines/.git # timeout=10 + > /usr/bin/git config remote.origin.url https://git.yunhengwang.net/cicd/pipeline-files.git # timeout=10 +Fetching upstream changes from https://git.yunhengwang.net/cicd/pipeline-files.git + > /usr/bin/git --version # timeout=10 + > git --version # 'git version 2.20.1' +using GIT_ASKPASS to set credentials git.yunhengwang.com + > /usr/bin/git fetch --tags --force --progress -- https://git.yunhengwang.net/cicd/pipeline-files.git +refs/heads/*:refs/remotes/origin/* # timeout=10 + > /usr/bin/git rev-parse refs/remotes/origin/master^{commit} # timeout=10 + > /usr/bin/git config core.sparsecheckout # timeout=10 + > /usr/bin/git checkout -f 405ec24f243830a7570aeeebd3cdb5cf774626c1 # timeout=10 ++ export APP_PROFILE=test ++ npm config set registry https://mirrors.huaweicloud.com/repository/npm/ ++ npm install rimraf@5.0.7 -g + +added 42 packages in 2s + +14 packages are looking for funding + run `npm fund` for details ++ rimraf node_modules ++ rimraf package-lock.json ++ rimraf node_modules/.cache ++ npm cache clean --force +npm WARN using --force Recommended protections disabled. ++ npm config set sass_binary_site https://repo.huaweicloud.com/node-sass/node-sass ++ npm install -g node-gyp@10.3.1 + +added 99 packages in 22s + +14 packages are looking for funding + run `npm fund` for details ++ npm install --unsafe-perm --legacy-peer-deps + +added 695 packages in 53s + +156 packages are looking for funding + run `npm fund` for details ++ npm run build:test + +> yunheng-adminui@3.0.0 build:test +> cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build --mode test + +[log] vite v4.5.3 building for test... +[log] transforming... +[warn] Browserslist: caniuse-lite is outdated. Please run: + npx update-browserslist-db@latest + Why you should do it regularly: https://github.com/browserslist/update-db#readme +[warn] [@vue/compiler-sfc] ::v-deep usage as a combinator has been deprecated. Use :deep() instead of ::v-deep . + +[log] ✓ 5389 modules transformed. +[log] rendering chunks... +[log] computing gzip size... +[log] dist/assets/3.37e6a346.png  5.81 kB +[log] dist/assets/default_avatar.fa19527b.png  6.09 kB +[log] dist/assets/4.aa5aa17c.png  6.19 kB +[log] dist/assets/2.6067a190.png  6.36 kB +[log] dist/assets/1.f40b1815.png  6.58 kB +[log] dist/assets/logo.1b8dd904.png  10.80 kB +[log] dist/assets/menu_head.0d78779b.png  12.67 kB +[log] dist/index.html  13.28 kB │ gzip: 4.49 kB +[log] dist/assets/coupon.5a150391.png  14.53 kB +[log] dist/assets/bg.7b14eacd.png  17.47 kB +[log] dist/assets/iphone_backImg.c9d30888.png  34.27 kB +[log] dist/assets/login_bg.cbfed30c.svg  68.74 kB │ gzip: 17.73 kB +[log] dist/assets/my_topbg.137a6c56.png  142.47 kB +[log] dist/assets/lockScreen.98132559.png  199.44 kB +[log] dist/assets/index.6d606818.css  0.02 kB │ gzip: 0.04 kB +[log] dist/assets/infoDialog.d06147d6.css  0.02 kB │ gzip: 0.04 kB +[log] dist/assets/zk.ebbbb24a.css  0.03 kB │ gzip: 0.05 kB +[log] dist/assets/pay.ebd99f3e.css  0.03 kB │ gzip: 0.05 kB +[log] dist/assets/order.428c09b2.css  0.03 kB │ gzip: 0.05 kB +[log] dist/assets/custom.eff12cc6.css  0.03 kB │ gzip: 0.05 kB +[log] dist/assets/sharging.be41dee9.css  0.03 kB │ gzip: 0.05 kB +[log] dist/assets/invoicing.ef69cb1e.css  0.04 kB │ gzip: 0.06 kB +[log] dist/assets/info.e10beb77.css  0.04 kB │ gzip: 0.06 kB +[log] dist/assets/info.cd0ed081.css  0.04 kB │ gzip: 0.06 kB +[log] dist/assets/edit.c814550f.css  0.04 kB │ gzip: 0.06 kB +[log] dist/assets/invoiceBox.9f05d3c1.css  0.04 kB │ gzip: 0.06 kB +[log] dist/assets/index.6cbf8285.css  0.04 kB │ gzip: 0.06 kB +[log] dist/assets/index.16dde973.css  0.04 kB │ gzip: 0.06 kB +[log] dist/assets/flow-data.45412fd5.css  0.04 kB │ gzip: 0.06 kB +[log] dist/assets/index.fb444352.css  0.05 kB │ gzip: 0.07 kB +[log] dist/assets/serviceFile.81fce546.css  0.05 kB │ gzip: 0.06 kB +[log] dist/assets/cancelDialog.c74c8486.css  0.05 kB │ gzip: 0.07 kB +[log] dist/assets/gwSuggestDialog.7294cfb8.css  0.05 kB │ gzip: 0.07 kB +[log] dist/assets/index.fc331aeb.css  0.05 kB │ gzip: 0.07 kB +[log] dist/assets/index.e8829f4b.css  0.05 kB │ gzip: 0.07 kB +[log] dist/assets/index.b39e4ab7.css  0.06 kB │ gzip: 0.08 kB +[log] dist/assets/edit.ae1884c3.css  0.06 kB │ gzip: 0.07 kB +[log] dist/assets/cache.f538350d.css  0.06 kB │ gzip: 0.07 kB +[log] dist/assets/edit.2d70866f.css  0.06 kB │ gzip: 0.08 kB +[log] dist/assets/edit.54e0cefa.css  0.06 kB │ gzip: 0.08 kB +[log] dist/assets/importFile.81063c4b.css  0.07 kB │ gzip: 0.08 kB +[log] dist/assets/generator.abd0356e.css  0.08 kB │ gzip: 0.09 kB +[log] dist/assets/info.add030aa.css  0.08 kB │ gzip: 0.09 kB +[log] dist/assets/columnFilter.4d7f0352.css  0.08 kB │ gzip: 0.09 kB +[log] dist/assets/route.0bb82113.css  0.09 kB │ gzip: 0.10 kB +[log] dist/assets/preview.ceb2cfa7.css  0.09 kB │ gzip: 0.10 kB +[log] dist/assets/bomBox.57bd8c9e.css  0.09 kB │ gzip: 0.10 kB +[log] dist/assets/index.27ed68d5.css  0.09 kB │ gzip: 0.10 kB +[log] dist/assets/params.c386fae9.css  0.09 kB │ gzip: 0.09 kB +[log] dist/assets/conditionDrawer.c21e761b.css  0.09 kB │ gzip: 0.11 kB +[log] dist/assets/audit-log.d1361c0a.css  0.09 kB │ gzip: 0.11 kB +[log] dist/assets/sys-log.77b776ff.css  0.09 kB │ gzip: 0.11 kB +[log] dist/assets/vertical.ede26da2.css  0.09 kB │ gzip: 0.11 kB +[log] dist/assets/jimu.f2f224b7.css  0.10 kB │ gzip: 0.11 kB +[log] dist/assets/report.e1b8ae6d.css  0.10 kB │ gzip: 0.11 kB +[log] dist/assets/current-user.fc1908ea.css  0.10 kB │ gzip: 0.11 kB +[log] dist/assets/condition.406672dd.css  0.10 kB │ gzip: 0.10 kB +[log] dist/assets/Date.c4a9930f.css  0.10 kB │ gzip: 0.10 kB +[log] dist/assets/DateTime.d0c51cba.css  0.10 kB │ gzip: 0.10 kB +[log] dist/assets/Time.bfcf869f.css  0.10 kB │ gzip: 0.10 kB +[log] dist/assets/Date.bc55d74a.css  0.10 kB │ gzip: 0.10 kB +[log] dist/assets/DateTime.c70bcb7b.css  0.10 kB │ gzip: 0.10 kB +[log] dist/assets/Time.2188077e.css  0.10 kB │ gzip: 0.10 kB +[log] dist/assets/index.398af40b.css  0.10 kB │ gzip: 0.12 kB +[log] dist/assets/step1.3c8696e7.css  0.11 kB │ gzip: 0.12 kB +[log] dist/assets/edit.a0e06b3d.css  0.11 kB │ gzip: 0.10 kB +[log] dist/assets/index.8714b257.css  0.11 kB │ gzip: 0.09 kB +[log] dist/assets/bcxyParams.29094b64.css  0.11 kB │ gzip: 0.09 kB +[log] dist/assets/index.e3d898d2.css  0.12 kB │ gzip: 0.11 kB +[log] dist/assets/index.ec6cc3ae.css  0.12 kB │ gzip: 0.12 kB +[log] dist/assets/content.3e409e95.css  0.13 kB │ gzip: 0.13 kB +[log] dist/assets/info.1fed25c9.css  0.13 kB │ gzip: 0.11 kB +[log] dist/assets/info.d84f909d.css  0.13 kB │ gzip: 0.13 kB +[log] dist/assets/pcbaParams.7f383723.css  0.14 kB │ gzip: 0.13 kB +[log] dist/assets/content.79372087.css  0.14 kB │ gzip: 0.14 kB +[log] dist/assets/formPerm.86f43157.css  0.15 kB │ gzip: 0.10 kB +[log] dist/assets/orderList.cc1974f6.css  0.15 kB │ gzip: 0.13 kB +[log] dist/assets/orderProcessInfo.82816c4a.css  0.15 kB │ gzip: 0.13 kB +[log] dist/assets/pcbParams.351a2d88.css  0.15 kB │ gzip: 0.12 kB +[log] dist/assets/form.9ff01399.css  0.16 kB │ gzip: 0.15 kB +[log] dist/assets/index.89506750.css  0.17 kB │ gzip: 0.15 kB +[log] dist/assets/content.b1da82b2.css  0.18 kB │ gzip: 0.14 kB +[log] dist/assets/index.831c9bff.css  0.18 kB │ gzip: 0.14 kB +[log] dist/assets/index.e4b5300a.css  0.18 kB │ gzip: 0.16 kB +[log] dist/assets/content.1b6d1e0b.css  0.19 kB │ gzip: 0.17 kB +[log] dist/assets/content.b105951c.css  0.19 kB │ gzip: 0.16 kB +[log] dist/assets/preview.0cd74529.css  0.19 kB │ gzip: 0.16 kB +[log] dist/assets/index.df9b3d9f.css  0.19 kB │ gzip: 0.16 kB +[log] dist/assets/decoration-img.15a528bb.css  0.19 kB │ gzip: 0.16 kB +[log] dist/assets/dialog.615a3025.css  0.20 kB │ gzip: 0.15 kB +[log] dist/assets/queryTree.66669b81.css  0.20 kB │ gzip: 0.14 kB +[log] dist/assets/index.476dae99.css  0.20 kB │ gzip: 0.14 kB +[log] dist/assets/file.cec61c77.css  0.22 kB │ gzip: 0.14 kB +[log] dist/assets/printer.db74331c.css  0.22 kB │ gzip: 0.18 kB +[log] dist/assets/payType.c4301abd.css  0.22 kB │ gzip: 0.15 kB +[log] dist/assets/params.009730e4.css  0.22 kB │ gzip: 0.15 kB +[log] dist/assets/editMsg.e6749d68.css  0.22 kB │ gzip: 0.16 kB +[log] dist/assets/logForm.8800c6e2.css  0.23 kB │ gzip: 0.18 kB +[log] dist/assets/info.33d63cc6.css  0.23 kB │ gzip: 0.15 kB +[log] dist/assets/menu.b82fe353.css  0.23 kB │ gzip: 0.15 kB +[log] dist/assets/edit.c2252db8.css  0.24 kB │ gzip: 0.18 kB +[log] dist/assets/index.ba741a77.css  0.25 kB │ gzip: 0.20 kB +[log] dist/assets/dataList.1f346ae6.css  0.25 kB │ gzip: 0.14 kB +[log] dist/assets/params.39cae655.css  0.29 kB │ gzip: 0.15 kB +[log] dist/assets/contextmenu.1b050f60.css  0.29 kB │ gzip: 0.17 kB +[log] dist/assets/MultiSelect.1c77d202.css  0.29 kB │ gzip: 0.18 kB +[log] dist/assets/SingleSelect.a5235a5c.css  0.29 kB │ gzip: 0.18 kB +[log] dist/assets/scheduling.a5a4f679.css  0.30 kB │ gzip: 0.16 kB +[log] dist/assets/submit.3c8f1ca2.css  0.30 kB │ gzip: 0.21 kB +[log] dist/assets/calendar.f6cf815b.css  0.31 kB │ gzip: 0.22 kB +[log] dist/assets/edit.477845ac.css  0.31 kB │ gzip: 0.21 kB +[log] dist/assets/index.610c226b.css  0.32 kB │ gzip: 0.21 kB +[log] dist/assets/checkForm.034330ed.css  0.32 kB │ gzip: 0.21 kB +[log] dist/assets/edit.4234901f.css  0.34 kB │ gzip: 0.17 kB +[log] dist/assets/baojia.f186d57e.css  0.34 kB │ gzip: 0.18 kB +[log] dist/assets/horizontal.a196a0bf.css  0.35 kB │ gzip: 0.20 kB +[log] dist/assets/form.287092a0.css  0.36 kB │ gzip: 0.21 kB +[log] dist/assets/index.ab229e5b.css  0.41 kB │ gzip: 0.19 kB +[log] dist/assets/index.15d07c08.css  0.43 kB │ gzip: 0.24 kB +[log] dist/assets/index.51150d4e.css  0.43 kB │ gzip: 0.25 kB +[log] dist/assets/printer.e9d2b1d4.css  0.44 kB │ gzip: 0.22 kB +[log] dist/assets/index.c445c39f.css  0.46 kB │ gzip: 0.23 kB +[log] dist/assets/search.db29754e.css  0.47 kB │ gzip: 0.20 kB +[log] dist/assets/index.079c7acd.css  0.50 kB │ gzip: 0.19 kB +[log] dist/assets/file.0f5ef70f.css  0.51 kB │ gzip: 0.22 kB +[log] dist/assets/edit.ac993fbc.css  0.55 kB │ gzip: 0.27 kB +[log] dist/assets/edit.3e11da2b.css  0.55 kB │ gzip: 0.27 kB +[log] dist/assets/index.9490d9f8.css  0.58 kB │ gzip: 0.28 kB +[log] dist/assets/picker.e3301720.css  0.61 kB │ gzip: 0.24 kB +[log] dist/assets/favorite.19eedc59.css  0.62 kB │ gzip: 0.31 kB +[log] dist/assets/closeFull.a808a64b.css  0.63 kB │ gzip: 0.27 kB +[log] dist/assets/index.4b47f54b.css  0.64 kB │ gzip: 0.31 kB +[log] dist/assets/index.47b5c078.css  0.68 kB │ gzip: 0.28 kB +[log] dist/assets/index.abd3a0df.css  0.70 kB │ gzip: 0.31 kB +[log] dist/assets/main.a4428fb7.css  0.74 kB │ gzip: 0.30 kB +[log] dist/assets/selectResult.75d6dee0.css  0.79 kB │ gzip: 0.33 kB +[log] dist/assets/megerInquiryEdit.14e119ca.css  0.85 kB │ gzip: 0.37 kB +[log] dist/assets/index.822dec21.css  0.86 kB │ gzip: 0.34 kB +[log] dist/assets/index.9cc43542.css  0.91 kB │ gzip: 0.34 kB +[log] dist/assets/index.7136e3a7.css  0.95 kB │ gzip: 0.28 kB +[log] dist/assets/link.4bbc8d46.css  0.97 kB │ gzip: 0.37 kB +[log] dist/assets/breadcrumb.8b948af7.css  0.97 kB │ gzip: 0.31 kB +[log] dist/assets/all.c5b08bf6.css  1.02 kB │ gzip: 0.40 kB +[log] dist/assets/user.4f47806c.css  1.03 kB │ gzip: 0.36 kB +[log] dist/assets/index.4ce32358.css  1.04 kB │ gzip: 0.43 kB +[log] dist/assets/index.500fab11.css  1.09 kB │ gzip: 0.41 kB +[log] dist/assets/goodsImport.a527fa58.css  1.09 kB │ gzip: 0.40 kB +[log] dist/assets/list.f4dc2b7f.css  1.09 kB │ gzip: 0.34 kB +[log] dist/assets/404.1a3c9af2.css  1.10 kB │ gzip: 0.36 kB +[log] dist/assets/401.aad0f557.css  1.10 kB │ gzip: 0.35 kB +[log] dist/assets/index.608c8d1b.css  1.14 kB │ gzip: 0.37 kB +[log] dist/assets/compareDialog.1af879ee.css  1.38 kB │ gzip: 0.40 kB +[log] dist/assets/userNews.355dce93.css  1.54 kB │ gzip: 0.40 kB +[log] dist/assets/Images.192fa293.css  1.71 kB │ gzip: 0.60 kB +[log] dist/assets/index.9fe4bc2a.css  1.82 kB │ gzip: 0.57 kB +[log] dist/assets/roleDialog.6a0d1354.css  1.86 kB │ gzip: 1.22 kB +[log] dist/assets/columnsAside.3f028923.css  1.86 kB │ gzip: 0.50 kB +[log] dist/assets/employeesDialog.e2b287e3.css  1.94 kB │ gzip: 1.24 kB +[log] dist/assets/processSchedulingDialog.ba41487b.css  2.02 kB │ gzip: 0.58 kB +[log] dist/assets/coupon.7a17e36b.css  2.62 kB │ gzip: 0.69 kB +[log] dist/assets/index.a1e7a474.css  3.26 kB │ gzip: 0.83 kB +[log] dist/assets/index.5c57cbcb.css  3.34 kB │ gzip: 0.97 kB +[log] dist/assets/news-form.b322bae3.css  3.38 kB │ gzip: 1.00 kB +[log] dist/assets/tenant.74708db5.css  3.75 kB │ gzip: 0.97 kB +[log] dist/assets/selectBox.0c48734d.css  3.81 kB │ gzip: 1.90 kB +[log] dist/assets/index.de85ee71.css  4.17 kB │ gzip: 1.27 kB +[log] dist/assets/personal.66695421.css  5.14 kB │ gzip: 1.00 kB +[log] dist/assets/setings.51df1813.css  5.24 kB │ gzip: 0.82 kB +[log] dist/assets/index.d92dd355.css  5.41 kB │ gzip: 1.05 kB +[log] dist/assets/index.14782495.css  5.83 kB │ gzip: 2.27 kB +[log] dist/assets/tagsView.950659ef.css  6.69 kB │ gzip: 2.15 kB +[log] dist/assets/index.fed89984.css  9.43 kB │ gzip: 2.31 kB +[log] dist/assets/step3.cfcd0a77.css  28.34 kB │ gzip: 5.59 kB +[log] dist/assets/errorDialog.9af387e2.css  28.65 kB │ gzip: 5.65 kB +[log] dist/assets/nodeWrap.7774c1fa.css  28.79 kB │ gzip: 5.70 kB +[log] dist/assets/approval.43ddbe06.css  28.79 kB │ gzip: 5.71 kB +[log] dist/assets/index.bf199ca5.css  30.47 kB │ gzip: 6.77 kB +[log] dist/assets/addNode.f1421c96.css  30.55 kB │ gzip: 5.90 kB +[log] dist/assets/Verify.73a2a1a5.css  52.76 kB │ gzip: 36.80 kB +[log] dist/assets/index.ca4efe7f.css  547.11 kB │ gzip: 76.58 kB +[log] dist/assets/attr.bebf7262.js  0.12 kB │ gzip: 0.12 kB +[log] dist/assets/attr.c7e7fe89.js  0.12 kB │ gzip: 0.13 kB +[log] dist/assets/attr.f0719e0f.js  0.12 kB │ gzip: 0.13 kB +[log] dist/assets/attr.0a59943a.js  0.12 kB │ gzip: 0.12 kB +[log] dist/assets/full-width-button-handler.e977dd16.js  0.17 kB │ gzip: 0.12 kB +[log] dist/assets/useLockFn.edea85f7.js  0.19 kB │ gzip: 0.17 kB +[log] dist/assets/attr.vue_vue_type_script_setup_true_lang.bcc61ab3.js  0.22 kB │ gzip: 0.18 kB +[log] dist/assets/attr.vue_vue_type_script_setup_true_lang.799e7f8c.js  0.22 kB │ gzip: 0.18 kB +[log] dist/assets/attr.vue_vue_type_script_setup_true_lang.76dc7ea5.js  0.22 kB │ gzip: 0.18 kB +[log] dist/assets/log.42236b56.js  0.27 kB │ gzip: 0.21 kB +[log] dist/assets/pay.cf69fa12.js  0.29 kB │ gzip: 0.21 kB +[log] dist/assets/quality.eb824335.js  0.29 kB │ gzip: 0.22 kB +[log] dist/assets/audit.0ec69bde.js  0.31 kB │ gzip: 0.22 kB +[log] dist/assets/approve.a4477324.js  0.33 kB │ gzip: 0.23 kB +[log] dist/assets/msg.7c4ab104.js  0.33 kB │ gzip: 0.21 kB +[log] dist/assets/returnOrder.b4096dba.js  0.34 kB │ gzip: 0.23 kB +[log] dist/assets/flow.1c747469.js  0.34 kB │ gzip: 0.22 kB +[log] dist/assets/bill.fbe0aa6f.js  0.36 kB │ gzip: 0.23 kB +[log] dist/assets/index.86f83df3.js  0.37 kB │ gzip: 0.23 kB +[log] dist/assets/agree.f7cc8c42.js  0.37 kB │ gzip: 0.23 kB +[log] dist/assets/info.f108ed84.js  0.37 kB │ gzip: 0.24 kB +[log] dist/assets/give.16904ce9.js  0.37 kB │ gzip: 0.24 kB +[log] dist/assets/auth.8e2d79ed.js  0.37 kB │ gzip: 0.24 kB +[log] dist/assets/refuse.3b3b470e.js  0.37 kB │ gzip: 0.23 kB +[log] dist/assets/gyp.bb7364c5.js  0.37 kB │ gzip: 0.24 kB +[log] dist/assets/bcxyParams.80ad24ea.js  0.37 kB │ gzip: 0.24 kB +[log] dist/assets/zkBase.b1874802.js  0.37 kB │ gzip: 0.24 kB +[log] dist/assets/payBox.23fc1949.js  0.37 kB │ gzip: 0.24 kB +[log] dist/assets/invoiceBox.f3052d9f.js  0.37 kB │ gzip: 0.24 kB +[log] dist/assets/credit.1bba73af.js  0.37 kB │ gzip: 0.23 kB +[log] dist/assets/billAmount.034f02d4.js  0.37 kB │ gzip: 0.24 kB +[log] dist/assets/repayment.aac43505.js  0.37 kB │ gzip: 0.24 kB +[log] dist/assets/refund.dd2c4313.js  0.37 kB │ gzip: 0.23 kB +[log] dist/assets/attr.f1aad105.js  0.38 kB │ gzip: 0.24 kB +[log] dist/assets/attr.f842baf7.js  0.38 kB │ gzip: 0.24 kB +[log] dist/assets/content.25cf22d8.js  0.38 kB │ gzip: 0.24 kB +[log] dist/assets/content.373161bd.js  0.38 kB │ gzip: 0.24 kB +[log] dist/assets/content.74697f80.js  0.38 kB │ gzip: 0.24 kB +[log] dist/assets/add-nav.a6414f0a.js  0.38 kB │ gzip: 0.24 kB +[log] dist/assets/setting.a5cfeb5f.js  0.38 kB │ gzip: 0.24 kB +[log] dist/assets/registrationCheck.647f0d11.js  0.40 kB │ gzip: 0.24 kB +[log] dist/assets/step2.vue_vue_type_script_setup_true_lang.3074926e.js  0.40 kB │ gzip: 0.30 kB +[log] dist/assets/edit.1727f864.js  0.40 kB │ gzip: 0.25 kB +[log] dist/assets/edit.c177523d.js  0.40 kB │ gzip: 0.25 kB +[log] dist/assets/index.64c8bc13.js  0.40 kB │ gzip: 0.29 kB +[log] dist/assets/checkForm.354edfd2.js  0.40 kB │ gzip: 0.25 kB +[log] dist/assets/consult.0bc168fb.js  0.41 kB │ gzip: 0.24 kB +[log] dist/assets/consult.7c2f892b.js  0.41 kB │ gzip: 0.24 kB +[log] dist/assets/workflow.6508bab4.js  0.41 kB │ gzip: 0.25 kB +[log] dist/assets/const.686e94ae.js  0.41 kB │ gzip: 0.23 kB +[log] dist/assets/tpl.6425f7b4.js  0.42 kB │ gzip: 0.24 kB +[log] dist/assets/serviceList.bb20d6e3.js  0.42 kB │ gzip: 0.25 kB +[log] dist/assets/pcbaParams.f78f95ae.js  0.44 kB │ gzip: 0.25 kB +[log] dist/assets/common.092790f1.js  0.45 kB │ gzip: 0.26 kB +[log] dist/assets/brandInfo.8135cbe1.js  0.46 kB │ gzip: 0.25 kB +[log] dist/assets/delivery.33afc401.js  0.47 kB │ gzip: 0.26 kB +[log] dist/assets/serviceRegistration.70db0020.js  0.47 kB │ gzip: 0.25 kB +[log] dist/assets/salesUser.e3eabfa2.js  0.48 kB │ gzip: 0.27 kB +[log] dist/assets/index.a6600a39.js  0.48 kB │ gzip: 0.27 kB +[log] dist/assets/coupon.9c81ea48.js  0.48 kB │ gzip: 0.25 kB +[log] dist/assets/discount.2b57ef60.js  0.48 kB │ gzip: 0.26 kB +[log] dist/assets/index.20254207.js  0.49 kB │ gzip: 0.32 kB +[log] dist/assets/attr.8648905f.js  0.49 kB │ gzip: 0.26 kB +[log] dist/assets/attr.7ad4629d.js  0.49 kB │ gzip: 0.26 kB +[log] dist/assets/freightParams.6fbd41c7.js  0.49 kB │ gzip: 0.26 kB +[log] dist/assets/Description.e5d3e414.js  0.49 kB │ gzip: 0.35 kB +[log] dist/assets/craft.8f010e12.js  0.50 kB │ gzip: 0.26 kB +[log] dist/assets/importFile.139cb5d1.js  0.51 kB │ gzip: 0.27 kB +[log] dist/assets/website.95ef839d.js  0.51 kB │ gzip: 0.25 kB +[log] dist/assets/list.b791afd3.js  0.51 kB │ gzip: 0.25 kB +[log] dist/assets/sensitive.f0443d05.js  0.51 kB │ gzip: 0.26 kB +[log] dist/assets/dept.c8aebc26.js  0.51 kB │ gzip: 0.27 kB +[log] dist/assets/goods.e8673b54.js  0.51 kB │ gzip: 0.25 kB +[log] dist/assets/trade.05ee674a.js  0.51 kB │ gzip: 0.25 kB +[log] dist/assets/batchUploadForm.00605310.js  0.51 kB │ gzip: 0.29 kB +[log] dist/assets/imgZipUploadForm.d708038c.js  0.52 kB │ gzip: 0.29 kB +[log] dist/assets/index.c16a0e59.js  0.52 kB │ gzip: 0.29 kB +[log] dist/assets/channel.02a03288.js  0.52 kB │ gzip: 0.26 kB +[log] dist/assets/billToday.fb59ef8c.js  0.52 kB │ gzip: 0.27 kB +[log] dist/assets/outWare.43acf8e2.js  0.53 kB │ gzip: 0.26 kB +[log] dist/assets/userGroup.6dfd19a7.js  0.53 kB │ gzip: 0.26 kB +[log] dist/assets/appsocial.9bc497d9.js  0.53 kB │ gzip: 0.25 kB +[log] dist/assets/social.b2bc3772.js  0.54 kB │ gzip: 0.27 kB +[log] dist/assets/nav.66fc287d.js  0.54 kB │ gzip: 0.26 kB +[log] dist/assets/typegroup.5197789a.js  0.54 kB │ gzip: 0.26 kB +[log] dist/assets/prodBatch.534440a5.js  0.54 kB │ gzip: 0.27 kB +[log] dist/assets/index.b19df304.js  0.54 kB │ gzip: 0.27 kB +[log] dist/assets/regCustomer.17eeeded.js  0.54 kB │ gzip: 0.27 kB +[log] dist/assets/unit.ca270a15.js  0.54 kB │ gzip: 0.27 kB +[log] dist/assets/Area.e910ad44.js  0.54 kB │ gzip: 0.36 kB +[log] dist/assets/infoType.cbc8f0f0.js  0.55 kB │ gzip: 0.26 kB +[log] dist/assets/Sequence.3ad3b47e.js  0.55 kB │ gzip: 0.38 kB +[log] dist/assets/unit.68a1446e.js  0.55 kB │ gzip: 0.27 kB +[log] dist/assets/index.e815a08e.js  0.55 kB │ gzip: 0.38 kB +[log] dist/assets/RichEditor.acd2c720.js  0.55 kB │ gzip: 0.37 kB +[log] dist/assets/friendlylinks.7a03f9d9.js  0.55 kB │ gzip: 0.26 kB +[log] dist/assets/appArticle.c9021c21.js  0.55 kB │ gzip: 0.27 kB +[log] dist/assets/index.c1b9f930.js  0.55 kB │ gzip: 0.28 kB +[log] dist/assets/nav.38dc71bd.js  0.55 kB │ gzip: 0.26 kB +[log] dist/assets/prodToday.597c101c.js  0.55 kB │ gzip: 0.28 kB +[log] dist/assets/customerIndustry.ef7cdeb9.js  0.56 kB │ gzip: 0.28 kB +[log] dist/assets/demand.f1b9b8d9.js  0.56 kB │ gzip: 0.27 kB +[log] dist/assets/product.7889901c.js  0.56 kB │ gzip: 0.27 kB +[log] dist/assets/product.d7ae1ed8.js  0.57 kB │ gzip: 0.27 kB +[log] dist/assets/order.46b47482.js  0.57 kB │ gzip: 0.27 kB +[log] dist/assets/printing.6f36d14b.js  0.57 kB │ gzip: 0.27 kB +[log] dist/assets/edit.cf46b5c9.js  0.57 kB │ gzip: 0.28 kB +[log] dist/assets/bank.61f00ec1.js  0.58 kB │ gzip: 0.27 kB +[log] dist/assets/pay.7dbd704f.js  0.58 kB │ gzip: 0.27 kB +[log] dist/assets/group.cc0239b6.js  0.58 kB │ gzip: 0.27 kB +[log] dist/assets/bomBox.a47ffb99.js  0.58 kB │ gzip: 0.29 kB +[log] dist/assets/couponParams.38007f98.js  0.59 kB │ gzip: 0.29 kB +[log] dist/assets/dataList.08b86c7f.js  0.59 kB │ gzip: 0.28 kB +[log] dist/assets/coupon.c5b4edfd.js  0.59 kB │ gzip: 0.28 kB +[log] dist/assets/template.21a0711a.js  0.60 kB │ gzip: 0.27 kB +[log] dist/assets/base.ff461f40.js  0.60 kB │ gzip: 0.29 kB +[log] dist/assets/datasource.92e07633.js  0.60 kB │ gzip: 0.27 kB +[log] dist/assets/brand.ea43e1a5.js  0.60 kB │ gzip: 0.28 kB +[log] dist/assets/index.d76c722a.js  0.60 kB │ gzip: 0.26 kB +[log] dist/assets/serviceType.3e5748fa.js  0.61 kB │ gzip: 0.27 kB +[log] dist/assets/content.10867966.js  0.61 kB │ gzip: 0.43 kB +[log] dist/assets/bill.942d90b2.js  0.62 kB │ gzip: 0.28 kB +[log] dist/assets/lanmu.3f110d86.js  0.62 kB │ gzip: 0.28 kB +[log] dist/assets/schedule.e3d8b9ba.js  0.62 kB │ gzip: 0.27 kB +[log] dist/assets/scale.3fc9a95c.js  0.62 kB │ gzip: 0.28 kB +[log] dist/assets/index.678da90d.js  0.63 kB │ gzip: 0.31 kB +[log] dist/assets/coupon.8feac6f2.js  0.63 kB │ gzip: 0.28 kB +[log] dist/assets/credit.52ef8001.js  0.63 kB │ gzip: 0.28 kB +[log] dist/assets/payType.c3807faa.js  0.63 kB │ gzip: 0.28 kB +[log] dist/assets/content.vue_vue_type_script_setup_true_lang.35dd8ac6.js  0.63 kB │ gzip: 0.41 kB +[log] dist/assets/index.76a3217e.js  0.64 kB │ gzip: 0.37 kB +[log] dist/assets/content.7dd8be9d.js  0.64 kB │ gzip: 0.44 kB +[log] dist/assets/userRoute.f3e61f6e.js  0.65 kB │ gzip: 0.28 kB +[log] dist/assets/selectAndShow.838e7d60.js  0.65 kB │ gzip: 0.32 kB +[log] dist/assets/Description.de27a60c.js  0.65 kB │ gzip: 0.42 kB +[log] dist/assets/business.d4a32065.js  0.66 kB │ gzip: 0.30 kB +[log] dist/assets/discount.69f0b7e4.js  0.66 kB │ gzip: 0.28 kB +[log] dist/assets/appArticleCategory.d396bfc3.js  0.66 kB │ gzip: 0.28 kB +[log] dist/assets/wx-account-tag.0caa7b74.js  0.66 kB │ gzip: 0.29 kB +[log] dist/assets/pay.e3ba4f2b.js  0.66 kB │ gzip: 0.29 kB +[log] dist/assets/list.3520561d.js  0.67 kB │ gzip: 0.28 kB +[log] dist/assets/invoiceLog.5651937a.js  0.67 kB │ gzip: 0.30 kB +[log] dist/assets/invoice.e23323e1.js  0.69 kB │ gzip: 0.29 kB +[log] dist/assets/customer.dc0a82fb.js  0.69 kB │ gzip: 0.29 kB +[log] dist/assets/wx-material.fd327d32.js  0.70 kB │ gzip: 0.30 kB +[log] dist/assets/index.d23c8b26.js  0.70 kB │ gzip: 0.31 kB +[log] dist/assets/closeFull.a93e85eb.js  0.70 kB │ gzip: 0.46 kB +[log] dist/assets/orderList.c0305fd4.js  0.70 kB │ gzip: 0.32 kB +[log] dist/assets/content.vue_vue_type_script_setup_true_lang.a48ef7c8.js  0.71 kB │ gzip: 0.45 kB +[log] dist/assets/type.53a7894f.js  0.71 kB │ gzip: 0.30 kB +[log] dist/assets/styles-handler.30514ae9.js  0.71 kB │ gzip: 0.42 kB +[log] dist/assets/header.17e8891c.js  0.72 kB │ gzip: 0.44 kB +[log] dist/assets/stocklog.32c81a33.js  0.72 kB │ gzip: 0.30 kB +[log] dist/assets/content.vue_vue_type_script_setup_true_lang.b33a93c6.js  0.72 kB │ gzip: 0.46 kB +[log] dist/assets/orgItem.bb22c9b4.js  0.72 kB │ gzip: 0.47 kB +[log] dist/assets/factory.9e7e2f20.js  0.74 kB │ gzip: 0.31 kB +[log] dist/assets/newsletter.904df2f4.js  0.75 kB │ gzip: 0.46 kB +[log] dist/assets/coupon.9b5cb351.js  0.76 kB │ gzip: 0.30 kB +[log] dist/assets/job.4d0d5086.js  0.77 kB │ gzip: 0.30 kB +[log] dist/assets/FlowNodeFormat.5019939f.js  0.77 kB │ gzip: 0.35 kB +[log] dist/assets/pcbParams.864883bd.js  0.77 kB │ gzip: 0.32 kB +[log] dist/assets/report.27f5f653.js  0.77 kB │ gzip: 0.54 kB +[log] dist/assets/businessCheck.16cfa511.js  0.78 kB │ gzip: 0.31 kB +[log] dist/assets/contract.c18efab5.js  0.78 kB │ gzip: 0.31 kB +[log] dist/assets/report.4080e2e9.js  0.78 kB │ gzip: 0.28 kB +[log] dist/assets/authredirect.40f4e7d2.js  0.80 kB │ gzip: 0.51 kB +[log] dist/assets/Sign.a9820dc8.js  0.81 kB │ gzip: 0.50 kB +[log] dist/assets/model.1fbcde7d.js  0.81 kB │ gzip: 0.31 kB +[log] dist/assets/production.cff5ccba.js  0.81 kB │ gzip: 0.30 kB +[log] dist/assets/dict.3871f135.js  0.82 kB │ gzip: 0.47 kB +[log] dist/assets/client.a79de6b8.js  0.82 kB │ gzip: 0.39 kB +[log] dist/assets/wx-account-fans.29dc1f69.js  0.83 kB │ gzip: 0.31 kB +[log] dist/assets/menu.20a90643.js  0.83 kB │ gzip: 0.52 kB +[log] dist/assets/index.222d28f2.js  0.84 kB │ gzip: 0.53 kB +[log] dist/assets/fieldtype.be336a19.js  0.85 kB │ gzip: 0.39 kB +[log] dist/assets/attr-setting.vue_vue_type_script_setup_true_lang.1b36f3be.js  0.86 kB │ gzip: 0.50 kB +[log] dist/assets/wx-account.7b996c94.js  0.86 kB │ gzip: 0.32 kB +[log] dist/assets/index.875c8177.js  0.87 kB │ gzip: 0.55 kB +[log] dist/assets/complaint.3b059b82.js  0.89 kB │ gzip: 0.30 kB +[log] dist/assets/RichEditor.45287e86.js  0.89 kB │ gzip: 0.55 kB +[log] dist/assets/jimu.bf9024e9.js  0.89 kB │ gzip: 0.61 kB +[log] dist/assets/UploadImage.d3bfdef6.js  0.91 kB │ gzip: 0.59 kB +[log] dist/assets/base.d4cc8106.js  0.91 kB │ gzip: 0.36 kB +[log] dist/assets/Sequence.34e86769.js  0.93 kB │ gzip: 0.55 kB +[log] dist/assets/list.28052135.js  0.95 kB │ gzip: 0.32 kB +[log] dist/assets/FlowNodeFormatData.b31d519d.js  0.97 kB │ gzip: 0.38 kB +[log] dist/assets/param.7d65a315.js  0.97 kB │ gzip: 0.34 kB +[log] dist/assets/custom.ac059f7d.js  0.98 kB │ gzip: 0.58 kB +[log] dist/assets/UploadImage.cb9b0984.js  1.00 kB │ gzip: 0.59 kB +[log] dist/assets/decoration-img.87f1946e.js  1.00 kB │ gzip: 0.59 kB +[log] dist/assets/message.80484033.js  1.02 kB │ gzip: 0.33 kB +[log] dist/assets/appuser.55de73f9.js  1.03 kB │ gzip: 0.48 kB +[log] dist/assets/index.24d3898b.js  1.03 kB │ gzip: 0.51 kB +[log] dist/assets/index.b8d7a937.js  1.06 kB │ gzip: 0.56 kB +[log] dist/assets/serviceList.7d493de0.js  1.09 kB │ gzip: 0.36 kB +[log] dist/assets/list.29f1af97.js  1.10 kB │ gzip: 0.35 kB +[log] dist/assets/index.152e95f9.js  1.11 kB │ gzip: 0.40 kB +[log] dist/assets/post.07c4d6d2.js  1.11 kB │ gzip: 0.43 kB +[log] dist/assets/Layout.8e502ed7.js  1.11 kB │ gzip: 0.59 kB +[log] dist/assets/zk.2700c436.js  1.11 kB │ gzip: 0.63 kB +[log] dist/assets/SelectDept.b786e149.js  1.12 kB │ gzip: 0.68 kB +[log] dist/assets/userNews.1185e63f.js  1.13 kB │ gzip: 0.63 kB +[log] dist/assets/content.b754b0be.js  1.13 kB │ gzip: 0.66 kB +[log] dist/assets/edit.9ec2ecc9.js  1.14 kB │ gzip: 0.68 kB +[log] dist/assets/Area.65347975.js  1.14 kB │ gzip: 0.66 kB +[log] dist/assets/tenant.b2483a2f.js  1.16 kB │ gzip: 0.45 kB +[log] dist/assets/flow-data.1a1a9a86.js  1.16 kB │ gzip: 0.59 kB +[log] dist/assets/transverse.19ba0903.js  1.17 kB │ gzip: 0.60 kB +[log] dist/assets/approle.da93ea76.js  1.18 kB │ gzip: 0.51 kB +[log] dist/assets/SelectUser.7f8b0218.js  1.18 kB │ gzip: 0.70 kB +[log] dist/assets/param.df8ff939.js  1.19 kB │ gzip: 0.45 kB +[log] dist/assets/index.cf0e4e58.js  1.20 kB │ gzip: 0.61 kB +[log] dist/assets/common.86fea41d.js  1.21 kB │ gzip: 0.36 kB +[log] dist/assets/index.582ad4c4.js  1.22 kB │ gzip: 0.69 kB +[log] dist/assets/Input.65cc40fe.js  1.22 kB │ gzip: 0.66 kB +[log] dist/assets/sys-log.4a0a9e5a.js  1.22 kB │ gzip: 0.72 kB +[log] dist/assets/link.e1c21f60.js  1.23 kB │ gzip: 0.72 kB +[log] dist/assets/index.f0df1592.js  1.23 kB │ gzip: 0.72 kB +[log] dist/assets/table.623767f2.js  1.24 kB │ gzip: 0.47 kB +[log] dist/assets/list.4bc8f678.js  1.24 kB │ gzip: 0.73 kB +[log] dist/assets/index.954954d0.js  1.27 kB │ gzip: 0.55 kB +[log] dist/assets/audit-log.f3bd8088.js  1.28 kB │ gzip: 0.74 kB +[log] dist/assets/index.60d55f25.js  1.29 kB │ gzip: 0.76 kB +[log] dist/assets/selectAndShow.vue_vue_type_script_setup_true_lang.296ae97c.js  1.30 kB │ gzip: 0.72 kB +[log] dist/assets/negative.0ec1c422.js  1.30 kB │ gzip: 0.70 kB +[log] dist/assets/Empty.74c469b5.js  1.30 kB │ gzip: 0.77 kB +[log] dist/assets/index.0937b48e.js  1.30 kB │ gzip: 0.54 kB +[log] dist/assets/index.eadb9181.js  1.30 kB │ gzip: 0.72 kB +[log] dist/assets/attr.vue_vue_type_script_setup_true_lang.fc10fecf.js  1.32 kB │ gzip: 0.60 kB +[log] dist/assets/index.3af6feae.js  1.33 kB │ gzip: 0.73 kB +[log] dist/assets/UploadFile.0c22cac4.js  1.33 kB │ gzip: 0.72 kB +[log] dist/assets/index.vue_vue_type_style_index_0_lang.eedae7ce.js  1.33 kB │ gzip: 0.56 kB +[log] dist/assets/Textarea.580496a4.js  1.34 kB │ gzip: 0.71 kB +[log] dist/assets/index.638f3f1a.js  1.34 kB │ gzip: 0.67 kB +[log] dist/assets/inWare.9a11e723.js  1.35 kB │ gzip: 0.39 kB +[log] dist/assets/attr.vue_vue_type_script_setup_true_lang.c9dd2c69.js  1.35 kB │ gzip: 0.69 kB +[log] dist/assets/index.e6898d32.js  1.35 kB │ gzip: 0.57 kB +[log] dist/assets/sharging.edef22c2.js  1.35 kB │ gzip: 0.74 kB +[log] dist/assets/404.53f7aca2.js  1.35 kB │ gzip: 0.75 kB +[log] dist/assets/attr.vue_vue_type_script_setup_true_lang.1aa695b1.js  1.36 kB │ gzip: 0.65 kB +[log] dist/assets/index.9070e5f7.js  1.37 kB │ gzip: 0.80 kB +[log] dist/assets/FlowNodeFormatData.vue_vue_type_script_setup_true_lang.1e0a4e01.js  1.37 kB │ gzip: 0.71 kB +[log] dist/assets/401.840aa68d.js  1.37 kB │ gzip: 0.77 kB +[log] dist/assets/Date.e9689b02.js  1.38 kB │ gzip: 0.66 kB +[log] dist/assets/Time.9a04c28e.js  1.39 kB │ gzip: 0.66 kB +[log] dist/assets/production.56110ed2.js  1.39 kB │ gzip: 0.39 kB +[log] dist/assets/current-user.d39a02a3.js  1.40 kB │ gzip: 0.80 kB +[log] dist/assets/Number.2d17c00f.js  1.40 kB │ gzip: 0.74 kB +[log] dist/assets/SingleSelect.f74759db.js  1.41 kB │ gzip: 0.77 kB +[log] dist/assets/index.cd768704.js  1.43 kB │ gzip: 0.79 kB +[log] dist/assets/DateTime.5abbad74.js  1.43 kB │ gzip: 0.68 kB +[log] dist/assets/index.11bdafa9.js  1.43 kB │ gzip: 0.59 kB +[log] dist/assets/index.ccdc406c.js  1.45 kB │ gzip: 0.83 kB +[log] dist/assets/content.f2f44804.js  1.45 kB │ gzip: 0.71 kB +[log] dist/assets/failDialog.8d7d9e8f.js  1.47 kB │ gzip: 0.80 kB +[log] dist/assets/MultiSelect.1ccafa29.js  1.48 kB │ gzip: 0.81 kB +[log] dist/assets/index.40e1f8b4.js  1.49 kB │ gzip: 0.81 kB +[log] dist/assets/agree.vue_vue_type_script_setup_true_lang.94395e01.js  1.50 kB │ gzip: 0.86 kB +[log] dist/assets/refuse.vue_vue_type_script_setup_true_lang.752bba37.js  1.50 kB │ gzip: 0.86 kB +[log] dist/assets/logForm.94200024.js  1.53 kB │ gzip: 0.89 kB +[log] dist/assets/order.1d59ba11.js  1.53 kB │ gzip: 0.44 kB +[log] dist/assets/Time.ea0ba2b0.js  1.53 kB │ gzip: 0.82 kB +[log] dist/assets/Date.1e423d3b.js  1.53 kB │ gzip: 0.82 kB +[log] dist/assets/index.3ef03178.js  1.54 kB │ gzip: 0.79 kB +[log] dist/assets/order.2932516a.js  1.55 kB │ gzip: 0.81 kB +[log] dist/assets/DateTime.6e0f8097.js  1.56 kB │ gzip: 0.84 kB +[log] dist/assets/pay.c1a6ac36.js  1.57 kB │ gzip: 0.80 kB +[log] dist/assets/invoicing.6394dedc.js  1.58 kB │ gzip: 0.80 kB +[log] dist/assets/zk.2c0bd032.js  1.59 kB │ gzip: 0.92 kB +[log] dist/assets/defaults.1a3fb04b.js  1.59 kB │ gzip: 0.68 kB +[log] dist/assets/index.19439791.js  1.59 kB │ gzip: 0.62 kB +[log] dist/assets/iframes.fd699df5.js  1.62 kB │ gzip: 0.85 kB +[log] dist/assets/content.ee343f29.js  1.65 kB │ gzip: 0.90 kB +[log] dist/assets/index.d711ad48.js  1.67 kB │ gzip: 0.97 kB +[log] dist/assets/index.bb7ccc9d.js  1.68 kB │ gzip: 0.98 kB +[log] dist/assets/info.vue_vue_type_script_setup_true_lang.298a068e.js  1.71 kB │ gzip: 0.99 kB +[log] dist/assets/dict.5b6e8722.js  1.72 kB │ gzip: 0.60 kB +[log] dist/assets/edit.95d39b8e.js  1.74 kB │ gzip: 0.50 kB +[log] dist/assets/SelectDept.549583f3.js  1.79 kB │ gzip: 0.98 kB +[log] dist/assets/errorDialog.5027df5d.js  1.79 kB │ gzip: 0.98 kB +[log] dist/assets/sort.49b19fdf.js  1.81 kB │ gzip: 0.99 kB +[log] dist/assets/index.vue_vue_type_script_setup_true_lang.2609c82d.js  1.81 kB │ gzip: 0.96 kB +[log] dist/assets/sort.449d23f0.js  1.81 kB │ gzip: 0.98 kB +[log] dist/assets/sort.9bd1dc69.js  1.81 kB │ gzip: 0.98 kB +[log] dist/assets/classic.5752b75e.js  1.82 kB │ gzip: 0.79 kB +[log] dist/assets/sort.997ff388.js  1.82 kB │ gzip: 0.98 kB +[log] dist/assets/sort.10f89128.js  1.82 kB │ gzip: 0.98 kB +[log] dist/assets/columns.67c470ae.js  1.83 kB │ gzip: 0.71 kB +[log] dist/assets/user.37bfeb92.js  1.84 kB │ gzip: 0.71 kB +[log] dist/assets/Sign.27ff9f8b.js  1.84 kB │ gzip: 1.04 kB +[log] dist/assets/index.db48d0ac.js  1.84 kB │ gzip: 0.89 kB +[log] dist/assets/UploadFile.0bf08d3a.js  1.85 kB │ gzip: 0.82 kB +[log] dist/assets/index.4e662e13.js  1.86 kB │ gzip: 1.03 kB +[log] dist/assets/index.b7b79691.js  1.87 kB │ gzip: 0.84 kB +[log] dist/assets/index.365f4aee.js  1.87 kB │ gzip: 0.83 kB +[log] dist/assets/Number.33185e3a.js  1.87 kB │ gzip: 0.75 kB +[log] dist/assets/file.5f348748.js  1.88 kB │ gzip: 0.93 kB +[log] dist/assets/sort.5f52699d.js  1.88 kB │ gzip: 1.02 kB +[log] dist/assets/Textarea.700ba3c5.js  1.89 kB │ gzip: 0.72 kB +[log] dist/assets/index.3f2eaf3b.js  1.89 kB │ gzip: 0.93 kB +[log] dist/assets/Input.7de1f737.js  1.89 kB │ gzip: 0.73 kB +[log] dist/assets/Money.c851310a.js  1.91 kB │ gzip: 0.79 kB +[log] dist/assets/index.bacc18a8.js  1.91 kB │ gzip: 0.84 kB +[log] dist/assets/main.1cad07e0.js  1.92 kB │ gzip: 0.86 kB +[log] dist/assets/search.d1c19979.js  1.94 kB │ gzip: 1.05 kB +[log] dist/assets/editLogistics.5505f7fb.js  1.94 kB │ gzip: 0.98 kB +[log] dist/assets/initScheduling.e1f8a65c.js  1.94 kB │ gzip: 0.96 kB +[log] dist/assets/edit.c6ad9ee6.js  1.95 kB │ gzip: 1.03 kB +[log] dist/assets/selectGoods.dba8f2a9.js  1.96 kB │ gzip: 1.03 kB +[log] dist/assets/SelectUser.22ebf600.js  1.97 kB │ gzip: 1.01 kB +[log] dist/assets/calendar.dbf9ce2a.js  1.98 kB │ gzip: 0.95 kB +[log] dist/assets/index.ffdd94af.js  1.98 kB │ gzip: 1.00 kB +[log] dist/assets/role.8eaf2ca8.js  1.98 kB │ gzip: 0.67 kB +[log] dist/assets/form.e9ecc777.js  2.00 kB │ gzip: 1.08 kB +[log] dist/assets/index.8e75c90c.js  2.02 kB │ gzip: 1.00 kB +[log] dist/assets/index.fbe1c795.js  2.04 kB │ gzip: 0.87 kB +[log] dist/assets/imgZipUploadForm.vue_vue_type_script_setup_true_lang.4e2631bd.js  2.05 kB │ gzip: 1.12 kB +[log] dist/assets/batchUploadForm.vue_vue_type_script_setup_true_lang.e5c2aa6e.js  2.06 kB │ gzip: 1.10 kB +[log] dist/assets/parent.96e2c317.js  2.07 kB │ gzip: 1.01 kB +[log] dist/assets/protocolEdit.c739fe62.js  2.08 kB │ gzip: 1.12 kB +[log] dist/assets/index.fcc6472b.js  2.09 kB │ gzip: 1.11 kB +[log] dist/assets/favorite.6efb613b.js  2.09 kB │ gzip: 1.11 kB +[log] dist/assets/roleDialog.c1271907.js  2.10 kB │ gzip: 1.08 kB +[log] dist/assets/social.e54ad08c.js  2.11 kB │ gzip: 1.04 kB +[log] dist/assets/selectResult.7ffca017.js  2.11 kB │ gzip: 0.94 kB +[log] dist/assets/back.1ea15e18.js  2.12 kB │ gzip: 1.10 kB +[log] dist/assets/form.ad3c786a.js  2.14 kB │ gzip: 1.08 kB +[log] dist/assets/attr-setting.4d5530a0.js  2.15 kB │ gzip: 0.53 kB +[log] dist/assets/index.c1cb49e1.js  2.18 kB │ gzip: 1.11 kB +[log] dist/assets/promoterDrawer.2b246648.js  2.18 kB │ gzip: 1.08 kB +[log] dist/assets/info.bf12cf6d.js  2.20 kB │ gzip: 1.03 kB +[log] dist/assets/info.560dc785.js  2.20 kB │ gzip: 1.03 kB +[log] dist/assets/copyerDrawer.4e43b6a4.js  2.21 kB │ gzip: 1.07 kB +[log] dist/assets/index.2357d813.js  2.22 kB │ gzip: 1.16 kB +[log] dist/assets/serviceFile.vue_vue_type_style_index_0_lang.2735196b.js  2.23 kB │ gzip: 1.16 kB +[log] dist/assets/edit.4cfef5f6.js  2.24 kB │ gzip: 1.20 kB +[log] dist/assets/edit.c6d0c65b.js  2.24 kB │ gzip: 1.20 kB +[log] dist/assets/SingleSelect.77ea88dd.js  2.25 kB │ gzip: 1.12 kB +[log] dist/assets/goods.697776a6.js  2.27 kB │ gzip: 0.55 kB +[log] dist/assets/index.0849ed7f.js  2.29 kB │ gzip: 0.90 kB +[log] dist/assets/detail.21aef05d.js  2.30 kB │ gzip: 0.84 kB +[log] dist/assets/submit.ee22e5a7.js  2.30 kB │ gzip: 1.16 kB +[log] dist/assets/index.6cc01185.js  2.31 kB │ gzip: 0.85 kB +[log] dist/assets/MultiSelect.641798c0.js  2.33 kB │ gzip: 1.16 kB +[log] dist/assets/index.c1430a27.js  2.37 kB │ gzip: 0.92 kB +[log] dist/assets/index.a08ad7eb.js  2.40 kB │ gzip: 0.78 kB +[log] dist/assets/Money.6d6a85d9.js  2.41 kB │ gzip: 1.27 kB +[log] dist/assets/check.3bffd3f3.js  2.43 kB │ gzip: 1.22 kB +[log] dist/assets/remark.9c2c47a6.js  2.44 kB │ gzip: 1.30 kB +[log] dist/assets/info.e4001268.js  2.44 kB │ gzip: 1.04 kB +[log] dist/assets/remark.99c57571.js  2.46 kB │ gzip: 1.30 kB +[log] dist/assets/index.e50d7375.js  2.47 kB │ gzip: 1.16 kB +[log] dist/assets/add-nav.vue_vue_type_script_setup_true_lang.97a2f4b6.js  2.48 kB │ gzip: 1.22 kB +[log] dist/assets/index.f44c7537.js  2.48 kB │ gzip: 1.06 kB +[log] dist/assets/index.98f035f9.js  2.51 kB │ gzip: 0.98 kB +[log] dist/assets/Verify.ab15ac77.js  2.51 kB │ gzip: 1.16 kB +[log] dist/assets/edit.d20ed173.js  2.52 kB │ gzip: 1.24 kB +[log] dist/assets/index.415970f7.js  2.54 kB │ gzip: 1.27 kB +[log] dist/assets/edit.56a549aa.js  2.54 kB │ gzip: 1.23 kB +[log] dist/assets/index.35f215fc.js  2.55 kB │ gzip: 1.26 kB +[log] dist/assets/index.cbc35c53.js  2.55 kB │ gzip: 1.23 kB +[log] dist/assets/apply.8f541db5.js  2.59 kB │ gzip: 1.32 kB +[log] dist/assets/index.2e837153.js  2.59 kB │ gzip: 1.28 kB +[log] dist/assets/check.9417e8f5.js  2.59 kB │ gzip: 1.26 kB +[log] dist/assets/edit.eb3467f9.js  2.60 kB │ gzip: 1.21 kB +[log] dist/assets/edit.951dc935.js  2.60 kB │ gzip: 1.16 kB +[log] dist/assets/index.8a625772.js  2.60 kB │ gzip: 1.38 kB +[log] dist/assets/info.e30c8552.js  2.60 kB │ gzip: 1.10 kB +[log] dist/assets/index.71fd512e.js  2.64 kB │ gzip: 1.01 kB +[log] dist/assets/pp.c7e59835.js  2.65 kB │ gzip: 1.28 kB +[log] dist/assets/logistics.7c3c056d.js  2.68 kB │ gzip: 1.34 kB +[log] dist/assets/oemOrder.6fe09aed.js  2.69 kB │ gzip: 1.30 kB +[log] dist/assets/startFlow.vue_vue_type_script_setup_true_lang.6b6af9e5.js  2.69 kB │ gzip: 1.32 kB +[log] dist/assets/edit.4d22c043.js  2.71 kB │ gzip: 1.30 kB +[log] dist/assets/index.7f62a424.js  2.71 kB │ gzip: 1.33 kB +[log] dist/assets/FormRender.032bdcff.js  2.71 kB │ gzip: 0.85 kB +[log] dist/assets/contextmenu.d0a9b596.js  2.71 kB │ gzip: 1.30 kB +[log] dist/assets/mobile.13fc4e53.js  2.72 kB │ gzip: 1.21 kB +[log] dist/assets/index.0418b8e1.js  2.72 kB │ gzip: 1.33 kB +[log] dist/assets/index.cb3db446.js  2.72 kB │ gzip: 1.16 kB +[log] dist/assets/index.b768bef5.js  2.73 kB │ gzip: 1.34 kB +[log] dist/assets/index.0deffaf2.js  2.76 kB │ gzip: 1.38 kB +[log] dist/assets/breadcrumb.ba2faec5.js  2.77 kB │ gzip: 1.20 kB +[log] dist/assets/edit.1d268206.js  2.78 kB │ gzip: 1.32 kB +[log] dist/assets/edit.036006c2.js  2.78 kB │ gzip: 1.31 kB +[log] dist/assets/approval.f7247169.js  2.78 kB │ gzip: 1.33 kB +[log] dist/assets/baojia.1436d34e.js  2.78 kB │ gzip: 1.43 kB +[log] dist/assets/formPerm.0539c4cf.js  2.81 kB │ gzip: 1.10 kB +[log] dist/assets/queryTree.0daba0b8.js  2.82 kB │ gzip: 1.45 kB +[log] dist/assets/edit.189952b3.js  2.82 kB │ gzip: 1.37 kB +[log] dist/assets/step2.68941c59.js  2.82 kB │ gzip: 0.87 kB +[log] dist/assets/edit.6349672d.js  2.82 kB │ gzip: 1.38 kB +[log] dist/assets/index.ddd8118a.js  2.86 kB │ gzip: 1.35 kB +[log] dist/assets/edit.ee8a2f9c.js  2.86 kB │ gzip: 1.37 kB +[log] dist/assets/form.e2c3d6b6.js  2.89 kB │ gzip: 1.25 kB +[log] dist/assets/form.d34cdc41.js  2.90 kB │ gzip: 1.30 kB +[log] dist/assets/index.84b98e44.js  2.91 kB │ gzip: 1.37 kB +[log] dist/assets/index.fc084c00.js  2.91 kB │ gzip: 1.38 kB +[log] dist/assets/index.4fb73e13.js  2.92 kB │ gzip: 1.35 kB +[log] dist/assets/checkForm.96e986e6.js  2.93 kB │ gzip: 1.38 kB +[log] dist/assets/payType.0cb1cbd4.js  2.93 kB │ gzip: 1.43 kB +[log] dist/assets/edit.a55e7a5f.js  2.93 kB │ gzip: 1.35 kB +[log] dist/assets/printer.401819a6.js  2.94 kB │ gzip: 1.36 kB +[log] dist/assets/index.643d51b4.js  2.94 kB │ gzip: 1.42 kB +[log] dist/assets/quality.vue_vue_type_script_setup_true_lang.6791fecc.js  2.94 kB │ gzip: 1.24 kB +[log] dist/assets/employeesDialog.5d498f64.js  2.95 kB │ gzip: 1.35 kB +[log] dist/assets/specialDialog.1d08b8bc.js  2.95 kB │ gzip: 1.48 kB +[log] dist/assets/importFile.vue_vue_type_style_index_0_lang.3b456798.js  2.96 kB │ gzip: 1.45 kB +[log] dist/assets/index.5d43262a.js  2.97 kB │ gzip: 1.38 kB +[log] dist/assets/index.09fbe7eb.js  2.97 kB │ gzip: 1.38 kB +[log] dist/assets/password.d8e18cdf.js  2.98 kB │ gzip: 1.32 kB +[log] dist/assets/edit.b6a6b718.js  2.98 kB │ gzip: 1.37 kB +[log] dist/assets/check.94d47db8.js  2.99 kB │ gzip: 1.40 kB +[log] dist/assets/index.da9a0b25.js  2.99 kB │ gzip: 1.39 kB +[log] dist/assets/index.235a0e0f.js  2.99 kB │ gzip: 1.43 kB +[log] dist/assets/index.cb714566.js  3.00 kB │ gzip: 1.44 kB +[log] dist/assets/preview.3cf79358.js  3.00 kB │ gzip: 1.06 kB +[log] dist/assets/index.cc194f41.js  3.02 kB │ gzip: 1.45 kB +[log] dist/assets/checkForm.vue_vue_type_script_setup_true_lang.27f925d6.js  3.03 kB │ gzip: 1.43 kB +[log] dist/assets/form.4630b9f9.js  3.03 kB │ gzip: 1.44 kB +[log] dist/assets/edit.81cb30dc.js  3.04 kB │ gzip: 1.39 kB +[log] dist/assets/edit.fcf28930.js  3.04 kB │ gzip: 1.35 kB +[log] dist/assets/columnFilter.94b630a2.js  3.04 kB │ gzip: 1.21 kB +[log] dist/assets/preview.b68adfe8.js  3.07 kB │ gzip: 1.52 kB +[log] dist/assets/attr.vue_vue_type_script_setup_true_lang.1e8a8d48.js  3.07 kB │ gzip: 1.36 kB +[log] dist/assets/edit.0a70f276.js  3.11 kB │ gzip: 1.42 kB +[log] dist/assets/index.11fff2bc.js  3.11 kB │ gzip: 1.47 kB +[log] dist/assets/importFile.966515d3.js  3.12 kB │ gzip: 1.56 kB +[log] dist/assets/index.f07058cf.js  3.13 kB │ gzip: 1.47 kB +[log] dist/assets/index.85fdbf5f.js  3.14 kB │ gzip: 1.46 kB +[log] dist/assets/index.4e1873ff.js  3.16 kB │ gzip: 1.36 kB +[log] dist/assets/billAmount.vue_vue_type_script_setup_true_lang.a9594ae2.js  3.18 kB │ gzip: 1.26 kB +[log] dist/assets/check.df3e56dc.js  3.19 kB │ gzip: 1.44 kB +[log] dist/assets/discount.vue_vue_type_script_setup_true_lang.764514ee.js  3.20 kB │ gzip: 1.41 kB +[log] dist/assets/index.58732f23.js  3.22 kB │ gzip: 1.52 kB +[log] dist/assets/startFlow.76478011.js  3.22 kB │ gzip: 0.94 kB +[log] dist/assets/index.c910539a.js  3.22 kB │ gzip: 1.41 kB +[log] dist/assets/form.8fa1f272.js  3.23 kB │ gzip: 1.45 kB +[log] dist/assets/index.63fc0b42.js  3.24 kB │ gzip: 1.44 kB +[log] dist/assets/horizontal.6f174a08.js  3.26 kB │ gzip: 1.57 kB +[log] dist/assets/repayment.vue_vue_type_script_setup_true_lang.3e5d9ded.js  3.27 kB │ gzip: 1.17 kB +[log] dist/assets/index.65a51216.js  3.30 kB │ gzip: 1.52 kB +[log] dist/assets/index.57f6c6db.js  3.31 kB │ gzip: 1.46 kB +[log] dist/assets/index.f36467f1.js  3.31 kB │ gzip: 1.50 kB +[log] dist/assets/index.ae7047b9.js  3.32 kB │ gzip: 1.45 kB +[log] dist/assets/edit.d52de98b.js  3.33 kB │ gzip: 1.52 kB +[log] dist/assets/index.6cc656ff.js  3.34 kB │ gzip: 1.41 kB +[log] dist/assets/index.b6322739.js  3.36 kB │ gzip: 1.33 kB +[log] dist/assets/index.49db5d47.js  3.37 kB │ gzip: 1.58 kB +[log] dist/assets/gyp.vue_vue_type_script_setup_true_lang.06958318.js  3.37 kB │ gzip: 1.35 kB +[log] dist/assets/attr.vue_vue_type_script_setup_true_lang.fc565766.js  3.38 kB │ gzip: 1.43 kB +[log] dist/assets/index.6cbd57df.js  3.38 kB │ gzip: 1.46 kB +[log] dist/assets/step3.d66e60d4.js  3.38 kB │ gzip: 1.56 kB +[log] dist/assets/edit.5a6a51ac.js  3.40 kB │ gzip: 1.47 kB +[log] dist/assets/index.c4957d24.js  3.41 kB │ gzip: 1.41 kB +[log] dist/assets/edit.7f5782bc.js  3.41 kB │ gzip: 1.35 kB +[log] dist/assets/index.b3c63f9c.js  3.42 kB │ gzip: 1.50 kB +[log] dist/assets/index.143aecbd.js  3.42 kB │ gzip: 1.43 kB +[log] dist/assets/form.b8332c67.js  3.46 kB │ gzip: 1.40 kB +[log] dist/assets/edit.71e73e8d.js  3.50 kB │ gzip: 1.47 kB +[log] dist/assets/aside.4a3c2da4.js  3.51 kB │ gzip: 1.59 kB +[log] dist/assets/give.vue_vue_type_script_setup_true_lang.3ae28093.js  3.52 kB │ gzip: 1.27 kB +[log] dist/assets/form.3e472c37.js  3.52 kB │ gzip: 1.49 kB +[log] dist/assets/apply.f5025680.js  3.53 kB │ gzip: 1.66 kB +[log] dist/assets/addNode.0164581e.js  3.53 kB │ gzip: 1.30 kB +[log] dist/assets/yhuser.57492621.js  3.55 kB │ gzip: 0.71 kB +[log] dist/assets/index.b994407a.js  3.55 kB │ gzip: 1.51 kB +[log] dist/assets/form.3cd7a9a3.js  3.56 kB │ gzip: 1.47 kB +[log] dist/assets/index.8d5ec7ec.js  3.57 kB │ gzip: 1.48 kB +[log] dist/assets/form.bffdec7a.js  3.58 kB │ gzip: 1.47 kB +[log] dist/assets/index.84c17407.js  3.60 kB │ gzip: 1.45 kB +[log] dist/assets/individuation.afa6ff1f.js  3.61 kB │ gzip: 1.51 kB +[log] dist/assets/websiteEdit.f84cec47.js  3.61 kB │ gzip: 1.47 kB +[log] dist/assets/bill.vue_vue_type_script_setup_true_lang.acfeb3ab.js  3.61 kB │ gzip: 1.49 kB +[log] dist/assets/index.b2810008.js  3.63 kB │ gzip: 1.50 kB +[log] dist/assets/step1.40bf12b0.js  3.65 kB │ gzip: 1.55 kB +[log] dist/assets/string-styles.28d24ab6.js  3.66 kB │ gzip: 1.14 kB +[log] dist/assets/address.7f51ec64.js  3.66 kB │ gzip: 1.75 kB +[log] dist/assets/FlowNodeFormat.vue_vue_type_script_setup_true_lang.cf206631.js  3.69 kB │ gzip: 1.53 kB +[log] dist/assets/index.fd9b2096.js  3.69 kB │ gzip: 1.54 kB +[log] dist/assets/index.24426bc4.js  3.71 kB │ gzip: 1.65 kB +[log] dist/assets/columnsAside.a8cbd428.js  3.72 kB │ gzip: 1.63 kB +[log] dist/assets/edit.6f5aed44.js  3.72 kB │ gzip: 1.63 kB +[log] dist/assets/index.8e988200.js  3.75 kB │ gzip: 1.68 kB +[log] dist/assets/index.f8d6a831.js  3.75 kB │ gzip: 1.69 kB +[log] dist/assets/edit.7cf878d3.js  3.76 kB │ gzip: 1.60 kB +[log] dist/assets/job-log.07a42cac.js  3.76 kB │ gzip: 1.63 kB +[log] dist/assets/index.ec3eef71.js  3.76 kB │ gzip: 1.49 kB +[log] dist/assets/index.1b8bf8ca.js  3.77 kB │ gzip: 1.66 kB +[log] dist/assets/index.d3faa632.js  3.79 kB │ gzip: 1.78 kB +[log] dist/assets/index.d355d1f6.js  3.81 kB │ gzip: 1.78 kB +[log] dist/assets/index.40b27637.js  3.81 kB │ gzip: 1.65 kB +[log] dist/assets/edit.0b92f5dd.js  3.81 kB │ gzip: 1.69 kB +[log] dist/assets/form.4a9d5145.js  3.82 kB │ gzip: 1.56 kB +[log] dist/assets/production.c3f82cc5.js  3.84 kB │ gzip: 1.78 kB +[log] dist/assets/couponParams.vue_vue_type_script_setup_true_lang.bbedab77.js  3.86 kB │ gzip: 1.52 kB +[log] dist/assets/editMsg.4f088639.js  3.86 kB │ gzip: 1.69 kB +[log] dist/assets/edit.1c92309e.js  3.90 kB │ gzip: 1.75 kB +[log] dist/assets/edit.7f031504.js  3.93 kB │ gzip: 1.72 kB +[log] dist/assets/edit.1cf948d6.js  3.93 kB │ gzip: 1.71 kB +[log] dist/assets/index.eb80fedd.js  3.93 kB │ gzip: 1.65 kB +[log] dist/assets/form.27fb207f.js  3.94 kB │ gzip: 1.42 kB +[log] dist/assets/form.caa19619.js  3.94 kB │ gzip: 1.39 kB +[log] dist/assets/edit.1edf787f.js  3.95 kB │ gzip: 1.58 kB +[log] dist/assets/index.5c1975eb.js  3.96 kB │ gzip: 1.75 kB +[log] dist/assets/index.52a8f442.js  3.96 kB │ gzip: 1.70 kB +[log] dist/assets/register.54b3472c.js  3.97 kB │ gzip: 1.49 kB +[log] dist/assets/index.f2f48e77.js  3.98 kB │ gzip: 1.71 kB +[log] dist/assets/index.56dcb9c9.js  3.98 kB │ gzip: 1.78 kB +[log] dist/assets/child.229e3397.js  3.99 kB │ gzip: 1.48 kB +[log] dist/assets/edit.6c1c76af.js  4.02 kB │ gzip: 1.75 kB +[log] dist/assets/index.b47a3d67.js  4.02 kB │ gzip: 1.17 kB +[log] dist/assets/index.b2bf468e.js  4.03 kB │ gzip: 1.49 kB +[log] dist/assets/cancelDialog.24570e2f.js  4.03 kB │ gzip: 1.62 kB +[log] dist/assets/invoice.e74f5dbc.js  4.04 kB │ gzip: 1.75 kB +[log] dist/assets/index.7c12b073.js  4.08 kB │ gzip: 1.71 kB +[log] dist/assets/subItem.842f9186.js  4.10 kB │ gzip: 1.04 kB +[log] dist/assets/index.33e92917.js  4.14 kB │ gzip: 1.61 kB +[log] dist/assets/list.7429c297.js  4.15 kB │ gzip: 0.84 kB +[log] dist/assets/gwSuggestDialog.15c64314.js  4.16 kB │ gzip: 1.72 kB +[log] dist/assets/index.77ef85f2.js  4.18 kB │ gzip: 1.31 kB +[log] dist/assets/edit.9f984b09.js  4.18 kB │ gzip: 1.73 kB +[log] dist/assets/fileEdit.f8c91d0e.js  4.20 kB │ gzip: 1.78 kB +[log] dist/assets/index.vue_vue_type_script_setup_true_lang.e5956124.js  4.23 kB │ gzip: 1.93 kB +[log] dist/assets/index.a871bbe1.js  4.24 kB │ gzip: 1.82 kB +[log] dist/assets/material.fa1aad86.js  4.25 kB │ gzip: 1.55 kB +[log] dist/assets/editInvoice.aa511fc4.js  4.25 kB │ gzip: 1.59 kB +[log] dist/assets/index.d481ec95.js  4.26 kB │ gzip: 1.73 kB +[log] dist/assets/VerifyPoints.dfa6a80b.js  4.27 kB │ gzip: 2.01 kB +[log] dist/assets/index.967c2821.js  4.28 kB │ gzip: 1.71 kB +[log] dist/assets/form.9f7094a2.js  4.28 kB │ gzip: 1.83 kB +[log] dist/assets/index.3148ced9.js  4.29 kB │ gzip: 1.90 kB +[log] dist/assets/index.f29d3745.js  4.29 kB │ gzip: 1.89 kB +[log] dist/assets/coupon.970c6696.js  4.30 kB │ gzip: 1.92 kB +[log] dist/assets/index.6b2b3e9e.js  4.30 kB │ gzip: 1.76 kB +[log] dist/assets/index.3129a425.js  4.33 kB │ gzip: 1.77 kB +[log] dist/assets/index.27fc9c85.js  4.34 kB │ gzip: 1.63 kB +[log] dist/assets/option.a148a410.js  4.34 kB │ gzip: 1.92 kB +[log] dist/assets/file.vue_vue_type_style_index_0_lang.e14663d1.js  4.36 kB │ gzip: 2.14 kB +[log] dist/assets/index.630874da.js  4.37 kB │ gzip: 1.87 kB +[log] dist/assets/form.9cd03507.js  4.38 kB │ gzip: 1.57 kB +[log] dist/assets/index.fb8f4f49.js  4.40 kB │ gzip: 1.96 kB +[log] dist/assets/index.83f3ef66.js  4.41 kB │ gzip: 1.85 kB +[log] dist/assets/compareDialog.ba6986b9.js  4.41 kB │ gzip: 1.88 kB +[log] dist/assets/index.a1bb3887.js  4.42 kB │ gzip: 1.77 kB +[log] dist/assets/index.bdaf84ee.js  4.45 kB │ gzip: 1.78 kB +[log] dist/assets/index.87a5a552.js  4.48 kB │ gzip: 1.84 kB +[log] dist/assets/entry.b52361c1.js  4.50 kB │ gzip: 1.81 kB +[log] dist/assets/conditionDrawer.a51e8dd2.js  4.51 kB │ gzip: 1.91 kB +[log] dist/assets/index.53680423.js  4.52 kB │ gzip: 1.47 kB +[log] dist/assets/edit.c4d5632b.js  4.52 kB │ gzip: 1.72 kB +[log] dist/assets/index.8517ee0b.js  4.52 kB │ gzip: 1.70 kB +[log] dist/assets/main.e419fd02.js  4.52 kB │ gzip: 1.58 kB +[log] dist/assets/option.90423082.js  4.56 kB │ gzip: 1.96 kB +[log] dist/assets/index.c47f84c4.js  4.57 kB │ gzip: 1.83 kB +[log] dist/assets/index.61c94413.js  4.57 kB │ gzip: 1.85 kB +[log] dist/assets/index.65fad2ca.js  4.57 kB │ gzip: 1.80 kB +[log] dist/assets/edit.7fdc6870.js  4.58 kB │ gzip: 1.76 kB +[log] dist/assets/refund.42e8f505.js  4.59 kB │ gzip: 1.55 kB +[log] dist/assets/edit.f5cf7b7b.js  4.61 kB │ gzip: 2.03 kB +[log] dist/assets/edit.df95870f.js  4.64 kB │ gzip: 1.84 kB +[log] dist/assets/index.4f60bd46.js  4.64 kB │ gzip: 1.46 kB +[log] dist/assets/index.7993c964.js  4.64 kB │ gzip: 1.46 kB +[log] dist/assets/form.20d7694e.js  4.64 kB │ gzip: 1.48 kB +[log] dist/assets/priceEdit.d1d1cb9b.js  4.67 kB │ gzip: 1.76 kB +[log] dist/assets/index.3ab94f13.js  4.69 kB │ gzip: 1.87 kB +[log] dist/assets/form.2f10ff69.js  4.71 kB │ gzip: 1.67 kB +[log] dist/assets/info.f108c9d5.js  4.71 kB │ gzip: 1.92 kB +[log] dist/assets/vertical.03c8f40e.js  4.72 kB │ gzip: 1.76 kB +[log] dist/assets/edit.e63484ea.js  4.74 kB │ gzip: 1.96 kB +[log] dist/assets/printer.ccdc0622.js  4.76 kB │ gzip: 2.02 kB +[log] dist/assets/edit.0a32912f.js  4.76 kB │ gzip: 1.86 kB +[log] dist/assets/base.vue_vue_type_script_setup_true_lang.12ac6af9.js  4.77 kB │ gzip: 1.66 kB +[log] dist/assets/form.af0614de.js  4.77 kB │ gzip: 1.67 kB +[log] dist/assets/index.96df8d73.js  4.77 kB │ gzip: 1.90 kB +[log] dist/assets/index.b0e78564.js  4.77 kB │ gzip: 1.46 kB +[log] dist/assets/info.5469376e.js  4.78 kB │ gzip: 1.91 kB +[log] dist/assets/paramsEdit.822b445c.js  4.79 kB │ gzip: 1.81 kB +[log] dist/assets/payBox.vue_vue_type_script_setup_true_lang.f3e27a7a.js  4.80 kB │ gzip: 1.81 kB +[log] dist/assets/index.f1dc659d.js  4.82 kB │ gzip: 1.74 kB +[log] dist/assets/index.33bcc6e7.js  4.82 kB │ gzip: 1.92 kB +[log] dist/assets/index.a7dffd20.js  4.88 kB │ gzip: 1.93 kB +[log] dist/assets/index.56380dca.js  4.91 kB │ gzip: 1.92 kB +[log] dist/assets/index.19725494.js  4.95 kB │ gzip: 1.90 kB +[log] dist/assets/index.bee3a87c.js  4.95 kB │ gzip: 1.90 kB +[log] dist/assets/index.2f66e70e.js  4.96 kB │ gzip: 1.87 kB +[log] dist/assets/index.3fb60d74.js  4.97 kB │ gzip: 1.91 kB +[log] dist/assets/biaozhun.6a15e6cd.js  4.98 kB │ gzip: 1.95 kB +[log] dist/assets/index.87e046c4.js  4.99 kB │ gzip: 1.97 kB +[log] dist/assets/index.6609837a.js  4.99 kB │ gzip: 1.77 kB +[log] dist/assets/edit.475e132a.js  4.99 kB │ gzip: 2.02 kB +[log] dist/assets/index.0dfb388f.js  5.04 kB │ gzip: 2.03 kB +[log] dist/assets/edit.3fe655a1.js  5.04 kB │ gzip: 1.63 kB +[log] dist/assets/index.c594c760.js  5.07 kB │ gzip: 2.20 kB +[log] dist/assets/form.f16d5bc1.js  5.07 kB │ gzip: 1.78 kB +[log] dist/assets/jjEdit.072813ba.js  5.13 kB │ gzip: 1.97 kB +[log] dist/assets/edit.43d7fd76.js  5.13 kB │ gzip: 2.14 kB +[log] dist/assets/edit.37dcb754.js  5.13 kB │ gzip: 2.14 kB +[log] dist/assets/dialog.0ae2c99e.js  5.16 kB │ gzip: 2.10 kB +[log] dist/assets/selectBox.d3d62149.js  5.17 kB │ gzip: 2.13 kB +[log] dist/assets/index.8a7170c6.js  5.19 kB │ gzip: 2.01 kB +[log] dist/assets/index.6b0d0528.js  5.19 kB │ gzip: 1.91 kB +[log] dist/assets/index.0c5bc4a2.js  5.19 kB │ gzip: 1.99 kB +[log] dist/assets/auth.vue_vue_type_script_setup_true_lang.511bb2b8.js  5.23 kB │ gzip: 1.84 kB +[log] dist/assets/picker.6229b6d5.js  5.30 kB │ gzip: 2.10 kB +[log] dist/assets/cache.47c17bcf.js  5.31 kB │ gzip: 1.99 kB +[log] dist/assets/schedulingDialog.3c54d180.js  5.39 kB │ gzip: 2.13 kB +[log] dist/assets/index.04a86986.js  5.46 kB │ gzip: 2.18 kB +[log] dist/assets/Images.6728bbe4.js  5.46 kB │ gzip: 2.47 kB +[log] dist/assets/index.5f786884.js  5.49 kB │ gzip: 2.12 kB +[log] dist/assets/index.ced60b06.js  5.52 kB │ gzip: 2.22 kB +[log] dist/assets/index.47af5a42.js  5.53 kB │ gzip: 1.93 kB +[log] dist/assets/index.17ce154f.js  5.54 kB │ gzip: 1.53 kB +[log] dist/assets/form.70ca4a26.js  5.55 kB │ gzip: 1.76 kB +[log] dist/assets/edit.98490795.js  5.56 kB │ gzip: 1.80 kB +[log] dist/assets/index.29de8f5f.js  5.60 kB │ gzip: 2.06 kB +[log] dist/assets/index.e9f82990.js  5.62 kB │ gzip: 2.07 kB +[log] dist/assets/form.642c7584.js  5.66 kB │ gzip: 1.75 kB +[log] dist/assets/index.381ddd60.js  5.67 kB │ gzip: 2.10 kB +[log] dist/assets/infoDialog.1cd08f0a.js  5.69 kB │ gzip: 2.06 kB +[log] dist/assets/index.78960e35.js  5.74 kB │ gzip: 2.14 kB +[log] dist/assets/index.d957eecd.js  5.74 kB │ gzip: 2.08 kB +[log] dist/assets/form.0fd16f20.js  5.77 kB │ gzip: 2.12 kB +[log] dist/assets/index.5ea7e883.js  5.83 kB │ gzip: 2.14 kB +[log] dist/assets/index.7ab58e56.js  5.92 kB │ gzip: 2.31 kB +[log] dist/assets/index.0cb815ba.js  5.93 kB │ gzip: 2.13 kB +[log] dist/assets/index.147dd727.js  5.93 kB │ gzip: 2.18 kB +[log] dist/assets/index.ae36866f.js  5.97 kB │ gzip: 2.21 kB +[log] dist/assets/edit.927bfe6d.js  5.97 kB │ gzip: 1.68 kB +[log] dist/assets/index.7e2bac10.js  5.97 kB │ gzip: 2.28 kB +[log] dist/assets/coupon.vue_vue_type_script_setup_true_lang.87404349.js  6.00 kB │ gzip: 2.04 kB +[log] dist/assets/form.0a530f3e.js  6.01 kB │ gzip: 1.81 kB +[log] dist/assets/index.8ef8b686.js  6.09 kB │ gzip: 2.31 kB +[log] dist/assets/index.c5427bdb.js  6.09 kB │ gzip: 1.45 kB +[log] dist/assets/bcxyParams.vue_vue_type_style_index_0_lang.7fb3d7b7.js  6.26 kB │ gzip: 2.25 kB +[log] dist/assets/edit.9d677f97.js  6.29 kB │ gzip: 2.16 kB +[log] dist/assets/form.991406d1.js  6.30 kB │ gzip: 2.33 kB +[log] dist/assets/index.1fbd876b.js  6.32 kB │ gzip: 2.23 kB +[log] dist/assets/index.74169434.js  6.36 kB │ gzip: 2.31 kB +[log] dist/assets/edit.369e3efa.js  6.39 kB │ gzip: 2.64 kB +[log] dist/assets/craftDialog.127b60de.js  6.43 kB │ gzip: 2.35 kB +[log] dist/assets/index.6ab3b23f.js  6.48 kB │ gzip: 2.29 kB +[log] dist/assets/dataList.vue_vue_type_style_index_0_lang.7089dfa0.js  6.50 kB │ gzip: 1.71 kB +[log] dist/assets/credit.vue_vue_type_script_setup_true_lang.866fd025.js  6.53 kB │ gzip: 1.87 kB +[log] dist/assets/edit.5e4ebcc4.js  6.57 kB │ gzip: 2.54 kB +[log] dist/assets/index.7943fc35.js  6.58 kB │ gzip: 2.21 kB +[log] dist/assets/cc.6b66a6b3.js  6.63 kB │ gzip: 2.54 kB +[log] dist/assets/form.552f99a7.js  6.66 kB │ gzip: 2.16 kB +[log] dist/assets/VerifySlide.44a306ab.js  6.68 kB │ gzip: 2.58 kB +[log] dist/assets/completed.c335d17c.js  6.74 kB │ gzip: 2.58 kB +[log] dist/assets/index.cfb7dcf7.js  6.74 kB │ gzip: 2.32 kB +[log] dist/assets/condition.fdefa091.js  6.82 kB │ gzip: 1.84 kB +[log] dist/assets/orderProcessInfo.9e72c165.js  6.84 kB │ gzip: 2.04 kB +[log] dist/assets/edit.18b81b2b.js  6.89 kB │ gzip: 2.52 kB +[log] dist/assets/permession.2ea6a66b.js  6.90 kB │ gzip: 2.55 kB +[log] dist/assets/personal.5f9e4d55.js  6.94 kB │ gzip: 2.15 kB +[log] dist/assets/form.f64f68ba.js  6.98 kB │ gzip: 2.06 kB +[log] dist/assets/index.04925fe1.js  6.99 kB │ gzip: 2.28 kB +[log] dist/assets/index.6fcc3a4d.js  7.01 kB │ gzip: 2.49 kB +[log] dist/assets/approverDrawer.ddc3d4f2.js  7.06 kB │ gzip: 2.46 kB +[log] dist/assets/index.ece43338.js  7.08 kB │ gzip: 2.51 kB +[log] dist/assets/index.vue_vue_type_style_index_0_lang.455f97a4.js  7.08 kB │ gzip: 2.39 kB +[log] dist/assets/form.734a42ca.js  7.11 kB │ gzip: 2.13 kB +[log] dist/assets/editAddress.5dd8e699.js  7.17 kB │ gzip: 2.54 kB +[log] dist/assets/index.5ac8f99b.js  7.22 kB │ gzip: 2.62 kB +[log] dist/assets/news-form.23a0ac7a.js  7.27 kB │ gzip: 2.54 kB +[log] dist/assets/index.801ba859.js  7.31 kB │ gzip: 2.56 kB +[log] dist/assets/index.79d79566.js  7.36 kB │ gzip: 2.49 kB +[log] dist/assets/freightParams.vue_vue_type_script_setup_true_lang.4cfc7602.js  7.43 kB │ gzip: 2.76 kB +[log] dist/assets/checkForm.b914dfe9.js  7.47 kB │ gzip: 2.45 kB +[log] dist/assets/index.e05c9ed1.js  7.51 kB │ gzip: 2.65 kB +[log] dist/assets/form.74670d40.js  7.54 kB │ gzip: 2.18 kB +[log] dist/assets/started.0ac0b5ed.js  7.61 kB │ gzip: 2.82 kB +[log] dist/assets/submit.eca41cf2.js  7.63 kB │ gzip: 2.64 kB +[log] dist/assets/bc.5a7dc5c6.js  7.63 kB │ gzip: 2.32 kB +[log] dist/assets/user.1ce482d5.js  7.63 kB │ gzip: 2.85 kB +[log] dist/assets/index.35ecf307.js  7.65 kB │ gzip: 2.74 kB +[log] dist/assets/generator.860363df.js  7.74 kB │ gzip: 2.21 kB +[log] dist/assets/index.bf12d3e0.js  7.81 kB │ gzip: 2.34 kB +[log] dist/assets/index.1846a537.js  7.84 kB │ gzip: 2.79 kB +[log] dist/assets/index.ca17b9d9.js  7.84 kB │ gzip: 2.96 kB +[log] dist/assets/edit.32dc8221.js  7.92 kB │ gzip: 2.72 kB +[log] dist/assets/pending.236a7d68.js  7.97 kB │ gzip: 2.99 kB +[log] dist/assets/index.32df27a5.js  8.27 kB │ gzip: 2.46 kB +[log] dist/assets/all.015c4705.js  8.53 kB │ gzip: 2.97 kB +[log] dist/assets/edit.0f3989e2.js  8.62 kB │ gzip: 2.44 kB +[log] dist/assets/index.e18d7a08.js  8.67 kB │ gzip: 2.93 kB +[log] dist/assets/edit.c098e374.js  8.82 kB │ gzip: 2.91 kB +[log] dist/assets/params.41e4e837.js  8.88 kB │ gzip: 2.16 kB +[log] dist/assets/zkBase.vue_vue_type_script_setup_true_lang.8fa903d2.js  9.11 kB │ gzip: 2.56 kB +[log] dist/assets/index.f5d21336.js  9.21 kB │ gzip: 2.92 kB +[log] dist/assets/form.b1e5b468.js  9.23 kB │ gzip: 2.49 kB +[log] dist/assets/yqjBox.3d83e998.js  9.32 kB │ gzip: 2.85 kB +[log] dist/assets/pay.vue_vue_type_script_setup_true_lang.4849c213.js  9.36 kB │ gzip: 2.53 kB +[log] dist/assets/info.52d2d13d.js  9.48 kB │ gzip: 2.10 kB +[log] dist/assets/info.97dab059.js  9.48 kB │ gzip: 2.10 kB +[log] dist/assets/info.e0d02b0c.js  9.48 kB │ gzip: 2.10 kB +[log] dist/assets/index.05daab88.js  9.51 kB │ gzip: 2.86 kB +[log] dist/assets/scheduling.b7f001cf.js  9.53 kB │ gzip: 2.36 kB +[log] dist/assets/index.7489403c.js  9.58 kB │ gzip: 3.20 kB +[log] dist/assets/index.414f652f.js  9.70 kB │ gzip: 2.77 kB +[log] dist/assets/index.86a735fb.js  9.71 kB │ gzip: 2.91 kB +[log] dist/assets/edit.a6c4fb03.js  9.84 kB │ gzip: 3.29 kB +[log] dist/assets/index.50ed679c.js  9.87 kB │ gzip: 2.98 kB +[log] dist/assets/edit.5005ebbd.js  9.89 kB │ gzip: 3.07 kB +[log] dist/assets/form.3d6f1ab3.js  10.05 kB │ gzip: 3.11 kB +[log] dist/assets/info.97fc35b6.js  10.19 kB │ gzip: 3.06 kB +[log] dist/assets/edit.vue_vue_type_script_setup_true_lang.ce34e97c.js  10.25 kB │ gzip: 2.96 kB +[log] dist/assets/edit.fa3e3fc8.js  10.40 kB │ gzip: 3.35 kB +[log] dist/assets/index.0fc4f197.js  10.42 kB │ gzip: 3.55 kB +[log] dist/assets/info.f7dcfab0.js  10.67 kB │ gzip: 3.37 kB +[log] dist/assets/index.ea7cc47b.js  10.74 kB │ gzip: 3.35 kB +[log] dist/assets/index.3853dc98.js  10.80 kB │ gzip: 3.30 kB +[log] dist/assets/commonFunction.cfca4489.js  10.87 kB │ gzip: 4.01 kB +[log] dist/assets/tagsView.3ba8d8d6.js  11.08 kB │ gzip: 3.75 kB +[log] dist/assets/invoiceBox.vue_vue_type_style_index_0_lang.3ef76726.js  11.31 kB │ gzip: 3.08 kB +[log] dist/assets/edit.vue_vue_type_style_index_0_lang.f5bcf41f.js  11.36 kB │ gzip: 3.61 kB +[log] dist/assets/edit.6dfdb625.js  11.41 kB │ gzip: 2.70 kB +[log] dist/assets/nodeWrap.c6e6ddbc.js  11.49 kB │ gzip: 3.28 kB +[log] dist/assets/bomBox.vue_vue_type_style_index_0_lang.d7fb23ff.js  11.53 kB │ gzip: 3.46 kB +[log] dist/assets/index.768af43b.js  11.61 kB │ gzip: 3.27 kB +[log] dist/assets/index.19bbd2a4.js  11.74 kB │ gzip: 3.49 kB +[log] dist/assets/route.643e01d1.js  11.96 kB │ gzip: 4.30 kB +[log] dist/assets/check.b3eed923.js  12.10 kB │ gzip: 3.12 kB +[log] dist/assets/edit.d948719d.js  12.33 kB │ gzip: 3.54 kB +[log] dist/assets/params.a0c37f5b.js  12.47 kB │ gzip: 3.24 kB +[log] dist/assets/info.5d7e066e.js  12.54 kB │ gzip: 3.17 kB +[log] dist/assets/index.66e7cbb2.js  12.97 kB │ gzip: 3.87 kB +[log] dist/assets/index.f23d37d0.js  13.60 kB │ gzip: 2.88 kB +[log] dist/assets/edit.vue_vue_type_script_setup_true_lang.5b8d119a.js  13.86 kB │ gzip: 3.93 kB +[log] dist/assets/print.f4b402a6.js  14.45 kB │ gzip: 5.00 kB +[log] dist/assets/index.18ed627a.js  15.36 kB │ gzip: 3.98 kB +[log] dist/assets/edit.84cd7a59.js  15.90 kB │ gzip: 3.66 kB +[log] dist/assets/edit.8c285ebd.js  16.56 kB │ gzip: 3.92 kB +[log] dist/assets/processSchedulingDialog.d63eec89.js  16.83 kB │ gzip: 4.44 kB +[log] dist/assets/base.vue_vue_type_script_setup_true_lang.b610694b.js  16.91 kB │ gzip: 3.99 kB +[log] dist/assets/edit.41511838.js  17.05 kB │ gzip: 4.41 kB +[log] dist/assets/edit.6c5529b7.js  17.12 kB │ gzip: 5.09 kB +[log] dist/assets/edit.f89fcb5b.js  17.94 kB │ gzip: 4.30 kB +[log] dist/assets/baseEdit.d76f3cdf.js  18.38 kB │ gzip: 4.38 kB +[log] dist/assets/index.845bbea0.js  19.44 kB │ gzip: 6.11 kB +[log] dist/assets/pcbParams.vue_vue_type_style_index_0_lang.7e9e4cc4.js  19.82 kB │ gzip: 4.66 kB +[log] dist/assets/params.46fae15e.js  22.85 kB │ gzip: 3.49 kB +[log] dist/assets/edit.7b80064e.js  24.08 kB │ gzip: 5.57 kB +[log] dist/assets/edit.9bae65e2.js  24.19 kB │ gzip: 6.63 kB +[log] dist/assets/index.1c0a2e76.js  24.25 kB │ gzip: 4.12 kB +[log] dist/assets/goodsImport.989d6f85.js  25.16 kB │ gzip: 6.13 kB +[log] dist/assets/vue-qrcode.esm.e996f81e.js  25.59 kB │ gzip: 10.00 kB +[log] dist/assets/setings.da53f47f.js  26.66 kB │ gzip: 5.58 kB +[log] dist/assets/orderList.vue_vue_type_style_index_0_lang.969c69d5.js  26.78 kB │ gzip: 6.62 kB +[log] dist/assets/megerInquiryEdit.566f82e7.js  28.11 kB │ gzip: 8.41 kB +[log] dist/assets/index.c5c05b7f.js  28.30 kB │ gzip: 6.99 kB +[log] dist/assets/edit.vue_vue_type_style_index_0_lang.d7970b05.js  33.06 kB │ gzip: 6.11 kB +[log] dist/assets/pcbaParams.vue_vue_type_style_index_0_lang.36ecd482.js  36.03 kB │ gzip: 7.93 kB +[log] dist/assets/sortable.esm.b27f4248.js  36.36 kB │ gzip: 12.67 kB +[log] dist/assets/tenant.6d56938c.js  48.93 kB │ gzip: 14.14 kB +[log] dist/assets/vuedraggable.umd.02aa24ce.js  96.35 kB │ gzip: 33.70 kB +[log] dist/assets/index.aa3e887e.js  110.89 kB │ gzip: 48.19 kB +[log] dist/assets/vue.7210a7a0.js  122.49 kB │ gzip: 47.98 kB +[log] dist/assets/index.512dabe7.js  181.97 kB │ gzip: 61.11 kB +[log] dist/assets/html2canvas.esm.a81bcda4.js  203.82 kB │ gzip: 47.46 kB +[log] dist/assets/index.896fec12.js  844.99 kB │ gzip: 245.66 kB +[log] dist/assets/echarts.cd36a45b.js 1,036.55 kB │ gzip: 342.54 kB +[log] dist/assets/loadExcel.vue_vue_type_script_setup_true_lang.d88038d8.js 1,296.93 kB │ gzip: 356.70 kB +[log] dist/assets/vanilla-jsoneditor.cdb9801e.js 1,298.02 kB │ gzip: 382.25 kB +[log] dist/assets/tinymce.6f8be1fc.js 1,458.80 kB │ gzip: 466.73 kB +[log] dist/assets/index.ff154144.js 4,533.38 kB │ gzip: 1,277.94 kB +[warn]  +(!) Some chunks are larger than 1500 kBs after minification. Consider: +- Using dynamic import() to code-split the application +- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks +- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit. +[log] ✓ built in 2m 3s +[log] +✨ [vite-plugin-compression]:algorithm=gzip - compressed file successfully: +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/approval.43ddbe06.css.gz 28.13kb / gzip: 5.51kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/bomBox.vue_vue_type_style_index_0_lang.d7fb23ff.js.gz 11.26kb / gzip: 3.37kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/base.vue_vue_type_script_setup_true_lang.b610694b.js.gz 16.51kb / gzip: 3.89kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/addNode.f1421c96.css.gz 29.85kb / gzip: 5.71kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/baseEdit.d76f3cdf.js.gz 17.95kb / gzip: 4.26kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.6dfdb625.js.gz 11.14kb / gzip: 2.62kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.41511838.js.gz 16.65kb / gzip: 4.30kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/check.b3eed923.js.gz 11.82kb / gzip: 3.04kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/commonFunction.cfca4489.js.gz 10.62kb / gzip: 3.92kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.6c5529b7.js.gz 16.71kb / gzip: 4.97kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.7b80064e.js.gz 23.52kb / gzip: 5.43kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.84cd7a59.js.gz 15.52kb / gzip: 3.57kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.d948719d.js.gz 12.04kb / gzip: 3.46kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.vue_vue_type_script_setup_true_lang.5b8d119a.js.gz 13.54kb / gzip: 3.83kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.9bae65e2.js.gz 23.62kb / gzip: 6.47kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.fa3e3fc8.js.gz 10.15kb / gzip: 3.28kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.8c285ebd.js.gz 16.17kb / gzip: 3.82kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.vue_vue_type_script_setup_true_lang.ce34e97c.js.gz 10.01kb / gzip: 2.89kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.vue_vue_type_style_index_0_lang.f5bcf41f.js.gz 11.09kb / gzip: 3.52kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.vue_vue_type_style_index_0_lang.d7970b05.js.gz 32.29kb / gzip: 5.91kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/goodsImport.989d6f85.js.gz 24.57kb / gzip: 5.98kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/errorDialog.9af387e2.css.gz 27.98kb / gzip: 5.46kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/edit.f89fcb5b.js.gz 17.52kb / gzip: 4.20kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.0fc4f197.js.gz 10.18kb / gzip: 3.47kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.19bbd2a4.js.gz 11.46kb / gzip: 3.41kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.3853dc98.js.gz 10.55kb / gzip: 3.23kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.1c0a2e76.js.gz 23.68kb / gzip: 4.01kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.66e7cbb2.js.gz 12.67kb / gzip: 3.77kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.18ed627a.js.gz 15.00kb / gzip: 3.88kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.768af43b.js.gz 11.33kb / gzip: 3.19kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.845bbea0.js.gz 18.98kb / gzip: 5.97kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.bf199ca5.css.gz 29.80kb / gzip: 6.59kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.f23d37d0.js.gz 13.28kb / gzip: 2.81kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.ea7cc47b.js.gz 10.49kb / gzip: 3.27kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/info.f7dcfab0.js.gz 10.42kb / gzip: 3.29kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/info.5d7e066e.js.gz 12.24kb / gzip: 3.09kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.c5c05b7f.js.gz 27.64kb / gzip: 6.81kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/invoiceBox.vue_vue_type_style_index_0_lang.3ef76726.js.gz 11.05kb / gzip: 3.00kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/nodeWrap.7774c1fa.css.gz 28.13kb / gzip: 5.51kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/nodeWrap.c6e6ddbc.js.gz 11.22kb / gzip: 3.19kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/megerInquiryEdit.566f82e7.js.gz 27.45kb / gzip: 8.20kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/params.46fae15e.js.gz 22.31kb / gzip: 3.38kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/pcbParams.vue_vue_type_style_index_0_lang.7e9e4cc4.js.gz 19.36kb / gzip: 4.53kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/route.643e01d1.js.gz 11.68kb / gzip: 4.20kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/orderList.vue_vue_type_style_index_0_lang.969c69d5.js.gz 26.15kb / gzip: 6.45kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/params.a0c37f5b.js.gz 12.18kb / gzip: 3.16kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/processSchedulingDialog.d63eec89.js.gz 16.43kb / gzip: 4.32kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/tagsView.3ba8d8d6.js.gz 10.82kb / gzip: 3.66kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/print.f4b402a6.js.gz 14.11kb / gzip: 4.88kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/setings.da53f47f.js.gz 26.03kb / gzip: 5.44kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/pcbaParams.vue_vue_type_style_index_0_lang.36ecd482.js.gz 35.18kb / gzip: 7.71kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/sortable.esm.b27f4248.js.gz 35.51kb / gzip: 12.37kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/vue-qrcode.esm.e996f81e.js.gz 24.99kb / gzip: 9.76kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/step3.cfcd0a77.css.gz 27.68kb / gzip: 5.40kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/index.html.gz 13.03kb / gzip: 4.38kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/tenant.6d56938c.js.gz 47.78kb / gzip: 13.76kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide/content.css.gz 23.60kb / gzip: 5.67kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/styles/font-awesome.min.css.gz 30.27kb / gzip: 6.77kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/zh-Hans.js.gz 21.66kb / gzip: 6.89kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide/content.inline.min.css.gz 21.26kb / gzip: 5.38kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide/content.min.css.gz 21.32kb / gzip: 5.40kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide/content.inline.css.gz 23.52kb / gzip: 5.65kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide/skin.mobile.min.css.gz 20.51kb / gzip: 2.86kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide/skin.mobile.css.gz 24.04kb / gzip: 3.62kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide-dark/content.min.css.gz 20.95kb / gzip: 5.35kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide-dark/content.inline.min.css.gz 21.26kb / gzip: 5.38kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide-dark/content.css.gz 23.17kb / gzip: 5.63kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide-dark/content.inline.css.gz 23.52kb / gzip: 5.65kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide/skin.min.css.gz 60.10kb / gzip: 9.65kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide/skin.css.gz 71.31kb / gzip: 10.32kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide-dark/skin.mobile.css.gz 24.04kb / gzip: 3.62kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide-dark/skin.css.gz 71.17kb / gzip: 10.33kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide-dark/skin.min.css.gz 59.98kb / gzip: 9.66kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/tinymce/skins/ui/oxide-dark/skin.mobile.min.css.gz 20.51kb / gzip: 2.86kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/Verify.73a2a1a5.css.gz 51.53kb / gzip: 35.93kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.aa3e887e.js.gz 108.29kb / gzip: 46.96kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/html2canvas.esm.a81bcda4.js.gz 199.04kb / gzip: 46.05kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/vuedraggable.umd.02aa24ce.js.gz 94.09kb / gzip: 32.80kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/vue.7210a7a0.js.gz 119.62kb / gzip: 46.83kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.512dabe7.js.gz 177.70kb / gzip: 59.55kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.ca4efe7f.css.gz 534.33kb / gzip: 74.25kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.896fec12.js.gz 825.18kb / gzip: 239.19kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/echarts.cd36a45b.js.gz 1012.26kb / gzip: 333.79kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/vanilla-jsoneditor.cdb9801e.js.gz 1267.59kb / gzip: 372.26kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/tinymce.6f8be1fc.js.gz 1424.61kb / gzip: 453.00kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/loadExcel.vue_vue_type_script_setup_true_lang.d88038d8.js.gz 1266.54kb / gzip: 346.94kb +[log] dist//var/jenkins_home/workspace/web-v3-yunheng-adminui_test/assets/index.ff154144.js.gz 4427.16kb / gzip: 1243.46kb +[log] + +[Pipeline] } +[Pipeline] // script +[Pipeline] } +$ docker stop --time=1 bfcbc15ecfee4b137630e6d6289d5f1014a4230040634088941e736975dc9f2b +$ docker rm -f --volumes bfcbc15ecfee4b137630e6d6289d5f1014a4230040634088941e736975dc9f2b +[Pipeline] // withDockerContainer +[Pipeline] } +[Pipeline] // node +[Pipeline] } +[Pipeline] // stage +[Pipeline] stage +[Pipeline] { (Container Build) +[Pipeline] node +Running on jiangning-slave02-docker in /var/jenkins_home/workspace/web-v3-yunheng-adminui_test +[Pipeline] { +[Pipeline] script +[Pipeline] { +[Pipeline] echo +[INFO] standard container build +[Pipeline] echo +[INFO] The defaultHub registry is: jnhub.yunhengwang.net +[Pipeline] echo +[INFO] The defaultHub registryCredential is: jiangning-hub-credentials +[Pipeline] echo +[INFO] The defaultHub registry_project is: yhzzv3 +[Pipeline] sh ++ docker build -t web-yunheng-adminui:test-510 -f docker/Dockerfile --build-arg VAR_BASE_IMAGE=nginx:1.24.0 --build-arg VAR_PROFILE=test --build-arg VAR_PORT=8008 . +DEPRECATED: The legacy builder is deprecated and will be removed in a future release. + Install the buildx component to build images with BuildKit: + https://docs.docker.com/go/buildx/ + +Sending build context to Docker daemon 727.7MB +Step 1/11 : ARG VAR_BASE_IMAGE +Step 2/11 : ARG VAR_PROFILES +Step 3/11 : FROM ${VAR_BASE_IMAGE} as runner + ---> 7f0fd59e0094 +Step 4/11 : LABEL MAINTAINER=S"wangqi@yunhengzhizao.com" + ---> Using cache + ---> f4aeca270461 +Step 5/11 : ARG VAR_PROFILES + ---> Using cache + ---> 38e7712d0db3 +Step 6/11 : ENV PROFILES=${VAR_PROFILES} + ---> Using cache + ---> d2682761ee00 +Step 7/11 : EXPOSE 80 + ---> Using cache + ---> 65c3329cf17d +Step 8/11 : WORKDIR /data/webroot + ---> Using cache + ---> 7a0e2ed1ce45 +Step 9/11 : COPY ./dist ./dist/ + ---> 70004e04ac14 +Step 10/11 : RUN rm /etc/nginx/conf.d/default.conf + ---> Running in f56587848cda +Removing intermediate container f56587848cda + ---> 5d7ea7bc3546 +Step 11/11 : ADD docker/web.conf /etc/nginx/conf.d/default.conf + ---> 2f7b96efb84f +[Warning] One or more build-args [VAR_PORT VAR_PROFILE] were not consumed +Successfully built 2f7b96efb84f +Successfully tagged web-yunheng-adminui:test-510 +[Pipeline] echo +{"dev":["default"],"test":["default"],"uat":["acr"],"prod":["acr"]} +[Pipeline] echo +["default"] +[Pipeline] echo +default +[Pipeline] echo +[INFO] The registry hub is default +[Pipeline] sh ++ docker tag web-yunheng-adminui:test-510 yhzzv3/web-yunheng-adminui:test-510 +[Pipeline] withEnv +[Pipeline] { +[Pipeline] withDockerRegistry +$ docker login -u deploy -p ******** https://jnhub.yunhengwang.net +WARNING! Using --password via the CLI is insecure. Use --password-stdin. +WARNING! Your password will be stored unencrypted in /var/jenkins_home/workspace/web-v3-yunheng-adminui_test@tmp/ca490a21-bb73-45d3-8b20-d8474e64f727/config.json. +Configure a credential helper to remove this warning. See +https://docs.docker.com/engine/reference/commandline/login/#credentials-store + +Login Succeeded +[Pipeline] { +[Pipeline] isUnix +[Pipeline] withEnv +[Pipeline] { +[Pipeline] sh ++ docker tag yhzzv3/web-yunheng-adminui:test-510 jnhub.yunhengwang.net/yhzzv3/web-yunheng-adminui:test-510 +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] isUnix +[Pipeline] withEnv +[Pipeline] { +[Pipeline] sh ++ docker push jnhub.yunhengwang.net/yhzzv3/web-yunheng-adminui:test-510 +The push refers to repository [jnhub.yunhengwang.net/yhzzv3/web-yunheng-adminui] +d112351dde00: Preparing +44e340d27208: Preparing +36cb9ca59911: Preparing +b2b770215eb0: Preparing +34f2ddeba3a9: Preparing +0c47a9442648: Preparing +cfd3c53af539: Preparing +9dd63f53efa4: Preparing +b24e38f26f5b: Preparing +3c8879ab2cf2: Preparing +9dd63f53efa4: Waiting +b24e38f26f5b: Waiting +0c47a9442648: Waiting +cfd3c53af539: Waiting +3c8879ab2cf2: Waiting +b2b770215eb0: Layer already exists +34f2ddeba3a9: Layer already exists +0c47a9442648: Layer already exists +cfd3c53af539: Layer already exists +b24e38f26f5b: Layer already exists +9dd63f53efa4: Layer already exists +3c8879ab2cf2: Layer already exists +d112351dde00: Pushed +44e340d27208: Pushed +36cb9ca59911: Pushed +test-510: digest: sha256:178717664277e3b562d58d69f3d6740377b2dae0b3e612689f5034aa550df57c size: 2402 +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] } +[Pipeline] // withDockerRegistry +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] echo +[INFO] Docker Delete default Image +[Pipeline] sh ++ docker rmi yhzzv3/web-yunheng-adminui:test-510 +Untagged: yhzzv3/web-yunheng-adminui:test-510 ++ docker rmi jnhub.yunhengwang.net/yhzzv3/web-yunheng-adminui:test-510 +Untagged: jnhub.yunhengwang.net/yhzzv3/web-yunheng-adminui:test-510 +Untagged: jnhub.yunhengwang.net/yhzzv3/web-yunheng-adminui@sha256:178717664277e3b562d58d69f3d6740377b2dae0b3e612689f5034aa550df57c +[Pipeline] echo +[INFO] Docker Delete Image +[Pipeline] sh ++ docker rmi web-yunheng-adminui:test-510 +Untagged: web-yunheng-adminui:test-510 +Deleted: sha256:2f7b96efb84fe6af8e356dc8d4c4122699774ea332b758721c5f21cfbb191527 +Deleted: sha256:3eeaff88d0e8df76d092db6a87e4c2fc71847a9507478c6ea22f938b581b76cd +Deleted: sha256:5d7ea7bc3546ebe0eda3104a0dfe64d8218f1dfa1dfc059861f34a18c5ee88fd +Deleted: sha256:a3b99b2c58503683ba21d90b30b979e76066fc2fd8d3a3bda5fb739131ca8008 +Deleted: sha256:70004e04ac14bc04256f8174e2da2e84ae833f27df05941b197a5de1272892a2 +Deleted: sha256:e96f3a2f5fda051576c710f94d0b587bcea8a9dee6c10795afe5b4419d09a6bd +[Pipeline] } +[Pipeline] // script +[Pipeline] } +[Pipeline] // node +[Pipeline] } +[Pipeline] // stage +[Pipeline] stage +[Pipeline] { (Deploy) +[Pipeline] node +Running on jiangning-slave02-docker in /var/jenkins_home/workspace/web-v3-yunheng-adminui_test +[Pipeline] { +[Pipeline] withEnv +[Pipeline] { +[Pipeline] script +[Pipeline] { +[Pipeline] echo +[INFO] standard docker compose deploy +[Pipeline] echo +jenkins-ssh-152 +[Pipeline] echo +deployServerIP type: org.codehaus.groovy.runtime.GStringImpl +[Pipeline] echo +root@192.168.124.126 +[Pipeline] sshagent +[ssh-agent] Using credentials root (jenkins-ssh-152) +[ssh-agent] Looking for ssh-agent implementation... +[ssh-agent] Exec ssh-agent (binary ssh-agent on a remote machine) +$ ssh-agent +SSH_AUTH_SOCK=/tmp/ssh-1dr7PF3mH4Sl/agent.139690 +SSH_AGENT_PID=139692 +Running ssh-add (command line suppressed) +Identity added: /var/jenkins_home/workspace/web-v3-yunheng-adminui_test@tmp/private_key_18386475930054088165.key (root@jn-yunheng-master01) +[ssh-agent] Started. +[Pipeline] { +[Pipeline] sh ++ scp -o StrictHostKeyChecking=no .pipelines/scripts/deploy_env.sh root@192.168.124.126:/data/apps/yhsc/ ++ scp -o StrictHostKeyChecking=no .pipelines/scripts/docker-env-test.sh root@192.168.124.126:/data/apps/yhsc/.env ++ ssh -o StrictHostKeyChecking=no root@192.168.124.126 cd /data/apps/yhsc; sh ./deploy_env.sh deploy web-yunheng-adminui test-510 +---------------- +Pull Image +---------------- +WARNING! Your password will be stored unencrypted in /root/.docker/config.json. +Configure a credential helper to remove this warning. See +https://docs.docker.com/engine/reference/commandline/login/#credentials-store + +Login Succeeded +Error response from daemon: unknown: repository yhzz/web-yunheng-adminui not found + +---------------- +Delete Image +---------------- +c8e0387277e65799e534c2077265e781cbf96c89513d745174053a7008bb1fc2 +c8e0387277e65799e534c2077265e781cbf96c89513d745174053a7008bb1fc2 +Untagged: jnhub.yunhengwang.net/yhzzv3/web-yunheng-adminui:test-509 +Untagged: jnhub.yunhengwang.net/yhzzv3/web-yunheng-adminui@sha256:6855590f0be5c72ba4f5d36c5e5296594bda613ea8f0b89fd05940108cb78a37 +Deleted: sha256:8749738376fc4009c363aa8ce31eabfa2dc113c7fea214b11579a7d28c05529a +Deleted: sha256:42f250167d666c5d213a06b1e5e226893a75f6b1791f12c32a3bf1ea8d706f43 +Deleted: sha256:f65f54809c0168191f7e17895a4395cd87d64b3ce3d7d6090b5f4f69c19a65e0 +Deleted: sha256:ca5c88e4841efc10a310a18abc92f97c0ff41957b0620097b09a8df2c6378973 + +---------------- +Deploy Image +---------------- + web-yunheng-adminui Pulling + c0edef2937fa Already exists + 4fda39f0d0b0 Already exists + 697f6b054c93 Already exists + 9e4cc9a5ba09 Already exists + 55f9a23afd0b Already exists + 5981c066c964 Already exists + 432d9b009f38 Already exists + 109c5c017b7c Pulling fs layer + 9a4eb8bcc549 Pulling fs layer + 80586e23200d Pulling fs layer + 109c5c017b7c Downloading [> ] 97.38kB/9.374MB + 9a4eb8bcc549 Downloading [==================================================>] 182B/182B + 9a4eb8bcc549 Verifying Checksum + 9a4eb8bcc549 Download complete + 80586e23200d Downloading [==================================================>] 523B/523B + 80586e23200d Verifying Checksum + 80586e23200d Download complete + 109c5c017b7c Verifying Checksum + 109c5c017b7c Download complete + 109c5c017b7c Extracting [> ] 98.3kB/9.374MB + 109c5c017b7c Extracting [=======> ] 1.376MB/9.374MB + 109c5c017b7c Extracting [===============================> ] 5.997MB/9.374MB + 109c5c017b7c Extracting [==================================================>] 9.374MB/9.374MB + 109c5c017b7c Pull complete + 9a4eb8bcc549 Extracting [==================================================>] 182B/182B + 9a4eb8bcc549 Extracting [==================================================>] 182B/182B + 9a4eb8bcc549 Pull complete + 80586e23200d Extracting [==================================================>] 523B/523B + 80586e23200d Extracting [==================================================>] 523B/523B + 80586e23200d Pull complete + web-yunheng-adminui Pulled + Container web-yunheng-adminui Creating + Container web-yunheng-adminui Created + Container web-yunheng-adminui Starting + Container web-yunheng-adminui Started + +[Pipeline] } +$ ssh-agent -k +unset SSH_AUTH_SOCK; +unset SSH_AGENT_PID; +echo Agent pid 139692 killed; +[ssh-agent] Stopped. +[Pipeline] // sshagent +[Pipeline] } +[Pipeline] // script +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] } +[Pipeline] // node +[Pipeline] } +[Pipeline] // stage +[Pipeline] stage +[Pipeline] { (UAT Deploy) +Stage "UAT Deploy" skipped due to when conditional +[Pipeline] getContext +[Pipeline] } +[Pipeline] // stage +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] } +[Pipeline] // node +[Pipeline] End of Pipeline + +[钉钉插件]当前任务未配置机器人,已跳过 +[Gitea] Notifying branch build status: SUCCESS This commit looks good +[Gitea] Notified +Finished: SUCCESS diff --git a/document/#675.txt b/document/#675.txt new file mode 100644 index 0000000..d4e7f51 --- /dev/null +++ b/document/#675.txt @@ -0,0 +1,633 @@ +Started by user 刘阳 + +[钉钉插件]全局配置信息,{"proxyConfig":{"type":"HTTP","host":"","port":0},"verbose":true,"noticeOccasions":["SUCCESS","START","ABORTED","FAILURE","UNSTABLE"],"robotConfigs":[{"id":"bdd673c1-c779-4088-82be-b16624c53430","name":"【Jenkins构建消息通知】","webhook":{"value":"https://oapi.dingtalk.com/robot/send?access_token\u003d36238ae800104a95fc3e6e02ff12ccf471a55a47fcb9ad9501a7ddfb82d7a293","iv":[64,-55,-21,38,-93,-43,2,-46,75,-95,-32,-16,77,43,0,-117]},"securityPolicyConfigs":[{"type":"KEY","desc":"关键字","value":{"value":"发布","iv":[25,33,-25,-65,95,33,22,-43,47,93,35,43,-77,-77,-60,-63]}},{"type":"SECRET","desc":"加密","value":{"value":"","iv":[-19,96,-91,94,19,65,-82,-94,42,25,6,-33,-90,120,77,40]}}]}]} + +[钉钉插件]当前任务未配置机器人,已跳过 +Looking up repository yunheng/yunheng +Querying the current revision of branch test... +Current revision of branch test is 691a917303efbc4550d111f7cc9963216b087d4e +Obtained .jenkins/Jenkinsfile from 691a917303efbc4550d111f7cc9963216b087d4e +Loading library share-library@yunheng +Attempting to resolve yunheng from remote references... + > /usr/bin/git --version # timeout=10 + > git --version # 'git version 2.39.2' +using GIT_ASKPASS to set credentials gitee access token + > /usr/bin/git ls-remote -h -- https://gitee.com/lamefox/jenkins-share-library.git # timeout=10 +Found match: refs/heads/yunheng revision ed914f8c9a64514a1caf3747b13a7cd0ddc38222 +The recommended git tool is: NONE +using credential gitee-lamefox + > /usr/bin/git rev-parse --resolve-git-dir /var/jenkins_home/workspace/v3-yhzz-order-biz_test@libs/6e7b93324666b4dab5c7ec108dfb8a0401b885f9aef36f5f50cddf9142b4168d/.git # timeout=10 +Fetching changes from the remote Git repository + > /usr/bin/git config remote.origin.url https://gitee.com/lamefox/jenkins-share-library.git # timeout=10 +Using shallow fetch with depth 1 +Fetching upstream changes from https://gitee.com/lamefox/jenkins-share-library.git + > /usr/bin/git --version # timeout=10 + > git --version # 'git version 2.39.2' +using GIT_ASKPASS to set credentials gitee access token + > /usr/bin/git fetch --tags --force --progress --depth=1 -- https://gitee.com/lamefox/jenkins-share-library.git +refs/heads/*:refs/remotes/origin/* # timeout=10 +Checking out Revision ed914f8c9a64514a1caf3747b13a7cd0ddc38222 (yunheng) + > /usr/bin/git config core.sparsecheckout # timeout=10 + > /usr/bin/git checkout -f ed914f8c9a64514a1caf3747b13a7cd0ddc38222 # timeout=10 +Commit message: "add println" + > /usr/bin/git rev-list --no-walk ed914f8c9a64514a1caf3747b13a7cd0ddc38222 # timeout=10 +[Gitea] Notifying branch build status: PENDING Build started... +[Gitea] Notified +[Pipeline] Start of Pipeline +[Pipeline] node +Running on Jenkins in /var/jenkins_home/workspace/v3-yhzz-order-biz_test +[Pipeline] { +[Pipeline] stage +[Pipeline] { (Declarative: Checkout SCM) +[Pipeline] checkout +The recommended git tool is: NONE +using credential 03a74b6e-92de-429b-8d63-22fe15da43c2 + > /usr/bin/git rev-parse --resolve-git-dir /var/jenkins_home/workspace/v3-yhzz-order-biz_test/.git # timeout=10 +Fetching changes from the remote Git repository + > /usr/bin/git config remote.origin.url https://git.yunhengwang.net/yunheng/yunheng.git # timeout=10 +Fetching without tags +Fetching upstream changes from https://git.yunhengwang.net/yunheng/yunheng.git + > /usr/bin/git --version # timeout=10 + > git --version # 'git version 2.39.2' +using GIT_ASKPASS to set credentials git.yunhengwang.com + > /usr/bin/git fetch --no-tags --force --progress -- https://git.yunhengwang.net/yunheng/yunheng.git +refs/heads/test:refs/remotes/origin/test # timeout=10 +Checking out Revision 691a917303efbc4550d111f7cc9963216b087d4e (test) + > /usr/bin/git config core.sparsecheckout # timeout=10 + > /usr/bin/git checkout -f 691a917303efbc4550d111f7cc9963216b087d4e # timeout=10 +Commit message: "Merge branch 'v3.5.2' into test" + > /usr/bin/git rev-list --no-walk cf78f376978879cb862dbba22a3c50296be8a7d9 # timeout=10 +[Gitea] Notifying branch build status: PENDING Build started... +[Gitea] Notified +[Pipeline] } +[Pipeline] // stage +[Pipeline] withEnv +[Pipeline] { +[Pipeline] stage +[Pipeline] { (Setup Parameters & ENV) +[Pipeline] node +Running on jiangning-slave02-docker in /var/jenkins_home/workspace/v3-yhzz-order-biz_test +[Pipeline] { +[Pipeline] checkout +The recommended git tool is: NONE +using credential 03a74b6e-92de-429b-8d63-22fe15da43c2 +Fetching changes from the remote Git repository +Fetching without tags +Checking out Revision 691a917303efbc4550d111f7cc9963216b087d4e (test) +Commit message: "Merge branch 'v3.5.2' into test" +[Gitea] Notifying branch build status: PENDING Build started... +[Gitea] Notified +[Pipeline] withEnv +[Pipeline] { +[Pipeline] script +[Pipeline] { +[Pipeline] checkout +The recommended git tool is: NONE +using credential 03a74b6e-92de-429b-8d63-22fe15da43c2 +Fetching changes from the remote Git repository + > /usr/bin/git rev-parse --resolve-git-dir /var/jenkins_home/workspace/v3-yhzz-order-biz_test/.git # timeout=10 + > /usr/bin/git config remote.origin.url https://git.yunhengwang.net/yunheng/yunheng.git # timeout=10 +Fetching upstream changes from https://git.yunhengwang.net/yunheng/yunheng.git + > /usr/bin/git --version # timeout=10 + > git --version # 'git version 2.20.1' +using GIT_ASKPASS to set credentials git.yunhengwang.com + > /usr/bin/git fetch --no-tags --force --progress -- https://git.yunhengwang.net/yunheng/yunheng.git +refs/heads/test:refs/remotes/origin/test # timeout=10 + > /usr/bin/git config core.sparsecheckout # timeout=10 + > /usr/bin/git checkout -f 691a917303efbc4550d111f7cc9963216b087d4e # timeout=10 +Checking out Revision 405ec24f243830a7570aeeebd3cdb5cf774626c1 (refs/remotes/origin/master) +Commit message: "add digitize" + > /usr/bin/git rev-parse --resolve-git-dir /var/jenkins_home/workspace/v3-yhzz-order-biz_test/.pipelines/.git # timeout=10 + > /usr/bin/git config remote.origin.url https://git.yunhengwang.net/cicd/pipeline-files.git # timeout=10 +Fetching upstream changes from https://git.yunhengwang.net/cicd/pipeline-files.git + > /usr/bin/git --version # timeout=10 + > git --version # 'git version 2.20.1' +using GIT_ASKPASS to set credentials git.yunhengwang.com + > /usr/bin/git fetch --tags --force --progress -- https://git.yunhengwang.net/cicd/pipeline-files.git +refs/heads/*:refs/remotes/origin/* # timeout=10 + > /usr/bin/git rev-parse refs/remotes/origin/master^{commit} # timeout=10 + > /usr/bin/git config core.sparsecheckout # timeout=10 + > /usr/bin/git checkout -f 405ec24f243830a7570aeeebd3cdb5cf774626c1 # timeout=10 + > /usr/bin/git rev-list --no-walk 405ec24f243830a7570aeeebd3cdb5cf774626c1 # timeout=10 +[Gitea] Notifying branch build status: PENDING Build started... +[Gitea] Notified +[Pipeline] echo +[INFO] envFileName: .jenkins/.env.yml +[Pipeline] readYaml +[Pipeline] readYaml +[Pipeline] readYaml +[Pipeline] readJSON +[Pipeline] readJSON +[Pipeline] echo +[INFO] The branchConfig jenkinsAgent is: jiangning-docker +[Pipeline] echo +[INFO] The branchConfig mavenSet is: nexus-maven-yhzzv3-test +[Pipeline] fileExists +[Pipeline] readYaml +[Pipeline] echo +[INFO] The .env.yml file masterProfiles is: [, uat, prod] +[Pipeline] echo +[INFO] The .env.yml file Profiles is: [, dev] +[Pipeline] echo +[INFO] The .env.yml file jobConfigFile is: .pipelines/config-file/yunhengv3Config.yml +[Pipeline] echo +[INFO] The .env.yml file jobName is: v3-yhzz-order-biz +[Pipeline] echo +[INFO] The .env.yml file jenkinsAgent is: jiangning-docker +[Pipeline] echo +[INFO] The Default Registry Info: jnhub.yunhengwang.net/yhzzv3 +[Pipeline] echo +[INFO] The commonConfig registryInfo is: [default:[registry:jnhub.yunhengwang.net, registryCredential:jiangning-hub-credentials, registry_project:yhzzv3], acr:[registry:registry-vpc.cn-hangzhou.aliyuncs.com, registryCredential:aliyun-hub-credentials, registry_project:yhzzv3]] +[Pipeline] echo +[INFO] The commonConfig deployScript is: deploy_env.sh +[Pipeline] echo +[INFO] The commonConfig deployPath is: /data/apps/yhsc +[Pipeline] echo +[INFO] The commonConfig serverInfo is: [dev:[serverIP01:root@192.168.124.125, sshkey:jenkins-ssh-152], test:[serverIP01:root@192.168.124.126, sshkey:jenkins-ssh-152], uat:[serverIP01:[root@172.18.1.7], sshkey:jenkins-ssh-152], prod:[serverIP01:[root@172.18.1.5], sshkey:jenkins-ssh-152], newuat:[serverIP01:root@172.18.1.7, sshkey:jenkins-ssh-152], newprod:[serverIP01:root@172.18.1.5, sshkey:jenkins-ssh-152]] +[Pipeline] echo +[INFO] The commonConfig containerHub is: [dev:[default], test:[default], uat:[acr], prod:[acr]] +[Pipeline] echo +{"programLang":"java","programLangVersion":"8","containerName":"yhzz-order-biz","pomFilePath":"pom.xml","jarName":"yhzz-order-biz.jar","servicePath":"yhzz-order/yhzz-order-biz","port":"4003","javaOpts":"-server -Xms256m -Xmx4g -Xss1m","deployServerHost":"serverIP01","isApi":true,"apiProjectName":"yhzz-order-api","composeFile":"yhzz-docker-compose"} +[Pipeline] echo +[INFO] serviceData programLang: java/8 +[Pipeline] echo +[INFO] serviceData jarFile full path: yhzz-order/yhzz-order-biz/target/yhzz-order-biz.jar +[Pipeline] echo +[INFO] serviceData container info: yhzz-order-biz/4003 +[Pipeline] echo +[INFO] mvnDeploy is null +[Pipeline] echo +[INFO] compileModule default standard +[Pipeline] echo +[INFO] deployModule default standard +[Pipeline] echo +[INFO] The build image for the null branch is: lamefox/maven:3.8.8-jdk-8-dos +[Pipeline] echo +[INFO] The base image for the null branch is: lamefox/jre:8u202-rocky8-dos +[Pipeline] echo +[INFO] The serviceConfig programLang is: java +[Pipeline] echo +[INFO] The serviceConfig programLangVersion is: 8 +[Pipeline] echo +[INFO] The serviceConfig containerName is: yhzz-order-biz +[Pipeline] echo +[INFO] The serviceConfig pomFilePath is: pom.xml +[Pipeline] echo +[INFO] The serviceConfig jarName is: yhzz-order-biz.jar +[Pipeline] echo +[INFO] The serviceConfig servicePath is: yhzz-order/yhzz-order-biz +[Pipeline] echo +[INFO] The serviceConfig port is: 4003 +[Pipeline] echo +[INFO] The serviceConfig javaOpts is: -server -Xms256m -Xmx4g -Xss1m +[Pipeline] echo +[INFO] The serviceConfig deployServerHost is: serverIP01 +[Pipeline] echo +[INFO] The serviceConfig isApi is: true +[Pipeline] echo +[INFO] The serviceConfig apiProjectName is: yhzz-order-api +[Pipeline] echo +[INFO] The serviceConfig composeFile is: yhzz-docker-compose +[Pipeline] echo +[INFO] The serviceConfig jarFilePath is: yhzz-order/yhzz-order-biz/target +[Pipeline] echo +[INFO] The serviceConfig mvnDeploy is: isNull +[Pipeline] echo +[INFO] The serviceConfig compileModule is: standard +[Pipeline] echo +[INFO] The serviceConfig deployModule is: standard +[Pipeline] echo +[INFO] The serviceConfig buildImage is: lamefox/maven:3.8.8-jdk-8-dos +[Pipeline] echo +[INFO] The serviceConfig baseImage is: lamefox/jre:8u202-rocky8-dos +[Pipeline] echo +[INFO] The serviceConfig deployServerUser is: root +[Pipeline] echo +[INFO] The Maven Set XML for the test branch is: null +[Pipeline] properties +[Pipeline] echo +[INFO] deploy Server: root@192.168.124.126 +[Pipeline] } +[Pipeline] // script +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] } +[Pipeline] // node +[Pipeline] } +[Pipeline] // stage +[Pipeline] stage +[Pipeline] { (Compile API) +[Pipeline] node +Running on jiangning-slave02-docker in /var/jenkins_home/workspace/v3-yhzz-order-biz_test +[Pipeline] { +[Pipeline] isUnix +[Pipeline] withEnv +[Pipeline] { +[Pipeline] sh ++ docker inspect -f . lamefox/maven:3.8.8-jdk-8-dos +. +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] withDockerContainer +jiangning-slave02-docker seems to be running inside container 1deb1a89c78572d0c50c06179ee9d6136406ac06571d7d8a3d52d796bce78f88 +$ docker run -t -d -u 0:0 -v /data/apps/repository/maven/nexus-maven-yhzzv3-test:/root/.m2/repository -v /etc/hosts:/etc/hosts -w /var/jenkins_home/workspace/v3-yhzz-order-biz_test --volumes-from 1deb1a89c78572d0c50c06179ee9d6136406ac06571d7d8a3d52d796bce78f88 -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** lamefox/maven:3.8.8-jdk-8-dos cat +$ docker top 97cbae3fed6c63f6bbb17392084b544975feccad53920b0855655830eca7c734 -eo pid,comm +[Pipeline] { +[Pipeline] } +$ docker stop --time=1 97cbae3fed6c63f6bbb17392084b544975feccad53920b0855655830eca7c734 +$ docker rm -f --volumes 97cbae3fed6c63f6bbb17392084b544975feccad53920b0855655830eca7c734 +[Pipeline] // withDockerContainer +[Pipeline] } +[Pipeline] // node +Stage "Compile API" skipped due to when conditional +[Pipeline] getContext +[Pipeline] } +[Pipeline] // stage +[Pipeline] stage +[Pipeline] { (Compile Service) +[Pipeline] node +Running on jiangning-slave02-docker in /var/jenkins_home/workspace/v3-yhzz-order-biz_test +[Pipeline] { +[Pipeline] isUnix +[Pipeline] withEnv +[Pipeline] { +[Pipeline] sh ++ docker inspect -f . lamefox/maven:3.8.8-jdk-8-dos +. +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] withDockerContainer +jiangning-slave02-docker seems to be running inside container 1deb1a89c78572d0c50c06179ee9d6136406ac06571d7d8a3d52d796bce78f88 +$ docker run -t -d -u 0:0 -v /data/apps/repository/maven/nexus-maven-yhzzv3-test:/root/.m2/repository -v /etc/hosts:/etc/hosts -w /var/jenkins_home/workspace/v3-yhzz-order-biz_test --volumes-from 1deb1a89c78572d0c50c06179ee9d6136406ac06571d7d8a3d52d796bce78f88 -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** lamefox/maven:3.8.8-jdk-8-dos cat +$ docker top f61d402f2b8069e04bc65f33d653d8938b47004a6b5b886e3441d2eed12c6cbf -eo pid,comm +[Pipeline] { +[Pipeline] script +[Pipeline] { +[Pipeline] checkout +The recommended git tool is: NONE +using credential 03a74b6e-92de-429b-8d63-22fe15da43c2 +Warning: JENKINS-30600: special launcher org.jenkinsci.plugins.docker.workflow.WithContainerStep$Decorator$1@45ba3a56; decorates RemoteLauncher[hudson.remoting.Channel@446660e0:JNLP4-connect connection from nexus.yunhengwang.net/172.18.1.250:46936] will be ignored (a typical symptom is the Git executable not being run inside a designated container) +Fetching changes from the remote Git repository +Checking out Revision 405ec24f243830a7570aeeebd3cdb5cf774626c1 (refs/remotes/origin/master) +Commit message: "add digitize" +[Gitea] Notifying branch build status: PENDING Build started... +[Gitea] Notified +[Pipeline] echo +[INFO] Maven build operation with '-am -pl com.yunheng:yhzz-order-biz' +[Pipeline] echo +[INFO] build pom.xml is in yhzz-order/yhzz-order-biz/pom.xml +[Pipeline] sh ++ mvn -s .pipelines/jenkins-file/nexus-maven-yhzzv3-test.xml -f yhzz-order/yhzz-order-biz/pom.xml -P test -am -pl com.yunheng:yhzz-order-biz clean package -Dmaven.test.skip=true + > /usr/bin/git rev-parse --resolve-git-dir /var/jenkins_home/workspace/v3-yhzz-order-biz_test/.pipelines/.git # timeout=10 + > /usr/bin/git config remote.origin.url https://git.yunhengwang.net/cicd/pipeline-files.git # timeout=10 +Fetching upstream changes from https://git.yunhengwang.net/cicd/pipeline-files.git + > /usr/bin/git --version # timeout=10 + > git --version # 'git version 2.20.1' +using GIT_ASKPASS to set credentials git.yunhengwang.com + > /usr/bin/git fetch --tags --force --progress -- https://git.yunhengwang.net/cicd/pipeline-files.git +refs/heads/*:refs/remotes/origin/* # timeout=10 + > /usr/bin/git rev-parse refs/remotes/origin/master^{commit} # timeout=10 + > /usr/bin/git config core.sparsecheckout # timeout=10 + > /usr/bin/git checkout -f 405ec24f243830a7570aeeebd3cdb5cf774626c1 # timeout=10 +[INFO] Scanning for projects... +[INFO] +[INFO] ---------------------< com.yunheng:yhzz-order-biz >--------------------- +[INFO] Building yhzz-order-biz 3.0.0 +[INFO] from pom.xml +[INFO] --------------------------------[ jar ]--------------------------------- +[INFO] +[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ yhzz-order-biz --- +[INFO] Deleting /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/target +[INFO] +[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ yhzz-order-biz --- +[INFO] Using 'UTF-8' encoding to copy filtered resources. +[INFO] Copying 60 resources +[INFO] Copying 0 resource +[INFO] +[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ yhzz-order-biz --- +[INFO] Changes detected - recompiling the module! +[INFO] Compiling 289 source files to /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/target/classes +[WARNING] /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/src/main/java/com/yunheng/yhzz/order/wxpay/WXPayRefundReciver.java:[5,16] sun.misc.BASE64Decoder is internal proprietary API and may be removed in a future release +[WARNING] /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/src/main/java/com/yunheng/yhzz/order/wxpay/WXPayRefundReciver.java:[5,16] sun.misc.BASE64Decoder is internal proprietary API and may be removed in a future release +[WARNING] /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/src/main/java/com/yunheng/yhzz/order/wxpay/WXPayRefundReciver.java:[5,16] sun.misc.BASE64Decoder is internal proprietary API and may be removed in a future release +[WARNING] /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/src/main/java/com/yunheng/yhzz/order/wxpay/WXPayRefundReciver.java:[5,16] sun.misc.BASE64Decoder is internal proprietary API and may be removed in a future release +[WARNING] /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/src/main/java/com/yunheng/yhzz/order/wxpay/WXPayRefundReciver.java:[5,16] sun.misc.BASE64Decoder is internal proprietary API and may be removed in a future release +[WARNING] /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/src/main/java/com/yunheng/yhzz/order/wxpay/WXPayRefundReciver.java:[83,46] sun.misc.BASE64Decoder is internal proprietary API and may be removed in a future release +[INFO] /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/src/main/java/com/yunheng/yhzz/order/handler/ExcelMergeColumnHandler.java: /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/src/main/java/com/yunheng/yhzz/order/handler/ExcelMergeColumnHandler.java uses or overrides a deprecated API. +[INFO] /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/src/main/java/com/yunheng/yhzz/order/handler/ExcelMergeColumnHandler.java: Recompile with -Xlint:deprecation for details. +[INFO] /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/src/main/java/com/yunheng/yhzz/order/service/impl/YhOrderDeliveryServiceImpl.java: Some input files use unchecked or unsafe operations. +[INFO] /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/src/main/java/com/yunheng/yhzz/order/service/impl/YhOrderDeliveryServiceImpl.java: Recompile with -Xlint:unchecked for details. +[INFO] +[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ yhzz-order-biz --- +[INFO] Not copying test resources +[INFO] +[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ yhzz-order-biz --- +[INFO] Not compiling test sources +[INFO] +[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ yhzz-order-biz --- +[INFO] Tests are skipped. +[INFO] +[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ yhzz-order-biz --- +[INFO] Building jar: /var/jenkins_home/workspace/v3-yhzz-order-biz_test/yhzz-order/yhzz-order-biz/target/yhzz-order-biz.jar +[INFO] +[INFO] --- spring-boot-maven-plugin:2.7.16:repackage (default) @ yhzz-order-biz --- +[INFO] Replacing main artifact with repackaged archive +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 11.556 s +[INFO] Finished at: 2025-09-20T15:48:39+08:00 +[INFO] ------------------------------------------------------------------------ +[Pipeline] } +[Pipeline] // script +[Pipeline] } +$ docker stop --time=1 f61d402f2b8069e04bc65f33d653d8938b47004a6b5b886e3441d2eed12c6cbf +$ docker rm -f --volumes f61d402f2b8069e04bc65f33d653d8938b47004a6b5b886e3441d2eed12c6cbf +[Pipeline] // withDockerContainer +[Pipeline] } +[Pipeline] // node +[Pipeline] } +[Pipeline] // stage +[Pipeline] stage +[Pipeline] { (Container Build) +[Pipeline] node +Running on jiangning-slave02-docker in /var/jenkins_home/workspace/v3-yhzz-order-biz_test +[Pipeline] { +[Pipeline] script +[Pipeline] { +[Pipeline] echo +[INFO] standard container build +[Pipeline] echo +[INFO] The defaultHub registry is: jnhub.yunhengwang.net +[Pipeline] echo +[INFO] The defaultHub registryCredential is: jiangning-hub-credentials +[Pipeline] echo +[INFO] The defaultHub registry_project is: yhzzv3 +[Pipeline] sh ++ docker build -t yhzz-order-biz:test-675 -f docker/Dockerfile --build-arg VAR_BASE_IMAGE=lamefox/jre:8u202-rocky8-dos --build-arg VAR_PROFILE=test --build-arg VAR_PORT=4003 --build-arg VAR_JARFILE_PATH=yhzz-order/yhzz-order-biz/target --build-arg VAR_JARFILE=yhzz-order-biz.jar --build-arg VAR_JAVA_OPTS=-server -Xms256m -Xmx4g -Xss1m . +DEPRECATED: The legacy builder is deprecated and will be removed in a future release. + Install the buildx component to build images with BuildKit: + https://docs.docker.com/go/buildx/ + +Sending build context to Docker daemon 245.4MB +Step 1/18 : ARG VAR_BASE_IMAGE +Step 2/18 : ARG VAR_PROFILE +Step 3/18 : FROM ${VAR_BASE_IMAGE} + ---> 142fae67dcdb +Step 4/18 : LABEL MAINTAINER="wangqi@yunhengzhizao.com" + ---> Using cache + ---> 5bec5ac491e6 +Step 5/18 : ARG VAR_PORT + ---> Using cache + ---> 4c2a30a6e245 +Step 6/18 : ARG VAR_JARFILE + ---> Using cache + ---> 593c7a224b59 +Step 7/18 : ARG VAR_JARFILE_PATH + ---> Using cache + ---> c460737b4cc0 +Step 8/18 : ARG VAR_JAVA_OPTS + ---> Using cache + ---> 566a2b5bb2df +Step 9/18 : ARG VAR_PROFILE + ---> Using cache + ---> 1468a9c34746 +Step 10/18 : ENV HOST=0.0.0.0 + ---> Using cache + ---> 6f1dd79edb15 +Step 11/18 : ENV PORT=${VAR_PORT} + ---> Running in e26e05f9721f +Removing intermediate container e26e05f9721f + ---> cc5a3d73cdae +Step 12/18 : ENV JAVA_OPTS=${VAR_JAVA_OPTS:-"-server -Xms256m -Xmx1g -Xss1m"} + ---> Running in 7d7a7f0da5a6 +Removing intermediate container 7d7a7f0da5a6 + ---> f052a20eb669 +Step 13/18 : ENV JARFILE=${VAR_JARFILE} + ---> Running in 067ce0321676 +Removing intermediate container 067ce0321676 + ---> 04a9f13188fe +Step 14/18 : ENV PROFILE=${VAR_PROFILE} + ---> Running in 99c528c97bea +Removing intermediate container 99c528c97bea + ---> 07bacd0cb3d5 +Step 15/18 : EXPOSE ${PORT} + ---> Running in 7c784b7e6f07 +Removing intermediate container 7c784b7e6f07 + ---> 5bee5893179f +Step 16/18 : WORKDIR /opt/apps + ---> Running in 43637131c2cd +Removing intermediate container 43637131c2cd + ---> 7d082f3b2db4 +Step 17/18 : COPY ${VAR_JARFILE_PATH}/${VAR_JARFILE} ./ + ---> ebc0fce3d825 +Step 18/18 : CMD ["sh", "-c", "java ${JAVA_OPTS} -jar ${JARFILE} --spring.profiles.active=${PROFILE}"] + ---> Running in 0e4e2ef4bd1a +Removing intermediate container 0e4e2ef4bd1a + ---> 7330e4899f3e +Successfully built 7330e4899f3e +Successfully tagged yhzz-order-biz:test-675 +[Pipeline] echo +{"dev":["default"],"test":["default"],"uat":["acr"],"prod":["acr"]} +[Pipeline] echo +["default"] +[Pipeline] echo +default +[Pipeline] echo +[INFO] The registry hub is default +[Pipeline] sh ++ docker tag yhzz-order-biz:test-675 yhzzv3/yhzz-order-biz:test-675 +[Pipeline] withEnv +[Pipeline] { +[Pipeline] withDockerRegistry +$ docker login -u deploy -p ******** https://jnhub.yunhengwang.net +WARNING! Using --password via the CLI is insecure. Use --password-stdin. +WARNING! Your password will be stored unencrypted in /var/jenkins_home/workspace/v3-yhzz-order-biz_test@tmp/6fd57479-d0ca-4995-8240-f13adcb16f98/config.json. +Configure a credential helper to remove this warning. See +https://docs.docker.com/engine/reference/commandline/login/#credentials-store + +Login Succeeded +[Pipeline] { +[Pipeline] isUnix +[Pipeline] withEnv +[Pipeline] { +[Pipeline] sh ++ docker tag yhzzv3/yhzz-order-biz:test-675 jnhub.yunhengwang.net/yhzzv3/yhzz-order-biz:test-675 +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] isUnix +[Pipeline] withEnv +[Pipeline] { +[Pipeline] sh ++ docker push jnhub.yunhengwang.net/yhzzv3/yhzz-order-biz:test-675 +The push refers to repository [jnhub.yunhengwang.net/yhzzv3/yhzz-order-biz] +2f6680cd837c: Preparing +c52973a1a5e0: Preparing +db77fcebafe7: Preparing +1a50de55b916: Preparing +632b9386947c: Preparing +1a50de55b916: Layer already exists +c52973a1a5e0: Layer already exists +632b9386947c: Layer already exists +db77fcebafe7: Layer already exists +2f6680cd837c: Pushed +test-675: digest: sha256:f978e553f79db14d7afb30487a455044ee6fef1d3fde633f6cf68264b2ad1493 size: 1373 +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] } +[Pipeline] // withDockerRegistry +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] echo +[INFO] Docker Delete default Image +[Pipeline] sh ++ docker rmi yhzzv3/yhzz-order-biz:test-675 +Untagged: yhzzv3/yhzz-order-biz:test-675 ++ docker rmi jnhub.yunhengwang.net/yhzzv3/yhzz-order-biz:test-675 +Untagged: jnhub.yunhengwang.net/yhzzv3/yhzz-order-biz:test-675 +Untagged: jnhub.yunhengwang.net/yhzzv3/yhzz-order-biz@sha256:f978e553f79db14d7afb30487a455044ee6fef1d3fde633f6cf68264b2ad1493 +[Pipeline] echo +[INFO] Docker Delete Image +[Pipeline] sh ++ docker rmi yhzz-order-biz:test-675 +Untagged: yhzz-order-biz:test-675 +Deleted: sha256:7330e4899f3e5127dd5bdded7ab76ebb008f88df2dd3f9d38e1f2e1aa4ea23c8 +Deleted: sha256:ebc0fce3d825dbae8e6b049a34fefa5ea3129b7ab9693bdd17053054bbfd116d +Deleted: sha256:76ae5ef56e2bd24dd9e61e845fac30eb0150cc18b1761ee698d9ef2af8301ea2 +Deleted: sha256:7d082f3b2db41e78d58f7430973cc6db1a9eb19d46edde21152e0a7e2e545a2d +Deleted: sha256:5bee5893179f5ac48c998e51816da6be8ff57e46f35aa53383fd1314619fc100 +Deleted: sha256:07bacd0cb3d571cfa726bbd2a92e4f31f01a6362867612c6a0b70982c3cb3e66 +Deleted: sha256:04a9f13188febb58a19b4f3f8a08542916c64f86bbf23fa659e80c9a9a982069 +Deleted: sha256:f052a20eb669268a763c0f0f67b95ecafde503b0447e3f652f3d643908807791 +Deleted: sha256:cc5a3d73cdae27ff991be789c936f7088838c744380365df14aa2cac373ebf34 +[Pipeline] } +[Pipeline] // script +[Pipeline] } +[Pipeline] // node +[Pipeline] } +[Pipeline] // stage +[Pipeline] stage +[Pipeline] { (Deploy) +[Pipeline] node +Running on jiangning-slave02-docker in /var/jenkins_home/workspace/v3-yhzz-order-biz_test +[Pipeline] { +[Pipeline] withEnv +[Pipeline] { +[Pipeline] script +[Pipeline] { +[Pipeline] echo +[INFO] standard docker compose deploy +[Pipeline] echo +jenkins-ssh-152 +[Pipeline] echo +deployServerIP type: org.codehaus.groovy.runtime.GStringImpl +[Pipeline] echo +root@192.168.124.126 +[Pipeline] sshagent +[ssh-agent] Using credentials root (jenkins-ssh-152) +[ssh-agent] Looking for ssh-agent implementation... +[ssh-agent] Exec ssh-agent (binary ssh-agent on a remote machine) +$ ssh-agent +SSH_AUTH_SOCK=/tmp/ssh-OqZNXx2Yid6P/agent.142521 +SSH_AGENT_PID=142523 +Running ssh-add (command line suppressed) +Identity added: /var/jenkins_home/workspace/v3-yhzz-order-biz_test@tmp/private_key_3303668459101437157.key (root@jn-yunheng-master01) +[ssh-agent] Started. +[Pipeline] { +[Pipeline] sh ++ scp -o StrictHostKeyChecking=no .pipelines/scripts/deploy_env.sh root@192.168.124.126:/data/apps/yhsc/ ++ scp -o StrictHostKeyChecking=no .pipelines/scripts/docker-env-test.sh root@192.168.124.126:/data/apps/yhsc/.env ++ ssh -o StrictHostKeyChecking=no root@192.168.124.126 cd /data/apps/yhsc; sh ./deploy_env.sh deploy yhzz-order-biz test-675 +---------------- +Pull Image +---------------- +WARNING! Your password will be stored unencrypted in /root/.docker/config.json. +Configure a credential helper to remove this warning. See +https://docs.docker.com/engine/reference/commandline/login/#credentials-store + +Login Succeeded +Error response from daemon: unknown: artifact yhzz/yhzz-order-biz:test-675 not found + +---------------- +Delete Image +---------------- +81e8e527cbc54f58defda8036a5bc799b300b2fa040278a8dfdd1d903d999c81 +81e8e527cbc54f58defda8036a5bc799b300b2fa040278a8dfdd1d903d999c81 +Untagged: jnhub.yunhengwang.net/yhzzv3/yhzz-order-biz:test-674 +Untagged: jnhub.yunhengwang.net/yhzzv3/yhzz-order-biz@sha256:b150d04833da78e801cbbdec2b17d2fb01408b4ba8d9c6eef6c8fb08dbbd32db +Deleted: sha256:900537ba5c4856c7ad99087b798081fab117150395301a7eb74059ed2945c33b +Deleted: sha256:b5c15eec8bfb027978b57e91d154b11391650f6f26a6706ed7eff3c491f5ee19 + +---------------- +Deploy Image +---------------- + yhzz-order-biz Pulling + ff6ed60c2171 Already exists + c7b9e7473281 Already exists + 7b1b77d781a8 Already exists + 14ef921e8229 Already exists + 76d331e0635d Pulling fs layer + 76d331e0635d Downloading [> ] 539.7kB/145.4MB + 76d331e0635d Downloading [===> ] 11.35MB/145.4MB + 76d331e0635d Downloading [=======> ] 23.25MB/145.4MB + 76d331e0635d Downloading [============> ] 35.14MB/145.4MB + 76d331e0635d Downloading [================> ] 47.04MB/145.4MB + 76d331e0635d Downloading [====================> ] 58.93MB/145.4MB + 76d331e0635d Downloading [========================> ] 70.83MB/145.4MB + 76d331e0635d Downloading [============================> ] 82.72MB/145.4MB + 76d331e0635d Downloading [================================> ] 94.62MB/145.4MB + 76d331e0635d Downloading [====================================> ] 106.5MB/145.4MB + 76d331e0635d Downloading [========================================> ] 118.4MB/145.4MB + 76d331e0635d Downloading [============================================> ] 130.3MB/145.4MB + 76d331e0635d Downloading [================================================> ] 142.2MB/145.4MB + 76d331e0635d Verifying Checksum + 76d331e0635d Download complete + 76d331e0635d Extracting [> ] 557.1kB/145.4MB + 76d331e0635d Extracting [======> ] 19.5MB/145.4MB + 76d331e0635d Extracting [============> ] 37.32MB/145.4MB + 76d331e0635d Extracting [==================> ] 55.15MB/145.4MB + 76d331e0635d Extracting [=========================> ] 72.97MB/145.4MB + 76d331e0635d Extracting [===============================> ] 90.8MB/145.4MB + 76d331e0635d Extracting [=====================================> ] 109.2MB/145.4MB + 76d331e0635d Extracting [===========================================> ] 127.6MB/145.4MB + 76d331e0635d Extracting [=================================================> ] 144.8MB/145.4MB + 76d331e0635d Extracting [==================================================>] 145.4MB/145.4MB + 76d331e0635d Extracting [==================================================>] 145.4MB/145.4MB + 76d331e0635d Pull complete + yhzz-order-biz Pulled + Container yhzz-order-biz Creating + Container yhzz-order-biz Created + Container yhzz-order-biz Starting + Container yhzz-order-biz Started + +[Pipeline] } +$ ssh-agent -k +unset SSH_AUTH_SOCK; +unset SSH_AGENT_PID; +echo Agent pid 142523 killed; +[ssh-agent] Stopped. +[Pipeline] // sshagent +[Pipeline] } +[Pipeline] // script +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] } +[Pipeline] // node +[Pipeline] } +[Pipeline] // stage +[Pipeline] stage +[Pipeline] { (UAT Deploy) +[Pipeline] node +Running on jiangning-slave02-docker in /var/jenkins_home/workspace/v3-yhzz-order-biz_test +[Pipeline] { +[Pipeline] } +[Pipeline] // node +Stage "UAT Deploy" skipped due to when conditional +[Pipeline] getContext +[Pipeline] } +[Pipeline] // stage +[Pipeline] } +[Pipeline] // withEnv +[Pipeline] } +[Pipeline] // node +[Pipeline] End of Pipeline + +[钉钉插件]当前任务未配置机器人,已跳过 +[Gitea] Notifying branch build status: SUCCESS This commit looks good +[Gitea] Notified +Finished: SUCCESS diff --git a/document/Node基础镜像.zip b/document/Node基础镜像.zip new file mode 100644 index 0000000000000000000000000000000000000000..2d1ddd469ba91cadecd0f80151a52610d9de74b7 GIT binary patch literal 5451 zcmeI0c{o)4`^V43*kvhG_B|8Xw}&i|eaR$CMAnh)yGE8lcA>Fk%S2fc)q^ZaNc7lu z*~t<;8nS20Zzzw^!}sd@yMBNE&Rl2iIp_ME&*ydB_dTEceIJ-Q9zF@Mb2NnWUfccg z^NSpy11>pQ!pql+(%-yno#%IVf&id`mZz|tyOZcwQ|ZXh@u`!6(fqI@CxHO$2PfgP zfjijVba2Ja#teXLs>|{IHQPT!cBVavr6Jm78=$iUD}Vq%?-&3u0X#=c%LjMx;6vEk zxjS(IAlsK}AxBTYH`E7atX?B<(r4~D5!*n7s#q|QPqGGKfcd;`rsZe*6ke0TqNt4T zo}CZ|2OB2Tplho{eVNi70)dPA+~lmBt(~81i)b_lrC+fzK~^jn`SF&L?S`x-?VjYi zpUj+yL`=%@koq@*vMcE446g;|4@20K+?CLrJt^_RGAsHN;%C=XRE(%N9-;89iR3fo z0~w#B)P=;kKB8nK&?_;gP_y{}(U7d6dQF<^+&gbwUEeq=mP8iZ5Ugaa38OtZxqxc! zrAhDh7ZD3}2T#<|*fUhAc9NEzpn+i~qU-|;w;Lp4<$Jy^GP84t5~)LZ85=C#vLYy1 zC+Oi|4+I&($cgFLw0CpptcnSz4Ig~L<*FRX%GB20fadMd_8x-E$Ci<2^ys)*%ddhu zj&0sMUz}iPm>IFOxh2`S@a40!kEh>;Drz?a_CwVzgf}XW4IB)6vI2ZZgi7s4CRp9H zgS$I%1E48cE&ijUg)`dqYrO`y+PT~=7=Me(IkoO)eQaAU1k|Z;3V)9i6q36YR)jk*)YYkbTGwa;9zFO%n5Mb1_J;K0|3whXAZ~xfyoAd zLN)u$j>Oyca4t5KQoQ@9|U-y~sbv4NnLE$reIwVROIN zm^%T_xZk!tUZ*~yMR|R-Z{W;R)dwwG{fT#w^ zE;RtGUp-YuP#2=At{_QibQk_<=AGO$2HXJexM)DCxSHOkL26*3O(^5K%tW87Ja*#U z^wma4?}}(*Xuq>8*St%^wXXM5FN?o;Nz?kqZE|Z;7KWmtl6@j2qeeqjdz9~LSL^mV z$`;SiHtW!{$Il3(c}khx=I~jWR0WAN?6?ZwBx}ifO3N=I<`!I>#?z?1#qUuMgJMB5 zcT}{43<+O9jl63to6k=R1$}H_=>TH~PDfK!i>X)+SBw+d3$PgW#D8IdltDe_72Nvc zjyJHEm937h7~Gz68fhP6Ksc#N*lK$v9z(@e#&+s@S9{dW)LWHlDhti7cyReYrjpPr z9&pLyX^o*s9iZ^+FDE2^dl{iaGRflPT#YgE8}p2e+oX|{8brJvkvTu?rw~<8n)>8n zn~u3S%<%YPLhN_78sjjg{9O1rx441_sV6 zZ5ZMgbvGU4#KtGjM0*j1*dZCLhu+=@S*<=rEwap4U`9rE46c;eqTZLMq%9?Mv|QsdLt6tfS11XY5BE}o?d((ncj)pSq4cT$(r7|_2h{(vxyY< zc^LHIG21li3EMdaPJXP)y&or)n1z{b|FkT{m=8XwR4GM9Cphj53BAcHnYaOMr|QTG z%JtCBO@wN#rqaY!-%08k=vR9c1CfS=tP7G_6xPFxR$humb`AxvI2ZUVkA=vl`E4wu z@cV9mzb~5l^^W37dkvxI?C^-}^%|5sres>YE?(pBc*E=vNU%B^+R{#61auN$wS@zDl?a-vyywMO;e9B$($ zTH~(G@&ujckirko;EQ>!wKqP^n@~LjEd*2P5&CO)`1B`N1ziB0>W`tHj!FHL@veR% zYqS4!-S~)@zRy=$V`1s(hmGbzv00GOM_CuwCWMkxM>_A^N7Cmz@g`2Vda5plT&i&=p#wGkho?HA) zskO^Tvzc|}mY%G_?&)`|nja|X(_oE(Lj7#H>-wR4Bx7PH8?9)b}Q;S+-9&v&awoOC&AA6Z(~zl-s5@PI3l6wf_^r+C}kXquNRF*FHG=4uJo4XI=wQygD) zcw5~v4@yjcpGvx#9-x%tRdQh^opoz4LT;936tYA(+D_g4)VP8qS+&${D^>B*J%(H2 zx`QUSjIVNXOe$5CDi^9R#lGSe%Jf>~WVHJbnDt)7wg9T#Y^GJVHTG9sM?$xOvGTh9d$^68o;&e|dm61)CtF~Co%4tsfzy|Xg zT68}vdS;NZW6 z2Ynx*hgVV?7j8 zOcR72e5owg++dOU<}^2XKK1tTSm9LRGP*+4b5vyWA7t9BwIZqrVu5(Jn#I%)fQtZ<7o^E%Y#tbkFJT7hmc!q!p7t6Z$OFMr^H%jHSQ1#$G0D}(8hvG6DpQWlai zam$^ULVma|Od(en=4P8#;R-Tk3kNIdl%#xhh5og{;1kkyC%ixFUNWWa4?}XQ?5~kLi}5=5m=UAU z6)F=FZ}`Z-R+Et?G~`t5ht>J7LNc;<(Mqucv5&W(k*nMp0 zp!{JD#*_qL*}V+{P6MpszSGehgP=XjBZC~@x&BWr_|XeP0Og+x?kuc`J$G{BpGx>o zzl}@q+QMg-V9#cPZ|`3JR@=UVvfm9JY}~beqOf3X>X9gWy*VzDf2#K`!kz_QCgO{- z?KtJPTYZ@NL9~O7sx}UqGWIRQ;aHeG6_E*4zt*IIg;L1^eJ7o|T zEWi%ie_?ZH&+dc$PBZ%h=/dev/null || \ + log "警告: 删除某些容器时出错(可能正在使用中)" + else + log "未找到已退出的容器" + fi + fi + + # 清理悬空镜像 + if [ "$CLEAN_DANGLING_IMAGES" = true ]; then + log "清理悬空镜像..." + confirm_action "docker image prune -f" && \ + docker image prune -f + fi + + # 清理未使用的镜像 + if [ "$CLEAN_UNUSED_IMAGES" = true ]; then + log "清理未被使用的镜像..." + unused_images=$(docker images -q --filter "dangling=false") + if [ -n "$unused_images" ]; then + log "找到未使用的镜像,建议手动检查后再删除" + # 更安全的做法是手动确认,这里只做提示 + fi + fi + + # 清理构建缓存 + if [ "$CLEAN_BUILD_CACHE" = true ]; then + log "清理构建缓存..." + confirm_action "docker builder prune -f" && \ + docker builder prune -f + fi + + # 清理未使用的网络 + if [ "$CLEAN_UNUSED_NETWORKS" = true ]; then + log "清理未使用的网络..." + confirm_action "docker network prune -f" && \ + docker network prune -f + fi + + # 清理悬空卷(危险操作,默认关闭) + if [ "$CLEAN_DANGLING_VOLUMES" = true ]; then + log "警告:即将清理悬空卷,这可能删除重要数据!" + read -p "确认继续?(y/N): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + confirm_action "docker volume prune -f" && \ + docker volume prune -f + else + log "已跳过卷清理" + fi + fi + + # 使用 system prune 进行最终清理(相对安全) + log "执行系统清理..." + confirm_action "docker system prune -f" && \ + docker system prune -f + + log "清理完成后的 Docker 磁盘使用情况:" + docker system df + log "Docker 磁盘清理完成!" +} + +# 显示帮助信息 +show_help() { + cat << EOF +使用方式: $0 [选项] + +选项: + -d, --dry-run 干运行模式,只显示将要执行的操作 + -v, --verbose 详细输出模式 + -h, --help 显示此帮助信息 + +示例: + $0 # 正常执行清理 + $0 --dry-run # 只显示将要执行的操作而不实际执行 +EOF +} + +# 解析命令行参数 +while [[ $# -gt 0 ]]; do + case $1 in + -d|--dry-run) + DRY_RUN=true + shift + ;; + -v|--verbose) + set -x + shift + ;; + -h|--help) + show_help + exit 0 + ;; + *) + log "错误: 未知选项 $1" + show_help + exit 1 + ;; + esac +done + +# 检查 Docker 是否可用 +if ! command -v docker &> /dev/null; then + log "错误: Docker 未安装或未在 PATH 中找到" + exit 1 +fi + +# 检查是否具有 Docker 权限 +if ! docker info &> /dev/null; then + log "错误: 无 Docker 访问权限或 Docker 守护进程未运行" + exit 1 +fi + +# 执行主函数 +clean_docker + +exit 0 \ No newline at end of file diff --git a/resources/scripts/health-check.sh b/resources/scripts/health-check.sh new file mode 100644 index 0000000..e69de29 diff --git a/resources/scripts/jenkins-cleanup.sh b/resources/scripts/jenkins-cleanup.sh new file mode 100644 index 0000000..af9ed23 --- /dev/null +++ b/resources/scripts/jenkins-cleanup.sh @@ -0,0 +1,243 @@ +#!/bin/bash + +# Jenkins Workspace Cleanup Script +# Description: 安全清理Jenkins工作区,保留最近构建和重要文件 +# Author: System Admin +# Date: $(date +%Y-%m-%d) + +set -euo pipefail + +# 配置变量 - 根据实际情况修改 +JENKINS_HOME="${JENKINS_HOME:-/usr/local/docker/jenkins/jenkins_home}" # Jenkins主目录 +WORKSPACE_DIR="${JENKINS_HOME}/workspace" # 工作区目录 +DRY_RUN=false # 干运行模式 +RETENTION_DAYS=1 # 保留最近N天的构建 +MIN_FREE_SPACE_GB=20 # 最小保留磁盘空间(GB) +EXCLUDE_PATTERNS=(".*/builds/.*" ".*/lastStable.*" ".*/lastSuccessful.*" ".*/nextBuildNumber.*") # 排除模式 +LOG_FILE="/var/log/jenkins-cleanup.log" # 日志文件 + +# 日志函数 +log() { + local message="$1" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "[${timestamp}] ${message}" | tee -a "$LOG_FILE" +} + +# 检查目录是否存在 +check_directories() { + if [ ! -d "$JENKINS_HOME" ]; then + log "错误: Jenkins目录不存在: $JENKINS_HOME" + exit 1 + fi + + if [ ! -d "$WORKSPACE_DIR" ]; then + log "错误: 工作区目录不存在: $WORKSPACE_DIR" + exit 1 + fi +} + +# 检查磁盘空间 +check_disk_space() { + local available_space_gb=$(df -BG "$JENKINS_HOME" | awk 'NR==2 {print $4}' | sed 's/G//') + + if [ "$available_space_gb" -ge "$MIN_FREE_SPACE_GB" ]; then + log "磁盘空间充足 (${available_space_gb}GB可用),跳过清理" + return 1 + fi + + log "磁盘空间不足 (${available_space_gb}GB可用),需要清理" + return 0 +} + +# 安全删除目录 +safe_delete() { + local dir_path="$1" + + if [ "$DRY_RUN" = true ]; then + log "干运行: 将删除目录 - ${dir_path}" + return 0 + fi + + # 检查目录是否重要(包含排除模式) + for pattern in "${EXCLUDE_PATTERNS[@]}"; do + if [[ "$dir_path" =~ $pattern ]]; then + log "跳过重要目录: ${dir_path}" + return 0 + fi + done + + # 检查目录是否在最近修改时间内 + local last_modified=$(stat -c %Y "$dir_path") + local current_time=$(date +%s) + local age_days=$(( (current_time - last_modified) / 86400 )) + + if [ "$age_days" -le "$RETENTION_DAYS" ]; then + log "跳过最近修改的目录 (${age_days}天前): ${dir_path}" + return 0 + fi + + # 执行删除 + if rm -rf "$dir_path" 2>/dev/null; then + log "已删除: ${dir_path} (${age_days}天未修改)" + return 0 + else + log "警告: 删除失败: ${dir_path}" + return 1 + fi +} + +# 清理工作区 +clean_workspace() { + log "开始清理Jenkins工作区..." + log "工作区目录: ${WORKSPACE_DIR}" + log "保留最近 ${RETENTION_DAYS} 天的构建" + log "最小保留空间: ${MIN_FREE_SPACE_GB}GB" + + local total_size_before=$(du -sh "$WORKSPACE_DIR" 2>/dev/null | cut -f1) + log "清理前工作区大小: ${total_size_before}" + + # 查找并清理旧的工作区目录 + local cleaned_count=0 + local error_count=0 + + find "$WORKSPACE_DIR" -maxdepth 2 -type d -name "*" | while read -r dir_path; do + # 跳过根目录和一级目录 + if [ "$dir_path" = "$WORKSPACE_DIR" ] || [ "$(dirname "$dir_path")" = "$WORKSPACE_DIR" ]; then + continue + fi + + if safe_delete "$dir_path"; then + ((cleaned_count++)) || true + else + ((error_count++)) || true + fi + done + + local total_size_after=$(du -sh "$WORKSPACE_DIR" 2>/dev/null | cut -f1) + + log "清理完成: 删除了 ${cleaned_count} 个目录, ${error_count} 个错误" + log "清理后工作区大小: ${total_size_after}" + log "释放空间: $(echo "$total_size_before -> $total_size_after")" +} + +# 清理构建历史(可选) +clean_builds() { + local builds_dir="${JENKINS_HOME}/jobs" + local max_builds_to_keep=20 # 每个作业保留的最大构建数 + + log "开始清理旧的构建历史..." + + find "$builds_dir" -name "builds" -type d 2>/dev/null | while read -r builds_path; do + local job_dir=$(dirname "$builds_path") + local job_name=$(basename "$job_dir") + + # 获取所有构建目录并按编号排序 + local build_dirs=$(find "$builds_path" -maxdepth 1 -type d -name "[0-9]*" | sort -Vr) + local build_count=$(echo "$build_dirs" | wc -l) + + if [ "$build_count" -gt "$max_builds_to_keep" ]; then + local to_delete_count=$((build_count - max_builds_to_keep)) + local deleted_count=0 + + echo "$build_dirs" | tail -n "$to_delete_count" | while read -r build_dir; do + if safe_delete "$build_dir"; then + ((deleted_count++)) || true + fi + done + + log "作业 ${job_name}: 保留 ${max_builds_to_keep} 个构建,删除 ${deleted_count} 个旧构建" + fi + done +} + +# 显示帮助信息 +show_help() { + cat << EOF +使用方式: $0 [选项] + +选项: + -d, --dry-run 干运行模式,只显示将要执行的操作 + -a, --all 清理工作区和构建历史 + -w, --workspace 只清理工作区(默认) + -b, --builds 只清理构建历史 + -r DAYS, --retention-days DAYS 设置保留天数(默认: 7) + -h, --help 显示此帮助信息 + +示例: + $0 # 默认清理工作区 + $0 --dry-run # 干运行模式 + $0 --all # 清理工作区和构建历史 + $0 --retention-days 30 # 保留最近30天的构建 +EOF +} + +# 解析命令行参数 +MODE="workspace" + +while [[ $# -gt 0 ]]; do + case $1 in + -d|--dry-run) + DRY_RUN=true + shift + ;; + -a|--all) + MODE="all" + shift + ;; + -w|--workspace) + MODE="workspace" + shift + ;; + -b|--builds) + MODE="builds" + shift + ;; + -r|--retention-days) + RETENTION_DAYS="$2" + shift 2 + ;; + -h|--help) + show_help + exit 0 + ;; + *) + log "错误: 未知选项 $1" + show_help + exit 1 + ;; + esac +done + +# 主执行函数 +main() { + log "=== Jenkins 工作区清理开始 ===" + log "模式: ${MODE}, 干运行: ${DRY_RUN}, 保留天数: ${RETENTION_DAYS}" + + check_directories + + # 检查磁盘空间,如果充足且不是强制清理,则跳过 + if ! check_disk_space && [ "$MODE" != "all" ]; then + log "磁盘空间充足,跳过清理" + return 0 + fi + + case "$MODE" in + "workspace") + clean_workspace + ;; + "builds") + clean_builds + ;; + "all") + clean_workspace + clean_builds + ;; + esac + + log "=== Jenkins 工作区清理完成 ===" +} + +# 执行主函数 +main + +exit 0 \ No newline at end of file diff --git a/resources/scripts/setup-env.sh b/resources/scripts/setup-env.sh new file mode 100644 index 0000000..e69de29 diff --git a/resources/templates/build_email.html b/resources/templates/build_email.html new file mode 100644 index 0000000..06cb780 --- /dev/null +++ b/resources/templates/build_email.html @@ -0,0 +1,475 @@ + + + + + + Jenkins构建通知 - ${projectName} + + + +
+
+
+ ${statusEmoji} ${statusText} +
+

${projectName}

+

构建 #${buildNumber} • ${buildTime} • 耗时 ${buildDuration}

+
+ +
+ +
+

+ + + + 构建摘要 +

+ +
+
+
${totalStages}
+
总Stage数
+
+
+
${successStages}
+
成功Stage数
+
+
+
${failedStages}
+
失败Stage数
+
+
+
${successRate}%
+
成功率
+
+
+ +
+
+ 项目名称 + ${projectName} +
+
+ 构建状态 + + ${badgeText} + +
+
+ 构建编号 + #${buildNumber} +
+
+ 构建ID + ${buildId} +
+
+ 构建时间 + ${buildTime} +
+
+ 构建时长 + ${buildDuration} +
+
+ 环境 + + ${profiles} + +
+
+ 构建节点 + ${nodeName} +
+
+
+ + +
+

+ + + + Git信息 +

+ +
+
+ 代码分支 + ${gitBranch} +
+
+ 提交版本 + ${gitCommitShort} +
+
+ 完整提交ID + ${gitCommit} +
+
+ 代码库 + ${gitUrl} +
+
+
+ + +
+

构建产物

+ +
+
+ 镜像名称 + ${imageFullName} +
+
+ 镜像标签 + ${imageTags} +
+
+ +

部署信息

+ +
+
+ 部署策略 + ${deploymentStrategy} +
+
+ 健康检查 + + ${healthCheckStatus} + +
+
+
+ + +
+

+ + + + 构建时间线 +

+ +
+
+
代码检出
+
从 ${gitBranch} 分支成功检出代码
+
+ ${stagesContent} +
+
+ + +
+ 查看构建详情 +

点击查看完整的构建日志和详细信息

+
+
+ + +
+ + \ No newline at end of file diff --git a/src/com/common/DingTalkService.groovy b/src/com/common/DingTalkService.groovy new file mode 100644 index 0000000..cae8ed5 --- /dev/null +++ b/src/com/common/DingTalkService.groovy @@ -0,0 +1,738 @@ +package com.common + +import groovy.json.JsonOutput +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import java.net.URLEncoder +import java.util.Base64 + +/** + * 钉钉机器人消息发送工具类 + * 支持多种消息类型:文本、链接、Markdown、ActionCard、FeedCard等 + * 包含重试机制、签名验证、异常处理等增强功能 + * 使用前需要在钉钉群添加自定义机器人并获取webhook地址和密钥 + */ +class DingTalkService implements Serializable { + // Jenkins脚本实例,用于执行Jenkins相关操作 + private final script + // 管道参数 + private final pipelineParams + // 是否@所有人,默认false + private final atAll + // 需要@的手机号列表,默认空列表 + private final atMobiles + // 需要@的用户ID列表,默认空列表 + private final atUserIds + // 消息类型,默认markdown + private final msgType + // 最大重试次数,默认3次 + private final maxRetries + // 重试间隔(毫秒),默认1000ms + private final retryDelay + // 默认消息模板集合,包含各种场景的模板 + private final defaultTemplates + + /** + * 构造函数 + * @param script Jenkins脚本实例,必填 + * @param atAll 是否@所有人,默认false + * @param atMobiles 需要@的手机号列表,默认空列表 + * @param atUserIds 需要@的用户ID列表,默认空列表 + * @param msgType 消息类型,默认markdown + * @param maxRetries 最大重试次数,默认3次 + * @param retryDelay 重试间隔(毫秒),默认1000ms + */ + DingTalkService(script, pipelineParams, atAll = false, atMobiles = [], atUserIds = [], msgType = 'markdown', maxRetries = 3, retryDelay = 1000) { + this.script = script + this.pipelineParams = pipelineParams + this.atAll = atAll + this.atMobiles = atMobiles + this.atUserIds = atUserIds + this.msgType = msgType + this.maxRetries = maxRetries + this.retryDelay = retryDelay + + // 初始化默认模板(不包含动态URL) + // 使用闭包定义模板,支持动态参数注入 + this.defaultTemplates = [ + // 构建开始模板 + build_start : { params -> + """## 🚀 构建开始: ${params.projectName} + +- **项目名称**: ${params.projectName} +- **代码分支**: ${params.branch ?: '未指定'} +- **提交信息**: ${params.commitMsg ?: '未指定'} +- **提交作者**: ${params.commitAuthor ?: '未指定'} +- **开始时间**: ${new Date().format('yyyy-MM-dd HH:mm:ss')} +- **构建链接**: [查看构建详情](${params.buildUrl}) + +${params.extraInfo}""" + }, + // 构建成功模板 + build_success: { params -> + """## ✅ 构建成功: ${params.projectName} + +- **项目名称**: ${params.projectName} +- **代码分支**: ${params.branch ?: '未指定'} +- **构建状态**: ✅ 成功 +- **构建时间**: ${params.duration} +- **完成时间**: ${new Date().format('yyyy-MM-dd HH:mm:ss')} +- **构建链接**: [查看构建详情](${params.buildUrl}) + +${params.extraInfo}""" + }, + // 构建失败模板 + build_failure: { params -> + """## ❌ 构建失败: ${params.projectName} + +- **项目名称**: ${params.projectName} +- **代码分支**: ${params.branch ?: '未指定'} +- **构建状态**: ❌ 失败 +- **构建时间**: ${params.duration} +- **完成时间**: ${new Date().format('yyyy-MM-dd HH:mm:ss')} +- **错误信息**: ${params.errorMsg ?: '未提供详细错误信息'} +- **构建链接**: [查看构建详情](${params.buildUrl}) + +${params.extraInfo}""" + }, + // 构建中止模板 + build_aborted: { params -> + """## ⏹️ 构建中止: ${params.projectName} + +- **项目名称**: ${params.projectName} +- **代码分支**: ${params.branch ?: '未指定'} +- **构建状态**: ⏹️ 中止 +- **构建时间**: ${params.duration} +- **完成时间**: ${new Date().format('yyyy-MM-dd HH:mm:ss')} +- **构建链接**: [查看构建详情](${params.buildUrl}) + +${params.extraInfo}""" + }, + // 部署通知模板 + deployment : { params -> + """## 🚀 部署通知: ${params.projectName} + +- **环境**: ${params.environment ?: '未指定'} +- **版本**: ${params.version ?: '未指定'} +- **状态**: ${params.status ?: '进行中'} +- **时间**: ${new Date().format('yyyy-MM-dd HH:mm:ss')} +- **部署链接**: [查看部署详情](${params.deployUrl ?: params.buildUrl}) + +${params.extraInfo}""" + }, + // 测试报告模板 + test_report : { params -> + """## 📊 测试报告: ${params.projectName} + +- **总用例数**: ${params.total ?: 0} +- **通过数**: ${params.passed ?: 0} +- **失败数**: ${params.failed ?: 0} +- **跳过数**: ${params.skipped ?: 0} +- **通过率**: ${params.passRate ?: '0%'} +- **报告链接**: [查看详细报告](${params.reportUrl ?: params.buildUrl}) + +${params.extraInfo}""" + } + ] + } + + // ==================== 构建相关消息 ==================== + + /** + * 发送构建开始消息 + * @param projectName 项目名称 + * @param branch 分支名称,可选 + * @param commitMsg 提交信息,可选 + * @param commitAuthor 提交作者,可选 + * @param extraInfo 额外信息Map,可选 + * @return 发送结果 + */ + def sendBuildStartMessage(String projectName, String branch = '', String commitMsg = '', String commitAuthor = '', Map extraInfo = [:]) { + def buildUrl = getBuildUrl() + def extraParams = formatExtraInfo(extraInfo) + + def template = defaultTemplates.build_start + + def paramsMap = [ + projectName : projectName, + branch : branch, + commitMsg : commitMsg, + commitAuthor: commitAuthor, + buildUrl : buildUrl, + extraInfo : extraParams + ] + def text = template.call(paramsMap) + return sendMessage("🚀 构建开始: ${projectName}", text.stripIndent()) + } + + /** + * 发送构建成功消息 + * @param projectName 项目名称 + * @param branch 分支名称,可选 + * @param duration 构建时长(毫秒) + * @param extraInfo 额外信息Map,可选 + * @return 发送结果 + */ + def sendBuildSuccessMessage(String projectName, String branch = '', long duration = 0, Map extraInfo = [:]) { + def buildUrl = getBuildUrl() + def extraParams = formatExtraInfo(extraInfo) + + def template = defaultTemplates.build_success + + def paramsMap = [ + projectName: projectName, + branch : branch, + duration : formatDuration(duration), + buildUrl : buildUrl, + extraInfo : extraParams + ] + script.echo "成功邮件参数:${paramsMap}" + def text = template.call(paramsMap) + return sendMessage("✅ 构建成功: ${projectName}", text.stripIndent()) + } + + /** + * 发送构建失败消息 + * @param projectName 项目名称 + * @param branch 分支名称,可选 + * @param duration 构建时长(毫秒) + * @param errorMsg 错误信息,可选 + * @param extraInfo 额外信息Map,可选 + * @return 发送结果 + */ + def sendBuildFailureMessage(String projectName, String branch = '', long duration = 0, String errorMsg = '', Map extraInfo = [:]) { + def buildUrl = getBuildUrl() + def extraParams = formatExtraInfo(extraInfo) + + def template = defaultTemplates.build_failure + + def paramsMap = [ + projectName: projectName, + branch : branch, + duration : formatDuration(duration), + errorMsg : errorMsg, + buildUrl : buildUrl, + extraInfo : extraParams + ] + script.echo "失败邮件参数:${paramsMap}" + def text = template.call(paramsMap) + return sendMessage("❌ 构建失败: ${projectName}", text.stripIndent(), true) + } + + /** + * 发送构建中止消息 + * @param projectName 项目名称 + * @param branch 分支名称,可选 + * @param duration 构建时长(毫秒) + * @param extraInfo 额外信息Map,可选 + * @return 发送结果 + */ + def sendBuildAbortedMessage(String projectName, String branch = '', long duration = 0, Map extraInfo = [:]) { + def buildUrl = getBuildUrl() + def extraParams = formatExtraInfo(extraInfo) + def template = defaultTemplates.build_aborted + + def paramsMap = [ + projectName: projectName, + branch : branch, + duration : formatDuration(duration), + buildUrl : buildUrl, + extraInfo : extraParams + ] + script.echo "中止邮件参数:${paramsMap}" + def text = template.call(paramsMap) + return sendMessage("⏹️ 构建中止: ${projectName}", text.stripIndent()) + } + + // ==================== 部署相关消息 ==================== + + /** + * 发送部署通知消息 + * @param projectName 项目名称 + * @param environment 环境名称 + * @param version 版本号 + * @param status 部署状态 + * @param deployUrl 部署链接,可选 + * @param extraInfo 额外信息Map,可选 + * @return 发送结果 + */ + def sendDeploymentMessage(String projectName, String environment, String version, String status, String deployUrl = '', Map extraInfo = [:]) { + def buildUrl = getBuildUrl() + def extraParams = formatExtraInfo(extraInfo) + def template = defaultTemplates.deployment + + def paramsMap = [ + projectName: projectName, + environment: environment, + version : version, + status : status, + deployUrl : deployUrl, + buildUrl : buildUrl, + extraInfo : extraParams + ] + script.echo "部署邮件参数:${paramsMap}" + def text = template.call(paramsMap) + return sendMessage("🚀 部署通知: ${projectName}", text.stripIndent()) + } + + // ==================== 测试相关消息 ==================== + + /** + * 发送测试报告消息 + * @param projectName 项目名称 + * @param total 总用例数 + * @param passed 通过数 + * @param failed 失败数 + * @param skipped 跳过数 + * @param reportUrl 报告链接,可选 + * @param extraInfo 额外信息Map,可选 + * @return 发送结果 + */ + def sendTestReportMessage(String projectName, int total, int passed, int failed, int skipped, String reportUrl = '', Map extraInfo = [:]) { + def buildUrl = getBuildUrl() + def extraParams = formatExtraInfo(extraInfo) + def passRate = total > 0 ? String.format("%.2f%%", (passed / total) * 100) : "0%" + def template = defaultTemplates.test_report + + def paramsMap = [ + projectName: projectName, + total : total, + passed : passed, + failed : failed, + skipped : skipped, + passRate : passRate, + reportUrl : reportUrl, + buildUrl : buildUrl, + extraInfo : extraParams + ] + script.echo "测试报告邮件参数:${paramsMap}" + def text = template.call(paramsMap) + return sendMessage("📊 测试报告: ${projectName}", text.stripIndent()) + } + + // ==================== 基础消息类型 ==================== + + /** + * 发送文本消息 + * @param content 消息内容 + * @param atAll 是否@所有人,可选 + * @param customAtMobiles 自定义@手机号列表,可选 + * @param customAtUserIds 自定义@用户ID列表,可选 + * @return 发送结果 + */ + def sendTextMessage(String content, Boolean atAll = false, List customAtMobiles = [], List customAtUserIds = []) { + def data = [ + msgtype: "text", + text : [content: content], + at : [ + atMobiles: customAtMobiles ?: (atAll ? [] : this.atMobiles), + atUserIds: customAtUserIds ?: (atAll ? [] : this.atUserIds), + isAtAll : atAll ?: this.atAll + ] + ] + return sendRequestWithRetry(data) + } + + /** + * 发送Markdown消息 + * @param title 消息标题 + * @param text Markdown格式内容 + * @param atAll 是否@所有人,可选 + * @param customAtMobiles 自定义@手机号列表,可选 + * @param customAtUserIds 自定义@用户ID列表,可选 + * @return 发送结果 + */ + def sendMarkdownMessage(String title, String text, Boolean atAll = false, List customAtMobiles = [], List customAtUserIds = []) { + def data = [ + msgtype : "markdown", + markdown: [ + title: title, + text : text + formatAtUsers(customAtMobiles ?: atMobiles, customAtUserIds ?: atUserIds, atAll) + ], + at : [ + atMobiles: customAtMobiles ?: (atAll ? [] : this.atMobiles), + atUserIds: customAtUserIds ?: (atAll ? [] : this.atUserIds), + isAtAll : atAll ?: this.atAll + ] + ] + script.echo "准备发送MD消息:${data}" + return sendRequestWithRetry(data) + } + + /** + * 发送链接消息 + * @param title 消息标题 + * @param text 消息内容 + * @param messageUrl 跳转链接 + * @param picUrl 图片链接,可选 + * @return 发送结果 + */ + def sendLinkMessage(String title, String text, String messageUrl, String picUrl = '') { + def data = [ + msgtype: "link", + link : [ + title : title, + text : text, + messageUrl: messageUrl, + picUrl : picUrl + ] + ] + return sendRequestWithRetry(data) + } + + // ==================== ActionCard 消息类型 ==================== + + /** + * 发送独立跳转ActionCard消息 + * @param title 标题 + * @param text 内容 + * @param singleTitle 单个按钮标题 + * @param singleURL 单个按钮链接 + * @param btnOrientation 按钮方向 0-竖直,1-横向,默认0 + * @return 发送结果 + */ + def sendSingleActionCard(String title, String text, String singleTitle, String singleURL, String btnOrientation = '0') { + def data = [ + msgtype : "actionCard", + actionCard: [ + title : title, + text : text, + singleTitle : singleTitle, + singleURL : singleURL, + btnOrientation: btnOrientation + ] + ] + return sendRequestWithRetry(data) + } + + /** + * 发送多按钮ActionCard消息 + * @param title 标题 + * @param text 内容 + * @param btns 按钮列表 [[title: "按钮标题", actionURL: "按钮链接"], ...] + * @param btnOrientation 按钮方向 0-竖直,1-横向,默认0 + * @return 发送结果 + */ + def sendMultiActionCard(String title, String text, List btns, String btnOrientation = '0') { + def data = [ + msgtype : "actionCard", + actionCard: [ + title : title, + text : text, + btns : btns, + btnOrientation: btnOrientation + ] + ] + return sendRequestWithRetry(data) + } + + /** + * 发送构建详情ActionCard + * @param projectName 项目名称 + * @param status 构建状态 + * @param duration 构建时长 + * @param branch 分支名称,可选 + * @param commitMsg 提交信息,可选 + * @return 发送结果 + */ + def sendBuildActionCard(String projectName, String status, String duration, String branch = '', String commitMsg = '') { + def title = "${getStatusEmoji(status)} ${status}: ${projectName}" + def buildUrl = getBuildUrl() + def text = """ + ### ${title} + - 项目名称: ${projectName} + - 代码分支: ${branch ?: '未指定'} + - 状态: ${status} + - 耗时: ${formatDuration(duration)} + - 提交信息: ${commitMsg ?: '未指定'} + - 时间: ${new Date().format('yyyy-MM-dd HH:mm:ss')} + """.stripIndent() + + def btns = [ + [title: "查看构建详情", actionURL: buildUrl], + [title: "查看日志", actionURL: "${buildUrl}console"] + ] + + return sendMultiActionCard(title, text, btns) + } + + // ==================== FeedCard 消息类型 ==================== + + /** + * 发送FeedCard消息 + * @param links 链接列表 [[title: "标题", messageURL: "链接", picURL: "图片链接"], ...] + * @return 发送结果 + */ + def sendFeedCardMessage(List links) { + def data = [ + msgtype : "feedCard", + feedCard: [links: links] + ] + return sendRequestWithRetry(data) + } + + /** + * 发送多项目构建FeedCard + * @param projects 项目信息列表 [[name: "项目名", status: "状态", url: "链接", imageUrl: "图片链接"], ...] + * @return 发送结果 + */ + def sendMultiProjectFeedCard(List projects) { + def buildUrl = getBuildUrl() + def links = projects.collect { project -> + [ + title : "${getStatusEmoji(project.status)} ${project.name}", + messageURL: project.url ?: buildUrl, + picURL : project.imageUrl ?: getStatusImage(project.status) + ] + } + return sendFeedCardMessage(links) + } + + // ==================== 辅助方法 ==================== + + /** + * 根据配置的消息类型发送消息 + * @param title 消息标题 + * @param text 消息内容 + * @param atAll 是否@所有人,可选 + * @return 发送结果 + */ + private def sendMessage(String title, String text, Boolean atAll = false) { + script.echo "消息类型:${this.msgType}" + switch (this.msgType) { + case 'text': + return sendTextMessage(text, atAll) + case 'link': + return sendLinkMessage(title, text, getBuildUrl()) + case 'actionCard': + return sendBuildActionCard(title.split(':')[0], title.split(':')[1]?.trim(), text) + case 'feedCard': + return sendMultiProjectFeedCard([[name: title, status: "通知", url: getBuildUrl()]]) + case 'markdown': + return sendMarkdownMessage(title, text, atAll) + default: + return sendMarkdownMessage(title, text, atAll) + } + } + + /** + * 根据状态获取对应的表情符号 + * @param status 状态字符串 + * @return 表情符号 + */ + private def getStatusEmoji(String status) { + switch (status?.toLowerCase()) { + case 'success': return '✅' + case 'failure': return '❌' + case 'aborted': return '⏹️' + case 'running': return '🚀' + case 'deployed': return '🌐' + default: return '📝' + } + } + + /** + * 根据状态获取对应的图片URL(预留功能) + * @param status 状态字符串 + * @return 图片URL + */ + private def getStatusImage(String status) { + // 这里可以返回不同状态对应的图片URL + return '' + } + + /** + * 格式化@用户信息 + * @param mobiles 手机号列表 + * @param userIds 用户ID列表 + * @param atAll 是否@所有人 + * @return 格式化后的@字符串 + */ + private def formatAtUsers(List mobiles, List userIds, Boolean atAll) { + if (atAll) return '\n\n<@all>' + + def atText = '' + if (mobiles) { + atText += '\n\n' + mobiles.collect { mobile -> "<@${mobile}>" }.join(' ') + } + if (userIds) { + atText += '\n\n' + userIds.collect { userId -> "<@${userId}>" }.join(' ') + } + return atText + } + + // ==================== 核心发送方法 ==================== + + /** + * 带重试机制的消息发送方法 + * @param data 消息数据 + * @return 发送结果 + */ + private def sendRequestWithRetry(Map data) { + int attempt = 0 + boolean success = false + Exception lastException = null + + // 重试循环 + while (attempt < maxRetries && !success) { + attempt++ + try { + script.echo "尝试第 ${attempt} 次发送钉钉消息..." + success = sendRequest(data) + if (success) { + script.echo "钉钉消息发送成功(第 ${attempt} 次尝试)" + return true + } + } catch (Exception e) { + lastException = e + script.echo "第 ${attempt} 次发送失败: ${e.getMessage()}" + + // 如果还有重试机会,等待后重试 + if (attempt < maxRetries) { + script.echo "等待 ${retryDelay}ms 后重试..." + script.sleep(retryDelay / 1000) + } + } + } + + // 重试失败处理 + script.echo "钉钉消息发送失败,已达到最大重试次数(${maxRetries})" + if (lastException) { + script.echo "最后错误信息: ${lastException.getMessage()}" + } + return false + } + + /** + * 实际发送请求的方法 + * @param data 消息数据 + * @return 发送结果 + */ + private def sendRequest(Map data) { + return script.withCredentials([ + script.usernamePassword( + credentialsId: pipelineParams.dingTalkCredentialsId, + usernameVariable: 'DINGTALK_TOKEN', + passwordVariable: 'DINGTALK_SECRET' + ) + ]) { + def token = script.env.DINGTALK_TOKEN + def secret = script.env.DINGTALK_SECRET + + if (!token) throw new Exception("钉钉token不能为空") + + def baseUrl = "https://oapi.dingtalk.com/robot/send?access_token=${token}" + def finalUrl = buildSignedUrl(baseUrl, secret) + + def jsonData = toJsonString(data) + + try { + // 发送HTTP请求 + def response = script.httpRequest( + url: finalUrl, + httpMode: 'POST', + contentType: 'APPLICATION_JSON', + requestBody: jsonData, + consoleLogResponseBody: false, + validResponseCodes: '200', + timeout: 30000 + ) + + // 解析响应 + def responseJson = script.readJSON(text: response.content) + if (responseJson.errcode != 0) { + throw new Exception("钉钉API错误: ${responseJson.errmsg} (错误码: ${responseJson.errcode})") + } + return true + } catch (Exception e) { + throw new Exception("HTTP请求失败: ${e.getMessage()}") + } + } + } + + // ==================== 工具方法 ==================== + + /** + * 构建带签名的URL + * @param webhookUrl 原始webhook URL + * @param secret 签名密钥 + * @return 带签名的URL + */ + private def buildSignedUrl(String webhookUrl, String secret) { + // 如果密钥为空,直接返回原始URL + if (!secret || secret.trim().isEmpty() || secret == 'null') { + return webhookUrl + } + + try { + // 生成时间戳和签名字符串 + def timestamp = System.currentTimeMillis() + def stringToSign = "${timestamp}\n${secret}" + + // 使用HmacSHA256算法生成签名 + def mac = Mac.getInstance("HmacSHA256") + def secretKey = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256") + mac.init(secretKey) + def signData = mac.doFinal(stringToSign.getBytes("UTF-8")) + def sign = URLEncoder.encode(Base64.encoder.encodeToString(signData), "UTF-8") + + // 拼接URL + def separator = webhookUrl.contains('?') ? '&' : '?' + return "${webhookUrl}${separator}timestamp=${timestamp}&sign=${sign}" + + } catch (Exception e) { + script.echo "加签失败,使用原始URL: ${e.getMessage()}" + return webhookUrl + } + } + + /** + * 将Map转换为JSON字符串 + * @param data 数据Map + * @return JSON字符串 + */ + private def toJsonString(Map data) { + try { + return JsonOutput.toJson(data) + } catch (Exception e) { + throw new Exception("JSON数据格式错误: ${e.getMessage()}") + } + } + + /** + * 获取构建URL + * @return 构建URL + */ + private def getBuildUrl() { + try { + return script.env.BUILD_URL ?: '构建链接不可用' + } catch (Exception e) { + return '构建链接不可用' + } + } + + /** + * 格式化持续时间 + * @param duration 持续时间(毫秒) + * @return 格式化后的时间字符串 + */ + private static String formatDuration(long duration) { + long seconds = duration / 1000 + long minutes = seconds / 60 + long hours = minutes / 60 + + if (hours > 0) return "${hours}小时 ${minutes % 60}分钟 ${seconds % 60}秒" + if (minutes > 0) return "${minutes}分钟 ${seconds % 60}秒" + return "${seconds}秒" + } + + /** + * 格式化额外信息 + * @param extraInfo 额外信息Map + * @return 格式化后的字符串 + */ + private static String formatExtraInfo(Map extraInfo) { + if (!extraInfo) return '' + return '\n' + extraInfo.collect { key, value -> "- **${key}**: ${value}" }.join('\n') + } +} \ No newline at end of file diff --git a/src/com/common/EmailService.groovy b/src/com/common/EmailService.groovy new file mode 100644 index 0000000..a72e710 --- /dev/null +++ b/src/com/common/EmailService.groovy @@ -0,0 +1,305 @@ +package com.common + +import com.utils.DurationFormatter +import groovy.json.JsonOutput + +/** + * 邮件服务类 - 专门负责邮件发送功能 + * 主要功能: + * - 加载和解析HTML邮件模板 + * - 动态生成邮件内容 + * - 管理收件人列表 + * - 发送格式化的HTML邮件 + * - 错误处理和回退机制 + */ +class EmailService implements Serializable { + // Jenkins pipeline脚本对象,用于执行Jenkins DSL方法 + def script + + // Stage服务实例,用于获取stage执行信息和统计数据 + def stageService + + /** + * 构造函数 + * @param script Jenkins pipeline脚本对象,用于访问Jenkins环境和方法 + * @param stageService Stage服务实例,用于获取构建阶段信息 + */ + EmailService(script, stageService) { + this.script = script + this.stageService = stageService + } + + /** + * 发送构建通知邮件的主方法 + * 根据构建结果发送不同内容的通知邮件,包含stage执行详情 + * + * @param buildResult 构建结果状态,可选值:SUCCESS/FAILURE/UNSTABLE + * @param params 管道参数Map,包含项目信息、环境配置等 + * @param extraInfo 额外信息Map(可选),可用于传递失败原因等额外信息 + */ + def sendBuildNotification(String buildResult, Map params, Map extraInfo = [:]) { + try { + script.echo "📧 开始准备发送构建通知邮件..." + + // 加载HTML邮件模板 + def emailTemplate = loadEmailTemplate() + + // 构建邮件内容,包含stage执行详情 + def emailContent = buildEmailContent(emailTemplate, buildResult, params, extraInfo) + + // 获取收件人列表(根据构建结果和环境动态确定) + def recipients = getRecipients(buildResult, params) + + // 发送邮件 + sendEmail(recipients, buildResult, emailContent, params) + + script.echo "✅ 邮件发送成功" + + } catch (Exception e) { + // 异常处理,输出错误信息但不中断流程 + script.echo "⚠️ 邮件发送失败: ${e.message}" + } + } + + /** + * 加载邮件模板文件 + * 从Jenkins资源库加载预定义的HTML邮件模板 + * @return 邮件模板字符串 + */ + private def loadEmailTemplate() { + try { + // 从Jenkins资源库加载模板文件 + return script.libraryResource('templates/build_email.html') + } catch (Exception e) { + // 如果模板加载失败,使用默认的简单模板 + script.echo "⚠️ 加载邮件模板失败,使用默认模板: ${e.message}" + return getDefaultTemplate() + } + } + + /** + * 构建邮件内容 - 增强版,包含stage信息 + * 使用模板和实际数据动态生成邮件HTML内容 + * + * @param template 邮件模板字符串 + * @param buildResult 构建结果 + * @param params 构建参数 + * @param extraInfo 额外信息 + * @return 完整的邮件HTML内容 + */ + private def buildEmailContent(String template, String buildResult, Map params, Map extraInfo) { + // 获取Git相关信息(分支、提交等) + def gitInfo = getGitInfo() + // 获取构建相关信息(编号、URL、时间等) + def buildInfo = getBuildInfo() + // 获取stage统计摘要信息 + def stageSummary = stageService.getBuildSummary() + + // 根据构建结果获取状态配置(表情符号、文本、CSS类等) + def statusConfig = getStatusConfig(buildResult) + + // 获取stage执行时间线的HTML内容 + def stagesContent = stageService.getBuildStagesHTML() + + // 构建邮件参数映射表,用于替换模板中的变量 + def emailParams = [ + projectName : params.projectName, // 项目名称 + buildNumber : buildInfo.buildNumber, // 构建编号 + buildId : buildInfo.buildId, // 构建ID + buildUrl : buildInfo.buildUrl, // 构建详情URL + buildTime : buildInfo.timestamp, // 构建时间 + buildDuration : buildInfo.duration, // 构建持续时间 + profiles : params.PROFILES, // 环境配置(如dev、test、prod) + gitBranch : gitInfo.branch, // Git分支 + gitCommit : gitInfo.commit, // Git完整提交ID + gitCommitShort : gitInfo.commitShort, // Git短提交ID(前8位) + gitUrl : gitInfo.repositoryUrl, // Git仓库URL + imageFullName : params.imageFullName, // 镜像全名 + imageTags : params.imageTags?.join(', '), // 镜像标签(逗号分隔) + deploymentStrategy : params.deploymentStrategy ?: '滚动部署', // 部署策略 + healthCheckStatus : '通过', // 健康检查状态 + nodeName : buildInfo.nodeName, // Jenkins节点名称 + jenkinsUrl : buildInfo.jenkinsUrl, // Jenkins服务器URL + buildResult : buildResult, // 构建结果 + statusEmoji : statusConfig.emoji, // 状态表情符号 + statusText : statusConfig.text, // 状态文本 + statusClass : statusConfig.cssClass, // 状态CSS类名 + badgeClass : statusConfig.badgeClass, // 徽章CSS类名 + badgeText : statusConfig.badgeText, // 徽章文本 + stagesContent : stagesContent, // stage时间线HTML内容 + totalStages : stageSummary.totalStages, // 总stage数量 + successStages : stageSummary.successStages, // 成功stage数量 + failedStages : stageSummary.failedStages, // 失败stage数量 + successRate : String.format("%.1f", stageSummary.successRate), // 成功率(格式化) + totalDurationFormatted: stageSummary.totalDurationFormatted // 总持续时间(格式化) + ] + + // 输出调试信息(可选) + script.echo "邮件参数集合:${emailParams}" + + // 替换模板变量:将模板中的${variable}替换为实际值 + def content = template + emailParams.each { key, value -> + content = content.replaceAll('\\$\\{' + key + '\\}', value.toString()) + } + + return content + } + + /** + * 获取构建状态配置 + * 根据构建结果返回相应的显示配置 + * + * @param buildResult 构建结果 + * @return 包含状态显示配置的Map + */ + private def getStatusConfig(buildResult) { + switch (buildResult) { + case 'SUCCESS': + return [emoji : '✅', text: '构建成功', cssClass: 'status-success', + badgeClass: 'badge-success', badgeText: '成功'] + case 'FAILURE': + return [emoji : '❌', text: '构建失败', cssClass: 'status-failure', + badgeClass: 'badge-failure', badgeText: '失败'] + case 'UNSTABLE': + return [emoji : '⚠️', text: '构建不稳定', cssClass: 'status-unstable', + badgeClass: 'badge-warning', badgeText: '不稳定'] + default: + return [emoji : '🔍', text: '构建状态未知', cssClass: 'status-unknown', + badgeClass: 'badge-secondary', badgeText: '未知'] + } + } + + /** + * 获取Git相关信息 + * 从Jenkins环境变量中提取Git相关信息 + * + * @return 包含Git信息的Map + */ + private def getGitInfo() { + def gitCommit = script.env.GIT_COMMIT ?: '' // Git提交ID + def gitBranch = script.env.GIT_BRANCH ?: 'N/A' // Git分支名称 + + // 清理分支名称(移除origin/前缀) + if (gitBranch.contains('/')) { + gitBranch = gitBranch.split('/').last() + } + + return [ + commit : gitCommit, // 完整提交ID + commitShort : gitCommit ? gitCommit.substring(0, 8) : 'N/A', // 短提交ID(前8位) + branch : gitBranch, // 分支名称 + repositoryUrl: script.env.GIT_URL ?: 'N/A' // 仓库URL + ] + } + + /** + * 获取构建相关信息 + * 从Jenkins环境变量中提取构建相关信息 + * + * @return 包含构建信息的Map + */ + private def getBuildInfo() { + return [ + buildNumber: script.env.BUILD_NUMBER ?: 'N/A', // 构建编号 + buildId : script.env.BUILD_ID ?: 'N/A', // 构建ID + buildUrl : script.env.BUILD_URL ?: 'N/A', // 构建详情URL + jenkinsUrl : script.env.JENKINS_URL ?: 'N/A', // Jenkins服务器URL + nodeName : script.env.NODE_NAME ?: 'N/A', // 执行节点名称 + timestamp : new Date().format('yyyy-MM-dd HH:mm:ss'), // 当前时间戳 + duration : DurationFormatter.formatDuration(script.currentBuild.duration) // 构建持续时间 + ] + } + + /** + * 获取收件人列表 + * 根据构建结果和环境动态确定收件人 + * + * @param buildResult 构建结果 + * @param params 构建参数 + * @return 收件人列表(逗号分隔的字符串) + */ + private def getRecipients(String buildResult, Map params) { + // 默认收件人列表 + def defaultRecipients = [ + 'xiaomayicloud@163.com' + ] + + // 根据构建结果和环境添加额外收件人 + switch (buildResult) { + case 'FAILURE': + // 构建失败时添加运维人员 + defaultRecipients.add('xiaomayicloud@163.com') + break + case 'SUCCESS': + // 生产环境构建成功时添加管理人员 + if (params.PROFILES == 'prod') { + defaultRecipients.add('xiaomayicloud@163.com') + } + break + } + + // 添加参数中指定的额外收件人 + def additionalRecipients = params.email?.recipients ?: [] + defaultRecipients.addAll(additionalRecipients) + + // 去重并转换为逗号分隔的字符串 + return defaultRecipients.unique().join(',') + } + + /** + * 发送邮件 + * 使用Jenkins的emailext插件发送HTML格式的邮件 + * + * @param recipients 收件人列表 + * @param buildResult 构建结果 + * @param content 邮件内容(HTML格式) + * @param params 构建参数 + */ + private def sendEmail(String recipients, String buildResult, String content, Map params) { + // 构建邮件主题 + def subject = buildEmailSubject(buildResult, params) + + // 使用Jenkins邮件扩展插件发送邮件 + script.emailext( + to: recipients, // 收件人 + subject: subject, // 邮件主题 + body: content, // 邮件内容(HTML格式) + mimeType: 'text/html', // 内容类型 + replyTo: 'jenkins-noreply@xiaomayi.com', // 回复地址 + attachLog: buildResult == 'FAILURE', // 失败时附加日志 + compressLog: true // 压缩日志文件 + ) + } + + /** + * 构建邮件主题 + * 根据构建结果和环境生成格式化的邮件主题 + * + * @param buildResult 构建结果 + * @param params 构建参数 + * @return 邮件主题字符串 + */ + private def buildEmailSubject(String buildResult, Map params) { + // 获取状态配置 + def statusConfig = getStatusConfig(buildResult) + // 构建主题:表情符号 + 环境 + 项目名 + 状态 + 构建编号 + return "${statusConfig.emoji} [${params.PROFILES?.toUpperCase()}] ${params.projectName} 构建${statusConfig.badgeText} - #${script.env.BUILD_NUMBER}" + } + + /** + * 获取默认邮件模板 + * 当主模板加载失败时使用的备用模板 + * + * @return 简单的默认邮件模板 + */ + private def getDefaultTemplate() { + return ''' +

构建通知: ${buildStatus}

+

项目: ${projectName}

+

环境: ${environment}

+

构建编号: ${buildNumber}

+

查看详情

+ ''' + } +} \ No newline at end of file diff --git a/src/com/common/NotificationService.groovy b/src/com/common/NotificationService.groovy new file mode 100644 index 0000000..9c6be84 --- /dev/null +++ b/src/com/common/NotificationService.groovy @@ -0,0 +1,565 @@ +package com.common + +import groovy.json.JsonOutput + +/** + * 统一通知服务类 + * 负责处理构建结束后的各种通知发送,包括钉钉消息、邮件通知和短信通知 + * 根据配置文件中的notification设置决定发送哪些渠道的通知 + * + * 该类实现了Serializable接口,确保可以在Jenkins Pipeline中序列化 + */ +class NotificationService implements Serializable { + // Jenkins脚本实例,用于执行Jenkins相关操作 + private final script + // 管道参数,包含项目的配置信息,如notification配置等 + private final pipelineParams + // 钉钉服务实例,用于发送钉钉消息 + private DingTalkService dingTalkService + // 邮件服务实例,用于发送邮件通知 + private EmailService emailService + // 短信服务实例,用于发送短信通知 + private SmsService smsService + // Stage服务实例(用于邮件中的stage信息) + private StageService stageService + + /** + * 构造函数 + * @param script Jenkins脚本实例,通常是Jenkins pipeline的steps对象 + * @param pipelineParams 管道参数,包含notification配置等信息 + * @param stageService Stage服务实例(可选),用于邮件通知中展示stage信息 + */ + NotificationService(script, Map pipelineParams, StageService stageService = null) { + this.script = script + this.pipelineParams = pipelineParams + this.stageService = stageService + + // 初始化钉钉服务 + this.dingTalkService = new DingTalkService(script, pipelineParams) + + // 初始化邮件服务(如果有stageService) + if (stageService) { + this.emailService = new EmailService(script, stageService) + } + + // 初始化短信服务 + this.smsService = new SmsService(script, pipelineParams) + } + + // ==================== 构建结果通知方法(支持命名参数) ==================== + + /** + * 发送构建成功通知(支持命名参数) + * @param args 命名参数,可包含: + * - projectName: 项目名称 + * - branch: 分支名称 + * - duration: 构建时长 + * - extraInfo: 额外信息Map + */ + def sendBuildSuccess(Map args = [:]) { + script.echo "📨 发送构建成功通知..." + def params = getNotificationParams(args, 'success') + sendNotifications(params) + } + + /** + * 发送构建失败通知(支持命名参数) + * @param args 命名参数,可包含: + * - projectName: 项目名称 + * - branch: 分支名称 + * - duration: 构建时长 + * - extraInfo: 额外信息Map + * - errorMsg: 错误消息(特殊处理) + */ + def sendBuildFailure(Map args = [:]) { + script.echo "📨 发送构建失败通知..." + def params = getNotificationParams(args, 'failure') + params.isFailure = true + if (args.errorMsg) { + params.extraInfo.errorMsg = args.errorMsg + } + sendNotifications(params) + } + + /** + * 发送构建不稳定通知(支持命名参数) + * @param args 命名参数,可包含: + * - projectName: 项目名称 + * - branch: 分支名称 + * - duration: 构建时长 + * - extraInfo: 额外信息Map + */ + def sendBuildUnstable(Map args = [:]) { + script.echo "📨 发送构建不稳定通知..." + def params = getNotificationParams(args, 'unstable') + sendNotifications(params) + } + + /** + * 发送构建中止通知(支持命名参数) + * @param args 命名参数,可包含: + * - projectName: 项目名称 + * - branch: 分支名称 + * - duration: 构建时长 + * - extraInfo: 额外信息Map + */ + def sendBuildAborted(Map args = [:]) { + script.echo "📨 发送构建中止通知..." + def params = getNotificationParams(args, 'aborted') + sendNotifications(params) + } + + // ==================== 核心通知分发方法 ==================== + + /** + * 根据构建结果分发通知到各个渠道 + * @param params 通知参数,包含构建结果、项目信息等 + */ + private def sendNotifications(Map params) { + try { + // 获取通知配置,根据构建结果类型(success/failure等)获取对应的配置 + def notificationConfig = getNotificationConfig(params.buildResult) + if (!notificationConfig) { + script.echo "⚠️ 未找到 ${params.buildResult} 的通知配置,跳过通知发送" + return + } + + // // 获取通知渠道和配置 + // def channels = notificationConfig.channels ?: [] // 通知渠道列表,如['dingtalk', 'email', 'sms'] + // def recipients = notificationConfig.recipients ?: [] // 邮件收件人列表 + // def smsMobiles = notificationConfig.smsMobiles ?: [] // 短信接收人手机号列表 + + // script.echo "通知配置: 渠道=${channels}, 收件人=${recipients}, 短信=${smsMobiles}" + + // // 发送钉钉通知(如果配置了钉钉渠道) + // if (channels.contains('dingtalk')) { + // sendDingTalkNotification(params) + // } + + // // 发送邮件通知(如果配置了邮件渠道且邮件服务已初始化) + // if (channels.contains('email') && emailService) { + // sendEmailNotification(params, recipients) + // } + + // // 发送短信通知(如果配置了短信渠道且短信服务已初始化且有接收人) + // if (channels.contains('sms') && smsService && smsMobiles) { + // sendSmsNotification(params, smsMobiles) + // } + + // // 其他渠道(如Slack,暂未实现) + // if (channels.contains('slack')) { + // script.echo "📣 Slack通知暂未实现" + // } + + // 获取通知渠道配置 + def channelsConfig = notificationConfig.channels ?: [:] + script.echo "通知配置: 渠道配置=${channelsConfig}" + + // 发送钉钉通知(如果配置了钉钉渠道且为true) + if (channelsConfig.dingtalk == true) { + sendDingTalkNotification(params) + } + + // 发送邮件通知(如果配置了邮件渠道且有收件人) + if (channelsConfig.email && channelsConfig.email instanceof List && !channelsConfig.email.empty) { + if (emailService) { + sendEmailNotification(params, channelsConfig.email) + } else { + script.echo "⚠️ 邮件服务未初始化,无法发送邮件通知" + } + } + + // 发送短信通知(如果配置了短信渠道且有接收人手机号) + if (channelsConfig.sms && channelsConfig.sms instanceof List && !channelsConfig.sms.empty) { + if (smsService) { + sendSmsNotification(params, channelsConfig.sms) + } else { + script.echo "⚠️ 短信服务未初始化,无法发送短信通知" + } + } + + // 发送Slack通知(如果配置了Slack渠道且为true) + if (channelsConfig.slack == true) { + script.echo "📣 Slack通知暂未实现" + // 后续可以添加Slack通知的实现 + } + + script.echo "✅ 通知发送完成" + + } catch (Exception e) { + script.echo "⚠️ 通知发送过程中发生错误: ${e.message}" + script.echo "详细错误: ${e.stackTrace}" + } + } + + // ==================== 各渠道通知方法 ==================== + + /** + * 发送钉钉通知 + * @param params 通知参数,包含构建结果、项目信息等 + */ + private def sendDingTalkNotification(Map params) { + try { + // 根据构建结果类型调用不同的钉钉消息模板 + switch (params.buildResult) { + case 'success': + dingTalkService.sendBuildSuccessMessage( + params.projectName, + params.branch, + params.duration, + params.extraInfo + ) + break + case 'failure': + dingTalkService.sendBuildFailureMessage( + params.projectName, + params.branch, + params.duration, + params.extraInfo.errorMsg ?: '构建失败', + params.extraInfo + ) + break + case 'unstable': + // 不稳定状态使用成功消息模板,但添加不稳定标识 + params.extraInfo.status = '构建不稳定' + dingTalkService.sendBuildSuccessMessage( + params.projectName, + params.branch, + params.duration, + params.extraInfo + ) + break + case 'aborted': + dingTalkService.sendBuildAbortedMessage( + params.projectName, + params.branch, + params.duration, + params.extraInfo + ) + break + default: + script.echo "⚠️ 未知的构建结果: ${params.buildResult}" + } + script.echo "✅ 钉钉通知发送成功" + } catch (Exception e) { + script.echo "❌ 钉钉通知发送失败: ${e.message}" + } + } + + /** + * 发送邮件通知 + * @param params 通知参数,包含构建结果、项目信息等 + * @param recipients 邮件收件人列表 + */ + private def sendEmailNotification(Map params, List recipients) { + try { + // 构建邮件参数,整合pipeline参数和通知参数 + def emailParams = [ + projectName : params.projectName, + PROFILES : pipelineParams.PROFILES, // 环境配置 + imageFullName: pipelineParams.imageFullName, // 镜像全名 + imageTags : pipelineParams.imageTags, // 镜像标签 + email : [recipients: recipients] // 收件人列表 + ] + // 转换构建结果为EmailService需要的格式(全大写) + def emailBuildResult = convertBuildResultForEmail(params.buildResult) + // 调用邮件服务发送通知 + emailService.sendBuildNotification(emailBuildResult, emailParams, params.extraInfo) + script.echo "✅ 邮件通知发送成功" + } catch (Exception e) { + script.echo "❌ 邮件通知发送失败: ${e.message}" + } + } + + /** + * 发送短信通知 + * @param params 通知参数,包含构建结果、项目信息等 + * @param mobiles 短信接收人手机号列表 + */ + private def sendSmsNotification(Map params, List mobiles) { + try { + // 根据构建结果类型调用不同的短信发送方法 + switch (params.buildResult) { + case 'success': + smsService.sendBuildSuccessSms( + params.projectName, + mobiles, + params.branch, + params.duration, + params.extraInfo + ) + break + case 'failure': + smsService.sendBuildFailureSms( + params.projectName, + mobiles, + params.branch, + params.duration, + params.extraInfo.errorMsg ?: '构建失败', + params.extraInfo + ) + break + case 'unstable': + // 不稳定状态使用成功短信模板,但添加不稳定标识 + smsService.sendBuildSuccessSms( + params.projectName, + mobiles, + params.branch, + params.duration, + [status: '构建不稳定'] + params.extraInfo + ) + break + case 'aborted': + smsService.sendBuildAbortedSms( + params.projectName, + mobiles, + params.branch, + params.duration, + params.extraInfo + ) + break + } + script.echo "✅ 短信通知发送成功" + } catch (Exception e) { + script.echo "❌ 短信通知发送失败: ${e.message}" + } + } + + // ==================== 其他类型通知方法 ==================== + + /** + * 发送部署通知 + * @param args 命名参数,可包含: + * - projectName: 项目名称 + * - environment: 部署环境 + * - version: 版本号 + * - status: 部署状态 + * - deployUrl: 部署地址 + * - extraInfo: 额外信息Map + */ + def sendDeploymentNotification(Map args = [:]) { + try { + // 设置默认参数值 + def params = [ + projectName: args.projectName ?: getDefaultProjectName(), + environment: args.environment ?: 'dev', + version : args.version ?: 'unknown', + status : args.status ?: 'unknown', + deployUrl : args.deployUrl ?: '', + extraInfo : args.extraInfo ?: [:] + ] + + // 发送钉钉部署通知 + dingTalkService.sendDeploymentMessage( + params.projectName, + params.environment, + params.version, + params.status, + params.deployUrl, + params.extraInfo + ) + + // 发送短信部署通知(如果配置了短信渠道且有接收人) + def notificationConfig = getNotificationConfig('deployment') + if (notificationConfig?.channels?.contains('sms') && notificationConfig.smsMobiles) { + smsService.sendDeploymentSms( + params.projectName, + params.environment, + params.version, + params.status, + notificationConfig.smsMobiles, + params.extraInfo + ) + } + + script.echo "✅ 部署通知发送成功" + } catch (Exception e) { + script.echo "❌ 部署通知发送失败: ${e.message}" + } + } + + /** + * 发送告警通知 + * @param args 命名参数,可包含: + * - systemName: 系统名称 + * - alertLevel: 告警级别 + * - alertContent: 告警内容 + * - extraInfo: 额外信息Map + */ + def sendAlertNotification(Map args = [:]) { + try { + // 设置默认参数值 + def params = [ + systemName : args.systemName ?: getDefaultProjectName(), + alertLevel : args.alertLevel ?: 'warning', + alertContent: args.alertContent ?: '系统告警', + extraInfo : args.extraInfo ?: [:] + ] + + // 获取告警配置 + def alertConfig = getNotificationConfig('alert') + if (alertConfig) { + // 发送短信告警(如果配置了短信渠道且有接收人) + if (alertConfig.channels?.contains('sms') && alertConfig.smsMobiles) { + smsService.sendAlertSms( + params.systemName, + params.alertLevel, + params.alertContent, + alertConfig.smsMobiles, + params.extraInfo + ) + } + + // 发送钉钉告警(如果配置了钉钉渠道) + if (alertConfig.channels?.contains('dingtalk')) { + // 这里可以调用钉钉的告警方法 + script.echo "📢 发送钉钉告警: ${params.alertContent}" + } + } + + script.echo "✅ 告警通知发送成功" + } catch (Exception e) { + script.echo "❌ 告警通知发送失败: ${e.message}" + } + } + + // ==================== 工具方法 ==================== + + /** + * 获取通知参数,合并默认参数和传入参数 + * @param args 传入的参数 + * @param buildResult 构建结果类型 + * @return 合并后的参数Map + */ + private def getNotificationParams(Map args, String buildResult) { + // 设置默认参数 + def defaultParams = [ + // projectName: script.env.JOB_NAME, // 从环境变量获取项目名称 + projectName: pipelineParams.projectName, + branch : script.env.BRANCH_NAME, // 从环境变量获取分支名称 + duration : script.currentBuild.duration, // 获取构建时长 + extraInfo : [:], // 额外信息初始化为空Map + buildResult: buildResult, // 构建结果类型 + isFailure : false, // 是否为失败状态 + ] + + // 合并默认参数和传入参数 + def params = defaultParams + args + // 添加Jenkins环境信息到extraInfo + params.extraInfo = addJenkinsEnvInfo(params.extraInfo) + + // 确保项目名称不为空 + if (!params.projectName) { + params.projectName = getDefaultProjectName() + } + + return params + } + + /** + * 添加Jenkins环境信息和Git信息到extraInfo(用户参数优先) + * @param extraInfo 原始额外信息Map + * @return 添加了Jenkins环境信息和Git信息后的Map + */ + private def addJenkinsEnvInfo(Map extraInfo) { + try { + // 获取构建信息和Git信息 + def buildInfo = getBuildInfo() + // 获取Git信息 + def gitInfo = getGitInfo() + + // 构建Jenkins环境信息Map + def jenkinsInfo = [ + "构建环境": script.env.PROFILES, // 构建环境(如dev/test/prod) + "构建编号": "#" + buildInfo.buildNumber, // 构建编号 + "构建节点": buildInfo.nodeName, // 构建节点名称 + "提交编码": gitInfo.commitShort, // Git提交短哈希 + ] + + // 合并原始extraInfo和Jenkins环境信息 + return extraInfo + jenkinsInfo + } catch (Exception e) { + // 如果获取信息失败,返回原始extraInfo + return extraInfo + } + } + + /** + * 获取构建信息 + * @return 包含构建相关信息的Map + */ + private def getBuildInfo() { + return [ + buildNumber: script.env.BUILD_NUMBER ?: 'N/A', // 构建编号 + buildId : script.env.BUILD_ID ?: 'N/A', // 构建ID + buildUrl : script.env.BUILD_URL ?: 'N/A', // 构建详情URL + jenkinsUrl : script.env.JENKINS_URL ?: 'N/A', // Jenkins URL + nodeName : script.env.NODE_NAME ?: 'N/A', // 节点名称 + timestamp : new Date().format('yyyy-MM-dd HH:mm:ss'), // 当前时间戳 + duration : script.currentBuild.duration ?: 'N/A' // 构建时长 + ] + } + + /** + * 获取Git信息 + * @return 包含Git相关信息的Map + */ + private def getGitInfo() { + // 获取Git提交哈希 + def gitCommit = script.env.GIT_COMMIT ?: '' + // 获取Git分支名称,处理远程分支名称(去除origin/前缀) + def gitBranch = script.env.GIT_BRANCH ?: 'N/A' + + // 清理分支名称(移除origin/前缀) + if (gitBranch.contains('/')) { + gitBranch = gitBranch.split('/').last() + } + + return [ + commit : gitCommit, // 完整提交哈希 + commitShort : gitCommit ? gitCommit.substring(0, 8) : 'N/A', // 短提交哈希(前8位) + branch : gitBranch, // 分支名称 + repositoryUrl: script.env.GIT_URL ?: 'N/A' // Git仓库URL + ] + } + + /** + * 获取默认项目名称 + * @return 项目名称,优先使用pipelineParams中的配置,其次使用Jenkins任务名称 + */ + private def getDefaultProjectName() { + return pipelineParams.projectName ?: + script.env.JOB_NAME ?: + '未知项目' + } + + /** + * 获取通知配置 + * @param buildResult 构建结果类型 + * @return 对应的通知配置,如果没有找到则返回null + */ + private def getNotificationConfig(String buildResult) { + // 先从notification配置中查找 + def config = pipelineParams.notification?."${buildResult}" + // 如果没找到,再从common.notification中查找 + if (!config) { + config = pipelineParams.common?.notification?."${buildResult}" + } + return config + } + + /** + * 转换构建结果为邮件服务需要的格式(全大写) + * @param buildResult 构建结果类型(小写) + * @return 转换后的构建结果(全大写) + */ + private def convertBuildResultForEmail(String buildResult) { + switch (buildResult) { + case 'success': return 'SUCCESS' + case 'failure': return 'FAILURE' + case 'unstable': return 'UNSTABLE' + case 'aborted': return 'ABORTED' + default: return 'UNKNOWN' + } + } +} \ No newline at end of file diff --git a/src/com/common/SmsService.groovy b/src/com/common/SmsService.groovy new file mode 100644 index 0000000..e284adb --- /dev/null +++ b/src/com/common/SmsService.groovy @@ -0,0 +1,1000 @@ +package com.common + +import groovy.json.JsonOutput +import groovy.json.JsonSlurper + +/** + * 短信消息服务类 + * 支持多种短信服务商:阿里云、腾讯云、华为云等 + * 包含模板短信、验证码短信、通知短信等功能 + * 支持重试机制和失败回退 + */ +class SmsService implements Serializable { + // Jenkins脚本实例,用于执行Jenkins相关操作 + private final script + // 管道参数,包含整个流水线的配置信息 + private final pipelineParams + // 最大重试次数,当短信发送失败时的重试次数,默认3次 + private final maxRetries + // 重试间隔(毫秒),每次重试之间的等待时间,默认1000ms + private final retryDelay + // 短信服务商配置,从pipelineParams中提取的短信相关配置 + private final smsConfig + + /** + * 构造函数 + * @param script Jenkins脚本实例,用于执行Jenkins操作 + * @param pipelineParams 管道参数,包含流水线配置 + * @param maxRetries 最大重试次数,默认3次 + * @param retryDelay 重试间隔(毫秒),默认1000ms + */ + SmsService(script, pipelineParams, maxRetries = 3, retryDelay = 1000) { + this.script = script + this.pipelineParams = pipelineParams + this.maxRetries = maxRetries + this.retryDelay = retryDelay + // 从管道参数中获取短信配置,如果没有则使用空Map + this.smsConfig = pipelineParams.sms ?: [:] + } + + // ==================== 构建相关短信 ==================== + + /** + * 发送构建开始短信 + * @param projectName 项目名称 + * @param mobiles 手机号列表,接收短信的手机号码 + * @param branch 分支名称,可选参数 + * @param extraParams 额外参数,可选,用于扩展模板参数 + * @return 发送结果,成功返回true,失败返回false + */ + def sendBuildStartSms(String projectName, List mobiles, String branch = '', Map extraParams = [:]) { + // 构建模板参数,包含项目、分支、状态和时间等基本信息 + def templateParams = [ + project: projectName, + branch : branch, + status : '开始', + time : new Date().format('yyyy-MM-dd HH:mm:ss') + ] + extraParams + + // 调用模板短信发送方法 + return sendTemplateSms('build_start', mobiles, templateParams) + } + + /** + * 发送构建成功短信 + * @param projectName 项目名称 + * @param mobiles 手机号列表,接收短信的手机号码 + * @param branch 分支名称,可选参数 + * @param duration 构建时长(毫秒),用于显示构建耗时 + * @param extraParams 额外参数,可选,用于扩展模板参数 + * @return 发送结果,成功返回true,失败返回false + */ + def sendBuildSuccessSms(String projectName, List mobiles, String branch = '', long duration = 0, Map extraParams = [:]) { + script.echo "短信扩展参数:${extraParams}" + // 构建模板参数,包含项目、分支、状态、耗时和时间等信息 + def templateParams = [ + project : projectName, + branch : branch, + status : '成功', + duration: formatDuration(duration), + time : new Date().format('yyyy-MM-dd HH:mm:ss') + ] + extraParams + script.echo "准备发送成功短信通知,模板参数:${templateParams}" + return sendTemplateSms('build_success', mobiles, templateParams) + } + + /** + * 发送构建失败短信 + * @param projectName 项目名称 + * @param mobiles 手机号列表,接收短信的手机号码 + * @param branch 分支名称,可选参数 + * @param duration 构建时长(毫秒),用于显示构建耗时 + * @param errorMsg 错误信息,构建失败的原因 + * @param extraParams 额外参数,可选,用于扩展模板参数 + * @return 发送结果,成功返回true,失败返回false + */ + def sendBuildFailureSms(String projectName, List mobiles, String branch = '', long duration = 0, String errorMsg = '', Map extraParams = [:]) { + // 构建模板参数,包含项目、分支、状态、耗时、错误信息和时间等 + def templateParams = [ + project : projectName, + branch : branch, + status : '失败', + duration: formatDuration(duration), + error : errorMsg.take(20), // 截取前20个字符,避免短信内容过长 + time : new Date().format('yyyy-MM-dd HH:mm:ss') + ] + extraParams + + return sendTemplateSms('build_failure', mobiles, templateParams) + } + + /** + * 发送构建中止短信 + * @param projectName 项目名称 + * @param mobiles 手机号列表,接收短信的手机号码 + * @param branch 分支名称,可选参数 + * @param duration 构建时长(毫秒),用于显示构建耗时 + * @param extraParams 额外参数,可选,用于扩展模板参数 + * @return 发送结果,成功返回true,失败返回false + */ + def sendBuildAbortedSms(String projectName, List mobiles, String branch = '', long duration = 0, Map extraParams = [:]) { + // 构建模板参数,包含项目、分支、状态、耗时和时间等信息 + def templateParams = [ + project : projectName, + branch : branch, + status : '中止', + duration: formatDuration(duration), + time : new Date().format('yyyy-MM-dd HH:mm:ss') + ] + extraParams + + return sendTemplateSms('build_aborted', mobiles, templateParams) + } + + // ==================== 部署相关短信 ==================== + + /** + * 发送部署通知短信 + * @param projectName 项目名称 + * @param environment 环境名称,如dev、test、prod等 + * @param version 版本号,部署的版本信息 + * @param status 部署状态,如成功、失败、进行中等 + * @param mobiles 手机号列表,接收短信的手机号码 + * @param extraParams 额外参数,可选,用于扩展模板参数 + * @return 发送结果,成功返回true,失败返回false + */ + def sendDeploymentSms(String projectName, String environment, String version, String status, List mobiles, Map extraParams = [:]) { + // 构建模板参数,包含项目、环境、版本、状态和时间等信息 + def templateParams = [ + project : projectName, + environment: environment, + version : version, + status : status, + time : new Date().format('yyyy-MM-dd HH:mm:ss') + ] + extraParams + + return sendTemplateSms('deployment', mobiles, templateParams) + } + + // ==================== 告警相关短信 ==================== + + /** + * 发送系统告警短信 + * @param systemName 系统名称,发生告警的系统 + * @param alertLevel 告警级别,如critical、warning、info等 + * @param alertContent 告警内容,告警的详细描述 + * @param mobiles 手机号列表,接收短信的手机号码 + * @param extraParams 额外参数,可选,用于扩展模板参数 + * @return 发送结果,成功返回true,失败返回false + */ + def sendAlertSms(String systemName, String alertLevel, String alertContent, List mobiles, Map extraParams = [:]) { + // 构建模板参数,包含系统名称、告警级别、告警内容和时间等信息 + def templateParams = [ + system : systemName, + level : alertLevel, + content: alertContent.take(30), // 截取前30个字符,避免短信内容过长 + time : new Date().format('yyyy-MM-dd HH:mm:ss') + ] + extraParams + + return sendTemplateSms('alert', mobiles, templateParams) + } + + // ==================== 核心发送方法 ==================== + + /** + * 发送模板短信 + * 根据模板类型和参数发送短信到指定手机号列表 + * @param templateType 模板类型,如build_start、build_success等 + * @param mobiles 手机号列表,接收短信的手机号码 + * @param templateParams 模板参数,用于替换模板中的变量 + * @return 发送结果,成功返回true,失败返回false + */ + def sendTemplateSms(String templateType, List mobiles, Map templateParams) { + script.echo "开始准备发送构建通知短信..." + // 检查手机号列表是否为空 + if (!mobiles) { + script.echo "⚠️ 手机号列表为空,跳过短信发送" + return false + } + + try { + // 获取短信服务商配置,默认使用阿里云 + def provider = smsConfig.provider ?: 'aliyun' + // 获取指定模板类型的配置 + def templateConfig = smsConfig.templates?."${templateType}" ?: [:] + + // 检查模板配置是否存在 + if (!templateConfig) { + script.echo "⚠️ 未找到 ${templateType} 的短信模板配置" + return false + } + + script.echo "短信服务商:${provider}" + script.echo "短信模板配置:${templateConfig}" + + // 根据服务商选择发送方法 + switch (provider.toLowerCase()) { + case 'aliyun': + return sendAliyunSms(templateConfig, mobiles, templateParams) + case 'tencent': + return sendTencentSms(templateConfig, mobiles, templateParams) + case 'huawei': + return sendHuaweiSms(templateConfig, mobiles, templateParams) + default: + script.echo "⚠️ 不支持的短信服务商: ${provider}" + return false + } + } catch (Exception e) { + // 捕获并记录异常信息 + script.echo "❌ 短信发送失败: ${e.message}" + return false + } + } + + /** + * 发送阿里云短信 + * 使用阿里云短信服务发送模板短信 + * @param templateConfig 模板配置,包含签名和模板代码等信息 + * @param mobiles 手机号列表,接收短信的手机号码 + * @param templateParams 模板参数,用于替换模板中的变量 + * @return 发送结果,成功返回true,失败返回false + */ + private def sendAliyunSms(Map templateConfig, List mobiles, Map templateParams) { + script.echo "即将发送阿里云短信通知..." + // 获取凭据ID,如果没有配置则使用默认值 + def credentialsId = smsConfig.credentialsId ?: 'sms-credentials' + + // 使用Jenkins凭据管理获取阿里云AccessKey + script.withCredentials([ + script.usernamePassword( + credentialsId: credentialsId, + usernameVariable: 'SMS_ACCESS_KEY_ID', // 阿里云AccessKey ID环境变量名 + passwordVariable: 'SMS_ACCESS_KEY_SECRET' // 阿里云AccessKey Secret环境变量名 + ) + ]) { + script.echo "阿里云短信Jenkins凭证验证成功..." + // 构建请求数据 + def requestData = [ + PhoneNumbers : mobiles.join(','), // 手机号列表,用逗号分隔 + SignName : templateConfig.signName ?: smsConfig.signName, // 短信签名 + TemplateCode : templateConfig.templateCode, // 模板代码 + TemplateParam: JsonOutput.toJson(templateParams) // 模板参数,转为JSON格式 + ] + + script.echo "短信发送请求数据:${requestData}" + + // 调用通用短信发送方法 + return sendSmsRequest( + 'https://dysmsapi.aliyuncs.com', // 阿里云短信API端点 + '/', // API路径 + 'SendSms', // API动作 + '2017-05-25', // API版本 + script.env.SMS_ACCESS_KEY_ID, // AccessKey ID + script.env.SMS_ACCESS_KEY_SECRET, // AccessKey Secret + requestData // 请求数据 + ) + } + } + + /** + * 发送腾讯云短信 + * 使用腾讯云短信服务发送模板短信 + * @param templateConfig 模板配置,包含签名和模板ID等信息 + * @param mobiles 手机号列表,接收短信的手机号码 + * @param templateParams 模板参数,用于替换模板中的变量 + * @return 发送结果,成功返回true,失败返回false + */ + private def sendTencentSms(Map templateConfig, List mobiles, Map templateParams) { + // 获取凭据ID,如果没有配置则使用默认值 + def credentialsId = smsConfig.credentialsId ?: 'sms-credentials' + + // 使用Jenkins凭据管理获取腾讯云Secret + script.withCredentials([ + script.usernamePassword( + credentialsId: credentialsId, + usernameVariable: 'SMS_SECRET_ID', // 腾讯云Secret ID环境变量名 + passwordVariable: 'SMS_SECRET_KEY' // 腾讯云Secret Key环境变量名 + ) + ]) { + // 构建请求数据 + def requestData = [ + PhoneNumberSet : mobiles, // 手机号列表 + TemplateID : templateConfig.templateId, // 模板ID + SignName : templateConfig.signName ?: smsConfig.signName, // 短信签名 + TemplateParamSet: templateParams.values().collect { it.toString() }, // 模板参数集合 + SmsSdkAppId : smsConfig.appId // 短信应用ID + ] + + // 调用通用短信发送方法 + return sendSmsRequest( + 'https://sms.tencentcloudapi.com', // 腾讯云短信API端点 + '/', // API路径 + 'SendSms', // API动作 + '2021-01-11', // API版本 + script.env.SMS_SECRET_ID, // Secret ID + script.env.SMS_SECRET_KEY, // Secret Key + requestData, // 请求数据 + 'tc3' // 签名方法 + ) + } + } + + /** + * 发送华为云短信 + * 使用华为云短信服务发送模板短信 + * @param templateConfig 模板配置,包含签名和模板ID等信息 + * @param mobiles 手机号列表,接收短信的手机号码 + * @param templateParams 模板参数,用于替换模板中的变量 + * @return 发送结果,成功返回true,失败返回false + */ + private def sendHuaweiSms(Map templateConfig, List mobiles, Map templateParams) { + script.echo "即将发送华为云短信通知..." + // 获取凭据ID,如果没有配置则使用默认值 + def credentialsId = smsConfig.credentialsId ?: 'sms-credentials' + + // 使用Jenkins凭据管理获取华为云AK/SK + script.withCredentials([ + script.usernamePassword( + credentialsId: credentialsId, + usernameVariable: 'SMS_ACCESS_KEY', // 华为云Access Key环境变量名 + passwordVariable: 'SMS_SECRET_KEY' // 华为云Secret Key环境变量名 + ) + ]) { + // 构建请求数据 + def requestData = [ + from : templateConfig.sender ?: smsConfig.sender, // 发送方号码(国内短信一般为签名名称) + to : mobiles, // 接收方号码列表 + templateId : templateConfig.templateId, // 模板ID + templateParas: JsonOutput.toJson(templateParams.values().collect { it.toString() }), // 模板参数 + signature : templateConfig.signName ?: smsConfig.signName // 短信签名 + ] + + script.echo "华为云短信请求数据:${requestData}" + + // 调用华为云专用发送方法 + return sendHuaweiRequest( + 'https://smsapi.cn-north-4.myhuaweicloud.com', // 华为云短信API端点 + '/v3/sms/messages', // API路径 + script.env.SMS_ACCESS_KEY, // Access Key + script.env.SMS_SECRET_KEY, // Secret Key + requestData // 请求数据 + ) + } + } + + // ==================== 工具方法 ==================== + + /** + * 带重试机制的短信发送 + * 通用的短信发送方法,支持重试机制 + * @param endpoint API端点地址 + * @param path API路径 + * @param action API动作 + * @param version API版本 + * @param keyId 访问密钥ID + * @param keySecret 访问密钥 + * @param data 请求数据 + * @param signMethod 签名方法,可选参数 + * @return 发送结果,成功返回true,失败返回false + */ + private def sendSmsRequest(String endpoint, String path, String action, String version, + String keyId, String keySecret, Map data, String signMethod = '') { + int attempt = 0 + boolean success = false + + while (attempt < maxRetries && !success) { + attempt++ + try { + script.echo "📱 尝试第 ${attempt} 次发送短信到: ${endpoint}" + + // 根据不同的服务商端点选择不同的请求构建方式 + if (endpoint.contains('aliyuncs.com')) { + success = sendAliyunRequest(endpoint, keyId, keySecret, data) + } else if (endpoint.contains('tencentcloudapi.com')) { + success = sendTencentRequest(endpoint, action, version, keyId, keySecret, data) + } else { + script.echo "⚠️ 不支持的短信服务商端点: ${endpoint}" + return false + } + + if (success) { + script.echo "✅ 短信发送成功" + return true + } + + } catch (Exception e) { + script.echo "❌ 第 ${attempt} 次发送失败: ${e.message}" + if (attempt < maxRetries) { + script.echo "⏳ 等待 ${retryDelay}ms 后重试..." + script.sleep(retryDelay / 1000) + } + } + } + + script.echo "❌ 短信发送失败,已达最大重试次数: ${maxRetries}" + return false + } + + /** + * 发送华为云短信请求 + * @param endpoint API端点地址 + * @param path API路径 + * @param accessKey 华为云Access Key + * @param secretKey 华为云Secret Key + * @param data 请求数据 + * @return 发送结果,成功返回true,失败返回false + */ + private def sendHuaweiRequest(String endpoint, String path, String accessKey, String secretKey, Map data) { + int attempt = 0 + boolean success = false + + while (attempt < maxRetries && !success) { + attempt++ + try { + script.echo "📱 尝试第 ${attempt} 次发送华为云短信..." + + // 生成时间戳 + def timestamp = new Date().format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone('UTC')) + + // 构建请求头 + def headers = [ + 'Content-Type': 'application/json', + 'X-Sdk-Date' : timestamp + ] + + // 生成签名 + def signature = generateHuaweiSignature(accessKey, secretKey, 'POST', path, headers, JsonOutput.toJson(data)) + headers['Authorization'] = signature + + // 发送HTTP请求 + def response = script.httpRequest( + url: endpoint + path, + httpMode: 'POST', + contentType: 'APPLICATION_JSON', + requestBody: JsonOutput.toJson(data), + customHeaders: headers.collect { key, value -> + script.httpRequestHeader(key, value) + }, + validResponseCodes: '200,201,202', + timeout: 15000 + ) + + def result = new JsonSlurper().parseText(response.content) + if (result.code == '000000') { + script.echo "✅ 华为云短信发送成功" + return true + } else { + script.echo "❌ 华为云短信发送失败: ${result.description} (Code: ${result.code})" + } + + } catch (Exception e) { + script.echo "❌ 第 ${attempt} 次华为云短信发送失败: ${e.message}" + if (attempt < maxRetries) { + script.echo "⏳ 等待 ${retryDelay}ms 后重试..." + script.sleep(retryDelay / 1000) + } + } + } + + script.echo "❌ 华为云短信发送失败,已达最大重试次数: ${maxRetries}" + return false + } + + /** + * 生成华为云签名 + * @param accessKey 华为云Access Key + * @param secretKey 华为云Secret Key + * @param method HTTP方法 + * @param path API路径 + * @param headers 请求头 + * @param body 请求体 + * @return 签名字符串 + */ + private def generateHuaweiSignature(String accessKey, String secretKey, String method, String path, Map headers, String body) { + try { + // 构建规范请求 + def canonicalRequest = buildCanonicalRequest(method, path, headers, body) + + // 构建待签名字符串 + def stringToSign = "SDK-HMAC-SHA256\n${headers['X-Sdk-Date']}\n${canonicalRequest}" + + // 生成签名 + def signingKey = generateHmacSha256Signature(headers['X-Sdk-Date'], secretKey) + def signature = generateHmacSha256Signature(stringToSign, signingKey) + + return "SDK-HMAC-SHA256 Access=${accessKey}, Signature=${signature}" + } catch (Exception e) { + script.echo "❌ 华为云签名生成失败: ${e.message}" + throw e + } + } + + /** + * 构建规范请求 + */ + private def buildCanonicalRequest(String method, String path, Map headers, String body) { + // 规范方法 + def canonicalMethod = method.toUpperCase() + + // 规范URI + def canonicalUri = URLEncoder.encode(path, "UTF-8") + + // 规范查询字符串(华为云短信API通常没有查询参数) + def canonicalQueryString = "" + + // 规范头 + def sortedHeaders = headers.keySet().sort() + def canonicalHeaders = sortedHeaders.collect { key -> + "${key.toLowerCase()}:${headers[key].toString().trim()}" + }.join('\n') + + // 签名头 + def signedHeaders = sortedHeaders.collect { it.toLowerCase() }.join(';') + + // 哈希后的请求体 + def hashedPayload = generateSha256Hash(body) + + return "${canonicalMethod}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n\n${signedHeaders}\n${hashedPayload}" + } + + /** + * 生成SHA256哈希 + */ + private def generateSha256Hash(String data) { + try { + def digest = java.security.MessageDigest.getInstance('SHA-256') + def hash = digest.digest(data.getBytes('UTF-8')) + return bytesToHex(hash).toLowerCase() + } catch (Exception e) { + script.echo "❌ SHA256哈希生成失败: ${e.message}" + return "" + } + } + + /** + * 字节数组转十六进制字符串 + */ + private def bytesToHex(byte[] bytes) { + def hexArray = "0123456789ABCDEF".toCharArray() + def hexChars = new char[bytes.length * 2] + for (int i = 0; i < bytes.length; i++) { + int v = bytes[i] & 0xFF + hexChars[i * 2] = hexArray[v >>> 4] + hexChars[i * 2 + 1] = hexArray[v & 0x0F] + } + return new String(hexChars) + } + + /** + * 生成HMAC-SHA256签名 + */ + private def generateHmacSha256Signature(String data, String key) { + try { + def mac = javax.crypto.Mac.getInstance('HmacSHA256') + def secretKey = new javax.crypto.spec.SecretKeySpec(key.getBytes('UTF-8'), 'HmacSHA256') + mac.init(secretKey) + def signData = mac.doFinal(data.getBytes('UTF-8')) + return bytesToHex(signData).toLowerCase() + } catch (Exception e) { + script.echo "❌ HMAC-SHA256签名生成失败: ${e.message}" + throw e + } + } + + /** + * 发送阿里云短信请求 + */ + private def sendAliyunRequest(String endpoint, String keyId, String keySecret, Map data) { + try { + // 首先检查参数格式 + if (!checkAliyunParams(data)) { + return false + } + + // 获取当前时间戳(UTC格式) + def timestamp = new Date().format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone('UTC')) + def nonce = UUID.randomUUID().toString() + + // 构建基础参数 + def baseParams = [ + 'Action' : 'SendSms', + 'Version' : '2017-05-25', + 'Format' : 'JSON', + 'SignatureMethod' : 'HMAC-SHA1', + 'SignatureVersion': '1.0', + 'AccessKeyId' : keyId, + 'Timestamp' : timestamp, + 'SignatureNonce' : nonce, + 'RegionId' : 'cn-hangzhou', // 固定为杭州区域 + 'PhoneNumbers' : data.PhoneNumbers, + 'SignName' : encode(data.SignName), // 对签名进行编码 + 'TemplateCode' : data.TemplateCode + ] + + // 处理模板参数 - 过滤并截断超长参数 + if (data.TemplateParam) { + baseParams['TemplateParam'] = processTemplateParam(data.TemplateParam) + } + + script.echo "短信模板参数:${baseParams['TemplateParam']}" + + // 对参数按键进行排序 + def sortedKeys = baseParams.keySet().sort() + + // 构建规范查询字符串 + def queryString = sortedKeys.collect { key -> + def value = baseParams[key] + "${encode(key)}=${encode(value.toString())}" + }.join('&') + + // 生成签名字符串 + def stringToSign = "POST&%2F&" + encode(queryString) + + // 生成签名 + def signature = generateHmacSha1Signature(stringToSign, keySecret + "&") + + // 构建最终请求URL + def finalQueryString = queryString + "&Signature=" + encode(signature) + def requestUrl = "${endpoint}?${finalQueryString}" + + script.echo "阿里云短信请求信息:" + script.echo "端点: ${endpoint}" + script.echo "手机号: ${data.PhoneNumbers}" + script.echo "签名: ${data.SignName}" + script.echo "模板: ${data.TemplateCode}" + script.echo "区域: cn-hangzhou" + + // 使用更兼容的HTTP请求方式 + try { + def response = script.httpRequest( + url: requestUrl, + httpMode: 'POST', + validResponseCodes: '200,400,401,403,404,500,502,503,504', // 扩展可接受的状态码 + timeout: 15000, + quiet: true + ) + + script.echo "阿里云响应状态: ${response.status}" + def result = new JsonSlurper().parseText(response.content) + + if (result.Code == 'OK') { + script.echo "✅ 阿里云短信发送成功: ${result.Message}" + return true + } else { + script.echo "❌ 阿里云短信发送失败: ${result.Message} (Code: ${result.Code})" + return false + } + + } catch (Exception httpError) { + script.echo "❌ HTTP请求失败: ${httpError.message}" + script.echo "尝试使用备用方案..." + return sendAliyunWithCurl(requestUrl) + } + + } catch (Exception e) { + script.echo "❌ 阿里云短信发送异常: ${e.message}" + return false + } + } + + /** + * 使用curl发送阿里云短信(备用方案) + */ + private def sendAliyunWithCurl(String requestUrl) { + try { + script.echo "使用curl发送请求..." + + // 使用curl命令发送请求 + def command = """curl -X POST \ + -L \ + -s \ + -S \ + -w "\\n%{http_code}" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + "${requestUrl}\"""" + + // 执行curl命令 + def result = script.sh(script: command, returnStdout: true).trim() + def lines = result.split('\n') + def statusCode = lines[-1] + def content = lines[0..-2].join('\n') + + script.echo "Curl响应状态: ${statusCode}" + script.echo "Curl响应内容: ${content}" + + if (statusCode == '200') { + try { + def resultObj = new JsonSlurper().parseText(content) + if (resultObj.Code == 'OK') { + script.echo "✅ 阿里云短信发送成功(通过curl)" + return true + } else { + script.echo "❌ 阿里云短信业务失败: ${resultObj.Message} (Code: ${resultObj.Code})" + return false + } + } catch (Exception parseError) { + script.echo "❌ 响应解析失败: ${parseError.message}" + script.echo "原始响应: ${content}" + return false + } + } else { + script.echo "❌ HTTP状态码错误: ${statusCode}" + return false + } + + } catch (Exception e) { + script.echo "❌ Curl方案也失败: ${e.message}" + return false + } + } + + /** + * 检查阿里云参数格式 + */ + private def checkAliyunParams(Map data) { + // 确保必要参数存在 + def requiredParams = ['PhoneNumbers', 'SignName', 'TemplateCode'] + def missingParams = requiredParams.findAll { !data.containsKey(it) } + + if (missingParams) { + script.echo "❌ 缺少必要参数: ${missingParams}" + return false + } + + // 检查手机号格式 + def phoneNumbers = data.PhoneNumbers.split(',') + def invalidPhones = phoneNumbers.findAll { !it.matches(/^1[3-9]\d{9}$/) } + + if (invalidPhones) { + script.echo "❌ 无效的手机号格式: ${invalidPhones}" + return false + } + + // 检查签名和模板代码格式 + if (!data.SignName) { + script.echo "❌ 短信签名不能为空" + return false + } + + if (!data.TemplateCode || !data.TemplateCode.startsWith('SMS_')) { + script.echo "❌ 模板代码格式不正确,应以SMS_开头" + return false + } + + return true + } + + /** + * 处理模板参数,确保符合阿里云要求 + */ + private def processTemplateParam(templateParam) { + if (templateParam == null) { + return null + } + + try { + if (templateParam instanceof Map) { + return processMapTemplateParams(templateParam) + } else if (templateParam instanceof String) { + return processStringTemplateParam(templateParam) + } else { + return templateParam.toString() + } + } catch (Exception e) { + script.echo "⚠️ 模板参数处理失败: ${e.message}" + return null + } + } + + /** + * 处理Map类型的参数(阿里云专用) + */ + private def processMapTemplateParams(Map templateParams) { + if (!templateParams) return null + + def processedParams = [:] + templateParams.each { key, value -> + // 1. 检查KEY是否是英文数字 + if (isValidTemplateParamKey(key)) { + // 2. 处理VALUE(可以是中文,但需要控制长度) + def processedValue = processTemplateParamValue(value) + processedParams[key] = processedValue + } else { + script.echo "⚠️ 跳过无效的参数KEY(必须是英文数字): ${key}" + } + } + + return processedParams ? JsonOutput.toJson(processedParams) : null + } + + /** + * 检查是否是有效的阿里云参数KEY + */ + private def isValidTemplateParamKey(String key) { + // 阿里云要求参数KEY必须是字母数字 + return key ==~ /^[a-zA-Z0-9_]+$/ + } + + /** + * 处理阿里云参数值(可以是中文) + */ + private def processTemplateParamValue(value) { + if (value == null) return "" + + def strValue = value.toString() + + // 阿里云单个参数值限制为20个字符(注意:中文算多个字符) + // 这里使用更保守的限制:15个字符(因为UTF-8编码问题) + int maxLength = 15 + + if (getUtf8Length(strValue) <= maxLength) { + return strValue + } + + // 截断超长的中文内容 + def truncated = truncateChineseString(strValue, maxLength) + script.echo "⚠️ 参数值超长截断: ${strValue} -> ${truncated}" + return truncated + } + + /** + * 获取UTF-8字符串的实际长度(考虑中文) + */ + private def getUtf8Length(String str) { + if (str == null) return 0 + try { + return str.getBytes("UTF-8").length + } catch (Exception e) { + return str.length() + } + } + + /** + * 截断中文字符串 + */ + private def truncateChineseString(String str, int maxBytes) { + if (str == null) return "" + + try { + byte[] bytes = str.getBytes("UTF-8") + if (bytes.length <= maxBytes) { + return str + } + + // 找到合适的截断位置 + int truncateLength = maxBytes + while (truncateLength > 0) { + try { + String truncated = new String(bytes, 0, truncateLength, "UTF-8") + return truncated + "..." + } catch (Exception e) { + truncateLength-- + } + } + + return str.substring(0, Math.min(10, str.length())) + "..." + } catch (Exception e) { + return str.length() > 10 ? str.substring(0, 10) + "..." : str + } + } + + /** + * 处理字符串类型的模板参数 + */ + private def processStringTemplateParam(String templateParam) { + if (!templateParam) return null + + try { + // 尝试解析JSON字符串 + def parsedParams = new JsonSlurper().parseText(templateParam) + if (parsedParams instanceof Map) { + def processedParams = processMapTemplateParams(parsedParams) + return processedParams ?: templateParam + } else { + // 如果不是Map,检查长度并截断 + return checkAndTruncateStringParam(templateParam) + } + } catch (Exception e) { + script.echo "⚠️ 模板参数解析失败,使用原始字符串: ${e.message}" + // 检查字符串长度并截断 + return checkAndTruncateStringParam(templateParam) + } + } + + /** + * 检查并截断字符串参数 + */ + private def checkAndTruncateStringParam(String param) { + if (param == null) return null + + // 阿里云整个TemplateParam字段限制为500字符 + int maxTotalLength = 500 + + if (param.length() <= maxTotalLength) { + return param + } + + // 截断并添加省略号 + def truncated = param.substring(0, maxTotalLength - 3) + "..." + script.echo "⚠️ 模板参数字符串超长,截断: ${param.length()} -> ${truncated.length()}字符" + return truncated + } + + /** + * 改进的URL编码方法 + */ + private def encode(String value) { + try { + if (value == null) return "" + return URLEncoder.encode(value, "UTF-8") + .replace('+', '%20') + .replace('*', '%2A') + .replace('%7E', '~') + } catch (Exception e) { + return value + } + } + + /** + * 生成HMAC-SHA1签名 + */ + private def generateHmacSha1Signature(String data, String key) { + try { + def mac = javax.crypto.Mac.getInstance('HmacSHA1') + def secretKey = new javax.crypto.spec.SecretKeySpec(key.getBytes('UTF-8'), 'HmacSHA1') + mac.init(secretKey) + def signData = mac.doFinal(data.getBytes('UTF-8')) + return java.util.Base64.encoder.encodeToString(signData) + } catch (Exception e) { + script.echo "❌ 签名生成失败: ${e.message}" + throw e + } + } + + /** + * 构建短信文本(用于云片等需要自己拼接内容的服务商) + * 将模板和参数组合成完整的短信文本 + * @param templateConfig 模板配置 + * @param templateParams 模板参数 + * @return 完整的短信文本内容 + */ + private def buildSmsText(Map templateConfig, Map templateParams) { + // 获取模板内容,如果没有配置则使用默认模板 + def template = templateConfig.template ?: "\${project}构建\${status}" + def text = template + // 替换模板中的变量占位符 + templateParams.each { key, value -> + text = text.replaceAll('\\$\\{' + key + '\\}', value.toString()) + } + return text + } + + /** + * 将Map转换为表单数据 + * 用于HTTP POST请求的表单格式数据 + * @param data 要转换的数据Map + * @return 表单格式的字符串 + */ + private def toFormData(Map data) { + return data.collect { key, value -> + // 对key和value进行URL编码 + "${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value.toString(), "UTF-8")}" + }.join('&') // 用&符号连接所有参数 + } + + /** + * 格式化持续时间 + * 将毫秒转换为易读的时间格式 + * @param duration 持续时间(毫秒) + * @return 格式化后的时间字符串 + */ + private static String formatDuration(long duration) { + long seconds = duration / 1000 // 转换为秒 + long minutes = seconds / 60 // 转换为分钟 + long hours = minutes / 60 // 转换为小时 + + // 根据时间长度选择合适的格式 + if (hours > 0) return "${hours}h${minutes % 60}m" // 小时和分钟格式 + if (minutes > 0) return "${minutes}m${seconds % 60}s" // 分钟和秒格式 + return "${seconds}s" // 秒格式 + } + +} \ No newline at end of file diff --git a/src/com/common/StageService.groovy b/src/com/common/StageService.groovy new file mode 100644 index 0000000..60090f6 --- /dev/null +++ b/src/com/common/StageService.groovy @@ -0,0 +1,317 @@ +package com.common + +import com.utils.DurationFormatter +import org.jenkinsci.plugins.workflow.actions.TimingAction +import org.jenkinsci.plugins.workflow.graph.FlowNode + +/** + * Stage服务类 - 负责Stage信息的收集和管理 + * 主要功能: + * - 记录stage开始和结束时间 + * - 自动收集所有stage信息 + * - 提供stage相关的统计和分析功能 + * - 生成构建报告和时间线展示 + */ +class StageService implements Serializable { + // Jenkins pipeline脚本对象,用于执行echo、currentBuild等Jenkins DSL方法 + def script + + // 存储所有stage信息的列表,每个元素是一个包含stage详细信息的Map + def stageInfos = [] + + // 存储当前stage的开始时间,key为stage名称,value为开始时间的时间戳 + def currentStageStartTime = [:] + + /** + * 构造函数 + * @param script Jenkins pipeline脚本对象,用于访问Jenkins环境和方法 + */ + StageService(script) { + this.script = script + } + + /** + * 记录stage开始时间 + * 在stage开始时调用此方法,记录开始时间并存储到currentStageStartTime中 + * @param stageName stage名称 + */ + def recordStageStart(stageName) { + // 获取当前系统时间作为开始时间 + def startTime = System.currentTimeMillis() + // 将stage名称和开始时间存储到Map中 + currentStageStartTime[stageName] = startTime + + // 输出日志信息,记录stage开始时间 + script.echo "Stage '${stageName}' started at: ${new Date(startTime)}" + } + + /** + * 记录stage结束信息 + * 在stage结束时调用此方法,计算持续时间并创建完整的stage信息记录 + * @param stageName stage名称 + * @param result stage构建结果(可选,如SUCCESS, FAILURE等) + */ + def recordStageEnd(stageName, result = null) { + // 获取当前系统时间作为结束时间 + def endTime = System.currentTimeMillis() + // 从Map中获取开始时间,如果没有记录则使用结束时间(避免空指针异常) + def startTime = currentStageStartTime[stageName] ?: endTime + // 计算stage执行持续时间(毫秒) + def duration = endTime - startTime + + // 创建stage信息Map,包含所有相关信息 + def stageInfo = [ + name : stageName, // stage名称 + startTime : new Date(startTime), // 开始时间(Date对象) + endTime : new Date(endTime), // 结束时间(Date对象) + duration : duration, // 持续时间(毫秒) + durationFormatted: DurationFormatter.formatDuration(duration), // 格式化后的持续时间 + result : result ?: getStageResultFromAPI(stageName), // 构建结果,优先使用传入参数 + status : getStatusText(result) // 状态文本(中文) + ] + + // 将stage信息添加到列表中 + stageInfos << stageInfo + + // 清理当前stage的开始时间记录,避免内存泄漏 + currentStageStartTime.remove(stageName) + + // 输出日志信息,记录stage完成情况和结果 + script.echo "Stage '${stageName}' completed with result: ${result}" + } + + /** + * 自动收集所有stage信息 + * 通过Jenkins API获取流水线执行信息,自动识别和收集所有stage节点 + * 此方法作为备用方案,当手动记录不完整时使用 + */ + def collectAllStagesInfo() { + try { + // 获取当前构建的原始Build对象 + def build = script.currentBuild.rawBuild + // 获取流水线执行对象 + def flowExecution = build.execution + + if (flowExecution) { + // 获取所有执行节点 + def nodes = flowExecution.getCurrentExecutions(true) + + // 遍历所有节点 + nodes.each { node -> + // 检查节点是否为FlowNode类型 + if (node instanceof FlowNode) { + def displayName = node.displayName + + // 识别stage节点(根据Jenkins的命名约定) + if (displayName.startsWith('Stage: ') || displayName.startsWith('stage ')) { + // 提取stage名称(移除前缀) + def stageName = displayName.replace('Stage: ', '').replace('stage ', '') + // 获取节点开始时间 + def timing = TimingAction.getStartTime(node) + // 获取节点执行持续时间 + def duration = TimingAction.getDuration(node) + + // 确保时间有效 + if (timing > 0) { + // 创建stage信息Map + def stageInfo = [ + name : stageName, + startTime : new Date(timing), + endTime : new Date(timing + duration), + duration : duration, + durationFormatted: DurationFormatter.formatDuration(duration), + result : node.error ? 'FAILED' : 'SUCCESS', // 根据节点错误状态判断结果 + status : node.error ? '失败' : '成功' + ] + // 输出调试信息 + script.echo "收集数据:${stageInfo}" + + // 将stage信息添加到列表中 + stageInfos << stageInfo + } + } + } + } + } + } catch (Exception e) { + // 异常处理,输出警告信息但不中断流程 + script.echo "Warning: Failed to collect stages info automatically: ${e.message}" + } + } + + /** + * 获取构建摘要信息 + * 统计所有stage的执行情况,生成构建概览 + * @return 包含构建统计信息的Map + */ + def getBuildSummary() { + // 总stage数量 + def totalStages = stageInfos.size() + // 失败的stage数量 + def failedStages = stageInfos.count { it.result.toUpperCase() == 'FAILURE' } + // 成功的stage数量 + def successStages = totalStages - failedStages + // 总持续时间(所有stage持续时间之和) + def totalDuration = stageInfos.sum { it.duration } ?: 0 + + // 返回构建摘要信息 + return [ + totalStages : totalStages, // 总stage数 + successStages : successStages, // 成功stage数 + failedStages : failedStages, // 失败stage数 + successRate : totalStages > 0 ? (successStages / totalStages * 100) : 0, // 成功率(百分比) + totalDuration : totalDuration, // 总持续时间(毫秒) + totalDurationFormatted: DurationFormatter.formatDuration(totalDuration), // 格式化总持续时间 + buildResult : script.currentBuild.currentResult, // 当前构建结果 + buildNumber : script.currentBuild.number, // 构建编号 + jobName : script.env.JOB_NAME, // 任务名称 + buildUrl : script.env.BUILD_URL // 构建URL + ] + } + + /** + * 获取构建任务stage节点列表 + * 格式化stage信息,用于展示或报告生成 + * @return 格式化后的stage信息列表 + */ + def getStageList() { + // 转换stage信息,确保所有字段都有默认值 + def stages = stageInfos.collect { stage -> + def stageInfo = [ + name : stage.name ?: '未知阶段', // stage名称,默认值处理 + startTime: stage.startTime ?: new Date(0), // 开始时间,默认值处理 + endTime : stage.endTime ?: new Date(0), // 结束时间,默认值处理 + duration : stage.durationFormatted ?: '0s', // 格式化持续时间,默认值处理 + result : stage.result ?: 'UNKNOWN', // 构建结果,默认值处理 + status : stage.status ?: '未知', // 状态文本,默认值处理 + timestamp: stage.startTime?.time ?: 0, // 时间戳(用于排序) + isSuccess: (stage.result ?: '').toUpperCase() == 'SUCCESS' // 是否成功的布尔值 + ] + return stageInfo + } + } + + /** + * 生成时间线HTML内容 + * 创建用于在Jenkins界面展示的时间线HTML + * @return 时间线HTML字符串 + */ + def getBuildStagesHTML() { + // 如果没有stage信息,返回提示信息 + if (stageInfos.empty) { + return "

暂无stage执行信息

" + } + + // 使用StringBuilder构建HTML内容(性能优化) + def timelineHtml = new StringBuilder() + + // 遍历所有stage信息,生成HTML元素 + stageInfos.each { stage -> + // 根据stage结果确定CSS类名 + def statusClass = '' + if (stage.result) { + switch (stage.result.toUpperCase()) { + case 'SUCCESS': + statusClass = 'success' // 成功状态 + break + case 'FAILURE': + statusClass = 'failure' // 失败状态 + break + case 'UNSTABLE': + statusClass = 'warning' // 不稳定状态 + break + default: + statusClass = '' // 默认状态 + } + } + + // 格式化持续时间 + def durationText = stage.durationFormatted ?: DurationFormatter.formatDuration(stage.duration ?: 0) + // 格式化开始时间(如果有) + def startTimeText = stage.startTime ? "
开始时间: ${stage.startTime.format('yyyy-MM-dd HH:mm:ss')}" : '' + // 格式化结束时间(如果有) + def endTimeText = stage.endTime ? "
结束时间: ${stage.endTime.format('yyyy-MM-dd HH:mm:ss')}" : '' + + // 构建单个stage的HTML元素 + timelineHtml.append(""" +
+
${stage.name ?: '未知阶段'}
+
+ ${stage.status ?: '未知状态'} • 耗时 ${durationText} + ${startTimeText} + ${endTimeText} +
+
+ """) + } + + // 返回完整的HTML内容 + return timelineHtml.toString() + } + + /** + * 从Jenkins API获取stage结果(备用方法) + * 当没有直接传入result参数时,通过API获取stage结果 + * @param stageName stage名称 + * @return stage结果(SUCCESS或FAILED) + */ + private def getStageResultFromAPI(stageName) { + try { + // 获取流水线执行对象 + def flowExecution = script.currentBuild.rawBuild.execution + // 获取所有执行节点 + def nodes = flowExecution.getCurrentExecutions(true) + + // 遍历节点查找匹配的stage + for (node in nodes) { + if (node instanceof FlowNode) { + def displayName = node.displayName + // 检查节点名称是否包含stage名称 + if (displayName.contains(stageName)) { + // 根据节点错误状态返回结果 + return node.error ? 'FAILED' : 'SUCCESS' + } + } + } + } catch (Exception e) { + // 异常处理,输出警告信息 + script.echo "Warning: Failed to get stage result from API: ${e.message}" + } + // 默认返回UNKNOWN + return 'UNKNOWN' + } + + /** + * 获取状态文本 + * 将英文状态代码转换为中文状态文本 + * @param result 构建结果代码 + * @return 中文状态文本 + */ + private def getStatusText(result) { + switch (result?.toString()?.toUpperCase()) { + case 'SUCCESS': return '成功' // 成功 + case 'FAILURE': return '失败' // 失败 + case 'UNSTABLE': return '不稳定' // 不稳定 + case 'ABORTED': return '已中止' // 已中止 + case 'NOT_BUILT': return '未构建' // 未构建 + default: return '未知' // 默认未知 + } + } + + /** + * 重置stage信息 + * 清空所有存储的stage信息,用于重新开始记录 + */ + def reset() { + stageInfos.clear() // 清空stage信息列表 + currentStageStartTime.clear() // 清空开始时间记录 + } + + /** + * 获取所有stage信息 + * @return 所有stage信息的列表 + */ + def getStageInfos() { + return stageInfos + } +} \ No newline at end of file diff --git a/src/com/constants/ConfigConstants.groovy b/src/com/constants/ConfigConstants.groovy new file mode 100644 index 0000000..f717ebb --- /dev/null +++ b/src/com/constants/ConfigConstants.groovy @@ -0,0 +1,50 @@ +package com.constants + +/** + * 配置常量类 + * 集中管理所有配置常量和默认值,避免硬编码 + */ +class ConfigConstants { + // 环境配置默认值 + static final DEFAULT_ENV_CONFIG = [ + masterProfiles: ['', 'uat', 'prod'], + Profiles : ['', 'dev'], + jobConfigFile : "xiaomayiConfig.yml", + docker : [ + base_image: "192.168.10.102:8001/xiaomayi-base/jdk:17.0.16-rocky8-dos", + build_args: "--no-cache" + ], + maven : [ + skip_tests: true, + goals : "clean package" + ], + dev : [ + health_check_timeout: 5, + rollback_enabled : true, + health_check_enabled: true + ] + ] + + // 分支类型与profile映射 + static final BRANCH_PROFILES = [ + 'master' : ['', 'uat', 'prod'], + 'test' : ['', 'test'], + 'dev' : ['', 'dev'], + 'feature': ['', 'test', 'dev'] + ] + + // 默认profile映射 + static final DEFAULT_PROFILES = [ + 'master' : 'prod', + 'test' : 'test', + 'dev' : 'dev', + 'feature': 'test' + ] + + // SSH连接配置 + static final SSH_CONFIG = [ + strictHostKeyChecking: "no", + connectTimeout : "30", + serverAliveInterval : 60, + ] +} \ No newline at end of file diff --git a/src/com/docker/DeploymentService.groovy b/src/com/docker/DeploymentService.groovy new file mode 100644 index 0000000..77196a5 --- /dev/null +++ b/src/com/docker/DeploymentService.groovy @@ -0,0 +1,180 @@ +package com.docker + +import com.docker.stages.BuildService +import com.docker.stages.DeployService + +/** + * 部署服务类 - CI/CD流水线核心协调器 + * 负责执行各个CI/CD阶段的业务逻辑,不直接创建Jenkins stage + * 提供模块化的构建、部署、验证功能,作为流水线步骤的抽象层 + * + * 设计原则: + * - 单一职责:每个方法只负责一个特定的功能 + * - 错误隔离:每个阶段的异常不会影响其他阶段 + * - 可重用性:方法可以在不同的流水线中重用 + * - 可扩展性:易于添加新的部署阶段或功能 + * - 可维护性:清晰的代码结构和职责分离 + * + * 主要职责: + * 1. 协调各个部署阶段服务的执行 + * 2. 提供统一的参数传递接口 + * 3. 处理阶段间的依赖关系 + * 4. 提供错误处理和回滚机制 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class DeploymentService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储环境特定的配置参数,如服务器列表、环境变量等 + * 类型: Map 键值对配置映射 + */ + def envConfig + + // ========================================================================== + // 各个阶段的服务实例(依赖注入) + // ========================================================================== + + /** + * 构建阶段服务实例 + * 负责Maven项目的编译、测试和打包操作,以及Docker镜像构建 + * 类型: BuildService 专门处理构建逻辑的服务类 + * @see BuildService 处理资源准备、Docker镜像构建等任务 + */ + def buildService + + /** + * 部署阶段服务实例 + * 负责将应用部署到目标服务器,包括镜像推送和部署执行 + * 类型: DeployService 专门处理部署逻辑的服务类 + * @see DeployService 处理镜像推送、验证和清理等任务 + */ + def deployService + + /** + * 构造函数 - 初始化部署服务协调器 + * 负责创建和初始化所有阶段服务实例,建立服务间的依赖关系 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在各个服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:服务器配置、环境变量、部署策略等 + * - 示例:[ + * servers: [...], + * envVars: [...], + * timeout: 3600 + * ] + */ + DeploymentService(script, envConfig = [:]) { + // 初始化基础属性 + this.script = script + this.envConfig = envConfig + + // 初始化各个阶段服务(依赖注入模式) + // 通过分离关注点,提高代码的可测试性和可维护性 + this.buildService = new BuildService(script, envConfig) + this.deployService = new DeployService(script, envConfig) + + script.echo "🔧 DeploymentService 初始化完成" + script.echo " 📦 构建服务已注入: BuildService" + script.echo " 🚀 部署服务已注入: DeployService" + } + + /** + * 执行构建阶段 + * 委托给BuildService处理具体的构建逻辑,包括资源准备、Docker镜像构建等操作 + * 作为构建阶段的统一入口,提供参数验证和错误处理 + * + * @param params 构建参数Map,包含构建所需的所有配置信息,必须包含: + * - resourcesDir: String - 资源文件根目录路径 + * - workspace: String - 工作目录路径 + * - dockerContext: String - Docker构建上下文目录相对路径 + * - resources: Map - 资源配置映射,描述需要复制的文件 + * - serviceConfig: Map - 服务配置信息 + * - registry: String - Docker镜像仓库地址 + * - registryProject: String - 镜像仓库项目名称 + * 可选字段: + * - buildOptions: String - 额外的docker build选项 + * + * @return 构建结果,通常包含构建状态、镜像信息等 + * @throws Exception 当构建过程中发生错误时抛出异常 + * + * @example 使用示例: + * def buildParams = [ + * resourcesDir: "/opt/resources", + * workspace: "/var/jenkins/workspace/project", + * dockerContext: "docker", + * resources: [ + * fonts: [ + * [source: "fonts/arial.ttf", target: "fonts", rename: "font.ttf"], + * [source: "fonts/verdana.ttf", target: "fonts"] + * ], + * jdk: [ + * [source: "jdk/jdk-17.tar.gz", target: "jdk"] + * ] + * ], + * serviceConfig: [ + * imageName: "xiaomayi-admin", + * tags: ["latest", "v1.0.0"], + * dockerContext: "docker" + * ], + * registry: "registry.example.com", + * registryProject: "myproject" + * ] + * deploymentService.executeBuildStage(buildParams) + */ + def executeBuildStage(params) { + script.echo "🏗️ 开始执行构建阶段..." + // 委托给BuildService处理具体的构建逻辑 + // BuildService负责资源准备、Docker镜像构建等具体任务 + buildService.executeBuildStage(params) + } + + /** + * 执行部署阶段 + * 委托给DeployService处理具体的部署逻辑,包括镜像推送、验证和清理等操作 + * 作为部署阶段的统一入口,提供参数验证和错误处理 + * + * @param params 部署参数Map,包含部署所需的所有配置信息,必须包含: + * - registry: String - 镜像仓库地址 + * - registryCredential: String - 镜像仓库认证凭据ID + * - imageFullName: String - 完整镜像名称(不含标签) + * - imageTags: List - 需要推送的镜像标签列表 + * 可选字段: + * - verifyPush: Boolean - 是否验证推送结果(默认:true) + * - cleanLocal: Boolean - 是否清理本地镜像(默认:true) + * + * @return 部署结果,通常包含部署状态、推送统计等信息 + * @throws Exception 当部署过程中发生错误时抛出异常 + * + * @example 使用示例: + * def deployParams = [ + * registry: "registry.example.com", + * registryCredential: "docker-registry-creds", + * imageFullName: "registry.example.com/myapp/xiaomayi-admin", + * imageTags: ["latest", "v1.0.0", "build-123"], + * verifyPush: true, + * cleanLocal: true + * ] + * deploymentService.executeDeployStage(deployParams) + */ + def executeDeployStage(params) { + script.echo "🚀 开始执行部署阶段..." + // 委托给DeployService处理具体的部署逻辑 + // DeployService负责镜像推送、验证和清理等具体任务 + deployService.executeDeployStage(params) + } + +} \ No newline at end of file diff --git a/src/com/docker/ResolverService.groovy b/src/com/docker/ResolverService.groovy new file mode 100644 index 0000000..0b64695 --- /dev/null +++ b/src/com/docker/ResolverService.groovy @@ -0,0 +1,647 @@ +package com.docker + +import com.constants.ConfigConstants + +/** + * JDK API 服务类 + * 负责加载配置、管理分支和服务配置、设置环境参数 + * 适配新的配置文件格式,支持多环境、多服务配置 + * + * 主要功能: + * 1. 加载环境配置和主配置文件 + * 2. 解析分支和服务配置 + * 3. 设置Jenkins流水线参数 + * 4. 提供环境相关的配置获取方法 + * + * @version 2.0.0 + * @since 2025-09-09 + */ +class ResolverService implements Serializable { + + // Jenkins流水线脚本对象,用于执行Jenkins相关操作 + // 提供对Jenkins DSL方法的访问,如echo、error、fileExists等 + def script + + // 主配置文件内容(解析后的YAML数据) + // 包含common、branchConfig、serviceConfig等配置节 + def config + + // 组织名称,用于标识项目归属和组织级别的配置 + // 示例: "xiaomayi", "my-company" + def orgName + + // 环境配置文件内容,从.env.yml文件加载 + // 包含环境特定的配置,如配置文件路径、环境变量等 + def envConfig + + // 主配置文件路径,从环境配置中解析得到 + // 示例: ".pipeline-files/config-file/job-config.yml" + def jobConfigFile + + /** + * 构造函数 + * 初始化服务类实例,设置基本的脚本对象和组织名称 + * + * @param script Jenkins流水线脚本对象,提供echo、error等方法 + * - 类型: Object (通常是Jenkins pipeline的script对象) + * - 必须: 是 + * - 示例: 在Pipeline中通过`this`传递当前脚本上下文 + * + * @param orgName 组织名称,用于标识项目归属 + * - 类型: String + * - 必须: 是 + * - 示例: "xiaomayi", "my-company" + * - 用途: 用于构建镜像名称、选择组织级别的配置等 + */ + ResolverService(script, orgName) { + this.script = script + this.orgName = orgName + this.config = [:] + this.envConfig = [:] + } + + /** + * 加载所有配置文件 + * 按顺序加载环境配置和主配置文件,确保配置的完整性和正确性 + * + * @throws Exception 如果配置文件加载失败 + * @return void 无返回值,但会设置类的config和envConfig属性 + * + * 执行步骤: + * 1. 加载环境配置文件 (.env.yml) + * 2. 加载主配置文件 (根据环境配置中的路径) + * 3. 验证配置完整性 + */ + void loadConfigurations() { + script.echo "🚀 开始加载配置文件..." + + // 首先加载环境配置文件(.env.yml) + loadEnvConfig() + + // 然后加载主配置文件(根据环境配置中的路径) + loadJobConfig() + + script.echo "✅ 所有配置文件加载完成" + } + + /** + * 加载环境配置文件的方法 + * 从.jenkins/.env.yml读取环境配置,包含主配置文件路径等基本信息 + * + * @return 返回当前对象实例,支持链式调用 + * @throws Exception 如果文件存在但解析失败 + * + * 配置文件格式示例: + * jobConfigFile: "job-config.yml" + * masterProfiles: ["prod", "uat"] + * Profiles: ["dev", "test"] + */ + def loadEnvConfig() { + try { + // 环境配置文件相对路径 + def envFilePath = '.jenkins/.env.yml' + script.echo "📁 环境配置文件路径: ${envFilePath}" + + if (script.fileExists(envFilePath)) { + // 读取并解析YAML格式的环境配置文件 + envConfig = script.readYaml(file: envFilePath) + + // 从环境配置中获取主配置文件路径 + jobConfigFile = ".pipeline-files/config-file/" + envConfig.jobConfigFile + script.echo "✅ 成功加载环境配置文件" + + // 打印所有环境配置项(调试用) + if (!envConfig.isEmpty()) { + script.echo "📋 环境配置内容:" + envConfig.each { key, value -> + script.echo " ${key}: ${value}" + } + } + } else { + script.echo "⚠️ 警告: 未找到环境配置文件 .env.yml,使用默认配置" + } + } catch (Exception e) { + script.echo "⚠️ 警告: .env.yml 文件无效,使用默认配置: ${e.message}" + // 不抛出异常,允许继续执行 + } + + return this + } + + /** + * 加载主配置文件的方法 + * 读取新的配置文件格式,包含common、branchConfig、serviceConfig等部分 + * + * @return 返回当前对象实例,支持链式调用 + * @throws Exception 如果文件不存在或解析失败 + * + * 配置文件结构: + * common: 通用配置(git、registrys、profiles等) + * branchConfig: 分支特定配置 + * serviceConfig: 服务特定配置 + */ + def loadJobConfig() { + try { + // 检查主配置文件是否存在 + if (!script.fileExists(jobConfigFile)) { + script.error("❌ 错误: 配置文件不存在: ${jobConfigFile},请检查文件路径或创建配置文件") + } + + // 读取并解析YAML格式的主配置文件 + this.config = script.readYaml(file: jobConfigFile) + script.echo "✅ 成功加载主配置文件: ${jobConfigFile}" + + // 打印配置详情(调试用) + printConfigSummary() + + } catch (Exception e) { + script.error("❌ 错误: 加载配置文件失败: ${jobConfigFile}. 错误详情: ${e.message}") + } + + return this + } + + /** + * 打印主配置文件的详细内容 + * 格式化输出所有配置信息,便于调试和验证 + * + * @return void 无返回值,直接输出到控制台 + */ + def printConfigSummary() { + script.echo "=" * 70 + script.echo "📊 主配置文件详细内容" + script.echo "=" * 70 + + if (!this.config) { + script.echo "⚠️ 配置文件内容为空" + return + } + + // 打印通用配置信息 + if (this.config.common) { + script.echo "🌐 通用配置:" + script.echo "-" * 50 + + // 打印Git配置 + if (this.config.common.git) { + script.echo " Git配置:" + script.echo " 仓库地址: ${this.config.common.git.url}" + script.echo " 默认分支: ${this.config.common.git.defaultBranch}" + script.echo " 凭证ID: ${this.config.common.git.credentialsId}" + } + + // 打印注册表配置 + if (this.config.common.registrys) { + script.echo " 镜像仓库配置:" + this.config.common.registrys.each { registryName, registryConfig -> + script.echo " ${registryName}: ${registryConfig.registry} (${registryConfig.registryType})" + } + } + script.echo "" + } + + // 打印环境配置信息 + if (this.config.common?.profiles) { + script.echo "🏭 环境配置 (共 ${this.config.common.profiles.size()} 个环境):" + script.echo "-" * 50 + + this.config.common.profiles.each { envName, envConfig -> + script.echo " ${envName.toUpperCase()} 环境:" + script.echo " 镜像仓库: ${envConfig.registry}" + script.echo "" + } + } + + // 打印分支构建配置信息 + if (this.config.branchConfig) { + script.echo "🌿 分支构建配置 (共 ${this.config.branchConfig.size()} 个分支):" + script.echo "-" * 50 + + this.config.branchConfig.each { branchName, branchConfig -> + script.echo " ${branchName} 分支:" + script.echo " Jenkins代理: ${branchConfig.jenkinsAgent ?: '未设置'}" + script.echo " Docker构建选项: ${branchConfig.docker?.buildOptions ?: '默认选项'}" + } + } + + // 打印服务配置信息 + if (this.config.serviceConfig) { + script.echo "🔧 服务配置 (共 ${this.config.serviceConfig.size()} 个服务):" + script.echo "-" * 50 + + this.config.serviceConfig.each { serviceName, serviceConfig -> + script.echo " ${serviceName}:" + script.echo " 基础镜像: ${serviceConfig.baseImage}" + script.echo " Docker上下文: ${serviceConfig.dockerContext}" + script.echo " 镜像名称: ${serviceConfig.imageName}" + script.echo " 标签: ${serviceConfig.tags?.join(', ') ?: '无'}" + script.echo "" + } + } + + script.echo "=" * 70 + } + + /** + * 获取当前分支的配置 + * 根据当前Jenkins作业的分支名称查找对应的分支配置 + * + * @return 分支配置对象(Map类型) + * @throws Exception 如果分支配置不存在 + * + * 分支配置内容示例: + * jenkinsAgent: "docker-agent" + * docker: + * buildOptions: "--no-cache --pull" + */ + def getBranchConfig() { + // 获取当前Jenkins作业的分支名称 + def branchName = script.env.BRANCH_NAME + if (!branchName) { + script.error("❌ 错误: 无法获取分支名称,env.BRANCH_NAME 为空。请检查流水线配置") + } + + script.echo "🌿 正在查找分支配置: ${branchName}" + + // 检查分支配置是否存在 + if (!config.branchConfig || config.branchConfig.isEmpty()) { + script.error("❌ 错误: 分支配置列表为空或未定义。请检查配置文件结构") + } + + // 根据分支名称获取配置 + def branchConfig = config.branchConfig[branchName] + + if (branchConfig) { + script.echo "✅ 成功匹配分支配置:" + script.echo " - Jenkins代理: ${branchConfig.jenkinsAgent ?: '未设置'}" + script.echo " - Docker构建选项: ${branchConfig.docker?.buildOptions ?: '默认选项'}" + } else { + // 构建友好的错误信息,列出所有可用分支 + def availableBranches = config.branchConfig.keySet().join(', ') + script.error("❌ 错误: 找不到分支 '${branchName}' 的配置。可用分支: ${availableBranches}") + } + + return branchConfig + } + + /** + * 获取指定服务的配置信息 + * 根据服务名称从serviceConfig中查找对应的服务配置 + * + * @param serviceName 服务名称,需要与配置文件中serviceConfig的键匹配 + * - 类型: String + * - 必须: 是 + * - 示例: "xiaomayi-admin", "user-service" + * @return 服务配置对象(Map类型) + * @throws Exception 如果服务配置不存在 + * + * 服务配置内容示例: + * baseImage: "openjdk:17-jdk" + * dockerContext: "docker" + * imageName: "xiaomayi-admin" + * tags: ["latest", "v1.0.0"] + */ + def getServiceConfig(serviceName) { + if (!serviceName) { + script.error("❌ 错误: 服务名称参数不能为空") + } + + script.echo "🔧 正在查找服务配置: ${serviceName}" + + // 检查服务配置是否存在 + if (!config.serviceConfig || config.serviceConfig.isEmpty()) { + script.error("❌ 错误: 服务配置列表为空或未定义,请检查配置文件结构") + } + + // 根据服务名称获取配置 + def serviceConfig = config.serviceConfig[serviceName] + + if (serviceConfig) { + script.echo "✅ 成功找到服务配置: ${serviceName}" + script.echo " - 基础镜像: ${serviceConfig.baseImage}" + script.echo " - Docker上下文: ${serviceConfig.dockerContext}" + script.echo " - 镜像名称: ${serviceConfig.imageName}" + script.echo " - 标签数量: ${serviceConfig.tags?.size() ?: 0}" + } else { + // 构建友好的错误信息,列出所有可用服务 + def availableServices = config.serviceConfig.keySet().join(', ') + script.error("❌ 错误: 找不到服务 '${serviceName}' 的配置。可用服务: ${availableServices}") + } + + return serviceConfig + } + + /** + * 设置管道参数 + * 整合分支配置、服务配置和环境配置,生成完整的流水线参数 + * + * @return 参数映射表,包含所有必要的部署参数 + * @throws Exception 如果配置不完整或参数设置失败 + * + * 返回参数结构: + * - 项目基本信息: projectName, workspace, resourcesDir + * - 构建配置: jenkinsAgent, buildOptions, baseImage, dockerContext等 + * - 环境配置: PROFILES, availableProfiles + * - Git配置: gitUrl, gitCredentialsId, gitBranch + * - 其他配置: envConfig, orgName, branchConfig, serviceConfig + */ + def setupParameters() { + script.echo "⚙️ 开始设置管道参数..." + + // 获取当前分支配置 + def branchConfig = getBranchConfig() + + // 从Jenkins作业名称中提取服务名称 + // 格式通常为: "组织名/服务名" 或 "服务名" + def serviceName = script.env.JOB_NAME?.tokenize('/')?.get(0) + script.echo "📦 服务名称: ${serviceName}" + + // 获取服务配置 + def serviceConfig = getServiceConfig(serviceName) + + // 根据分支类型获取环境配置 + def profiles = getBranchProfiles(branchConfig) + script.echo "🌍 环境配置: ${profiles}" + + def workspace = script.env.WORKSPACE + + // 构建完整的参数映射表 + def params = [ + projectName : serviceName, + workspace : workspace, + resourcesDir : "${workspace}/resources", + // 构建相关参数 + jenkinsAgent : branchConfig.jenkinsAgent, + buildOptions : branchConfig.docker?.buildOptions, + baseImage : serviceConfig.baseImage, + dockerFile : serviceConfig.dockerFile ?: 'Dockerfile', + dockerContext : serviceConfig.dockerContext, + imageName : serviceConfig.imageName, + resources : serviceConfig.resources ?: [:], + imageTags : serviceConfig.tags ?: [:], + + // 环境相关参数 + PROFILES : profiles.default_profile, + availableProfiles : profiles.available_profiles, + + // Git配置 + gitUrl : config.common?.git?.url, + gitCredentialsId : config.common?.git?.credentialsId, + gitBranch : script.env.BRANCH_NAME, + + // 钉钉配置 + dingTalkCredentialsId: config.common?.dingtalk?.credentialsId, + + // 其他参数 + envConfig : this.envConfig, + orgName : this.orgName, + branchConfig : branchConfig, + serviceConfig : serviceConfig + ] + + // 获取分支环境变量配置 + getEnvironmentConfig(params) + + script.echo "基础参数:${params}" + + // 设置Jenkins参数(仅对标准流水线) + setupJenkinsParameters(profiles.available_profiles as List) + + script.echo "✅ 管道参数设置完成" + + // 返回参数 + return params + } + + /** + * 获取指定环境的全部配置参数 + * 从通用配置中提取指定环境的所有参数,包括服务器列表、环境变量、部署策略等 + * + * @param params 参数映射表(会被修改),用于存储提取的配置信息 + * - 类型: Map + * - 必须: 是 + * - 输入: 基础参数映射 + * - 输出: 包含环境配置的完整参数映射 + * @return 包含指定环境全部参数的Map + * @throws Exception 如果环境配置不存在或配置不完整 + */ + def getEnvironmentConfig(params) { + try { + // 从参数中获取环境名称,如果没有设置则使用默认值"dev" + def envName = params.PROFILES ?: "dev" + + // 将环境名称转换为小写,确保与配置中的key匹配 + def envKey = envName.toLowerCase() + + script.echo "🔍 开始获取 ${envKey} 环境的全部配置参数..." + + // 安全地获取环境配置 + def envConfig = config.common?.profiles?."${envKey}" + if (!envConfig) { + def availableProfiles = config?.common?.profiles?.keySet() ?: [] + throw new Exception("未找到 ${envKey} 环境的配置,可用环境: ${availableProfiles.join(', ')}") + } + + script.echo "✅ 找到 ${envKey} 环境配置,开始提取参数..." + + // ========================================================================== + // 添加镜像仓库配置 + // ========================================================================== + if (config.common?.registrys) { + params.registrys = config.common.registrys + script.echo "🐳 镜像仓库配置:" + params.registrys.each { registryName, registryConfig -> + script.echo " ${registryName}: ${registryConfig.registry}" + } + } + + // ========================================================================== + // 添加环境分支镜像仓库配置 + // ========================================================================== + def registryName = envConfig.registry ?: "default" + def registryConfig = config.common?.registrys?."${registryName}" + + if (registryConfig) { + params.registry = registryConfig.registry ?: "192.168.10.102:8001" + params.registryCredential = registryConfig.registryCredential ?: "xiaomayi-hub-credentials" + params.registryProject = registryConfig.registryProject ?: "xiaomayi-base" + params.registryType = registryConfig.registryType ?: "harbor" + params.imageFullName = "${params.registry}/${params.registryProject}/${params.imageName}" + + script.echo "📦 目标镜像仓库: ${params.registry}" + script.echo "🔑 仓库凭证: ${params.registryCredential}" + script.echo "📁 仓库项目: ${params.registryProject}" + script.echo "🏷️ 仓库类型: ${params.registryType}" + } else { + script.echo "⚠️ 警告: 未找到注册表配置 '${registryName}'" + } + + // ========================================================================== + // 添加通知配置 + // ========================================================================== + if (config.common?.notification) { + params.notification = config.common.notification + script.echo "📧 通知配置已加载" + } + + // ========================================================================== + // 添加短信配置 - 详细配置信息 + // ========================================================================== + if (config.common?.sms) { + // 确保sms配置存在 + params.sms = config.common.sms + + script.echo "📱 短信服务配置详情:" + script.echo " - 服务商: ${params.sms.provider ?: '未设置'}" + script.echo " - 凭据ID: ${params.sms.credentialsId ?: '未设置'}" + script.echo " - 默认签名: ${params.sms.signName ?: '未设置'}" + + // 打印所有模板配置 + if (params.sms.templates) { + script.echo " - 可用模板:" + params.sms.templates.each { templateName, templateConfig -> + def templateCode = templateConfig.templateCode ?: '未设置' + def templateSign = templateConfig.signName ?: params.sms.signName ?: '未设置' + script.echo " 📋 ${templateName}: ${templateCode} (签名: ${templateSign})" + } + } else { + script.echo " - 模板: 未配置" + } + } else { + script.echo "📱 短信服务: 未配置" + } + + // ========================================================================== + // 添加构建策略配置 + // ========================================================================== + if (config.common?.buildStrategy) { + params.buildStrategy = config.common.buildStrategy + script.echo "⚡ 构建策略:" + script.echo " - 重试次数: ${params.buildStrategy.retryCount}" + script.echo " - 超时时间: ${params.buildStrategy.timeoutMinutes}分钟" + script.echo " - 启用缓存: ${params.buildStrategy.enableCaching}" + } + + script.echo "✅ ${envKey} 环境全部参数获取完成" + + // 返回配置 + return params + } catch (Exception e) { + script.echo "❌ 获取环境配置失败: ${e.message}" + throw e + } + } + + /** + * 设置 Jenkins 作业参数 + * 为标准流水线作业创建环境选择参数和服务器选择参数 + * 根据可用环境配置动态生成下拉选择框,支持环境和服务器的两级选择 + * + * @param availableProfiles 可用的环境配置列表 + * - 类型: List 环境名称列表(如:['dev', 'test', 'prod']) + * - 来源: 通常从配置文件中解析得到的环境配置键值 + * - 要求: 列表不能为空,且必须与配置文件中的环境名称匹配 + * + * @note 在多分支流水线中会自动跳过参数设置(因为多分支通常根据分支自动确定环境) + * @note 在标准流水线中会创建用户可交互的选择参数 + * + * @example 生成的参数: + * - PROFILES: 环境选择下拉框(dev/test/prod) + * - SERVERS: 服务器选择下拉框(根据选择的环境动态显示对应服务器) + */ + private void setupJenkinsParameters(List availableProfiles) { + // 检查可用环境列表是否为空 + if (!availableProfiles || availableProfiles.isEmpty()) { + script.echo "⚠️ 可用环境配置列表为空,跳过参数设置" + return + } + + // 获取当前分支名称,用于判断流水线类型 + def branchName = script.env.BRANCH_NAME ?: '' + + // 判断是否为多分支流水线(分支名通常包含斜杠,如: feature/xxx) + boolean isMultibranchPipeline = branchName.contains('/') + + if (isMultibranchPipeline) { + script.echo "🔧 检测到多分支流水线,跳过参数设置(分支: ${branchName})" + script.echo "💡 多分支流水线通常根据分支名称自动确定目标环境" + return + } + + script.echo "🔧 标准流水线作业,设置部署环境参数" + script.echo "📋 可用环境选项: ${availableProfiles.join(', ')}" + + try { + // ========================================================================== + // 创建环境选择参数 + // ========================================================================== + def profileParameter = script.choice( + choices: availableProfiles.join('\n'), // 下拉选项,每行一个选择项 + description: '请选择要部署的目标环境', // 参数描述,显示在Jenkins界面上 + name: 'PROFILES' // 参数名称,后续通过params.PROFILES访问 + ) + + // ========================================================================== + // 设置Jenkins作业参数 + // ========================================================================== + script.properties([ + script.parameters([ + profileParameter, // 环境选择参数 + ]) + ]) + + script.echo "✅ 成功设置部署参数:" + script.echo " - PROFILES: 环境选择(${availableProfiles.size()} 个选项)" + + } catch (Exception e) { + script.echo "⚠️ 设置 Jenkins 参数失败: ${e.message}" + script.echo "💡 这可能是因为没有权限修改作业配置,或者参数格式不正确" + // 不抛出异常,避免影响后续流程,参数设置失败通常不会阻断部署 + } + } + + /** + * 根据分支配置获取对应的环境配置文件和默认配置文件 + * 根据分支类型和名称确定可用的环境配置和默认环境 + * + * @param branchConfig 分支配置信息,包含分支类型等 + * - 类型: Map + * - 必须: 是 + * @return Map 包含可用配置列表和默认配置 + * - available_profiles: List 可用环境列表 + * - default_profile: String 默认环境名称 + */ + private def getBranchProfiles(branchConfig) { + // 获取当前分支名称 + def branchName = script.env.BRANCH_NAME ?: '' + + // 1. 优先从常量配置中获取该分支类型对应的可用配置 + def availableProfiles = ConfigConstants.BRANCH_PROFILES[branchName] + + // 2. 如果常量配置中没有找到,根据分支类型选择不同的默认配置源 + if (!availableProfiles) { + availableProfiles = (branchName == 'master') ? + envConfig.masterProfiles : // master分支使用特殊配置 + envConfig.Profiles // 其他分支使用通用配置 + } + + // 确保availableProfiles不为null,如果是null则转为空列表 + availableProfiles = availableProfiles ?: [] + + // 3. 获取默认配置,优先从常量配置中获取 + def defaultProfile = ConfigConstants.DEFAULT_PROFILES[branchName] + + // 4. 如果常量配置中没有默认配置,则从可用配置中选择 + if (!defaultProfile) { + defaultProfile = availableProfiles.find { it } ? // 查找第一个非空配置 + availableProfiles[0] : // 如果都为空则取第一个 + null // 如果没有可用配置则返回null + } + + return [ + available_profiles: availableProfiles, + default_profile : defaultProfile + ] + } + +} \ No newline at end of file diff --git a/src/com/docker/stages/BuildService.groovy b/src/com/docker/stages/BuildService.groovy new file mode 100644 index 0000000..b2e2b3e --- /dev/null +++ b/src/com/docker/stages/BuildService.groovy @@ -0,0 +1,286 @@ +package com.docker.stages + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardCopyOption + +/** + * Docker镜像构建服务类 + * 负责处理Docker镜像的构建流程,包括资源准备、镜像构建等 + * 实现了可序列化接口,支持在Jenkins Pipeline中跨节点使用 + */ +class BuildService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储构建环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 构造函数 - 初始化BuildService实例 + * @param script Jenkins pipeline脚本对象,用于执行DSL命令 + * @param envConfig 环境配置映射,包含构建环境参数 + */ + BuildService(script, envConfig = [:]) { + this.script = script + this.envConfig = envConfig + + script.echo "🔧 BuildService 初始化完成" + } + + /** + * 执行构建阶段的主方法 + * 协调资源准备和镜像构建两个主要步骤 + * @param params 构建参数映射,包含工作目录、资源配置等信息 + * @throws Exception 当构建过程中发生错误时抛出异常 + */ + def executeBuildStage(params) { + script.echo "🔨 开始执行构建阶段..." + + try { + // 准备构建资源 - 拷贝配置文件、字体文件等 + prepareBuildResources(params) + + // 构建Docker镜像 - 执行docker build命令 + buildDockerImage(params) + + script.echo "✅ 构建阶段完成" + + } catch (Exception e) { + script.error("❌ 构建阶段失败: ${e.message}") + } + } + + /** + * 准备构建资源 - 纯Groovy实现 + * 动态处理所有配置的资源文件,支持目录拷贝和文件重命名 + * 根据配置文件将资源文件复制到Docker构建上下文目录 + * @param params 参数映射,包含: + * - resourcesDir: 资源文件根目录路径 + * - dockerContext: Docker构建上下文目录相对路径 + * - resources: 资源配置映射,描述需要复制的文件 + */ + private def prepareBuildResources(params) { + script.echo "📦 准备构建资源..." + + // 从参数中提取配置信息 + def resourcesDir = params.resourcesDir + def dockerContext = "${params.workspace}/${params.dockerContext}" + def resources = params.resources ?: [:] + + // 检查是否配置了资源文件 + if (!resources) { + script.echo "⚠️ 未配置任何资源文件" + return + } + + script.echo "资源配置类型: ${resources.keySet().join(', ')}" + + // 创建构建上下文目录(如果不存在) + def contextDir = createDirectory(dockerContext) + + // 遍历所有资源类型(fonts、jdk等)并处理每个资源文件 + resources.each { resourceType, resourceList -> + if (resourceList && resourceList.size() > 0) { + script.echo "---" + script.echo "处理资源类型: ${resourceType.toUpperCase()}" + script.echo "资源数量: ${resourceList.size()}" + + // 处理每个资源文件,支持重命名功能 + resourceList.eachWithIndex { resource, index -> + processSingleResource(resource, resourcesDir, contextDir, resourceType, index + 1) + } + } + } + + // 显示最终目录结构,验证文件复制结果 + displayDirectoryStructure(contextDir) + script.echo "✅ 资源准备完成" + } + + /** + * 处理单个资源文件 + * 复制单个文件到目标目录,支持文件重命名 + * @param resource 资源配置映射,包含source、target、rename等字段 + * @param resourcesDir 资源文件根目录路径 + * @param contextDir Docker构建上下文目录路径 + * @param resourceType 资源类型标识(如fonts、jdk等) + * @param index 资源序号,用于日志显示 + * @throws Exception 当文件复制失败时抛出异常 + */ + private def processSingleResource(resource, resourcesDir, contextDir, resourceType, index) { + try { + script.echo "处理资源 #${index}: ${resource}" + + // 构建完整的源文件路径 + def sourcePath = "${resourcesDir}/${resource.source}" + + // 验证源文件是否存在 + if (!script.fileExists(sourcePath)) { + script.error("❌ 源文件不存在: ${sourcePath}") + return + } + + // 构建目标目录路径 + def targetPath = "${contextDir}/${resource.target}" + + // 创建目标目录(如果不存在) + createDirectory(targetPath) + + // 确定目标文件名(支持重命名配置) + def fileName = resource.rename ?: getFileNameFromPath(resource.source) + def finalTargetPath = "${targetPath}/${fileName}" + + // 执行文件复制操作 + script.echo "复制: ${sourcePath} -> ${finalTargetPath}" + script.sh "cp -f '${sourcePath}' '${finalTargetPath}'" + + script.echo "✅ 资源 #${index} 处理完成" + + } catch (Exception e) { + script.echo "❌ 处理资源 #${index} 时出错: ${e.message}" + throw e + } + } + + /** + * 从文件路径中提取文件名 + * 处理包含目录路径的字符串,返回纯文件名 + * @param path 文件路径字符串(可能包含目录) + * @return 纯文件名(不包含路径) + */ + private String getFileNameFromPath(String path) { + return path.contains('/') ? path.substring(path.lastIndexOf('/') + 1) : path + } + + /** + * 创建目录(如果不存在) + * 使用mkdir -p命令递归创建目录结构 + * @param path 需要创建的目录路径 + * @return 创建的目录路径(与输入参数相同) + */ + private def createDirectory(String path) { + if (!script.fileExists(path)) { + script.sh "mkdir -p '${path}'" + script.echo "创建目录: ${path}" + } + return path + } + + /** + * 显示目录结构 + * 验证Docker构建上下文目录的内容,确保所有必要文件已就位 + * @param path 需要显示内容的目录路径 + * @return boolean 是否成功显示目录内容 + */ + private def displayDirectoryStructure(String path) { + script.echo "🚀 开始目录结构检查..." + + try { + // 验证路径是否存在 + if (!script.fileExists(path)) { + script.echo "⛔ 失败: 路径不存在 [${path}]" + return false + } + + // 切换到目标目录并显示内容 + script.dir(path) { + script.echo "✅ 成功访问目录: ${script.pwd()}" + + // 执行ls -la命令获取详细文件列表 + def content = script.sh( + script: "ls -la 2>/dev/null || echo '无法列出内容'", + returnStdout: true + ).trim() + + script.echo "📋 目录内容:" + script.echo "═" * 50 + script.echo content + script.echo "═" * 50 + + // 记录成功日志 + script.echo "✅ 目录结构显示完成" + return true + } + + } catch (Exception e) { + script.echo "💥 严重错误: ${e.getClass().getSimpleName()}" + script.echo "📝 详情: ${e.message}" + script.echo "🔧 建议: 检查目录权限和路径格式" + return false + } + } + + /** + * 构建Docker镜像 + * 执行docker build命令构建镜像,支持多标签和构建参数 + * @param params 构建参数映射,包含: + * - serviceConfig: 服务配置信息 + * - registry: Docker镜像仓库地址 + * - registryProject: 镜像仓库项目名称 + * - buildOptions: 额外的docker build选项 + */ + private def buildDockerImage(params) { + script.echo "🐳 开始构建Docker镜像..." + + // 从参数中提取配置信息 + def serviceConfig = params.serviceConfig + def registry = params.registry + def registryProject = params.registryProject + def imageName = serviceConfig.imageName + def tags = serviceConfig.tags ?: [] + def buildOptions = params.buildOptions ?: "" + def dockerContext = serviceConfig.dockerContext + + // 生成完整的镜像标签列表(包含仓库地址和项目名称) + def imageTags = tags.collect { tag -> + "${registry}/${registryProject}/${imageName}:${tag}" + } + + // // 添加构建编号标签(可选,用于临时标识) + // def buildTag = "${registry}/${registryProject}/${imageName}:build-${script.env.BUILD_NUMBER}" + // imageTags.add(buildTag) + + script.echo "镜像标签: ${imageTags.join(', ')}" + + // 构建所有标签的Docker命令参数 + def tagOptions = imageTags.collect { "-t ${it}" }.join(' ') + + // 执行Docker构建命令 + script.sh """ + echo "开始构建镜像..." + echo "构建上下文: ${dockerContext}" + echo "Dockerfile: ${dockerContext}/Dockerfile" + echo "构建选项: ${buildOptions}" + echo "标签数量: ${imageTags.size()}" + + # 切换到构建上下文目录并执行docker build + cd ${dockerContext} && \ + docker build \\ + ${buildOptions} \\ + -f Dockerfile \\ + ${tagOptions} \\ + --build-arg BUILD_DATE=\$(date -u +'%Y-%m-%dT%H:%M:%SZ') \\ + . + + echo "✅ 镜像构建完成" + + # 显示构建的镜像信息(验证构建结果) + echo "构建完成的镜像:" + docker images | grep "${registry}/${registryProject}/${imageName}" || echo "未找到相关镜像" + """ + + script.echo "✅ 镜像构建完成" + } + +} \ No newline at end of file diff --git a/src/com/docker/stages/DeployService.groovy b/src/com/docker/stages/DeployService.groovy new file mode 100644 index 0000000..057ee2b --- /dev/null +++ b/src/com/docker/stages/DeployService.groovy @@ -0,0 +1,326 @@ +package com.docker.stages + +/** + * 部署阶段服务 - 负责将Docker镜像部署到目标服务器 + * + * 主要职责: + * - 执行Docker镜像的推送、验证和清理操作 + * - 管理镜像仓库的认证和授权 + * - 处理镜像推送过程中的异常和错误 + * - 提供详细的部署日志和状态反馈 + * + * 设计原则: + * - 单一职责:专注于镜像推送和清理相关的逻辑 + * - 错误隔离:推送失败不会影响其他服务实例 + * - 配置驱动:通过参数Map接收所有配置信息 + * - 可扩展性:易于添加新的镜像管理功能 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class DeployService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储部署环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 构造函数 - 初始化部署服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:部署策略、超时设置等配置 + */ + DeployService(script, envConfig = [:]) { + this.script = script + this.envConfig = envConfig + + script.echo "🔧 DeployService 初始化完成" + script.echo " 📡 镜像推送服务已就绪" + } + + /** + * 执行部署阶段逻辑 + * 负责将Docker镜像推送到镜像仓库并清理本地镜像 + * 这是CI/CD流水线的镜像管理阶段,负责将构建好的镜像推送到仓库 + * + * 主要步骤: + * 1. 推送镜像到镜像仓库 + * 2. 验证镜像推送结果(可选) + * 3. 清理本地Docker镜像(可选) + * + * @param params 管道参数Map,包含部署所需的所有配置信息,必须包含: + * - registry: String - 镜像仓库地址 + * - registryCredential: String - 镜像仓库认证凭据ID + * - imageFullName: String - 完整镜像名称(不含标签) + * - imageTags: List - 需要推送的镜像标签列表 + * 可选字段: + * - verifyPush: Boolean - 是否验证推送结果(默认:true) + * - cleanLocal: Boolean - 是否清理本地镜像(默认:true) + * + * @throws Exception 当推送失败时抛出异常,包含详细的错误信息 + * + * @example 使用示例: + * def deployParams = [ + * registry: "registry.example.com", + * registryCredential: "docker-registry-creds", + * imageFullName: "registry.example.com/myapp", + * imageTags: ["latest", "v1.0.0", "build-123"], + * verifyPush: true, + * cleanLocal: true + * ] + * deployService.executeDeployStage(deployParams) + */ + def executeDeployStage(params) { + script.echo "🚀 开始执行部署阶段..." + + try { + // 验证必要参数 + // validateParams(params) + + // 推送镜像到镜像仓库 + pushDockerImage(params) + + // 可选:验证镜像推送结果 + if (params.get('verifyPush', true)) { + verifyImagePush(params) + } + + // 可选:清理本地镜像 + if (params.get('cleanLocal', true)) { + cleanDockerImage(params) + } + + script.echo "✅ 部署阶段完成" + + } catch (Exception e) { + script.error("❌ 部署阶段失败: ${e.message}") + } + } + + /** + * 验证部署参数是否完整 + * 检查所有必需的参数是否提供,确保后续操作能够正常执行 + * + * @param params 管道参数Map + * @throws IllegalArgumentException 当缺少必要参数时抛出异常 + */ + private def validateParams(params) { + // 定义必需参数列表 + def requiredParams = ['registry', 'registryCredential', 'imageFullName', 'imageTags'] + // 查找缺失的参数 + def missingParams = requiredParams.findAll { !params.containsKey(it) } + + if (missingParams) { + throw new IllegalArgumentException("缺少必要参数: ${missingParams.join(', ')}") + } + + // 验证imageTags参数是否为非空列表 + if (!params.imageTags || !(params.imageTags instanceof List) || params.imageTags.empty) { + throw new IllegalArgumentException("imageTags参数必须是非空的标签列表") + } + + script.echo "✅ 部署参数验证通过" + } + + /** + * 推送Docker镜像到镜像仓库 + * 登录镜像仓库并推送所有指定的镜像标签 + * + * 执行步骤: + * 1. 使用凭据登录Docker镜像仓库 + * 2. 遍历所有标签并推送到仓库 + * 3. 记录推送结果和统计信息 + * + * @param params 管道参数,必须包含: + * - registry: 镜像仓库地址 + * - registryCredential: Jenkins凭据ID + * - imageFullName: 完整镜像名称 + * - imageTags: 需要推送的标签列表 + */ + private def pushDockerImage(params) { + script.echo "📤 推送镜像到镜像仓库..." + + def registry = params.registry + def registryCredential = params.registryCredential + def imageFullName = params.imageFullName + def imageTags = params.imageTags + + script.echo "镜像仓库: ${registry}" + script.echo "镜像名称: ${imageFullName}" + script.echo "需要推送的标签: ${imageTags.join(', ')}" + + script.withCredentials([[ + $class : 'UsernamePasswordMultiBinding', + credentialsId : registryCredential, + usernameVariable: 'REGISTRY_USER', + passwordVariable: 'REGISTRY_PASSWORD' + ]]) { + // 使用脚本块执行Docker命令 + script.sh """ + # 登录镜像仓库 + echo "登录镜像仓库: ${registry}" + docker login -u \$REGISTRY_USER -p \$REGISTRY_PASSWORD ${registry} + + if [ \$? -ne 0 ]; then + echo "❌ Docker登录失败" + exit 1 + fi + + # 推送所有标签 + echo "开始推送所有镜像标签..." + pushed_count=0 + failed_count=0 + + for tag in ${imageTags.join(' ')}; do + # 构建完整镜像名称(包含标签) + full_image="${imageFullName}:\${tag}" + echo "推送镜像: \${full_image}" + + if docker push \${full_image}; then + echo "✅ 推送成功: \${full_image}" + pushed_count=\$((pushed_count + 1)) + else + echo "❌ 推送失败: \${full_image}" + failed_count=\$((failed_count + 1)) + # 继续推送其他标签,不退出 + fi + echo "" + done + + echo "镜像推送完成统计:" + echo "✅ 成功推送: \${pushed_count} 个标签" + echo "❌ 推送失败: \${failed_count} 个标签" + + if [ \${failed_count} -gt 0 ]; then + echo "警告: 有 \${failed_count} 个标签推送失败" + exit 1 + fi + """ + } + + script.echo "✅ 所有镜像标签推送完成" + } + + /** + * 验证镜像推送结果 + * 检查关键标签是否成功推送到镜像仓库 + * 通过尝试拉取镜像来验证其在仓库中的存在性 + * + * @param params 管道参数,必须包含: + * - registry: 镜像仓库地址 + * - registryCredential: Jenkins凭据ID + * - imageFullName: 完整镜像名称 + * - imageTags: 需要验证的标签列表 + */ + private def verifyImagePush(params) { + script.echo "🔍 验证镜像推送结果..." + + def registry = params.registry + def registryCredential = params.registryCredential + def imageFullName = params.imageFullName + def imageTags = params.imageTags + + // 选择关键标签进行验证(如latest标签或特定版本标签) + def criticalTags = imageTags.findAll { tag -> + tag.equals('latest') || tag.startsWith('v') // 版本标签通常以v开头 + } + + if (criticalTags.empty) { + script.echo "⚠️ 没有关键标签需要验证,跳过验证步骤" + return + } + + script.echo "验证关键标签: ${criticalTags.join(', ')}" + + script.withCredentials([[ + $class : 'UsernamePasswordMultiBinding', + credentialsId : registryCredential, + usernameVariable: 'REGISTRY_USER', + passwordVariable: 'REGISTRY_PASSWORD' + ]]) { + script.sh """ + # 登录镜像仓库(用于验证) + docker login -u \$REGISTRY_USER -p \$REGISTRY_PASSWORD ${registry} + + # 验证关键标签是否存在 + for tag in ${criticalTags.join(' ')}; do + full_image="${imageFullName}:\${tag}" + echo "验证标签: \${full_image}" + + # 尝试拉取镜像来验证是否存在 + if docker pull \${full_image} >/dev/null 2>&1; then + echo "✅ 验证成功: \${full_image} 存在于仓库中" + # 清理拉取的镜像 + docker rmi \${full_image} >/dev/null 2>&1 || true + else + echo "❌ 验证失败: \${full_image} 不存在于仓库中" + # 不退出,继续验证其他标签 + fi + done + """ + } + + script.echo "✅ 镜像推送验证完成" + } + + /** + * 清理本地Docker镜像 + * 删除本地已推送的镜像以释放磁盘空间 + * 根据镜像名称和标签列表精确删除指定的镜像 + * + * @param params 管道参数,必须包含: + * - imageFullName: 完整镜像名称 + * - imageTags: 需要清理的标签列表 + */ + private def cleanDockerImage(params) { + script.echo "🧹 清理本地镜像..." + + def imageFullName = params.imageFullName + def imageTags = params.imageTags + + script.sh """ + echo "清理本地镜像: ${imageFullName}" + deleted_count=0 + + # 删除所有指定标签的镜像 + for tag in ${imageTags.join(' ')}; do + full_image="${imageFullName}:\${tag}" + echo "删除镜像: \${full_image}" + + # 获取镜像ID + image_id=\$(docker images -q \${full_image} 2>/dev/null) + + if [ -n "\${image_id}" ]; then + # 删除镜像 + if docker rmi -f \${image_id} >/dev/null 2>&1; then + echo "✅ 删除成功: \${full_image}" + deleted_count=\$((deleted_count + 1)) + else + echo "⚠️ 无法删除: \${full_image} (可能已被其他进程删除)" + fi + else + echo "⚠️ 镜像不存在: \${full_image} (可能已被删除)" + fi + done + + echo "本地镜像清理完成: 删除了 \${deleted_count} 个镜像" + """ + + script.echo "✅ 本地镜像清理完成" + } +} \ No newline at end of file diff --git a/src/com/jdk/DeploymentService.groovy b/src/com/jdk/DeploymentService.groovy new file mode 100644 index 0000000..902da69 --- /dev/null +++ b/src/com/jdk/DeploymentService.groovy @@ -0,0 +1,253 @@ +package com.jdk + +import com.jdk.utils.PipelineUtils +import com.jdk.stages.BuildService +import com.jdk.stages.ContainerBuildService +import com.jdk.stages.DeployService +import com.jdk.stages.HealthCheckService +import com.jdk.stages.RollbackService + +/** + * 部署服务类 - CI/CD流水线核心协调器 + * 负责执行各个CI/CD阶段的业务逻辑,不直接创建Jenkins stage + * 提供模块化的构建、部署、验证功能,作为流水线步骤的抽象层 + * + * 设计原则: + * - 单一职责:每个方法只负责一个特定的功能 + * - 错误隔离:每个阶段的异常不会影响其他阶段 + * - 可重用性:方法可以在不同的流水线中重用 + * - 可扩展性:易于添加新的部署阶段或功能 + * - 可维护性:清晰的代码结构和职责分离 + * + * 主要职责: + * 1. 协调各个部署阶段服务的执行 + * 2. 提供统一的参数传递接口 + * 3. 处理阶段间的依赖关系 + * 4. 提供错误处理和回滚机制 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class DeploymentService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 管道工具类实例 + * 提供通用的工具方法和工具函数 + * 类型: PipelineUtils 自定义工具类 + */ + def utils + + /** + * 环境配置信息 + * 存储环境特定的配置参数,如服务器列表、环境变量等 + * 类型: Map 键值对配置映射 + */ + def envConfig + + // ========================================================================== + // 各个阶段的服务实例(依赖注入) + // ========================================================================== + + /** + * 构建阶段服务实例 + * 负责Maven项目的编译和打包操作 + * 类型: BuildService 专门处理构建逻辑的服务类 + */ + def buildService + + /** + * 容器构建阶段服务实例 + * 负责Docker镜像的构建和推送操作 + * 类型: ContainerBuildService 专门处理容器构建逻辑的服务类 + */ + def containerBuildService + + /** + * 部署阶段服务实例 + * 负责将应用部署到目标服务器 + * 类型: DeployService 专门处理部署逻辑的服务类 + */ + def deployService + + /** + * 健康检查阶段服务实例 + * 负责验证部署的应用是否正常运行 + * 类型: HealthCheckService 专门处理健康检查逻辑的服务类 + */ + def healthCheckService + + /** + * 回滚阶段服务实例 + * 负责在失败时执行回滚操作 + * 类型: RollbackService 专门处理回滚逻辑的服务类 + */ + def rollbackService + + /** + * 构造函数 - 初始化部署服务 + * 创建各个阶段的服务实例,建立服务依赖关系 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在各个服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:服务器列表、环境变量、部署策略等配置 + * - 示例:[ + * servers: [[serverIP: "192.168.1.1", serverPort: 22]], + * envVars: [JAVA_OPTS: "-Xms1g -Xmx1g"], + * deploymentStrategy: "rolling" + * ] + */ + DeploymentService(script, envConfig = [:]) { + // 初始化基础属性 + this.script = script + this.envConfig = envConfig + this.utils = new PipelineUtils(script, envConfig) + + // 初始化各个阶段服务(依赖注入模式) + // 通过分离关注点,提高代码的可测试性和可维护性 + this.buildService = new BuildService(script, utils, envConfig) + this.containerBuildService = new ContainerBuildService(script, utils, envConfig) + this.deployService = new DeployService(script, utils, envConfig) + this.healthCheckService = new HealthCheckService(script, utils, envConfig) + this.rollbackService = new RollbackService(script, utils, envConfig) + + script.echo "🔧 DeploymentService 初始化完成" + } + + /** + * 执行构建阶段逻辑 + * 委托给BuildService处理Maven项目的编译和打包 + * 这是CI/CD流水线的第一个阶段,负责源代码到产物的转换 + * + * @param params 管道参数Map,包含构建所需的所有配置信息,必须包含: + * - MAVEN_SET: String - Maven设置文件ID + * - PROFILES: String - Maven构建环境profiles + * - jarFilePath: String - JAR文件输出路径 + * - jarFile: String - 生成的JAR文件名 + * - skipTests: Boolean - 是否跳过测试(默认true) + * - goals: String - Maven执行目标(默认"clean package") + * - pomFilePath: String - POM文件路径(默认pom.xml) + * + * @throws Exception 当Maven构建失败时抛出异常,中断流水线 + * + * @example 使用示例: + * def params = [ + * MAVEN_SET: "nexus-maven-dev", + * PROFILES: "dev", + * jarFilePath: "target", + * jarFile: "app.jar" + * ] + * deploymentService.executeBuildStage(params) + */ + def executeBuildStage(params) { + // ☕ 开始构建阶段... + buildService.executeBuildStage(params) + } + + /** + * 执行容器构建阶段逻辑 + * 委托给ContainerBuildService处理Docker镜像的构建和推送 + * 这是CI/CD流水线的第二个阶段,负责应用容器化 + * + * @param params 管道参数Map,包含镜像构建所需的所有配置信息,必须包含: + * - registry: String - 镜像仓库地址 + * - registryCredential: String - Jenkins仓库认证凭证ID + * - registryProject: String - 仓库项目名称 + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称 + * 可选字段: + * - buildOptions: String - Docker构建选项 + * - baseImage: String - 基础镜像名称 + * + * @return String 构建的镜像标签(如:dev-123、prod-456) + * @throws Exception 当Docker构建或推送失败时抛出异常 + * + * @example 使用示例: + * def imageTag = deploymentService.executeContainerBuildStage(params) + * echo "镜像标签: ${imageTag}" + */ + def executeContainerBuildStage(params) { + // 🐳 开始容器构建阶段... + return containerBuildService.executeContainerBuildStage(params) + } + + /** + * 执行部署阶段逻辑 + * 委托给DeployService处理应用部署到目标服务器 + * 这是CI/CD流水线的第三个阶段,负责将容器部署到环境 + * + * @param params 管道参数Map,包含部署所需的所有配置信息,必须包含: + * - servers: List - 服务器列表数组 + * - deployServer: String - 部署服务器引用 + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称 + * 可选字段: + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - envVars: Map - 环境变量配置 + * - deploymentStrategy: String - 部署策略 + * + * @throws Exception 当部署失败时抛出异常,支持自动回滚 + * + * @note 支持多种部署模式: + * - 单服务器部署:deployServer = "servers[0]" + * - 多服务器部署:deployServer = "servers[0,1,2]" + * - 自动选择:deployServer = "auto" + */ + def executeDeployStage(params) { + // 🚀 开始部署阶段... + deployService.executeDeployStage(params) + } + + /** + * 执行健康检查阶段逻辑 + * 委托给HealthCheckService验证部署的应用是否正常运行 + * 这是CI/CD流水线的第四个阶段,负责部署后验证 + * + * @param params 管道参数Map,包含健康检查所需的所有配置信息,通常包含: + * - serverHosts: List - 服务器列表(可选) + * - serverIP01: String - 单个服务器IP(可选) + * - port: Integer - 服务端口号 + * - healthCheckPath: String - 健康检查端点路径 + * - healthCheckTimeout: Integer - 健康检查超时时间 + * + * @throws Exception 当健康检查失败时抛出异常,触发回滚机制 + * + * @note 健康检查失败时会自动触发回滚操作 + */ + def executeHealthCheckStage(params) { + // ❤️ 开始健康检查阶段... + healthCheckService.executeHealthCheckStage(params) + } + + /** + * 执行回滚操作 + * 委托给RollbackService处理部署失败时的回滚操作 + * 这是CI/CD流水线的安全机制,确保失败时能够恢复到稳定状态 + * + * @param params 管道参数Map,包含回滚所需的所有配置信息,通常包含: + * - serverHosts: List - 需要回滚的服务器列表 + * - serverIP01: String - 单个服务器IP + * - serverPort: Integer - 服务器端口 + * - projectName: String - 项目名称 + * - previousVersion: String - 上一个版本号(可选) + * + * @note 这是一个私有方法,通常在部署或健康检查失败时自动调用 + * @note 回滚操作本身也可能会失败,需要手动介入处理 + */ + def executeRollbackStage(params) { + // ↩️ 开始回滚操作... + rollbackService.executeRollbackStage(params) + } + +} \ No newline at end of file diff --git a/src/com/jdk/ResolverService.groovy b/src/com/jdk/ResolverService.groovy new file mode 100644 index 0000000..1a32383 --- /dev/null +++ b/src/com/jdk/ResolverService.groovy @@ -0,0 +1,865 @@ +package com.jdk + +import com.utils.CommonUtils +import com.constants.ConfigConstants +import com.utils.ServerParser + +/** + * JDK API 服务类 + * 负责加载配置、管理分支和服务配置、设置环境参数 + * 适配新的配置文件格式,支持多环境、多服务配置 + * + * 主要功能: + * 1. 加载环境配置和主配置文件 + * 2. 解析分支和服务配置 + * 3. 设置Jenkins流水线参数 + * 4. 提供环境相关的配置获取方法 + * + * @version 2.0.0 + * @since 2025-09-09 + */ +class ResolverService implements Serializable { + + // Jenkins流水线脚本对象,用于执行Jenkins相关操作 + def script + + // 主配置文件内容(解析后的YAML数据) + def config + + // 组织名称 + def orgName + + // 环境配置文件内容 + def envConfig + + // 主配置文件路径 + def jobConfigFile + + /** + * 服务器解析器实例 + * 负责解析服务器引用配置和索引处理 + * 类型: ServerParser 自定义服务器解析工具类 + */ + def serverParser + + /** + * 构造函数 + * 初始化服务类实例 + * + * @param script Jenkins流水线脚本对象,提供echo、error等方法 + * @param orgName 组织名称,用于标识项目归属 + */ + ResolverService(script, orgName) { + this.script = script + this.orgName = orgName + this.config = [:] + this.envConfig = [:] + // 初始化服务器解析器,用于处理服务器引用和索引 + this.serverParser = new ServerParser(script) + } + + /** + * 加载所有配置文件 + * 按顺序加载环境配置和主配置文件 + * + * @throws Exception 如果配置文件加载失败 + */ + void loadConfigurations() { + script.echo "🚀 开始加载配置文件..." + + // 首先加载环境配置文件(.env.yml) + loadEnvConfig() + + // 然后加载主配置文件(根据环境配置中的路径) + loadMainConfig() + + script.echo "✅ 所有配置文件加载完成" + } + + /** + * 加载环境配置文件的方法 + * 从.jenkins/.env.yml读取环境配置,包含主配置文件路径等基本信息 + * + * @return 返回当前对象实例,支持链式调用 + * @throws Exception 如果文件存在但解析失败 + */ + def loadEnvConfig() { + try { + // 环境配置文件相对路径 + def envFilePath = '.jenkins/.env.yml' + script.echo "📁 环境配置文件路径: ${envFilePath}" + + if (script.fileExists(envFilePath)) { + // 读取并解析YAML格式的环境配置文件 + envConfig = script.readYaml(file: envFilePath) + + // 从环境配置中获取主配置文件路径 + jobConfigFile = ".pipeline-files/config-file/" + envConfig.jobConfigFile + script.echo "✅ 成功加载环境配置文件" + + // 打印所有环境配置项(调试用) + if (!envConfig.isEmpty()) { + script.echo "📋 环境配置内容:" + envConfig.each { key, value -> + script.echo " ${key}: ${value}" + } + } + } else { + script.echo "⚠️ 警告: 未找到环境配置文件 .env.yml,使用默认配置" + } + } catch (Exception e) { + script.echo "⚠️ 警告: .env.yml 文件无效,使用默认配置: ${e.message}" + // 不抛出异常,允许继续执行 + } + + return this + } + + /** + * 加载主配置文件的方法 + * 读取新的配置文件格式,包含common、branchConfig、serviceConfig等部分 + * + * @return 返回当前对象实例,支持链式调用 + * @throws Exception 如果文件不存在或解析失败 + */ + def loadMainConfig() { + try { + // 检查主配置文件是否存在 + if (!script.fileExists(jobConfigFile)) { + script.error("❌ 错误: 配置文件不存在: ${jobConfigFile},请检查文件路径或创建配置文件") + } + + // 读取并解析YAML格式的主配置文件 + this.config = script.readYaml(file: jobConfigFile) + script.echo "✅ 成功加载主配置文件: ${jobConfigFile}" + + // 打印配置详情(调试用) + printConfigSummary() + + } catch (Exception e) { + script.error("❌ 错误: 加载配置文件失败: ${jobConfigFile}. 错误详情: ${e.message}") + } + + return this + } + + /** + * 打印主配置文件的详细内容 + * 格式化输出所有配置信息,便于调试和验证 + */ + def printConfigSummary() { + script.echo "=" * 70 + script.echo "📊 主配置文件详细内容" + script.echo "=" * 70 + + if (!this.config) { + script.echo "⚠️ 配置文件内容为空" + return + } + + // 打印通用配置信息 + if (this.config.common) { + script.echo "🌐 通用配置:" + script.echo "-" * 50 + script.echo " 部署脚本: ${this.config.common.deployScript ?: '未设置'}" + script.echo " 部署路径: ${this.config.common.deployPath ?: '未设置'}" + + // 打印注册表配置 + if (this.config.common.registry) { + script.echo " 注册表配置:" + this.config.common.registry.each { registryName, registryConfig -> + script.echo " ${registryName}: ${registryConfig.registry}" + } + } + script.echo "" + } + + // 打印环境配置信息 + if (this.config.common?.profiles) { + script.echo "🏭 环境配置 (共 ${this.config.common.profiles.size()} 个环境):" + script.echo "-" * 50 + + this.config.common.profiles.each { envName, envConfig -> + script.echo " ${envName.toUpperCase()} 环境:" + script.echo " 服务器数量: ${envConfig.servers ? envConfig.servers.size() : 0}" + script.echo " 部署策略: ${envConfig.deploymentStrategy ?: 'rolling'}" + script.echo " JVM参数: ${envConfig.envVars?.JAVA_OPTS ?: '未设置'}" + script.echo " 健康检查: ${envConfig.healthCheckPath ?: '/actuator/health'}" + script.echo "" + } + } + + // 打印分支构建配置信息 + if (this.config.branchConfig) { + script.echo "🌿 分支构建配置 (共 ${this.config.branchConfig.size()} 个分支):" + script.echo "-" * 50 + + this.config.branchConfig.each { branchName, branchConfig -> + script.echo " ${branchName} 分支:" + script.echo " Jenkins代理: ${branchConfig.jenkinsAgent ?: '未设置'}" + script.echo " Maven配置集: ${branchConfig.maven?.maven ?: '未设置'}" + } + } + + // 打印服务配置信息 + if (this.config.serviceConfig) { + script.echo "🔧 服务配置 (共 ${this.config.serviceConfig.size()} 个服务):" + script.echo "-" * 50 + + this.config.serviceConfig.each { serviceName, serviceConfig -> + script.echo " ${serviceName} 服务:" + script.echo " 编程语言: ${serviceConfig.programLang ?: '未设置'}" + script.echo " 语言版本: ${serviceConfig.programLangVersion ?: '未设置'}" + script.echo " 容器名称: ${serviceConfig.containerName ?: '未设置'}" + script.echo " POM路径: ${serviceConfig.pomFilePath ?: '未设置'}" + script.echo " 服务路径: ${serviceConfig.servicePath ?: '未设置'}" + script.echo " 容器名称: ${serviceConfig.containerName ?: '未设置'}" + script.echo " JAR包名称: ${serviceConfig.jarName ?: '未设置'}" + script.echo " 服务端口: ${serviceConfig.port ?: '未设置'}" + } + } + + script.echo "=" * 70 + } + + /** + * 获取当前分支的配置 + * 根据当前Jenkins作业的分支名称查找对应的分支配置 + * + * @return 分支配置对象(Map类型) + * @throws Exception 如果分支配置不存在 + */ + def getBranchConfig() { + // 获取当前Jenkins作业的分支名称 + def branchName = script.env.BRANCH_NAME + + if (!branchName) { + script.error("❌ 错误: 无法获取分支名称,env.BRANCH_NAME 为空。请检查流水线配置") + } + + script.echo "🌿 正在查找分支配置: ${branchName}" + + // 检查分支配置是否存在 + if (!config.branchConfig || config.branchConfig.isEmpty()) { + script.error("❌ 错误: 分支配置列表为空或未定义。请检查配置文件结构") + } + + // 根据分支名称获取配置 + def branchConfig = config.branchConfig[branchName] + + if (branchConfig) { + script.echo "✅ 成功匹配分支配置:" + script.echo " - Jenkins代理: ${branchConfig.jenkinsAgent ?: '未设置'}" + script.echo " - Maven配置集: ${branchConfig.mavenSet ?: '未设置'}" + script.echo " - 构建参数: ${branchConfig.buildArgs ?: '默认参数'}" + } else { + // 构建友好的错误信息,列出所有可用分支 + def availableBranches = config.branchConfig.keySet().join(', ') + script.error("❌ 错误: 找不到分支 '${branchName}' 的配置。可用分支: ${availableBranches}") + } + + return branchConfig + } + + /** + * 获取指定服务的配置信息 + * 根据服务名称从serviceConfig中查找对应的服务配置 + * + * @param serviceName 服务名称,需要与配置文件中serviceConfig的键匹配 + * @return 服务配置对象(Map类型) + * @throws Exception 如果服务配置不存在 + */ + def getServiceConfig(serviceName) { + if (!serviceName) { + script.error("❌ 错误: 服务名称参数不能为空") + } + + script.echo "🔧 正在查找服务配置: ${serviceName}" + + // 检查服务配置是否存在 + if (!config.serviceConfig || config.serviceConfig.isEmpty()) { + script.error("❌ 错误: 服务配置列表为空或未定义,请检查配置文件结构") + } + + // 根据服务名称获取配置 + def serviceConfig = config.serviceConfig[serviceName] + + if (serviceConfig) { + script.echo "✅ 成功找到服务配置: ${serviceName}" + script.echo " - 容器名称: ${serviceConfig.containerName ?: '未设置'}" + script.echo " - 服务路径: ${serviceConfig.servicePath ?: '未设置'}" + script.echo " - 服务端口: ${serviceConfig.port ?: '未设置'}" + script.echo " - JAR包名称: ${serviceConfig.jarName ?: '未设置'}" + } else { + // 构建友好的错误信息,列出所有可用服务 + def availableServices = config.serviceConfig.keySet().join(', ') + script.error("❌ 错误: 找不到服务 '${serviceName}' 的配置。可用服务: ${availableServices}") + } + + return serviceConfig + } + + /** + * 设置管道参数 + * 整合分支配置、服务配置和环境配置,生成完整的流水线参数 + * + * @return 参数映射表,包含所有必要的部署参数 + */ + def setupParameters() { + script.echo "⚙️ 开始设置管道参数..." + + // 获取当前分支配置 + def branchConfig = getBranchConfig() + + // 从Jenkins作业名称中提取服务名称 + // 格式通常为: "组织名/服务名" 或 "服务名" + def serviceName = script.env.JOB_NAME?.tokenize('/')?.get(0) + script.echo "📦 服务名称: ${serviceName}" + + // 获取服务配置 + def serviceConfig = getServiceConfig(serviceName) + + // 根据分支类型获取环境配置 + def profiles = getBranchProfiles(branchConfig) + script.echo "🌍 环境配置: ${profiles}" + + // 生成唯一的镜像标签,格式: {环境}-{构建ID} + def imageTag = CommonUtils.generateImageTag(script.env.PROFILES, script.env.BUILD_ID) + script.echo "自定义镜像标签:${imageTag}" + + // 构建完整的参数映射表 + def params = [ + // 构建相关参数 + jenkinsAgent : branchConfig.jenkinsAgent, + MAVEN_SET : branchConfig.maven?.mavenSet, + skipTests : branchConfig.maven?.skipTests, + goals : branchConfig.maven?.goals, + dockerFile : branchConfig.docker?.dockerFile ?: '.jenkins/Dockerfile', + buildOptions : branchConfig.docker?.buildOptions, + + // 服务相关参数 + projectName : serviceConfig.containerName, + packageName : serviceConfig?.packageName, + servicePath : serviceConfig.servicePath, + jarName : serviceConfig.jarName, + jarFile : serviceConfig.jarName, + jarFilePath : "${serviceConfig.servicePath}/target", + port : serviceConfig.port, + pomFilePath : serviceConfig.pomFilePath, + deployServer : serviceConfig.deployServer, + + // 环境相关参数 + PROFILES : profiles.default_profile, + availableProfiles : profiles.available_profiles, + imageTags : [imageTag], + + + // Git配置 + gitUrl : config.common?.git?.url, + gitCredentialsId : config.common?.git?.credentialsId, + gitBranch : script.env.BRANCH_NAME, + + // 钉钉配置 + dingTalkCredentialsId: config.common?.dingtalk?.credentialsId, + + // 其他参数 + envConfig : this.envConfig, + orgName : this.orgName, + branchConfig : branchConfig, + serviceConfig : serviceConfig + ] + + // 获取分支环境变量配置 + getEnvironmentConfig(params) + + // 设置容器基础镜像 + params.baseImage = "${params.registry}/${params.registryProject}-base/${params.registryImage}" + + script.echo "基础参数:${params}" + + // 设置Jenkins参数(仅对标准流水线) + setupJenkinsParameters(profiles.available_profiles as List) + + script.echo "✅ 管道参数设置完成" + + // 返回参数 + return params + } + + /** + * 获取指定环境的全部配置参数 + * 从通用配置中提取指定环境的所有参数,包括服务器列表、环境变量、部署策略等 + * + * @param params 参数映射表(会被修改),用于存储提取的配置信息 + * @return 包含指定环境全部参数的Map + */ + def getEnvironmentConfig(params) { + try { + // 从参数中获取环境名称,如果没有设置则使用默认值"dev" + // 使用Elvis操作符 ?: 提供默认值,避免空指针异常 + def envName = params.PROFILES ?: "dev" + + // 将环境名称转换为小写,确保与配置中的key匹配(配置通常使用小写) + // 例如:将"DEV"转换为"dev","Prod"转换为"prod" + def envKey = envName.toLowerCase() + + script.echo "🔍 开始获取 ${envKey} 环境的全部配置参数..." + + // ========================================================================== + // 添加通知配置 + // ========================================================================== + if (config.common?.notification) { + params.notification = config.common.notification + script.echo "📧 通知配置已加载" + } + + // ========================================================================== + // 添加短信配置 - 详细配置信息 + // ========================================================================== + if (config.common?.sms) { + // 确保sms配置存在 + params.sms = config.common.sms + + script.echo "📱 短信服务配置详情:" + script.echo " - 服务商: ${params.sms.provider ?: '未设置'}" + script.echo " - 凭据ID: ${params.sms.credentialsId ?: '未设置'}" + script.echo " - 默认签名: ${params.sms.signName ?: '未设置'}" + + // 打印所有模板配置 + if (params.sms.templates) { + script.echo " - 可用模板:" + params.sms.templates.each { templateName, templateConfig -> + def templateCode = templateConfig.templateCode ?: '未设置' + def templateSign = templateConfig.signName ?: params.sms.signName ?: '未设置' + script.echo " 📋 ${templateName}: ${templateCode} (签名: ${templateSign})" + } + } else { + script.echo " - 模板: 未配置" + } + } else { + script.echo "📱 短信服务: 未配置" + } + + // ========================================================================== + // 添加构建策略配置 + // ========================================================================== + if (config.common?.buildStrategy) { + params.buildStrategy = config.common.buildStrategy + script.echo "⚡ 构建策略:" + script.echo " - 重试次数: ${params.buildStrategy.retryCount}" + script.echo " - 超时时间: ${params.buildStrategy.timeoutMinutes}分钟" + script.echo " - 启用缓存: ${params.buildStrategy.enableCaching}" + } + + // 安全地获取环境配置 - 使用Map方式访问 + // 使用安全导航操作符 ?. 避免空指针异常: + // - config.common?:如果config为null则返回null,不会抛出异常 + // - .profiles?:如果common为null则返回null + // - ."${envKey}":动态访问指定环境的配置 + def envConfig = config.common?.profiles?."${envKey}" + if (!envConfig) { + def availableProfiles = config?.common?.profiles?.keySet() ?: [] + throw new Exception("未找到 ${envKey} 环境的配置,可用环境: ${availableProfiles.join(', ')}") + } + + script.echo "✅ 找到 ${envKey} 环境配置,开始提取参数..." + + // ========================================================================== + // 提取服务器配置 + // ========================================================================== + + // 检查环境配置是否存在且包含服务器列表,并且服务器列表不为空 + // envConfig?.servers:安全访问servers属性 + // !envConfig.servers.empty:检查servers列表是否非空 + if (envConfig?.servers && !envConfig.servers.empty) { + // 使用环境配置中的服务器列表更新参数对象 + // 将找到的服务器列表赋值给params.servers,供后续部署使用 + params.servers = envConfig.servers + + // 输出日志信息,显示成功找到服务器配置 + // 使用表情符号增强可读性,显示服务器数量 + script.echo "🖥️ 服务器配置: 共 ${envConfig.servers.size()} 台服务器" + + // 打印服务器详情(用于调试和验证) + // 使用eachWithIndex迭代服务器列表,同时获取索引和服务器对象 + envConfig.servers.eachWithIndex { server, index -> + // 输出每台服务器的详细信息 + // ${index + 1}:显示序号(从1开始) + // ${server.serverName}:服务器名称标识 + // ${server.serverIP}:${server.serverPort}:服务器的IP地址和端口号 + script.echo " ${index + 1}. ${server.serverName} (${server.serverIP}:${server.serverPort})" + } + } else { + // 如果没有找到对应环境的服务器配置或服务器列表为空 + // 输出警告信息,提示用户检查配置 + script.echo "⚠️ 警告: 未找到 ${envName} 环境的服务器配置" + params.servers = [] + + // 注意:这里没有设置params.servers,调用方需要处理这种情况 + // 可能的处理方式:使用默认服务器、终止部署流程或提示用户输入 + } + + // ========================================================================== + // 提取目标服务器配置 + // ========================================================================== + + // 解析部署服务器引用,获取目标服务器列表 + // 使用ServerParser处理复杂的服务器引用表达式 + def targetServers = serverParser.parseTargetServers(params) + params.targetServers = targetServers + script.echo "🎯 部署目标服务器数量: ${targetServers.size()}" + + // ========================================================================== + // 提取环境变量配置 + // ========================================================================== + if (envConfig?.envVars && !envConfig.envVars.empty) { + params.envVars = envConfig.envVars + script.echo "🌡️ 环境变量: 共 ${envConfig.envVars.size()} 个" + envConfig.envVars.each { key, value -> + script.echo " ${key}=${value}" + } + } else { + script.echo "⚠️ 警告: ${envKey} 环境没有配置环境变量" + params.envVars = [:] + } + + // ========================================================================== + // 提取部署策略配置 + // ========================================================================== + if (envConfig?.deploymentStrategy) { + params.deploymentStrategy = envConfig.deploymentStrategy + script.echo "🎯 部署策略: ${envConfig.deploymentStrategy}" + } + + if (envConfig?.deploymentTimeout) { + params.deploymentTimeout = envConfig.deploymentTimeout + script.echo "⏱️ 部署超时: ${envConfig.deploymentTimeout} 秒" + } + + if (envConfig?.maxParallel) { + params.maxParallel = envConfig.maxParallel + script.echo "🔀 最大并行数: ${envConfig.maxParallel}" + } + + if (envConfig?.retryAttempts) { + params.retryAttempts = envConfig.retryAttempts + script.echo "🔄 重试次数: ${envConfig.retryAttempts}" + } + + if (envConfig?.canaryPercentage) { + params.canaryPercentage = envConfig.canaryPercentage + script.echo "📊 金丝雀比例: ${envConfig.canaryPercentage}%" + } + + // ========================================================================== + // 提取健康检查配置 + // ========================================================================== + if (envConfig?.healthCheckEnabled) { + params.healthCheckEnabled = envConfig.healthCheckEnabled + script.echo "❤️ 启用健康检查: ${envConfig.healthCheckEnabled}" + } + + if (envConfig?.healthCheckPath) { + params.healthCheckPath = envConfig.healthCheckPath + script.echo "❤️ 健康检查路径: ${envConfig.healthCheckPath}" + } + + if (envConfig?.healthCheckTimeout) { + params.healthCheckTimeout = envConfig.healthCheckTimeout + script.echo "⏱️ 健康检查超时: ${envConfig.healthCheckTimeout} 秒" + } + + if (envConfig?.healthCheckRetries) { + params.healthCheckRetries = envConfig.healthCheckRetries + script.echo "❤️ 健康检查重试次数: ${envConfig.healthCheckRetries}" + } + + if (envConfig?.healthCheckInterval) { + params.healthCheckInterval = envConfig.healthCheckInterval + script.echo "🔄 健康检查间隔: ${envConfig.healthCheckInterval} 秒" + } + + if (envConfig?.successThreshold) { + params.successThreshold = envConfig.successThreshold + script.echo "✅ 成功阈值: ${envConfig.successThreshold} 次" + } + + // ========================================================================== + // 提取回滚配置(生产环境特有) + // ========================================================================== + if (envConfig?.rollbackEnabled != null) { + params.rollbackEnabled = envConfig.rollbackEnabled + script.echo "↩️ 自动回滚: ${envConfig.rollbackEnabled}" + } + + if (envConfig?.rollbackOnFailure != null) { + params.rollbackOnFailure = envConfig.rollbackOnFailure + script.echo "❌ 失败时回滚: ${envConfig.rollbackOnFailure}" + } + + if (envConfig?.rollbackTimeout) { + params.rollbackTimeout = envConfig.rollbackTimeout + script.echo "⏱️ 回滚超时: ${envConfig.rollbackTimeout} 秒" + } + + // // ========================================================================== + // // 提取通知配置(生产环境特有) + // // ========================================================================== + // if (envConfig?.notifications) { + // params.notifications = envConfig.notifications + // script.echo "📧 通知配置:" + // if (envConfig.notifications.slackChannel) { + // script.echo " Slack频道: ${envConfig.notifications.slackChannel}" + // } + // if (envConfig.notifications.email) { + // script.echo " 邮件地址: ${envConfig.notifications.email}" + // } + // if (envConfig.notifications.recipients) { + // script.echo " 额外收件人: ${envConfig.notifications.recipients.join(', ')}" + // } + // script.echo " 通知时机: 开始=${envConfig.notifications.onStart}, 成功=${envConfig.notifications.onSuccess}, 失败=${envConfig.notifications.onFailure}" + // } + + // ========================================================================== + // 添加通用配置 + // ========================================================================== + params.deployScript = config.common?.deployScript + params.deployPath = config.common?.deployPath + + script.echo "📜 部署脚本: ${params.deployScript}" + script.echo "📁 部署路径: ${params.deployPath}" + + // ========================================================================== + // 添加镜像仓库配置 + // ========================================================================== + if (config.common?.registrys) { + params.registrys = config.common?.registrys + script.echo "🐳 镜像仓库配置:" + params.registrys.each { registryName, registryConfig -> + script.echo " ${registryName}: ${registryConfig.registry}" + } + } + + // ========================================================================== + // 添加环境分支镜像仓库配置 + // ========================================================================== + def registryConfig = config.common?.registrys?."${envConfig.registry}" + params.registry = registryConfig?.registry ?: "192.168.10.102:8001" + params.registryCredential = registryConfig?.registryCredential ?: "xiaomayi-hub-credentials" + params.registryProject = registryConfig?.registryProject ?: "xiaomayi" + params.registryImage = registryConfig?.registryImage ?: "jdk:17.0.16-centos8-dos" + params.registryType = registryConfig.registryType ?: "harbor" + params.imageFullName = "${params.registry}/${params.registryProject}/${params.projectName}" + + script.echo "✅ ${envKey} 环境全部参数获取完成" + + // 返回配置 + return params + } catch (Exception e) { + script.echo "❌ 获取环境配置失败: ${e.message}" + throw e + } + } + + /** + * 设置 Jenkins 作业参数 + * 为标准流水线作业创建环境选择参数和服务器选择参数 + * 根据可用环境配置动态生成下拉选择框,支持环境和服务器的两级选择 + * + * @param availableProfiles 可用的环境配置列表 + * - 类型: List 环境名称列表(如:['dev', 'test', 'prod']) + * - 来源: 通常从配置文件中解析得到的环境配置键值 + * - 要求: 列表不能为空,且必须与配置文件中的环境名称匹配 + * + * @note 在多分支流水线中会自动跳过参数设置(因为多分支通常根据分支自动确定环境) + * @note 在标准流水线中会创建用户可交互的选择参数 + * + * @example 生成的参数: + * - PROFILES: 环境选择下拉框(dev/test/prod) + * - SERVERS: 服务器选择下拉框(根据选择的环境动态显示对应服务器) + */ + private void setupJenkinsParameters(List availableProfiles) { + // 检查可用环境列表是否为空 + if (!availableProfiles || availableProfiles.isEmpty()) { + script.echo "⚠️ 可用环境配置列表为空,跳过参数设置" + return + } + + // 获取当前分支名称,用于判断流水线类型 + def branchName = script.env.BRANCH_NAME ?: '' + + // 判断是否为多分支流水线(分支名通常包含斜杠,如: feature/xxx) + boolean isMultibranchPipeline = branchName.contains('/') + + if (isMultibranchPipeline) { + script.echo "🔧 检测到多分支流水线,跳过参数设置(分支: ${branchName})" + script.echo "💡 多分支流水线通常根据分支名称自动确定目标环境" + return + } + + script.echo "🔧 标准流水线作业,设置部署环境参数" + script.echo "📋 可用环境选项: ${availableProfiles.join(', ')}" + + try { + // ========================================================================== + // 创建环境选择参数 + // ========================================================================== + def profileParameter = script.choice( + choices: availableProfiles.join('\n'), // 下拉选项,每行一个选择项 + description: '请选择要部署的目标环境', // 参数描述,显示在Jenkins界面上 + name: 'PROFILES' // 参数名称,后续通过params.PROFILES访问 + ) + + // ========================================================================== + // 创建服务器选择参数(动态生成所有环境的服务器选项) + // ========================================================================== + def allServers = getAllServersForParameters(availableProfiles) + def serverChoices = generateServerChoices(allServers) + + def serverParameter = script.choice( + choices: serverChoices.join('\n'), // 服务器下拉选项 + description: '请选择要部署的目标服务器(格式:环境-服务器名-IP)', // 参数描述 + name: 'SERVERS' // 参数名称 + ) + + // ========================================================================== + // 设置Jenkins作业参数 + // ========================================================================== + script.properties([ + script.parameters([ + profileParameter, // 环境选择参数 + serverParameter // 服务器选择参数 + ]) + ]) + + script.echo "✅ 成功设置部署参数:" + script.echo " - PROFILES: 环境选择(${availableProfiles.size()} 个选项)" + script.echo " - SERVERS: 服务器选择(${serverChoices.size()} 个选项)" + + } catch (Exception e) { + script.echo "⚠️ 设置 Jenkins 参数失败: ${e.message}" + script.echo "💡 这可能是因为没有权限修改作业配置,或者参数格式不正确" + // 不抛出异常,避免影响后续流程,参数设置失败通常不会阻断部署 + } + } + + /** + * 获取所有环境的服务器信息用于参数生成 + * 从配置文件中提取每个环境的服务器列表,用于构建服务器选择参数 + * 自动过滤空字符串和无效的环境名称 + * + * @param availableProfiles 可用环境列表,可能包含空字符串或无效值 + * @return Map 各环境的服务器列表映射(只包含有效环境) + */ + private Map getAllServersForParameters(List availableProfiles) { + def allServers = [:] + + // 过滤掉空字符串和null值,只处理有效的环境名称 + def validProfiles = availableProfiles.findAll { profile -> + profile != null && profile.trim() != '' + } + + script.echo "🔍 有效环境列表: ${validProfiles.join(', ')}" + script.echo "🗑️ 已过滤空环境: ${availableProfiles.size() - validProfiles.size()} 个" + + validProfiles.each { profile -> + try { + // 安全地获取环境配置中的服务器列表 + // 使用安全导航操作符避免空指针异常 + def envConfig = config?.common?.profiles?."${profile}" + + if (envConfig?.servers) { + allServers[profile] = envConfig.servers + script.echo " 📡 ${profile} 环境: ${envConfig.servers.size()} 台服务器" + + // 可选:输出服务器详情用于调试 + envConfig.servers.eachWithIndex { server, index -> + script.echo " ${index + 1}. ${server.serverName} (${server.serverIP}:${server.serverPort})" + } + } else { + script.echo " ⚠️ ${profile} 环境: 无服务器配置或服务器列表为空" + allServers[profile] = [] + } + } catch (Exception e) { + script.echo " ❌ 获取 ${profile} 环境服务器失败: ${e.message}" + script.echo " 🐛 错误详情: ${e.getStackTrace().find { it.contains('getAllServersForParameters') } ?: '未知位置'}" + allServers[profile] = [] + } + } + + // 检查是否有任何有效环境配置了服务器 + def environmentsWithServers = allServers.count { it.value && !it.value.empty } + script.echo "📊 统计: ${environmentsWithServers} 个环境配置了服务器,${allServers.size() - environmentsWithServers} 个环境无服务器" + + return allServers + } + + /** + * 生成服务器选择选项 + * 创建格式化的服务器选择项,便于用户识别和选择 + * + * @param allServers 所有环境的服务器映射 + * @return List 格式化后的服务器选项列表 + */ + private List generateServerChoices(Map allServers) { + def choices = [] + + // 添加默认选项 + choices.add("auto-自动选择(根据环境默认服务器)") + + // 为每个环境的每台服务器生成选项 + allServers.each { profile, servers -> + if (servers && !servers.empty) { + servers.each { server -> + // 格式化选项:环境-服务器名-IP:端口 + def displayName = "${profile}-${server.serverName}-${server.serverIP}:${server.serverPort}" + choices.add(displayName) + } + } else { + // 如果某个环境没有服务器,添加提示选项 + choices.add("${profile}-无可用服务器") + } + } + + return choices + } + + /** + * 根据分支配置获取对应的环境配置文件和默认配置文件 + * @param branchConfig 分支配置信息,包含分支类型等 + * @return Map 包含可用配置列表和默认配置 + */ + private def getBranchProfiles(branchConfig) { + // 获取当前分支名称 + def branchName = script.env.BRANCH_NAME ?: '' + + // 1. 优先从常量配置中获取该分支类型对应的可用配置 + def availableProfiles = ConfigConstants.BRANCH_PROFILES[branchName] + + // 2. 如果常量配置中没有找到,根据分支类型选择不同的默认配置源 + if (!availableProfiles) { + availableProfiles = (branchName == 'master') ? + envConfig.masterProfiles : // master分支使用特殊配置 + envConfig.Profiles // 其他分支使用通用配置 + } + + // 确保availableProfiles不为null,如果是null则转为空列表 + availableProfiles = availableProfiles ?: [] + + // 3. 获取默认配置,优先从常量配置中获取 + def defaultProfile = ConfigConstants.DEFAULT_PROFILES[branchName] + + // 4. 如果常量配置中没有默认配置,则从可用配置中选择 + if (!defaultProfile) { + defaultProfile = availableProfiles.find { it } ? // 查找第一个非空配置 + availableProfiles[0] : // 如果都为空则取第一个 + null // 如果没有可用配置则返回null + } + + return [ + available_profiles: availableProfiles, + default_profile : defaultProfile + ] + } + +} \ No newline at end of file diff --git a/src/com/jdk/stages/BuildService.groovy b/src/com/jdk/stages/BuildService.groovy new file mode 100644 index 0000000..ab15cc9 --- /dev/null +++ b/src/com/jdk/stages/BuildService.groovy @@ -0,0 +1,216 @@ +package com.jdk.stages + +/** + * 构建阶段服务 - 负责Maven项目的编译和打包 + * + * 主要职责: + * - 执行Maven项目的清理、编译、打包操作 + * - 验证构建环境和配置的有效性 + * - 处理构建过程中的异常和错误 + * - 提供构建结果的状态反馈 + * + * 设计原则: + * - 单一职责:专注于Maven构建相关的逻辑 + * - 可重用性:可以在不同的流水线项目中重用 + * - 错误隔离:构建失败不会影响其他服务实例 + * - 配置驱动:通过参数Map接收所有配置信息 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class BuildService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 管道工具类实例 + * 提供通用的工具方法和工具函数,如Maven构建命令执行 + * 类型: PipelineUtils 自定义工具类 + */ + def utils + + /** + * 环境配置信息 + * 存储构建环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 构造函数 - 初始化构建服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param utils 管道工具类实例(必须) + * - 用途:提供构建相关的工具方法 + * - 示例:执行Maven命令、处理依赖等 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:构建环境相关的配置参数 + */ + BuildService(script, utils, envConfig = [:]) { + this.script = script + this.utils = utils + this.envConfig = envConfig + + script.echo "🔧 BuildService 初始化完成" + } + + /** + * 执行构建阶段逻辑 + * 负责Maven项目的编译和打包,是CI/CD流水线的核心构建阶段 + * + * 主要步骤: + * 1. 输出构建开始信息和配置详情 + * 2. 验证环境配置的有效性 + * 3. 执行Maven构建命令 + * 4. 处理构建结果和异常 + * + * @param params 管道参数Map,包含构建所需的所有配置信息,必须包含: + * - MAVEN_SET: String - Maven设置文件ID(Jenkins Config File ID) + * - PROFILES: String - Maven构建环境profiles(如:dev, test, prod) + * 可选字段: + * - jarFilePath: String - JAR文件输出路径(默认:target) + * - jarFile: String - 生成的JAR文件名 + * - skipTests: Boolean - 是否跳过测试(默认:true) + * - goals: String - Maven执行目标(默认:"clean package") + * - pomFilePath: String - POM文件路径(默认:pom.xml) + * - orgName: String - 组织名称(用于JDK服务验证) + * - availableProfiles: List - 可用的环境profile列表 + * + * @throws Exception 当Maven构建失败时抛出异常,中断流水线 + * 异常信息包含详细的错误原因和建议解决方案 + * + * @example 使用示例: + * def buildParams = [ + * MAVEN_SET: "nexus-maven-dev", + * PROFILES: "dev", + * jarFilePath: "target", + * jarFile: "xiaomayi-admin.jar", + * skipTests: true, + * orgName: "xiaomayi", + * availableProfiles: ["dev", "test", "prod"] + * ] + * buildService.executeBuildStage(buildParams) + */ + def executeBuildStage(params) { + try { + // 输出构建开始信息和配置详情 + script.echo "🏗️ 开始构建阶段..." + script.echo "📋 构建配置详情:" + script.echo " 🔧 Maven配置: ${params.MAVEN_SET}" + script.echo " 🌍 构建Profile: ${params.PROFILES}" + script.echo " 📦 输出路径: ${params.jarFilePath ?: 'target'}" + script.echo " 🏷️ JAR文件: ${params.jarFile ?: '未指定'}" + script.echo " ⚡ 跳过测试: ${params.skipTests ?: true}" + + // 验证环境profile的有效性(确保配置的一致性) + validateProfile(params) + + // 执行Maven构建命令 + // 使用PipelineUtils中的buildMavenProject方法执行实际构建 + script.echo "🔨 执行Maven构建命令..." + utils.buildMavenProject(params) + + // 构建成功完成 + script.echo "✅ 代码构建完成" + script.echo "🎉 构建产物已生成到: ${params.jarFilePath ?: 'target'} 目录" + + } catch (Exception e) { + // 构建失败处理 + script.echo "❌ 构建阶段失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - Maven settings.xml 配置是否正确" + script.echo " - 网络连接是否正常(依赖下载)" + script.echo " - 代码编译是否有语法错误" + script.echo " - 单元测试是否通过" + + // 重新抛出异常,确保流水线标记为失败状态 + throw e + } + } + + /** + * 验证环境profile有效性 + * 检查选择的部署环境是否在可用环境列表中,确保部署配置的正确性 + * + * 主要功能: + * - 验证必要参数的存在性和有效性 + * - 检查所选环境是否在可用环境列表中 + * - 提供详细的错误信息和调试建议 + * + * @param params 参数Map,必须包含以下字段: + * - PROFILES: String - 要验证的环境profile名称 + * - availableProfiles: List - 可用的环境profile列表 + * - orgName: String - 组织名称(用于日志和错误消息) + * + * @throws IllegalArgumentException 当缺少必要参数或参数无效时抛出 + * @throws Exception 当环境验证失败时抛出,包含详细的错误信息 + */ + private def validateProfile(params) { + script.echo "🔍 开始验证环境profile有效性..." + + // ========================================================================== + // 参数完整性检查 + // ========================================================================== + + // 检查PROFILES参数是否存在且不为空 + if (!params.PROFILES) { + def errorMsg = "缺少必要的PROFILES参数,请指定要部署的环境" + script.echo "❌ ${errorMsg}" + throw new IllegalArgumentException(errorMsg) + } + + // 检查可用环境列表是否存在且不为空 + if (!params.availableProfiles || params.availableProfiles.isEmpty()) { + def errorMsg = "缺少可用的环境profile列表,请检查配置文件中环境配置" + script.echo "❌ ${errorMsg}" + throw new IllegalArgumentException(errorMsg) + } + + // 检查组织名称参数(用于更详细的错误消息) + if (!params.orgName) { + script.echo "⚠️ 缺少组织名称参数orgName,使用默认标识" + // 这里不抛出异常,因为orgName不是核心必需参数 + } + + // ========================================================================== + // 环境有效性验证 + // ========================================================================== + + try { + def selectedProfile = params.PROFILES.trim() + def availableProfiles = params.availableProfiles.collect { it.toString().trim() } + + script.echo "📋 验证详情:" + script.echo " 🎯 选择的环境: ${selectedProfile}" + script.echo " 📊 可用环境: ${availableProfiles.join(', ')}" + + // 检查选择的环境是否在可用列表中 + if (!availableProfiles.contains(selectedProfile)) { + def errorMsg = "无效的部署环境: '${selectedProfile}'。可用环境: ${availableProfiles.join(', ')}" + // 使用script.error使构建标记为失败 + script.error("环境验证失败: ${errorMsg}") + } + + // 验证成功 + script.echo "✅ 环境profile验证通过: ${selectedProfile}" + } catch (Exception e) { + // 处理验证过程中的异常 + script.echo "❌ 环境profile验证过程中发生错误: ${e.message}" + script.echo "📋 可用环境列表: ${params.availableProfiles.join(', ')}" + + // 重新抛出异常,确保构建失败 + throw e + } + } + +} \ No newline at end of file diff --git a/src/com/jdk/stages/ContainerBuildService.groovy b/src/com/jdk/stages/ContainerBuildService.groovy new file mode 100644 index 0000000..5a543f7 --- /dev/null +++ b/src/com/jdk/stages/ContainerBuildService.groovy @@ -0,0 +1,153 @@ +package com.jdk.stages + +/** + * 容器构建阶段服务 - 负责Docker镜像的构建和推送 + * + * 主要职责: + * - 执行Docker镜像的构建、标签生成和推送操作 + * - 管理镜像标签的生命周期和环境变量设置 + * - 处理容器构建过程中的异常和错误 + * - 提供构建结果的状态反馈和镜像信息 + * + * 设计原则: + * - 单一职责:专注于Docker容器构建相关的逻辑 + * - 可重用性:可以在不同的流水线项目中重用 + * - 错误隔离:容器构建失败不会影响其他服务实例 + * - 配置驱动:通过参数Map接收所有配置信息 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class ContainerBuildService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 管道工具类实例 + * 提供通用的工具方法和工具函数,如Docker构建命令执行 + * 类型: PipelineUtils 自定义工具类 + */ + def utils + + /** + * 环境配置信息 + * 存储容器构建环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 构造函数 - 初始化容器构建服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param utils 管道工具类实例(必须) + * - 用途:提供容器构建相关的工具方法 + * - 示例:执行Docker命令、生成镜像标签等 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:镜像仓库配置、构建选项等 + */ + ContainerBuildService(script, utils, envConfig = [:]) { + this.script = script + this.utils = utils + this.envConfig = envConfig + + script.echo "🔧 ContainerBuildService 初始化完成" + } + + /** + * 执行容器构建阶段逻辑 + * 负责Docker镜像的构建和推送,是CI/CD流水线的容器化阶段 + * + * 主要步骤: + * 1. 输出容器构建开始信息 + * 2. 调用工具类执行Docker镜像构建和推送 + * 3. 存储镜像标签到环境变量供后续阶段使用 + * 4. 处理构建异常并提供详细的错误信息 + * + * @param params 管道参数Map,包含镜像构建所需的所有配置信息,必须包含: + * - registry: String - 镜像仓库地址(如:192.168.10.102:8001) + * - registryCredential: String - Jenkins仓库认证凭证ID + * - registryProject: String - 仓库项目/命名空间名称(如:xiaomayi) + * - projectName: String - 项目名称(如:xiaomayi-admin) + * - PROFILES: String - 环境名称(dev/test/uat/prod) + * 可选字段: + * - buildOptions: String - Docker构建选项(如:--no-cache --pull) + * - baseImage: String - 基础镜像名称 + * - serverPort: String/Integer - 服务端口号 + * - jarFile: String - JAR文件名 + * - jarFilePath: String - JAR文件路径 + * + * @return String 构建的镜像标签(格式:{环境}-{构建ID} 或 {构建ID}) + * @throws Exception 当Docker构建或推送失败时抛出异常,包含详细的错误信息 + * + * @example 使用示例: + * def containerParams = [ + * registry: "192.168.10.102:8001", + * registryCredential: "xiaomayi-hub-credentials", + * registryProject: "xiaomayi", + * projectName: "xiaomayi-admin", + * PROFILES: "dev", + * buildOptions: "--no-cache", + * baseImage: "openjdk:17-jdk-alpine" + * ] + * def imageTag = containerBuildService.executeContainerBuildStage(containerParams) + */ + def executeContainerBuildStage(params) { + try { + script.echo "🐳 开始容器构建阶段..." + script.echo "📋 容器构建配置:" + script.echo " 📦 项目名称: ${params.projectName}" + script.echo " 🌍 目标环境: ${params.PROFILES}" + script.echo " 🏷️ 镜像仓库: ${params.registry}" + script.echo " 📁 命名空间: ${params.registryProject}" + + // 输出详细的构建选项(如果有) + if (params.buildOptions) { + script.echo " ⚙️ 构建选项: ${params.buildOptions}" + } + if (params.baseImage) { + script.echo " 🖼️ 基础镜像: ${params.baseImage}" + } + + // 构建Docker镜像并获取镜像标签 + // 使用PipelineUtils中的buildDockerImage方法执行实际构建 + // 该方法会处理Docker构建、标签生成和镜像推送 + script.echo "🔨 执行Docker镜像构建..." + def imageTag = utils.buildDockerImage(params) + + // 将镜像标签存储到环境变量,供后续部署阶段使用 + // 环境变量可以在整个Pipeline过程中共享使用 + script.env.IMAGE_TAG = imageTag + script.echo "💾 镜像标签已存储到环境变量: IMAGE_TAG=${imageTag}" + + script.echo "✅ 镜像构建完成" + script.echo "🎉 镜像标签: ${imageTag}" + script.echo "🚀 镜像已推送到仓库: ${params.registry}/${params.registryProject}/${params.projectName}:${imageTag}" + + return imageTag + + } catch (Exception e) { + // 容器构建失败处理 + script.echo "❌ 容器构建失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - Dockerfile是否存在且语法正确" + script.echo " - 镜像仓库凭证是否有效" + script.echo " - 网络连接是否正常(镜像推送)" + script.echo " - 基础镜像是否可以访问" + script.echo " - 磁盘空间是否充足" + + // 重新抛出异常,确保流水线标记为失败状态 + throw e + } + } +} \ No newline at end of file diff --git a/src/com/jdk/stages/DeployService.groovy b/src/com/jdk/stages/DeployService.groovy new file mode 100644 index 0000000..e36e707 --- /dev/null +++ b/src/com/jdk/stages/DeployService.groovy @@ -0,0 +1,581 @@ +package com.jdk.stages + +/** + * 部署阶段服务 - 负责将Docker镜像部署到目标服务器 + * + * 主要职责: + * - 执行单服务器和多服务器的应用部署操作 + * - 解析服务器引用配置,确定部署目标 + * - 管理部署参数和环境配置 + * - 处理部署过程中的异常和错误 + * - 生成部署结果报告和状态反馈 + * + * 设计原则: + * - 单一职责:专注于应用部署相关的逻辑 + * - 灵活性:支持单服务器和多服务器部署模式 + * - 错误隔离:部署失败不会影响其他服务实例 + * - 配置驱动:通过参数Map接收所有配置信息 + * - 可扩展性:易于添加新的部署策略和功能 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class DeployService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 管道工具类实例 + * 提供通用的工具方法和工具函数,如部署命令执行 + * 类型: PipelineUtils 自定义工具类 + */ + def utils + + /** + * 环境配置信息 + * 存储部署环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 构造函数 - 初始化部署服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param utils 管道工具类实例(必须) + * - 用途:提供部署相关的工具方法 + * - 示例:执行部署命令、处理服务器连接等 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:部署策略、超时设置、健康检查等配置 + */ + DeployService(script, utils, envConfig = [:]) { + this.script = script + this.utils = utils + this.envConfig = envConfig + + script.echo "🔧 DeployService 初始化完成" + script.echo " 📡 服务器解析器已就绪" + } + + /** + * 执行部署阶段逻辑 + * 负责将Docker镜像部署到目标服务器,支持单服务器和多服务器部署模式 + * 这是CI/CD流水线的核心部署阶段,负责将构建好的镜像部署到目标环境 + * + * 主要步骤: + * 1. 输出部署开始信息和配置验证 + * 2. 解析服务器引用配置,确定部署目标 + * 3. 根据服务器数量选择部署模式(单机/多机) + * 4. 执行具体的部署操作 + * 5. 处理部署结果和异常情况 + * + * @param params 管道参数Map,包含部署所需的所有配置信息,必须包含: + * - servers: List - 服务器列表数组,包含所有可用的服务器配置 + * - deployServer: String - 部署服务器引用表达式 + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称(dev/test/uat/prod) + * 可选字段: + * - deployScript: String - 部署脚本文件名(默认:deploy.sh) + * - deployPath: String - 部署脚本路径 + * - envVars: Map - 环境变量配置 + * - deploymentStrategy: String - 部署策略 + * - deploymentTimeout: Integer - 部署超时时间(秒) + * - healthCheckPath: String - 健康检查端点路径 + * - healthCheckTimeout: Integer - 健康检查超时时间 + * - stopOnFailure: Boolean - 失败时是否停止部署 + * + * @throws Exception 当部署失败时抛出异常,包含详细的错误信息 + * 异常信息会区分单服务器和多服务器部署的失败情况 + * + * @example 使用示例: + * def deployParams = [ + * servers: config.common.profiles.dev.servers, + * deployServer: "servers[0,2]", // 部署到第1和第3台服务器 + * projectName: "xiaomayi-admin", + * PROFILES: "dev", + * deploymentStrategy: "rolling", + * deploymentTimeout: 3600 + * ] + * deployService.executeDeployStage(deployParams) + */ + def executeDeployStage(params) { + try { + script.echo "🚀 开始部署阶段..." + script.echo "📋 部署配置:" + script.echo " 📦 项目名称: ${params.projectName}" + script.echo " 🌍 目标环境: ${params.PROFILES}" + script.echo " 📡 服务器引用: ${params.deployServer ?: 'servers[0]'}" + script.echo " 📊 总服务器数: ${params.servers?.size() ?: 0}" + script.echo " 📦 目标服务器: ${params.targetServers}" + + // 目标服务器 + def targetServers = params.targetServers + // 根据目标服务器数量选择部署模式 + if (targetServers.size() > 1) { + script.echo "🔀 多服务器部署模式..." + deployMultipleServers(targetServers, params) + } else if (targetServers.size() == 1) { + script.echo "🔷 单服务器部署模式..." + deploySingleServer(targetServers[0], params) + } else { + throw new Exception("未找到有效的部署目标服务器") + } + + script.echo "✅ 应用部署完成" + + } catch (Exception e) { + script.echo "❌ 部署阶段失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - 服务器配置是否正确" + script.echo " - SSH密钥凭证是否有效" + script.echo " - 网络连接是否正常" + script.echo " - 部署脚本是否存在且可执行" + + // 重新抛出异常,确保流水线标记为失败状态 + throw e + } + } + + /** + * 部署到单个服务器 + * 负责将应用部署到指定的目标服务器,适用于单实例部署场景 + * + * 主要步骤: + * 1. 输出服务器信息和部署详情 + * 2. 构建完整的部署参数Map + * 3. 添加额外的部署配置参数 + * 4. 执行部署操作并处理结果 + * + * @param server 服务器配置对象,包含以下字段: + * - serverIP: String - 服务器IP地址(支持user@host格式) + * - serverPort: Integer - SSH端口号(默认22) + * - serverName: String - 服务器名称标识 + * - sshKey: String - Jenkins SSH密钥凭证ID + * - weight: Integer - 负载权重(可选) + * - isPrimary: Boolean - 是否为主节点(可选) + * - priority: Integer - 部署优先级(可选) + * + * @param params 部署参数Map,包含以下字段: + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称 + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - envVars: Map - 环境变量配置 + * - deploymentStrategy: String - 部署策略 + * - 其他部署相关配置 + * + * @throws Exception 部署失败时抛出异常,包含详细的错误信息和诊断建议 + * + * @note 支持敏感信息保护,自动隐藏密钥和密码字段的日志输出 + */ + private def deploySingleServer(server, params) { + script.echo "🎯 开始部署到单服务器: ${server.serverName}" + script.echo " 📍 目标地址: ${server.serverIP}:${server.serverPort}" + script.echo " 🔑 SSH凭证: ${server.sshKey}" + + try { + // ========================================================================== + // 构建部署参数Map + // 包含部署所需的所有必要信息,确保参数格式正确和完整 + // ========================================================================== + def deployParams = [ + // serverIP : server.serverIP, // 服务器IP地址(格式:user@host或host) + // serverPort : server.serverPort, // SSH端口号(默认22) + // serverName : server.serverName, // 服务器名称标识,用于日志和监控 + // sshKey : server.sshKey, // Jenkins SSH密钥凭证ID(非实际密钥内容) + projectName : params.projectName, // 项目名称(如:xiaomayi-admin) + profile : params.PROFILES, // 环境名称(dev/test/uat/prod) + buildId : script.env.BUILD_ID ?: 'unknown', // 构建ID,用于版本追踪,提供默认值避免空指针 + deployScript: params.deployScript, // 部署脚本文件名(如:deploy.sh) + deployPath : params.deployPath, // 部署脚本所在路径(如:/usr/local/docker/xiaomayi-web/) + ] + + // ========================================================================== + // 添加额外部署参数 + // 从主要参数和服务器配置中提取额外的部署参数 + // ========================================================================== + def extraParams = getDeploymentExtraParams(params, server) + if (extraParams) { + // 使用putAll方法合并Map,确保所有参数都传递给部署工具 + deployParams.putAll(extraParams) + } + + // 打印部署参数详情(调试用),自动隐藏敏感信息 + script.echo "📋 部署参数详情:" + deployParams.each { key, value -> + if (!key.toLowerCase().contains('key') && !key.toLowerCase().contains('password')) { + script.echo " ${key}: ${value}" + } else { + script.echo " ${key}: ********" // 敏感信息隐藏 + } + } + + // 执行部署操作 + script.echo "🚀 正在执行部署..." + utils.deployToServer(deployParams) + + script.echo "✅ 单服务器部署完成: ${server.serverName}" + + } catch (Exception e) { + script.echo "❌ 单服务器部署失败: ${server.serverName}" + script.echo " 💥 错误信息: ${e.message}" + script.echo " 🔍 错误类型: ${e.getClass().name}" + + // 记录详细的错误信息,但不暴露敏感信息 + def errorDetails = "部署到服务器 ${server.serverName} (${server.serverIP}) 失败" + if (e.message?.contains('Authentication')) { + errorDetails += " - 认证失败,请检查SSH密钥" + } else if (e.message?.contains('Connection')) { + errorDetails += " - 连接失败,请检查网络和端口" + } else if (e.message?.contains('Timeout')) { + errorDetails += " - 操作超时,请检查服务器状态" + } else if (e.message?.contains('Permission')) { + errorDetails += " - 权限不足,请检查文件权限" + } + + throw new Exception(errorDetails, e) + } + } + + /** + * 部署到多个服务器 + * 并行或串行地将应用部署到多台目标服务器,支持失败处理和结果报告 + * 适用于集群部署、多节点部署等场景,支持灵活的失败处理策略 + * + * 主要步骤: + * 1. 初始化部署结果跟踪和失败记录 + * 2. 遍历服务器列表,逐台执行部署 + * 3. 根据配置决定失败时是否继续部署 + * 4. 生成部署结果报告和统计信息 + * 5. 处理最终部署结果,决定是否抛出异常 + * + * @param servers 服务器列表,包含每台服务器的配置信息: + * - serverIP: String - 服务器IP地址(支持user@host格式) + * - serverPort: Integer - SSH端口号 + * - serverName: String - 服务器名称标识 + * - sshKey: String - Jenkins SSH密钥凭证ID + * - weight: Integer - 负载权重(可选) + * - isPrimary: Boolean - 是否为主节点(可选) + * - priority: Integer - 部署优先级(可选) + * + * @param params 部署参数Map,包含以下字段: + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称 + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - envVars: Map - 环境变量配置 + * - deploymentStrategy: String - 部署策略 + * - stopOnFailure: Boolean - 失败时是否停止部署 + * - 其他部署相关配置 + * + * @throws Exception 当配置了失败停止且有服务器部署失败时抛出异常 + * 异常信息包含失败的服务器列表和详细错误信息 + * + * @note 支持两种失败处理模式: + * - 失败停止:一旦有服务器部署失败就中断整个部署流程 + * - 失败继续:继续部署其他服务器,最后汇总报告所有失败情况 + */ + private def deployMultipleServers(servers, params) { + // 输出部署开始信息,显示目标服务器数量 + script.echo "🔀 开始部署到 ${servers.size()} 台服务器" + script.echo "📊 失败处理策略: ${shouldStopOnFailure(params) ? '失败停止' : '失败继续'}" + + // 初始化部署结果跟踪Map,用于记录每台服务器的部署状态 + def deploymentResults = [:] + // 初始化失败服务器列表,用于收集部署失败的服务器信息 + def failedServers = [] + + // 遍历服务器列表,逐台进行部署 + servers.each { server -> + try { + // 输出当前部署的服务器信息 + script.echo "➡️ 正在部署到: ${server.serverName} (${server.serverIP}:${server.serverPort})" + + // ========================================================================== + // 构建部署参数Map + // 包含部署所需的所有必要信息,确保参数格式正确和完整 + // ========================================================================== + def deployParams = [ + // serverIP : server.serverIP, // 服务器IP地址(格式:user@host或host) + // serverPort : server.serverPort, // SSH端口号(默认22) + // serverName : server.serverName, // 服务器名称标识,用于日志和监控 + // sshKey : server.sshKey, // Jenkins SSH密钥凭证ID(非实际密钥内容) + projectName : params.projectName, // 项目名称(如:xiaomayi-admin) + profile : params.PROFILES, // 环境名称(dev/test/uat/prod) + buildId : script.env.BUILD_ID ?: 'unknown', // 构建ID,用于版本追踪,提供默认值避免空指针 + deployScript: params.deployScript, // 部署脚本文件名(如:deploy.sh) + deployPath : params.deployPath, // 部署脚本所在路径(如:/usr/local/docker/xiaomayi-web/) + ] + + // ========================================================================== + // 添加额外部署参数 + // 从主要参数和服务器配置中提取额外的部署参数 + // ========================================================================== + def extraParams = getDeploymentExtraParams(params, server) + if (extraParams) { + // 使用putAll方法合并Map,确保所有参数都传递给部署工具 + deployParams.putAll(extraParams) + } + + // ========================================================================== + // 执行部署操作 + // 调用部署工具类执行实际的部署命令 + // ========================================================================== + script.echo "🚀 正在执行部署到 ${server.serverName}..." + // 调用部署工具类,传递完整的部署参数 + utils.deployToServer(deployParams) + + // 输出部署成功信息 + script.echo "✅ 部署成功: ${server.serverName}" + + // 记录部署结果 + deploymentResults[server.serverName] = "成功" + + } catch (Exception e) { + // ========================================================================== + // 异常处理:部署失败的情况 + // ========================================================================== + + // 记录失败的部署结果,包含错误信息 + deploymentResults[server.serverName] = "失败: ${e.message}" + // 将失败的服务器添加到失败列表 + failedServers << server.serverName + // 输出详细的错误信息 + script.echo "❌ 部署失败: ${server.serverName} - ${e.message}" + + // ========================================================================== + // 失败处理策略:根据配置决定是否继续部署 + // ========================================================================== + if (shouldStopOnFailure(params)) { + // 如果配置了失败停止,抛出异常中断部署流程 + throw new Exception("部署中断,因为在 ${server.serverName} 上失败且配置了失败停止", e) + } else { + // 如果未配置失败停止,继续部署其他服务器 + script.echo "⏭️ 继续部署其他服务器(配置了失败继续)" + } + } + } + + // ========================================================================== + // 生成部署报告 + // 汇总所有服务器的部署结果,提供清晰的部署状态概览 + // ========================================================================== + generateDeploymentReport(deploymentResults) + + // ========================================================================== + // 最终结果检查 + // 如果有服务器部署失败,抛出异常通知调用方 + // ========================================================================== + if (failedServers) { + // 构建详细的错误信息 + def errorMessage = "多服务器部署完成,但有 ${failedServers.size()} 台服务器失败: ${failedServers.join(', ')}" + // 输出警告信息(不中断流程,但让调用方知道有部分失败) + script.echo "⚠️ ${errorMessage}" + // 抛出异常,让调用方决定如何处理部分失败的情况 + throw new Exception(errorMessage) + } else { + // 所有服务器都部署成功 + script.echo "🎉 所有服务器部署成功!" + } + } + + /** + * 获取部署额外参数 + * 从主要参数和服务器配置中提取并合并部署所需的额外参数 + * 用于构建完整的部署参数Map,确保部署过程拥有所有必要信息 + * + * @param params 主要部署参数Map,包含环境变量、部署策略等通用配置 + * @param server 服务器配置对象,包含服务器特定的属性信息 + * @return 合并后的参数Map,包含所有部署所需的额外参数 + */ + private def getDeploymentExtraParams(params, server) { + // 初始化空的额外参数Map,用于存储合并后的参数 + def extraParams = [:] + + // ========================================================================== + // 添加环境相关参数(从主要参数中提取) + // ========================================================================== + + // 添加环境变量配置(如果存在) + // envVars通常包含应用运行时需要的环境变量,如数据库连接、日志级别等 + if (params?.envVars) { + extraParams.envVars = params.envVars + } + + // 添加部署策略配置(如果存在) + // deploymentStrategy定义部署方式,如rolling(滚动部署)、blue-green(蓝绿部署)等 + if (params?.deploymentStrategy) { + extraParams.deploymentStrategy = params.deploymentStrategy + } + + // 部署超时时间 + if (params?.deploymentStrategy) { + extraParams.deploymentTimeout = params.deploymentTimeout + } + + // 部署失败重试次数 + if (params?.retryAttempts) { + extraParams.retryAttempts = params.retryAttempts + } + + // ========================================================================== + // 添加服务器特定信息(从服务器配置中提取) + // ========================================================================== + + // 将服务器IP地址添加到额外参数Map中 + // serverIP是部署过程中必需的核心连接参数 + if (server?.serverIP) { + extraParams.serverIP = server.serverIP + } + + // 将服务器SSH端口添加到额外参数Map中 + // serverPort指定了SSH服务的监听端口,默认22 + if (server?.serverPort) { + extraParams.serverPort = server.serverPort + } + + // 添加服务器名称标识,用于日志记录和监控 + if (server?.serverName) { + extraParams.serverName = server.serverName + } + + // 添加SSH密钥凭证ID,用于远程服务器认证 + // sshKey是Jenkins中配置的凭证ID,不是实际的密钥内容 + if (server?.sshKey) { + extraParams.sshKey = server.sshKey + } + + // 添加服务器权重配置,用于负载均衡流量分配 + // weight为null时使用默认值50,表示平均分配流量 + if (server?.weight != null) { + extraParams.weight = server.weight ?: 50 // 默认权重为50 + } + + // 添加主节点标识,用于标识服务器是否为主节点 + // isPrimary为null时使用默认值false + if (server?.isPrimary != null) { + extraParams.isPrimary = server.isPrimary ?: false // 默认非主节点 + } + + // 添加部署优先级,数字越小优先级越高 + // 用于控制部署顺序,高优先级服务器优先部署 + if (server?.priority != null) { + extraParams.priority = server.priority + } + + // ========================================================================== + // 添加计算得到的配置参数 + // ========================================================================== + + // 添加部署超时配置,根据环境自动计算合适的超时时间 + // 生产环境通常需要更长的超时时间 + if (params?.deploymentTimeout) { + extraParams.deploymentTimeout = params.deploymentTimeout + } else { + extraParams.deploymentTimeout = getDeploymentTimeout(params) + } + + // ========================================================================== + // 添加健康检查相关配置(如果存在) + // ========================================================================== + + // 添加健康检查端点路径,用于部署后验证应用状态 + if (params.healthCheckPath) { + extraParams.healthCheckPath = params.healthCheckPath + } + + // 添加健康检查超时时间,控制健康检查的最大等待时间 + if (params.healthCheckTimeout) { + extraParams.healthCheckTimeout = params.healthCheckTimeout + } + + // 返回合并后的额外参数Map + // 这个Map将被合并到主要的部署参数中,形成完整的部署配置 + return extraParams + } + + /** + * 辅助方法:获取部署超时时间(根据环境配置) + * 不同环境设置不同的超时时间,生产环境需要更长的部署时间 + * + * @param params 部署参数,包含PROFILES字段指示环境类型 + * @return 超时时间(秒) + */ + private def getDeploymentTimeout(params) { + // 定义各环境的默认超时时间(单位:秒) + def timeouts = [ + 'dev' : 1800, // 开发环境:30分钟(快速失败) + 'test': 2700, // 测试环境:45分钟 + 'uat' : 3600, // UAT环境:60分钟 + 'prod': 7200 // 生产环境:120分钟(谨慎部署) + ] + + // 获取环境类型,默认为开发环境 + def env = params.PROFILES?.toLowerCase() ?: 'dev' + + // 返回对应环境的超时时间,如果没有匹配则使用开发环境超时 + return timeouts[env] ?: 1800 + } + + /** + * 辅助方法:检查是否应该在部署失败时停止 + * 根据部署参数决定单个服务器失败时是否停止整个部署流程 + * + * @param params 部署参数Map + * @return boolean true-失败时停止,false-失败时继续 + */ + private def shouldStopOnFailure(params) { + // 生产环境通常应该失败时停止,其他环境可以继续 + def isProduction = params.PROFILES?.toLowerCase() == 'prod' + // 优先使用显式配置,如果没有配置则根据环境决定 + return params.stopOnFailure ?: isProduction + } + + /** + * 辅助方法:生成部署报告 + * 创建格式化的部署结果报告,便于查看和分析 + * + * @param results 部署结果Map,格式:["服务器名称": "状态描述"] + */ + private def generateDeploymentReport(results) { + script.echo "=" * 60 + script.echo "📊 多服务器部署结果报告" + script.echo "=" * 60 + + // 统计成功和失败数量 + def successCount = results.count { it.value == '成功' } + def failureCount = results.size() - successCount + + // 输出每台服务器的详细结果 + results.each { serverName, status -> + def emoji = status.startsWith('成功') ? '✅' : '❌' + script.echo "${emoji} ${serverName}: ${status}" + } + + script.echo "-" * 60 + // 输出统计信息 + script.echo "📈 统计: 成功 ${successCount}, 失败 ${failureCount}, 总计 ${results.size()}" + + // 根据结果输出不同的总结信息 + if (failureCount == 0) { + script.echo "🎉 所有服务器部署成功!" + } else if (successCount == 0) { + script.echo "💥 所有服务器部署失败!" + } else { + script.echo "⚠️ 部分服务器部署失败,请检查日志" + } + + script.echo "=" * 60 + } + +} \ No newline at end of file diff --git a/src/com/jdk/stages/HealthCheckService.groovy b/src/com/jdk/stages/HealthCheckService.groovy new file mode 100644 index 0000000..7a4f781 --- /dev/null +++ b/src/com/jdk/stages/HealthCheckService.groovy @@ -0,0 +1,180 @@ +package com.jdk.stages + +/** + * 健康检查阶段服务 - 负责验证部署的应用是否正常运行 + * + * 主要职责: + * - 执行部署后的应用健康状态验证 + * - 监控服务可用性和功能性 + * - 在健康检查失败时触发自动回滚机制 + * - 提供详细的健康检查结果和诊断信息 + * + * 设计原则: + * - 单一职责:专注于健康检查相关的逻辑 + * - 安全性:健康检查失败时提供自动回滚保护 + * - 可配置性:支持灵活的健康检查配置和开关 + * - 错误隔离:健康检查失败不会影响其他服务实例 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + * @see rollbackService 依赖回滚服务处理健康检查失败的情况 + */ +class HealthCheckService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 管道工具类实例 + * 提供通用的工具方法和工具函数,如健康检查命令执行 + * 类型: PipelineUtils 自定义工具类 + */ + def utils + + /** + * 环境配置信息 + * 存储健康检查环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 回滚服务实例 + * 负责在健康检查失败时执行回滚操作 + * 类型: RollbackService 回滚服务类 + */ + def rollbackService + + /** + * 构造函数 - 初始化健康检查服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param utils 管道工具类实例(必须) + * - 用途:提供健康检查相关的工具方法 + * - 示例:执行健康检查命令、处理HTTP请求等 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:健康检查配置、回滚设置等 + * + * @note 自动初始化RollbackService实例用于健康检查失败时的回滚操作 + */ + HealthCheckService(script, utils, envConfig = [:]) { + this.script = script + this.utils = utils + this.envConfig = envConfig + // 初始化回滚服务,用于健康检查失败时的自动回滚 + this.rollbackService = new RollbackService(script, utils, envConfig) + + script.echo "🔧 HealthCheckService 初始化完成" + script.echo " ❤️ 回滚服务已就绪" + } + + /** + * 执行健康检查阶段逻辑 + * 负责验证部署的应用是否正常运行,是CI/CD流水线的质量保障阶段 + * + * 主要步骤: + * 1. 检查健康检查功能是否启用 + * 2. 确定要检查的目标服务器列表 + * 3. 对每个目标服务器执行健康检查 + * 4. 处理健康检查结果和异常情况 + * 5. 在失败时触发自动回滚机制 + * + * @param params 管道参数Map,包含健康检查所需的所有配置信息,通常包含: + * - serverHosts: List - 服务器列表(多服务器部署时使用) + * - serverIP01: String - 单个服务器IP(单服务器部署时使用) + * - port: Integer - 服务端口号(必须) + * 可选字段: + * - healthCheckPath: String - 健康检查端点路径(默认:/actuator/health) + * - healthCheckTimeout: Integer - 健康检查超时时间(秒) + * - healthCheckInterval: Integer - 健康检查间隔时间(秒) + * - successThreshold: Integer - 成功阈值(连续成功次数) + * - retryAttempts: Integer - 重试次数 + * + * @throws Exception 当健康检查失败时抛出异常,触发回滚机制 + * 异常信息包含健康检查失败的详细原因 + * + * @example 使用示例: + * def healthCheckParams = [ + * serverHosts: ["192.168.1.1", "192.168.1.2"], + * port: 8080, + * healthCheckPath: "/actuator/health", + * healthCheckTimeout: 30, + * retryAttempts: 3 + * ] + * healthCheckService.executeHealthCheckStage(healthCheckParams) + */ + def executeHealthCheckStage(params) { + try { + script.echo "❤️ 开始健康检查..." + script.echo "📋 健康检查配置:" + script.echo " 🚪 服务端口: ${params.serverPort}" + + // 输出健康检查路径(如果配置) + if (params.healthCheckPath) { + script.echo " 📍 健康检查端点: ${params.healthCheckPath}" + } + + // 检查是否启用健康检查功能 + // 支持通过环境配置动态启用/禁用健康检查 + def healthCheckEnabled = envConfig.deployment?.health_check_enabled ?: true + script.echo " ⚙️ 健康检查功能: ${healthCheckEnabled ? '已启用' : '已禁用'}" + + if (healthCheckEnabled) { + // 确定要检查的目标服务器列表 + // 支持多服务器部署和单服务器部署两种模式 + def targets = params.serverHosts ?: [params.serverIP01] + script.echo " 📡 目标服务器数量: ${targets.size()}" + + // 对每个目标服务器执行健康检查 + targets.each { serverIP -> + if (serverIP) { + script.echo " 🔍 对服务器 ${serverIP}:${params.serverPort} 进行健康检查..." + + // 使用PipelineUtils中的healthCheck方法执行实际健康检查 + // 该方法会处理HTTP请求、超时控制、重试机制等 + utils.healthCheck(serverIP, params.port) + } + } + + script.echo "✅ 所有服务器健康检查通过" + } else { + script.echo "⏭️ 健康检查功能已禁用,跳过检查" + } + + script.echo "❤️ 健康检查完成" + + } catch (Exception e) { + // 健康检查失败处理 + script.echo "❌ 健康检查失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - 应用是否正常启动" + script.echo " - 服务端口是否正确暴露" + script.echo " - 健康检查端点是否可以访问" + script.echo " - 网络连接是否正常" + script.echo " - 应用日志是否有错误信息" + + // 健康检查失败时尝试回滚部署 + // 回滚功能可以通过配置启用或禁用 + def rollbackEnabled = envConfig?.rollbackEnabled ?: true + if (rollbackEnabled) { + script.echo "🔄 健康检查失败,触发自动回滚..." + rollbackService.executeRollbackStage(params) + } else { + script.echo "⏭️ 自动回滚功能已禁用,跳过回滚" + } + + // 重新抛出异常,确保流水线标记为失败状态 + throw e + } + } + +} \ No newline at end of file diff --git a/src/com/jdk/stages/RollbackService.groovy b/src/com/jdk/stages/RollbackService.groovy new file mode 100644 index 0000000..d9a3aaf --- /dev/null +++ b/src/com/jdk/stages/RollbackService.groovy @@ -0,0 +1,216 @@ +package com.jdk.stages + +import com.utils.ServerParser + +/** + * 回滚阶段服务 - 负责在部署失败时执行回滚操作 + * + * 主要职责: + * - 在部署失败或健康检查失败时执行自动回滚 + * - 将应用回退到上一个稳定版本 + * - 处理回滚过程中的异常和错误 + * - 提供回滚结果的状态反馈和报警通知 + * + * 设计原则: + * - 安全性:确保回滚操作不会造成二次故障 + * - 可靠性:回滚操作本身需要高度可靠 + * - 可追溯性:记录详细的回滚日志和操作记录 + * - 优雅降级:回滚失败时提供明确的处理指引 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class RollbackService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 管道工具类实例 + * 提供通用的工具方法和工具函数,如回滚命令执行 + * 类型: PipelineUtils 自定义工具类 + */ + def utils + + /** + * 环境配置信息 + * 存储回滚环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * 服务器解析器实例 + * 负责解析服务器引用配置和索引处理 + * 类型: ServerParser 自定义服务器解析工具类 + */ + def serverParser + + /** + * 构造函数 - 初始化回滚服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param utils 管道工具类实例(必须) + * - 用途:提供回滚相关的工具方法 + * - 示例:执行回滚命令、版本管理等 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:回滚策略、超时设置、通知配置等 + */ + RollbackService(script, utils, envConfig = [:]) { + this.script = script + this.utils = utils + this.envConfig = envConfig + + // 初始化服务器解析器,用于处理服务器引用和索引 + this.serverParser = new ServerParser(script) + + script.echo "🔧 RollbackService 初始化完成" + script.echo " 🔄 回滚服务就绪" + } + + /** + * 执行回滚操作 + * 在部署或健康检查失败时回滚到上一个可用版本,是CI/CD流水线的安全机制 + * + * 主要步骤: + * 1. 输出回滚开始信息和配置详情 + * 2. 确定要回滚的目标服务器列表 + * 3. 对每个目标服务器执行回滚操作 + * 4. 处理回滚结果和异常情况 + * 5. 提供回滚完成状态和后续处理指引 + * + * @param params 管道参数Map,包含回滚所需的所有配置信息,通常包含: + * - serverHosts: List - 需要回滚的服务器列表(多服务器部署时使用) + * - serverIP01: String - 单个服务器IP(单服务器部署时使用) + * - serverPort: Integer - 服务器SSH端口号(必须) + * - projectName: String - 项目名称(必须) + * 可选字段: + * - previousVersion: String - 上一个版本号(用于指定回滚版本) + * - rollbackTimeout: Integer - 回滚操作超时时间(秒) + * - rollbackStrategy: String - 回滚策略(如:immediate、graceful) + * - notifyOnRollback: Boolean - 是否发送回滚通知 + * + * @note 回滚操作是安全机制,应该尽可能确保其可靠性 + * @note 回滚失败时需要明确的手动处理指引 + * + * @example 使用示例: + * def rollbackParams = [ + * serverHosts: ["192.168.1.1", "192.168.1.2"], + * serverPort: 22, + * projectName: "xiaomayi-admin", + * previousVersion: "v1.2.0", + * rollbackTimeout: 300 + * ] + * rollbackService.executeRollbackStage(rollbackParams) + */ + private def executeRollbackStage(params) { + try { + script.echo "🔄 开始回滚操作..." + script.echo "📋 回滚配置:" + script.echo " 📦 项目名称: ${params.projectName}" + + // 输出回滚版本信息(如果指定) + if (params.previousVersion) { + script.echo " 📋 回滚版本: ${params.previousVersion}" + } else { + script.echo " 📋 回滚版本: 上一个稳定版本(自动检测)" + + // 获取上一个稳定版本 + params.previousVersion = getPreviousStableVersion() + } + + // 确定要回滚的目标服务器列表 + // 支持多服务器部署和单服务器部署两种模式 + def targetServers = params.targetServers + script.echo " 📡 目标服务器数量: ${targetServers.size()}" + script.echo " 🖥️ 目标服务器列表: ${targetServers.join(', ')}" + + // 对每个目标服务器执行回滚操作 + targetServers.each { server -> + if (server) { + script.echo " 🔄 正在回滚服务器: ${server.serverName} (${server.serverIP}:${server.serverPort}" + + // ========================================================================== + // 构建部署参数Map + // 包含部署所需的所有必要信息,确保参数格式正确和完整 + // ========================================================================== + def rollbackParams = [ + serverIP : server.serverIP, // 服务器IP地址(格式:user@host或host) + serverPort : server.serverPort, // SSH端口号(默认22) + serverName : server.serverName, // 服务器名称标识,用于日志和监控 + sshKey : server.sshKey, // Jenkins SSH密钥凭证ID(非实际密钥内容) + timeout : params.deploymentTimeout, // 部署超时时间 + projectName : params.projectName, // 项目名称(如:xiaomayi-admin) + deployScript: params.deployScript, // 部署脚本文件名(如:deploy.sh) + deployPath : params.deployPath, // 部署脚本所在路径(如:/usr/local/docker/xiaomayi-web/) + ] + + // 该方法会处理SSH连接、回滚命令执行、版本切换等 + utils.rollbackDeployment(rollbackParams) + } + } + + script.echo "✅ 回滚操作完成" + script.echo "🎉 应用已成功回退到稳定状态" + script.echo "💡 建议: 检查日志确定部署失败原因后再重新部署" + + // 发送回滚通知 + sendRollbackNotification() + + } catch (Exception rollbackError) { + // 回滚操作失败处理 + script.echo "💥 回滚操作失败: ${rollbackError.message}" + script.echo "🚨 需要手动介入处理" + script.echo "💡 手动处理步骤:" + script.echo " 1. 登录服务器检查应用状态" + script.echo " 2. 查看应用日志确定问题原因" + script.echo " 3. 手动执行回滚命令或修复问题" + script.echo " 4. 验证应用是否恢复正常" + script.echo " 5. 联系开发团队分析根本原因" + + // 输出具体的回滚命令示例,便于手动操作 + script.echo "🔧 手动回滚命令示例:" + script.echo " ssh user@${targets[0]} 'cd /app && ./rollback.sh'" + + // 这里不重新抛出异常,因为回滚失败已经是最终状态 + // 需要人工介入处理,避免无限循环的错误处理 + } + } + + /** + * 获取上一个稳定版本的方法示例 + */ + def getPreviousStableVersion() { + // 这里可以实现从版本管理系统中获取上一个稳定版本的逻辑 + // 例如:从Git标签、Docker镜像标签、版本文件中获取 + try { + def previousVersion = sh(script: "git describe --tags --abbrev=0", returnStdout: true).trim() + echo "📋 检测到上一个稳定版本: ${previousVersion}" + return previousVersion + } catch (Exception e) { + echo "⚠️ 无法自动检测上一个版本,使用默认回滚策略" + return null // 返回null让工具类使用默认回滚逻辑 + } + } + + /** + * 发送回滚通知的方法示例 + */ + def sendRollbackNotification() { + echo "📧 发送回滚通知..." + // 这里可以实现邮件、Slack、钉钉等通知逻辑 + // 例如: + // slackSend channel: '#deployments', message: "🚨 部署失败已触发回滚" + // emailext subject: '部署回滚通知', body: '应用部署失败已自动回滚' + } + +} \ No newline at end of file diff --git a/src/com/jdk/utils/PipelineUtils.groovy b/src/com/jdk/utils/PipelineUtils.groovy new file mode 100644 index 0000000..c3015b6 --- /dev/null +++ b/src/com/jdk/utils/PipelineUtils.groovy @@ -0,0 +1,1389 @@ +package com.jdk.utils + +import com.constants.ConfigConstants + +/** + * PipelineUtils 类 - Jenkins Pipeline 工具类 + * 实现 Serializable 接口的 Jenkins 共享库工具类,用于在 Pipeline 中提供可序列化的工具方法 + * + * 这个类设计为在 Jenkins Pipeline 中使用,特别是当需要在多个节点或重启后保持状态时 + * 实现 Serializable 接口是 Jenkins Pipeline 的要求,确保类实例可以在流水线执行过程中被序列化和反序列化 + * + * @see Serializable Java 序列化接口,允许对象状态保存和恢复 + * + * 主要用途: + * - 封装常用的 Jenkins Pipeline 操作 + * - 管理环境特定的配置参数 + * - 提供可重用的部署和构建方法 + * - 支持流水线的暂停和恢复 + * + * 注意事项: + * - 所有字段都必须是可序列化的类型 + * - 避免使用非序列化的对象(如 Groovy 的闭包) + * - 确保包含的脚本对象也是可序列化的 + */ +class PipelineUtils implements Serializable { + + /** + * Jenkins Pipeline 脚本对象引用 + * 用于在类方法中访问 Jenkins DSL(Domain Specific Language)方法 + * + * 类型: Object (通常是 Jenkins pipeline 的 script 对象) + * 序列化: 必须确保 script 对象是可序列化的 + * + * 用途: + * - 执行 shell 命令: script.sh("command") + * - 输出日志: script.echo("message") + * - 错误处理: script.error("error message") + * - 访问环境变量: script.env.VAR_NAME + * + * 示例: 在 Pipeline 中通过 `this` 传递当前脚本上下文 + */ + def script + + /** + * 环境配置参数存储 + * 用于保存和管理环境特定的配置信息 + * + * 类型: Map 或具体的配置对象 + * 序列化: 必须使用可序列化的数据类型(String、Number、List、Map 等) + * + * 典型内容: + * - servers: 服务器列表配置 + * - envVars: 环境变量配置 + * - deploymentStrategy: 部署策略 + * - 其他环境相关的参数 + * + * 示例: + * [ + * servers: [[serverIP: "192.168.1.1", serverPort: 22]], + * envVars: [JAVA_OPTS: "-Xms1g -Xmx1g"], + * deploymentStrategy: "rolling" + * ] + */ + def envConfig + + /** + * PipelineUtils 构造函数 + * 初始化流水线工具类,用于在Jenkins Pipeline中提供各种工具方法和配置管理 + * 这是一个工具类的构造函数,负责初始化脚本对象和环境配置 + * + * @param script Jenkins pipeline脚本对象(必填) + * - 类型: Object (通常是Jenkins pipeline的script对象) + * - 用途: 用于在类中访问Jenkins DSL方法,如sh、echo、error等 + * - 示例: 在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置参数(可选) + * - 类型: Map 键值对配置映射 + * - 默认值: 如果未提供或为空,使用ConfigConstants.DEFAULT_ENV_CONFIG + * - 用途: 存储环境特定的配置信息,如服务器列表、部署参数等 + * - 示例: [servers: [...], envVars: [...], deploymentStrategy: "rolling"] + * + * @throws IllegalArgumentException 如果script参数为null或未提供时可能抛出异常 + */ + PipelineUtils(script, envConfig = [:]) { + // 设置脚本对象引用,用于在类方法中访问Jenkins DSL + // this.script 将在所有实例方法中可用,用于执行shell命令、输出日志等 + this.script = script + + // 设置环境配置,使用提供的配置或默认配置 + // 使用Elvis操作符 ?: 进行空值检查,确保envConfig不为null + // 如果envConfig为空Map或null,则使用默认配置 + this.envConfig = envConfig ?: ConfigConstants.DEFAULT_ENV_CONFIG + + // 输出初始化日志(可选,用于调试) + this.script.echo "🔧 初始化完成" + this.script.echo " 📝 脚本对象: ${script != null ? '已设置' : '未设置'}" + this.script.echo " ⚙️ 环境配置: 包含 ${this.envConfig.size()} 个配置项" + } + + /** + * 使用Maven构建项目 + * 执行Maven构建命令,包括依赖清理、编译、打包等操作,并显示构建结果信息 + * 支持自定义Maven设置、构建环境、跳过测试等配置 + * + * @param params 构建参数Map,包含以下字段: + * - MAVEN_SET: String - Maven设置文件ID(Jenkins中配置的Config File ID) + * - PROFILES: String - Maven构建环境profiles(如:dev, test, prod) + * - jarFilePath: String - JAR文件输出路径 + * - jarFile: String - 生成的JAR文件名 + * - skipTests: Boolean - 是否跳过测试(默认true,生产环境建议设为false) + * - goals: String - Maven执行目标(默认"clean package") + * - pomFilePath: String - POM文件路径(默认pom.xml) + * + * @throws Exception 当Maven构建失败时抛出异常 + */ + def buildMavenProject(params) { + // 从参数中获取Maven设置文件ID,用于指定Maven配置 + def mavenSettings = params.MAVEN_SET + // 获取构建环境配置,用于激活对应的Maven profile + def profiles = params.PROFILES + // 获取JAR文件输出目录路径 + def jarFilePath = params.jarFilePath + // 获取生成的JAR文件名 + def jarFile = params.jarFile + // 是否跳过测试,默认true(生产环境建议设置为false) + def skipTests = params.skipTests ?: true + // Maven执行目标,默认执行clean和package + def mavenGoals = params.goals ?: "clean package" + // POM文件路径,默认为项目根目录的pom.xml + def pomFilePath = params.pomFilePath + + script.echo "🔨 开始Maven构建项目..." + script.echo "📋 构建参数:" + script.echo " 📁 Maven配置: ${mavenSettings}" + script.echo " 🌍 环境Profile: ${profiles}" + script.echo " 📦 输出路径: ${jarFilePath}" + script.echo " 🏷️ JAR文件: ${jarFile}" + script.echo " ⚡ 跳过测试: ${skipTests}" + script.echo " 🎯 Maven目标: ${mavenGoals}" + script.echo " 📄 POM文件: ${pomFilePath}" + + try { + // 使用Config File Provider插件提供Maven settings.xml配置文件 + script.configFileProvider([script.configFile(fileId: mavenSettings, variable: 'MAVEN_SETTINGS_XML')]) { + // 执行Maven构建命令 + script.sh """ + /usr/local/maven/bin/mvn --settings ${script.env.MAVEN_SETTINGS_XML} \\ + dependency:purge-local-repository -Dinclude=com.jdk \\ + -f ${pomFilePath} ${mavenGoals} \\ + -P${profiles} \\ + -Dmaven.test.skip=${skipTests} + """ + } + + script.echo "✅ Maven构建完成" + + // 显示构建结果信息 + script.echo "📊 构建结果验证:" + + // 显示JAR文件的详细列表信息(大小、权限等) + script.sh "ls -lsh ${jarFilePath}/${jarFile}" + + // 计算JAR文件的MD5校验和,用于验证文件完整性 + script.sh "md5sum ${jarFilePath}/${jarFile}" + + // 可选:显示文件大小(人类可读格式) + script.sh "du -h ${jarFilePath}/${jarFile}" + + // 可选:显示最后修改时间 + script.sh "stat ${jarFilePath}/${jarFile}" + + } catch (Exception e) { + script.echo "❌ Maven构建失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - Maven settings.xml配置是否正确" + script.echo " - 网络连接是否正常(依赖下载)" + script.echo " - POM文件路径是否正确" + script.echo " - 代码编译是否有错误" + throw e // 重新抛出异常,让调用方处理构建失败 + } + } + + + /** + * 构建Docker镜像 + * 使用Docker Pipeline插件构建和推送Docker镜像到指定的镜像仓库 + * 支持自定义构建参数、构建选项和自动清理本地镜像 + * + * @param params 构建参数Map,包含以下必需字段: + * - registry: String - 镜像仓库地址(如:192.168.10.102:8001) + * - registryCredential: String - Jenkins凭证ID,用于仓库认证 + * - registryProject: String - 仓库中的项目/命名空间名称(如:xiaomayi) + * - projectName: String - 项目名称(如:xiaomayi-admin) + * - PROFILES: String - 环境名称(dev/test/uat/prod),用于生成镜像标签 + * 可选字段: + * - buildOptions: String - Docker构建选项(如:--no-cache --pull) + * - baseImage: String - 基础镜像名称,通过--build-arg传递 + * - port: String/Integer - 服务端口号,通过--build-arg传递 + * - jarFile: String - JAR文件名,通过--build-arg传递 + * - jarFilePath: String - JAR文件路径,通过--build-arg传递 + * + * @return String 构建的镜像标签(如:dev-123、prod-456) + * @throws Exception 当Docker构建或推送失败时抛出异常 + */ + def buildDockerImage(params) { + // 生成唯一的镜像标签,格式: {环境}-{构建ID} + def imageTag = generateImageTag(params.PROFILES) + script.echo "🏷️ 生成的镜像标签: ${imageTag}" + + // 获取构建选项,如果未提供则使用空字符串 + def buildOptions = params.buildOptions ?: "" + script.echo "⚙️ 构建选项: ${buildOptions}" + + // 输出构建开始信息和详细参数 + script.echo "🐳 开始构建Docker镜像..." + script.echo " 📦 镜像仓库: ${params.registry}" + script.echo " 📁 命名空间: ${params.registryProject}" + script.echo " 🏷️ 项目名称: ${params.projectName}" + script.echo " 🎯 镜像标签: ${imageTag}" + script.echo " 🔑 仓库凭证: ${params.registryCredential}" + + try { + // 使用Docker Pipeline插件连接到镜像仓库并进行构建推送 + script.docker.withRegistry("https://${params.registry}", params.registryCredential) { + // 构建Docker镜像 + // 镜像名称格式: {命名空间}/{项目名称}:{标签} + def dockerImage = script.docker.build( + // 完整的镜像名称(包含标签) + "${params.registryProject}/${params.projectName}:${imageTag}", + // 构建命令参数组合 + "${buildOptions} " + + // 传递构建参数给Dockerfile + // 环境配置 + "--build-arg VAR_PROFILES=${params.PROFILES} " + + // 基础镜像 + "--build-arg VAR_BASE_IMAGE=${params.baseImage} " + + // 服务端口 + "--build-arg VAR_PORT=${params.port} " + + // JAR文件名 + "--build-arg VAR_JARFILE=${params.jarFile} " + + // JAR文件路径 + "--build-arg VAR_JARFILE_PATH=${params.jarFilePath} " + + // 指定Dockerfile路径和构建上下文,使用项目根目录下的.jenkins/Dockerfile文件 + "-f ${params.dockerFile} ./" + ) + + // 推送镜像到远程仓库 + script.echo "🚀 推送镜像到仓库: ${params.registry}" + dockerImage.push() + + // 清理本地Docker镜像,释放磁盘空间 + script.echo "🧹 开始清理本地镜像..." + cleanupDockerImages(params.registry, params.registryProject, params.projectName, imageTag) + } + + script.echo "✅ Docker镜像构建和推送完成" + return imageTag + + } catch (Exception e) { + script.echo "❌ Docker镜像构建失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - Dockerfile路径是否正确" + script.echo " - 镜像仓库凭证是否有效" + script.echo " - 网络连接是否正常" + throw e // 重新抛出异常,让调用方处理 + } + } + + /** + * 生成Docker镜像标签 + * 根据环境profile和构建ID创建唯一的镜像标签,用于标识不同环境和版本的Docker镜像 + * 标签格式: {环境}-{构建ID} 或 {构建ID}(当环境为空时) + * + * @param profile 环境名称/配置文件(如:dev、test、uat、prod) + * - 用于区分不同环境的镜像 + * - 可以为null或空字符串,此时只使用构建ID + * @return 完整的Docker镜像标签字符串 + * @example + * - generateImageTag("dev") → "dev-123" + * - generateImageTag("prod") → "prod-123" + * - generateImageTag(null) → "123" + */ + private def generateImageTag(profile) { + // 使用三元运算符判断profile是否存在 + // 如果profile存在且不为空,返回"profile-BUILD_ID"格式 + // 如果profile为空,只返回BUILD_ID + return profile ? "${profile}-${script.env.BUILD_ID}" : script.env.BUILD_ID + } + + /** + * 清理Docker镜像 + * 删除本地Docker环境中指定的镜像,避免磁盘空间占用和镜像冲突 + * 主要用于构建后清理,移除不再需要的临时镜像 + * + * @param registry 镜像仓库地址(如:192.168.10.102:8001) + * @param registryProject 仓库中的项目/命名空间名称(如:xiaomayi) + * @param projectName 项目名称(如:xiaomayi-admin) + * @param imageTag 镜像标签(由generateImageTag方法生成) + * + * @note 使用"|| true"确保清理失败不会中断构建流程 + * @note 尝试清理两种格式的镜像: + * 1. {registryProject}/{projectName}:{imageTag} + * 2. {registry}/{registryProject}/{projectName}:{imageTag} + */ + private def cleanupDockerImages(registry, registryProject, projectName, imageTag) { + try { + // 输出清理开始信息 + script.echo "🧹 开始清理Docker镜像: ${projectName}:${imageTag}" + + // 清理第一种格式的镜像:{项目}/{应用}:{标签} + // 例如:xiaomayi/xiaomayi-admin:dev-123 + // 使用"|| true"确保命令即使失败也不会抛出异常中断脚本 + script.sh "docker rmi -f ${registryProject}/${projectName}:${imageTag} || true" + + // 清理第二种格式的镜像:{仓库地址}/{项目}/{应用}:{标签} + // 例如:192.168.10.102:8001/xiaomayi/xiaomayi-admin:dev-123 + script.sh "docker rmi -f ${registry}/${registryProject}/${projectName}:${imageTag} || true" + + script.echo "✅ Docker镜像清理完成" + + } catch (Exception e) { + // 捕获并处理异常,输出错误信息但不会中断主流程 + // 镜像清理失败通常不会影响主要功能,所以只是记录日志 + script.echo "⚠️ 清理Docker镜像失败: ${e.message}" + script.echo " 此错误通常可以忽略,不会影响部署流程" + } + } + + /** + * 部署到单个服务器 + * 执行远程部署命令,将指定版本的Docker镜像部署到目标服务器 + * 该方法包含完整的部署流程:文件传输、权限设置、命令执行和结果验证 + * + * @param deployParams 部署参数Map,包含以下必需字段: + * - serverIP: String - 服务器IP地址(支持user@host格式,如esxi@124.222.176.210) + * - serverPort: Integer - 服务器SSH端口号 + * - projectName: String - 项目名称(如xiaomayi-admin) + * - profile: String - 环境profile(dev/test/uat/prod) + * - buildId: String - 构建ID(用于版本追踪) + * - deployPath: String - 远程服务器部署路径(如/usr/local/docker/xiaomayi-web/) + * - deployScript: String - 部署脚本文件名(如deploy_env.sh) + * 可选字段: + * - sshKey: String - SSH密钥凭证ID(默认为jenkins-ssh-205) + * - timeout: Integer - 命令执行超时时间(秒,默认300秒) + * - extraArgs: String - 额外的部署参数 + * - retryAttempts: Integer - 失败重试次数(默认3次) + * + * @throws IllegalArgumentException 参数缺失或无效时抛出 + * @throws Exception SSH连接、文件传输或命令执行失败时抛出 + * + * @example + * deployToServer([ + * serverIP: "esxi@124.222.176.210", + * serverPort: 12422, + * projectName: "xiaomayi-admin", + * profile: "prod", + * buildId: "31", + * deployPath: "/usr/local/docker/xiaomayi-web/", + * deployScript: "deploy_env.sh", + * sshKey: "jenkins-ssh-205", + * timeout: 300 + * ]) + */ + def deployToServer(deployParams) { + // 参数解析和默认值设置 + def serverIP = deployParams.serverIP + def serverPort = deployParams.serverPort ?: 22 // 默认SSH端口 + def projectName = deployParams.projectName + def profile = deployParams.profile + def buildId = deployParams.buildId + def sshKey = deployParams.sshKey ?: 'jenkins-ssh-205' // 默认SSH凭证 + def timeout = deployParams.deploymentTimeout ?: 300 // 默认5分钟超时 + def extraArgs = deployParams.extraArgs ?: '' // 额外参数 + def retryAttempts = deployParams.retryAttempts ?: 3 // 默认重试3次 + def deployPath = deployParams.deployPath // 远程部署路径 + def deployScript = deployParams.deployScript // 部署脚本文件名 + + // 日志输出部署信息 + script.echo "🚀 开始部署到服务器: ${serverIP}:${serverPort}" + script.echo "📦 项目: ${projectName}, 环境: ${profile}, 构建ID: ${buildId}" + script.echo "📁 部署路径: ${deployPath}" + script.echo "📜 部署脚本: ${deployScript}" + + try { + // 生成镜像标签(包含环境、构建ID和时间戳信息) + def imageTag = generateImageTag(profile) + script.echo "🏷️ 生成的镜像标签: ${imageTag}" + + // 第一步:传输部署文件到远程服务器 + // 支持多文件和重命名功能,包括部署脚本和环境配置文件 + script.echo "📤 开始文件传输阶段..." + + // 构建文件传输列表 + def transferFiles = [ + // 部署脚本(保持原文件名) + [localPath: ".pipeline-files/scripts-file/${deployScript}", mode: "755"], + // 环境文件(重命名为 .env) + [localPath: ".pipeline-files/scripts-file/docker-env-${profile}.sh", remoteName: ".env", mode: "644"], + // Docker执行文件 + [localPath: ".pipeline-files/compose-file/docker-compose-${profile}.yml", remoteName: "docker-compose.yml"], + ] + transferDeploymentFiles([ + serverIP: serverIP, + serverPort: serverPort, + sshKey: sshKey, + remotePath: deployPath, + files: transferFiles, // 使用动态文件列表 + timeout: 300 + ]) + + // 第二步:设置远程文件执行权限 + // 确保部署脚本有可执行权限,避免"Permission denied"错误 + script.echo "🔧 开始权限设置阶段..." + setRemoteFilePermissions([ + serverIP: serverIP, + serverPort: serverPort, + sshKey: sshKey, + remotePath: deployPath, // 远程路径 + deployScript: deployScript, // 要设置权限的脚本文件 + timeout: 60 // 权限设置超时时间 + ]) + + // 第三步:构建部署命令 + // 使用bash显式执行脚本,双重保障避免权限问题 + script.echo "🔨 构建部署命令..." + def command = buildDeploymentCommand(deployPath, deployScript, projectName, imageTag, extraArgs) + script.echo "💻 部署命令已构建完成" + + // 第四步:执行远程部署命令(支持重试机制) + // 使用指数退避策略进行重试,提高部署成功率 + script.echo "⚡ 开始执行部署命令..." + def success = executeRemoteCommandWithRetry( + serverIP, + serverPort, + sshKey, + command, + timeout, + retryAttempts + ) + + // 部署成功日志 + script.echo "✅ 部署成功完成: ${projectName} 已部署到 ${serverIP}" + script.echo "🎯 镜像版本: ${imageTag}" + script.echo "📍 部署路径: ${deployPath}" + + // 第五步:生成部署摘要报告 + // 包含部署详情、时间戳、版本信息等 + script.echo "📊 生成部署摘要报告..." + generateDeploymentSummary(deployParams, imageTag, success) + + script.echo "🎉 部署流程全部完成" + + } catch (Exception e) { + // 部署失败处理 + script.echo "❌ 部署失败: ${e.message}" + script.echo "🔍 失败详情: ${e.getClass().getName()}" + script.echo "💡 建议检查网络连接、服务器状态和文件权限" + + // 重新抛出异常,让上层调用者处理 + throw new Exception("部署到服务器 ${serverIP} 失败: ${e.message}", e) + } + } + + // /** + // * 传输部署文件到远程服务器 + // * 使用SCP将部署脚本和环境文件传输到目标服务器 + // * + // * @param scpParams SCP传输参数Map,包含以下字段: + // * - serverIP: String - 服务器IP地址 + // * - serverPort: Integer - 服务器SSH端口(默认22) + // * - sshKey: String - SSH密钥凭证ID + // * - localScriptPath: String - 本地部署脚本路径 + // * - localEnvPath: String - 本地环境文件路径 + // * - remotePath: String - 远程服务器目标路径 + // * - remoteEnvFileName: String - 远程环境文件名(默认.env) + // * - timeout: Integer - 超时时间(秒,默认300) + // * + // * @throws Exception 当SCP传输失败时抛出异常 + // * + // * @example + // * scpParams = [ + // * serverIP: "192.168.124.126", + // * sshKey: "jenkins-ssh-205", + // * localScriptPath: ".pipeline-files/scripts/deploy_env.sh", + // * localEnvPath: ".pipeline-files/scripts/docker-env-test.sh", + // * remotePath: "/data/apps/yhsc/", + // * remoteEnvFileName: ".env" + // * ] + // */ + // def transferDeploymentFiles(scpParams) { + // def serverIP = scpParams.serverIP + // def serverPort = scpParams.serverPort ?: 22 + // def sshKey = scpParams.sshKey ?: 'jenkins-ssh-205' + // def localScriptPath = scpParams.localScriptPath + // def localEnvPath = scpParams.localEnvPath + // def remotePath = scpParams.remotePath + // def remoteEnvFileName = scpParams.remoteEnvFileName ?: '.env' + // def timeout = scpParams.timeout ?: 300 + + // script.echo "📤 开始传输部署文件到服务器: ${serverIP}:${serverPort}" + // script.echo "📋 传输参数:" + // script.echo " 📁 本地脚本: ${localScriptPath}" + // script.echo " 📁 本地环境文件: ${localEnvPath}" + // script.echo " 📁 远程路径: ${remotePath}" + // script.echo " 🏷️ 远程环境文件名: ${remoteEnvFileName}" + // script.echo " 🔑 SSH凭证: ${sshKey}" + // script.echo " ⏱️ 超时时间: ${timeout}秒" + + // try { + // // 使用withCredentials处理SSH密钥 + // script.withCredentials([script.sshUserPrivateKey( + // credentialsId: sshKey, + // keyFileVariable: 'SSH_KEY_FILE', + // usernameVariable: 'SSH_USERNAME' + // )]) { + // // 解析用户名和主机名(支持user@host格式) + // def (username, hostname) = parseServerAddress(serverIP) + + // // 传输部署脚本文件 + // script.echo "📤 传输部署脚本文件..." + // executeSCPCommand( + // localScriptPath, + // "${username}@${hostname}:${remotePath}", + // serverPort, + // sshKey, + // timeout + // ) + + // // 传输环境文件(重命名为.env) + // script.echo "📤 传输环境配置文件..." + // executeSCPCommand( + // localEnvPath, + // "${username}@${hostname}:${remotePath}/${remoteEnvFileName}", + // serverPort, + // sshKey, + // timeout + // ) + // } + + // script.echo "✅ 文件传输完成" + // script.echo "💡 传输的文件:" + // script.echo " - ${localScriptPath} → ${remotePath}/" + // script.echo " - ${localEnvPath} → ${remotePath}/${remoteEnvFileName}" + + // } catch (Exception e) { + // script.echo "❌ 文件传输失败: ${e.message}" + // script.echo "💡 建议检查:" + // script.echo " - 本地文件路径是否正确" + // script.echo " - 远程目录权限是否足够" + // script.echo " - SSH密钥凭证是否有效" + // script.echo " - 网络连接是否正常" + // throw e + // } + // } + + // /** + // * 执行SCP文件传输命令 + // * 使用SCP协议将文件从本地传输到远程服务器 + // * + // * @param localFile 本地文件路径 + // * @param remoteTarget 远程目标路径(格式: user@host:path) + // * @param serverPort SSH端口号 + // * @param sshKey SSH密钥凭证ID + // * @param timeout 超时时间(秒) + // * + // * @throws Exception 当SCP命令执行失败时抛出异常 + // */ + // private def executeSCPCommand(localFile, remoteTarget, serverPort, sshKey, timeout) { + // script.echo " 📁 传输: ${localFile} → ${remoteTarget}" + + // // 构建SCP命令 + // def scpCommand = """ + // scp -o StrictHostKeyChecking=${ConfigConstants.SSH_CONFIG.strictHostKeyChecking} \ + // -o ConnectTimeout=${ConfigConstants.SSH_CONFIG.connectTimeout} \ + // -o ServerAliveInterval=${ConfigConstants.SSH_CONFIG.serverAliveInterval} \ + // -P ${serverPort} \ + // -i ${script.SSH_KEY_FILE} \ + // ${localFile} \ + // ${remoteTarget} + // """.trim() + + // // 执行SCP命令(带超时设置) + // script.timeout(time: timeout, unit: 'SECONDS') { + // script.sh(script: scpCommand, returnStatus: false) + // } + // } + + /** + * 传输部署文件到远程服务器(支持多文件和重命名) + * 使用SCP将多个文件传输到目标服务器,支持自定义远程文件名和权限设置 + * 该方法提供了灵活的文件传输解决方案,适用于复杂的部署场景 + * + * @param scpParams SCP传输参数Map,包含以下字段: + * - serverIP: String - 服务器IP地址(支持user@host格式) + * - serverPort: Integer - 服务器SSH端口(默认22) + * - sshKey: String - SSH密钥凭证ID(默认为jenkins-ssh-205) + * - remotePath: String - 远程服务器目标路径(必需) + * - timeout: Integer - 超时时间(秒,默认300) + * - files: List - 文件传输列表,每个文件包含配置信息: + * - localPath: String - 本地文件路径(必需) + * - remoteName: String - 远程文件名(可选,不提供则使用本地文件名) + * - mode: String - 文件权限模式(可选,如"755"、"644") + * + * @throws Exception 当SCP传输失败时抛出异常,包含详细的错误信息 + * + * @example + * // 高级用法:传输多个文件并重命名 + * transferDeploymentFiles([ + * serverIP: "user@192.168.124.126", + * serverPort: 12422, + * sshKey: "jenkins-ssh-205", + * remotePath: "/usr/local/docker/xiaomayi-web/", + * timeout: 600, + * files: [ + * [localPath: ".pipeline-files/scripts/deploy_env.sh", remoteName: "deploy.sh", mode: "755"], + * [localPath: ".pipeline-files/scripts/docker-env-test.sh", remoteName: ".env", mode: "644"], + * [localPath: ".pipeline-files/config/app-config.json", mode: "644"], + * [localPath: ".pipeline-files/certs/ssl.crt", remoteName: "server.crt"], + * [localPath: ".pipeline-files/certs/ssl.key", remoteName: "server.key"] + * ] + * ]) + */ + def transferDeploymentFiles(scpParams) { + // 参数解析和默认值设置 + def serverIP = scpParams.serverIP // 服务器IP地址(必需) + def serverPort = scpParams.serverPort ?: 22 // SSH端口,默认22 + def sshKey = scpParams.sshKey ?: 'jenkins-ssh-205' // SSH凭证,默认jenkins-ssh-205 + def remotePath = scpParams.remotePath // 远程目标路径(必需) + def timeout = scpParams.timeout ?: 300 // 超时时间,默认300秒(5分钟) + def files = scpParams.files ?: [] // 文件列表,默认为空列表 + + // 日志输出传输信息 + script.echo "📤 开始传输部署文件到服务器: ${serverIP}:${serverPort}" + script.echo "📋 传输参数:" + script.echo " 📁 远程路径: ${remotePath}" + script.echo " 🔑 SSH凭证: ${sshKey}" + script.echo " ⏱️ 超时时间: ${timeout}秒" + script.echo " 📊 文件数量: ${files.size()}" + + // 检查文件列表是否为空,避免不必要的操作 + if (files.isEmpty()) { + script.echo "⚠️ 没有文件需要传输,跳过文件传输阶段" + return // 直接返回,不执行后续操作 + } + + try { + // 使用Jenkins凭证管理安全地处理SSH密钥 + // withCredentials确保密钥在使用后自动清理,避免安全风险 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, // SSH凭证ID + keyFileVariable: 'SSH_KEY_FILE', // 密钥文件路径存储变量 + usernameVariable: 'SSH_USERNAME' // 用户名存储变量 + )]) { + // 解析服务器地址(支持user@host格式和单独host格式) + def (username, hostname) = parseServerAddress(serverIP) + script.echo " 👤 连接用户: ${username}" + script.echo " 🌐 目标主机: ${hostname}" + + // 遍历文件列表,逐个传输文件 + // eachWithIndex提供索引信息,用于进度显示 + files.eachWithIndex { fileConfig, index -> + // 解析单个文件的配置 + def localPath = fileConfig.localPath // 本地文件路径(必需) + // 远程文件名:如果未指定则使用本地文件名 + def remoteName = fileConfig.remoteName ?: getFileName(localPath) + def mode = fileConfig.mode // 文件权限模式(可选) + + // 输出传输进度信息 + script.echo " 📤 传输文件 (${index + 1}/${files.size()}): ${localPath} → ${remotePath}/${remoteName}" + if (mode) { + script.echo " 🔧 将设置权限: ${mode}" + } + + // 执行SCP文件传输命令 + executeSCPCommand( + localPath, // 本地文件路径 + "${username}@${hostname}:${remotePath}/${remoteName}", // 远程目标路径 + serverPort, // SSH端口 + sshKey, // SSH凭证 + timeout, // 超时时间 + 3 // 重试次数(默认3次) + ) + + // 如果指定了文件权限模式,设置远程文件权限 + if (mode) { + setRemoteFileMode(serverIP, serverPort, sshKey, "${remotePath}/${remoteName}", mode, timeout) + } + + script.echo " ✅ 文件传输完成: ${remoteName}" + } + } + + // 传输完成后的摘要信息 + script.echo "✅ 所有文件传输完成" + script.echo "💡 传输摘要:" + script.echo " =========================================" + files.eachWithIndex { fileConfig, index -> + def localPath = fileConfig.localPath + def remoteName = fileConfig.remoteName ?: getFileName(localPath) + def mode = fileConfig.mode ?: "默认权限" + script.echo " ${index + 1}. ${localPath} → ${remotePath}/${remoteName} (权限: ${mode})" + } + script.echo " =========================================" + + } catch (Exception e) { + // 错误处理:提供详细的错误信息和排查建议 + script.echo "❌ 文件传输失败: ${e.message}" + script.echo "🔍 错误类型: ${e.getClass().getSimpleName()}" + script.echo "💡 排查建议:" + script.echo " - 检查本地文件路径是否正确: ${files.collect{ it.localPath }}" + script.echo " - 验证远程目录是否存在且有写权限: ${remotePath}" + script.echo " - 确认SSH密钥凭证是否有效: ${sshKey}" + script.echo " - 检查网络连接和防火墙设置" + script.echo " - 验证目标服务器SSH服务是否正常运行" + + // 重新抛出异常,让上层调用者处理 + throw new Exception("文件传输到服务器 ${serverIP} 失败: ${e.message}", e) + } + } + + /** + * 从文件路径中提取文件名 + * 用于当未指定远程文件名时,自动使用本地文件名 + * + * @param filePath 文件完整路径(如:/path/to/file.txt 或 relative/path/file.sh) + * @return String 文件名(如:file.txt 或 file.sh) + * + * @example + * getFileName("/opt/apps/deploy.sh") → "deploy.sh" + * getFileName("config/app-config.json") → "app-config.json" + */ + private def getFileName(filePath) { + // 使用Java的File类来提取文件名,处理各种路径格式 + return new File(filePath).getName() + } + + /** + * 设置远程文件权限模式 + * 通过SSH连接到远程服务器执行chmod命令设置文件权限 + * + * @param serverIP 服务器IP地址 + * @param serverPort SSH端口 + * @param sshKey SSH密钥凭证ID + * @param remoteFilePath 远程文件完整路径 + * @param mode 权限模式(如:"755"、"644"、"600") + * @param timeout 超时时间(秒) + * + * @note 该方法设计为容错模式,权限设置失败不会阻止后续操作 + */ + private def setRemoteFileMode(serverIP, serverPort, sshKey, remoteFilePath, mode, timeout) { + try { + // 使用凭证管理确保SSH密钥安全 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, + keyFileVariable: 'SSH_KEY_FILE', + usernameVariable: 'SSH_USERNAME' + )]) { + // 解析服务器地址 + def (username, hostname) = parseServerAddress(serverIP) + + // 构建chmod命令:设置远程文件权限 + def chmodCommand = "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ServerAliveInterval=60 -p ${serverPort} -i ${script.SSH_KEY_FILE} ${username}@${hostname} 'chmod ${mode} \"${remoteFilePath}\"'" + + // 执行权限设置命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def result = script.sh(script: chmodCommand, returnStatus: true) + if (result == 0) { + script.echo " ✅ 权限设置成功: ${remoteFilePath} → ${mode}" + } else { + script.echo "⚠️ 权限设置失败,但继续执行: ${remoteFilePath}" + } + } + } + } catch (Exception e) { + // 权限设置失败处理(容错模式) + script.echo "⚠️ 设置文件权限时出错: ${e.message},但继续执行后续操作" + script.echo " 💡 文件仍然可以传输,但可能需要手动设置权限" + } + } + + /** + * 执行SCP文件传输命令(增强版) + * 支持错误重试机制和详细的日志输出,提高传输可靠性 + * + * @param localFile 本地文件路径 + * @param remoteTarget 远程目标路径(格式: user@host:path) + * @param serverPort SSH端口号 + * @param sshKey SSH密钥凭证ID + * @param timeout 单次尝试超时时间(秒) + * @param retries 最大重试次数(默认3次) + * + * @throws Exception 当所有重试尝试都失败后抛出异常 + */ + private def executeSCPCommand(localFile, remoteTarget, serverPort, sshKey, timeout, retries = 3) { + def attempt = 1 + def lastError = null + + // 重试循环:尝试多次传输,提高成功率 + while (attempt <= retries) { + try { + script.echo " 🔄 SCP传输尝试 (${attempt}/${retries}): ${localFile}" + + // 构建SCP命令 + def scpCommand = """ + scp -o StrictHostKeyChecking=no \ + -o ConnectTimeout=30 \ + -o ServerAliveInterval=60 \ + -P ${serverPort} \ + -i ${script.SSH_KEY_FILE} \ + ${localFile} \ + ${remoteTarget} + """.trim() + + // 执行SCP命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def result = script.sh(script: scpCommand, returnStatus: true) + if (result != 0) { + throw new Exception("SCP命令返回非零状态码: ${result}") + } + } + + script.echo " ✅ SCP传输成功: ${localFile}" + return // 传输成功,退出方法 + + } catch (Exception e) { + lastError = e + script.echo "❌ SCP传输失败 (尝试 ${attempt}): ${e.message}" + + // 如果不是最后一次尝试,等待后重试 + if (attempt < retries) { + def waitTime = attempt * 5 // 指数退避策略:5, 10, 15秒... + script.echo "⏳ 等待 ${waitTime} 秒后重试..." + script.sleep(waitTime) // 等待一段时间 + } + attempt++ // 增加尝试次数 + } + } + + // 所有重试都失败,抛出异常 + throw new Exception("SCP传输失败,重试 ${retries} 次后仍失败: ${lastError?.message}", lastError) + } + + /** + * 设置远程文件执行权限 + * 确保部署脚本在远程服务器上有执行权限,避免"Permission denied"错误 + * 该方法会尝试设置权限,但即使失败也不会中断部署流程(会尝试使用bash执行) + * + * @param permissionParams 权限设置参数Map,包含以下字段: + * - serverIP: String - 服务器IP地址 + * - serverPort: Integer - 服务器SSH端口(默认22) + * - sshKey: String - SSH密钥凭证ID + * - remotePath: String - 远程服务器路径 + * - deployScript: String - 要设置权限的脚本文件名 + * - timeout: Integer - 超时时间(秒,默认60) + * + * @throws Exception 当SSH连接完全失败时抛出异常 + * + * @note 该方法设计为容错模式,权限设置失败不会阻止后续部署尝试 + */ + def setRemoteFilePermissions(permissionParams) { + // 参数解析和默认值设置 + def serverIP = permissionParams.serverIP + def serverPort = permissionParams.serverPort ?: 22 + def sshKey = permissionParams.sshKey ?: 'jenkins-ssh-205' + def remotePath = permissionParams.remotePath + def deployScript = permissionParams.deployScript + def timeout = permissionParams.timeout ?: 60 + + script.echo "🔧 设置远程文件执行权限..." + script.echo " 📁 远程路径: ${remotePath}" + script.echo " 📜 脚本文件: ${deployScript}" + script.echo " 🔑 SSH凭证: ${sshKey}" + + try { + // 使用Jenkins凭证管理安全地处理SSH密钥 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, // SSH凭证ID + keyFileVariable: 'SSH_KEY_FILE', // 密钥文件路径环境变量 + usernameVariable: 'SSH_USERNAME' // 用户名环境变量 + )]) { + // 解析服务器地址(支持user@host格式和单独host格式) + def (username, hostname) = parseServerAddress(serverIP) + script.echo " 👤 用户名: ${username}" + script.echo " 🌐 主机名: ${hostname}" + + // ssh -o StrictHostKeyChecking=no \\ # 禁用严格主机密钥检查 + // -o ConnectTimeout=30 \\ # 连接超时30秒 + // -o ServerAliveInterval=60 \\ # 保持连接活跃间隔60秒 + // -p ${serverPort} \\ # SSH端口 + // -i ${script.SSH_KEY_FILE} \\ # SSH密钥文件路径 + // ${username}@${hostname} \\ # 目标服务器 + // 'cd ${remotePath} && \\ # 切换到部署目录 + // chmod +x ${deployScript} && \\ # 设置执行权限 + // ls -la ${deployScript}' # 验证权限设置结果 + + // 构建chmod命令:设置执行权限并验证 + def chmodCommand = """ + ssh -o StrictHostKeyChecking=no \ + -o ConnectTimeout=30 \ + -o ServerAliveInterval=60 \ + -p ${serverPort} \ + -i ${script.SSH_KEY_FILE} \ + ${username}@${hostname} \ + 'cd ${remotePath} && \ + chmod +x ${deployScript} && \ + ls -la ${deployScript}' + """.trim() + + script.echo " ⚡ 执行权限设置命令..." + + // 执行权限设置命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def result = script.sh(script: chmodCommand, returnStdout: true, returnStatus: true) + + if (result == 0) { + script.echo "✅ 文件权限设置成功" + script.echo " 📋 文件权限已更新,可执行权限已添加" + } else { + script.echo "⚠️ 文件权限设置可能存在问题,但继续执行部署" + script.echo " 💡 将尝试使用bash命令执行脚本" + } + } + } + } catch (Exception e) { + // 权限设置失败处理(容错模式) + script.echo "⚠️ 设置文件权限时出错: ${e.message}" + script.echo " 💡 错误类型: ${e.getClass().getSimpleName()}" + script.echo " 🔄 继续执行部署,将使用bash命令执行脚本" + // 这里不抛出异常,因为即使权限设置失败,我们还可以尝试使用bash执行 + } + } + + /** + * 构建部署命令 + * 创建用于远程执行的部署命令字符串,使用bash显式执行脚本避免权限问题 + * + * @param deployPath 远程部署路径(如/usr/local/docker/xiaomayi-web/) + * @param deployScript 部署脚本文件名(如deploy_env.sh) + * @param projectName 项目名称(如xiaomayi-admin) + * @param imageTag 镜像标签(如prod-31) + * @param extraArgs 额外参数 + * + * @return String 完整的部署命令字符串 + * + * @note 使用bash显式执行脚本,即使脚本没有执行权限也能正常工作 + */ + private def buildDeploymentCommand(deployPath, deployScript, projectName, imageTag, extraArgs) { + script.echo " 🛠️ 构建部署命令..." + script.echo " 📁 路径: ${deployPath}" + script.echo " 📜 脚本: ${deployScript}" + script.echo " 🏷️ 项目: ${projectName}" + script.echo " 🎯 版本: ${imageTag}" + script.echo " ➕ 参数: ${extraArgs}" + + // 构建完整的部署命令 + // 使用bash显式执行脚本,避免"./script.sh"的权限问题 + def command = """ + export PROJECT_NAME=${projectName}; \ + export IMAGE_TAG=${imageTag}; \ + cd ${deployPath}; \ + ./${deployScript} deploy ${projectName} ${imageTag} ${extraArgs} + """.trim().replaceAll("\n", " ") // 去除换行符,转换为单行命令 + + script.echo " ✅ 命令构建完成" + return command + } + + /** + * 执行远程命令(带重试机制) + * 使用指数退避策略执行SSH命令,提高在网络不稳定情况下的成功率 + * + * @param serverIP 服务器IP地址 + * @param serverPort SSH端口 + * @param sshKey SSH凭证ID + * @param command 要执行的远程命令 + * @param timeout 单次执行超时时间(秒) + * @param retryAttempts 最大重试次数 + * + * @return Boolean 命令执行是否成功 + * @throws Exception 所有重试尝试都失败后抛出异常 + */ + private def executeRemoteCommandWithRetry(serverIP, serverPort, sshKey, command, timeout, retryAttempts) { + def attempt = 1 + def lastException = null + + script.echo " 🔄 启用重试机制,最大重试次数: ${retryAttempts}" + + // 重试循环 + while (attempt <= retryAttempts) { + try { + script.echo " 🎯 执行远程命令 (尝试 ${attempt}/${retryAttempts})..." + + // 使用Jenkins凭证安全地执行SSH命令 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: sshKey, + keyFileVariable: 'SSH_KEY_FILE', + usernameVariable: 'SSH_USERNAME' + )]) { + // 解析服务器地址 + def (username, hostname) = parseServerAddress(serverIP) + + // // 构建SSH命令 + // def sshCommand = """ + // ssh -o StrictHostKeyChecking=no \\ # 禁用严格主机密钥检查 + // -o ConnectTimeout=30 \\ # 连接超时30秒 + // -o ServerAliveInterval=60 \\ # 保持连接活跃间隔60秒 + // -p ${serverPort} \\ # SSH端口 + // -i ${script.SSH_KEY_FILE} \\ # SSH密钥文件 + // ${username}@${hostname} \\ # 目标服务器 + // '${command}' # 要执行的命令 + // """.trim() + + // 构建SSH命令 + def sshCommand = """ + ssh -o StrictHostKeyChecking=no \ + -o ConnectTimeout=30 \ + -o ServerAliveInterval=60 \ + -p ${serverPort} \ + -i ${script.SSH_KEY_FILE} \ + ${username}@${hostname} \ + '${command}' + """.trim() + + script.echo " ⚡ 执行SSH命令..." + + // 执行命令(带超时控制) + script.timeout(time: timeout, unit: 'SECONDS') { + def output = script.sh(script: sshCommand, returnStdout: true, returnStatus: false) + script.echo " 📋 命令输出:\n${output}" + } + } + + script.echo "✅ 远程命令执行成功" + return true + + } catch (Exception e) { + lastException = e + script.echo "❌ 第 ${attempt} 次尝试失败: ${e.message}" + + // 如果不是最后一次尝试,等待后重试 + if (attempt < retryAttempts) { + def waitTime = attempt * 10 // 指数退避:10, 20, 30秒... + script.echo "⏳ 等待 ${waitTime} 秒后重试..." + script.sleep(waitTime) // 等待一段时间 + } + attempt++ // 增加尝试次数 + } + } + + // 所有重试都失败,抛出异常 + throw new Exception("远程命令执行失败,重试 ${retryAttempts} 次后仍失败: ${lastException?.message}", lastException) + } + + /** + * 解析服务器地址 + * 支持 user@host 格式和单独的host格式,提供灵活的服务器地址解析 + * + * @param serverAddress 服务器地址字符串 + * @return List 包含[用户名, 主机名]的列表 + * + * @example + * parseServerAddress("user@example.com") → ["user", "example.com"] + * parseServerAddress("192.168.1.100") → ["root", "192.168.1.100"] (使用默认用户名) + */ + private def parseServerAddress(serverAddress) { + script.echo " 🔍 解析服务器地址: ${serverAddress}" + + if (serverAddress.contains('@')) { + // 处理 user@host 格式 + def parts = serverAddress.split('@') + def result = [parts[0], parts[1]] + script.echo " ✅ 解析结果: 用户名=${result[0]}, 主机名=${result[1]}" + return result + } else { + // 处理单独的host格式,使用SSH凭证中的用户名或默认root + def username = script.SSH_USERNAME ?: 'root' + def result = [username, serverAddress] + script.echo " ✅ 解析结果: 用户名=${result[0]}(默认), 主机名=${result[1]}" + return result + } + } + + /** + * 生成部署摘要报告 + */ + private def generateDeploymentSummary(deployParams, imageTag, success) { + def summary = """ + ==================== 部署摘要 ==================== + 项目名称: ${deployParams.projectName} + 目标环境: ${deployParams.profile} + 目标服务器: ${deployParams.serverIP}:${deployParams.serverPort} + 镜像标签: ${imageTag} + 构建ID: ${deployParams.buildId} + 部署状态: ${success ? '✅ 成功' : '❌ 失败'} + 时间: ${new Date().format('yyyy-MM-dd HH:mm:ss')} + ================================================ + """ + + script.echo summary + return summary + } + + /** + * 执行应用健康检查 + * 通过HTTP端点检查目标服务的健康状态,支持超时控制和重试机制 + * 用于部署后验证应用是否正常启动和运行 + * + * @param serverIP 服务器IP地址 + * - 类型: String + * - 格式: IP地址或主机名(不需要http://前缀) + * - 示例: "192.168.1.100", "example.com", "localhost" + * - 必填: 是 + * + * @param port 服务端口号 + * - 类型: Integer + * - 范围: 1-65535 + * - 示例: 8080, 80, 443 + * - 必填: 是 + * + * @param timeoutMinutes 超时时间(分钟) + * - 类型: Integer + * - 默认值: 5分钟(优先使用参数值,其次环境配置,最后默认5分钟) + * - 建议: 生产环境建议设置较长超时(10-15分钟),开发环境可设置较短(3-5分钟) + * - 可选: 是 + * + * @throws Exception 当健康检查超时或连续失败时抛出异常 + * + * @note 使用Spring Boot Actuator的健康检查端点(/actuator/health) + * @note 支持自动重试机制,直到服务健康或超时 + * @note 包含详细的日志输出和错误诊断信息 + * + * @example 使用示例: + * // 基本使用 + * healthCheck("192.168.1.100", 8080) + * + * // 自定义超时 + * healthCheck("192.168.1.100", 8080, 10) + * + * // 生产环境检查 + * healthCheck("prod-server.example.com", 8080, 15) + */ + def healthCheck(serverIP, port, timeoutMinutes = null) { + // ========================================================================== + // 参数处理和配置初始化 + // ========================================================================== + + // 确定超时时间(优先级: 参数 > 环境配置 > 默认值) + def timeout = timeoutMinutes ?: envConfig?.healthCheckTimeout ?: 5 + // 健康检查端点路径(Spring Boot Actuator标准端点) + def healthEndpoint = "/actuator/health" + // 构建完整的健康检查URL + def healthCheckUrl = "http://${serverIP}:${port}${healthEndpoint}" + + script.echo "❤️ 开始健康检查..." + script.echo "📋 检查配置:" + script.echo " 📍 目标服务: ${serverIP}:${port}" + script.echo " 🌐 检查地址: ${healthCheckUrl}" + script.echo " ⏱️ 超时时间: ${timeout} 分钟" + script.echo " 🔄 检查机制: 自动重试直到健康或超时" + + // ========================================================================== + // 参数验证 + // ========================================================================== + + // 验证服务器IP地址 + if (!serverIP) { + throw new IllegalArgumentException("服务器IP地址不能为空") + } + + // 验证端口范围 + if (port < 1 || port > 65535) { + throw new IllegalArgumentException("端口号必须在1-65535范围内,当前: ${port}") + } + + // 验证超时时间 + if (timeout < 1) { + script.echo "⚠️ 超时时间过短(${timeout}分钟),使用最小值1分钟" + timeout = 1 + } + + // ========================================================================== + // 健康检查执行 + // ========================================================================== + + try { + // 设置整体超时(防止健康检查无限期等待) + script.timeout(time: timeout, unit: 'MINUTES') { + // 使用waitUntil实现自动重试机制 + script.waitUntil { + try { + script.echo "🔍 执行健康检查请求..." + + // 执行HTTP健康检查(使用curl命令) + // -s: 静默模式,不显示进度信息 + // -o /dev/null: 将响应体输出到空设备 + // -w '%{http_code}': 只输出HTTP状态码 + // --connect-timeout 5: 连接超时5秒 + // --max-time 10: 最大请求时间10秒 + def response = script.sh( + script: "curl -s -o /dev/null -w '%{http_code}' '${healthCheckUrl}' --connect-timeout 5 --max-time 10", + returnStdout: true + ).trim() + + script.echo "📊 健康检查响应: HTTP ${response}" + + // 记录详细的响应信息(用于调试) + if (response != "200") { + // 非200响应时获取更多信息 + try { + def detailResponse = script.sh( + script: "curl -s '${healthCheckUrl}' --connect-timeout 5 --max-time 10", + returnStdout: true + ).trim() + script.echo "📝 详细响应: ${detailResponse.take(200)}..." // 限制输出长度 + } catch (Exception detailError) { + script.echo "⚠️ 获取详细响应失败: ${detailError.message}" + } + } + + // 返回检查结果(200表示健康) + return response == "200" + + } catch (Exception e) { + // 记录检查失败信息 + script.echo "❌ 健康检查请求失败: ${e.message}" + script.echo "💡 可能的原因:" + script.echo " - 应用尚未完全启动" + script.echo " - 网络连接问题" + script.echo " - 防火墙或安全组限制" + script.echo " - 服务配置错误" + + return false // 继续重试 + } + } + } + + // 健康检查成功 + script.echo "✅ 健康检查成功!服务状态: HEALTHY" + script.echo "🎉 应用已正常启动并运行在 ${serverIP}:${port}" + script.echo "💡 建议: 继续执行后续部署验证步骤" + + } catch (Exception e) { + // ========================================================================== + // 健康检查失败处理 + // ========================================================================== + + script.echo "❌ 健康检查失败!服务状态: UNHEALTHY" + script.echo "📋 失败详情:" + script.echo " 🎯 目标服务: ${serverIP}:${port}" + script.echo " 🌐 检查地址: ${healthCheckUrl}" + script.echo " ⏱️ 超时时间: ${timeout} 分钟" + script.echo " 💥 错误信息: ${e.message}" + + script.echo "🔧 故障排除建议:" + script.echo " 1. 📡 检查网络连通性: ping ${serverIP}" + script.echo " 2. 🚪 检查端口访问: telnet ${serverIP} ${port} || nc -zv ${serverIP} ${port}" + script.echo " 3. 📋 检查应用日志: docker logs 或 journalctl -u " + script.echo " 4. 🔍 手动验证: curl -v ${healthCheckUrl}" + script.echo " 5. ⚙️ 检查应用配置: 确认actuator端点已启用" + script.echo " 6. 🐛 查看启动错误: 检查应用启动时的异常信息" + script.echo " 7. 📊 检查资源使用: 内存、CPU、磁盘空间是否充足" + + // 重新抛出异常,让调用方能够处理健康检查失败 + throw new Exception("健康检查失败: 服务未在${timeout}分钟内达到健康状态", e) + } + } + + /** + * 执行回滚部署操作 + * 负责将指定项目回退到上一个稳定版本,支持远程服务器操作和完整的错误处理 + * + * @param rollbackParams 回滚参数Map,包含以下字段: + * - timeout: Integer - 操作超时时间(秒) + * - projectName: String - 项目名称(必须) + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - retryAttempts: Integer - 重试次数 + * + * @throws Exception 当回滚操作失败时抛出异常,包含详细的错误信息和诊断建议 + * + * @note 该方法依赖于远程服务器上已部署的回滚脚本和正确的SSH配置 + * @note 支持重试机制,提高在网络不稳定情况下的成功率 + */ + def rollbackDeployment(rollbackParams) { + // ========================================================================== + // 参数提取和解构 + // ========================================================================== + + // 从服务器配置中提取IP地址(支持user@host格式) + def serverIP = rollbackParams.serverIP + // 从服务器配置中提取SSH端口号(默认22) + def serverPort = rollbackParams.serverPort + // 从服务器配置中提取SSH密钥凭证ID(Jenkins中配置的凭证名称) + def sshKey = rollbackParams.sshKey + // 从回滚参数中提取操作超时时间(秒) + def timeout = rollbackParams.timeout + // 从回滚参数中提取项目名称(用于标识要回滚的具体应用) + def projectName = rollbackParams.projectName + // 从回滚参数中提取部署脚本文件名(默认:deploy.sh) + def deployScript = rollbackParams.deployScript + // 从回滚参数中提取部署脚本所在路径 + def deployPath = rollbackParams.deployPath + // 从回滚参数中提取重试次数(默认:3次) + def retryAttempts = rollbackParams.retryAttempts ?: 3 + + // ========================================================================== + // 回滚信息输出和验证 + // ========================================================================== + + // 输出回滚操作开始信息,便于调试和审计追踪 + script.echo "🔄 执行回滚操作:" + script.echo " 📍 目标服务器: ${serverIP}:${serverPort}" + script.echo " 📦 项目名称: ${projectName}" + script.echo " 📁 脚本路径: ${deployPath}" + script.echo " 🔑 SSH凭证: ${sshKey}" + script.echo " ⏱️ 超时时间: ${timeout}秒" + script.echo " 🔄 重试次数: ${retryAttempts}次" + + // 验证必要参数是否存在 + if (!serverIP) { + throw new IllegalArgumentException("缺少服务器IP地址参数") + } + if (!projectName) { + throw new IllegalArgumentException("缺少项目名称参数") + } + + try { + // ========================================================================== + // 回滚命令构建 + // ========================================================================== + + // 构建回滚命令字符串 + // 格式: cd {部署脚本路径}; ./{部署脚本} rollback {项目名称} + def command = "cd ${deployPath}; ./${deployScript} rollback ${projectName}" + script.echo " 🖥️ 回滚命令: ${command}" + + // ========================================================================== + // 远程命令执行 + // ========================================================================== + + // 执行远程回滚命令,支持重试机制 + // executeRemoteCommandWithRetry 方法处理SSH连接、命令执行和重试逻辑 + def success = executeRemoteCommandWithRetry( + serverIP, // 目标服务器地址 + serverPort, // SSH端口号 + sshKey, // SSH密钥凭证 + command, // 要执行的命令 + timeout, // 超时时间 + retryAttempts // 重试次数 + ) + + // ========================================================================== + // 成功处理 + // ========================================================================== + + // 回滚成功日志输出 + script.echo "✅ 回滚成功完成: ${projectName} 已回退到上一个稳定版本" + script.echo "💡 建议: 检查应用日志确认回滚状态" + script.echo " 📋 验证步骤:" + script.echo " 1. 检查应用进程状态" + script.echo " 2. 查看应用启动日志" + script.echo " 3. 验证服务健康状态" + script.echo " 4. 确认业务功能正常" + + return success + + } catch (Exception e) { + // ========================================================================== + // 失败处理 + // ========================================================================== + + // 回滚失败错误信息输出 + script.echo "❌ 回滚失败: ${projectName} - ${e.message}" + script.echo "💡 故障排除建议:" + script.echo " 1. 🔍 检查服务器SSH连接是否正常" + script.echo " 2. 📁 确认部署脚本是否存在且可执行" + script.echo " 3. 🏷️ 验证项目名称是否正确" + script.echo " 4. 💾 检查服务器磁盘空间和权限" + script.echo " 5. 🔑 验证SSH密钥凭证是否有效" + script.echo " 6. 🌐 检查网络连接和防火墙设置" + script.echo " 7. ⏱️ 确认超时时间设置是否合理" + + // 重新抛出异常,让调用方能够处理回滚失败 + // 保留原始异常信息以便于调试和根因分析 + throw new Exception("回滚操作失败: ${e.message}", e) + } + } + +} \ No newline at end of file diff --git a/src/com/node/DeploymentService.groovy b/src/com/node/DeploymentService.groovy new file mode 100644 index 0000000..8855871 --- /dev/null +++ b/src/com/node/DeploymentService.groovy @@ -0,0 +1,149 @@ +package com.node + +import com.node.stages.BuildService +import com.node.stages.DeployService + +/** + * 部署服务类 - CI/CD流水线核心协调器 + * 负责执行各个CI/CD阶段的业务逻辑,不直接创建Jenkins stage + * 提供模块化的构建、部署、验证功能,作为流水线步骤的抽象层 + * + * 设计原则: + * - 单一职责:每个方法只负责一个特定的功能 + * - 错误隔离:每个阶段的异常不会影响其他阶段 + * - 可重用性:方法可以在不同的流水线中重用 + * - 可扩展性:易于添加新的部署阶段或功能 + * - 可维护性:清晰的代码结构和职责分离 + * + * 主要职责: + * 1. 协调各个部署阶段服务的执行 + * 2. 提供统一的参数传递接口 + * 3. 处理阶段间的依赖关系 + * 4. 提供错误处理和回滚机制 + * + * @see Serializable 实现序列化接口,支持Jenkins流水线的暂停和恢复 + */ +class DeploymentService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储环境特定的配置参数,如服务器列表、环境变量等 + * 类型: Map 键值对配置映射 + */ + def envConfig + + // ========================================================================== + // 各个阶段的服务实例(依赖注入) + // ========================================================================== + + /** + * 构建阶段服务实例 + * 负责Maven项目的编译和打包操作 + * 类型: BuildService 专门处理构建逻辑的服务类 + */ + def buildService + + /** + * 部署阶段服务实例 + * 负责将应用部署到目标服务器 + * 类型: DeployService 专门处理部署逻辑的服务类 + */ + def deployService + + /** + * 构造函数 - 初始化部署服务 + * 创建各个阶段的服务实例,建立服务依赖关系 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在各个服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:服务器列表、环境变量、部署策略等配置 + * - 示例:[ + * servers: [[serverIP: "192.168.1.1", serverPort: 22]], + * envVars: [JAVA_OPTS: "-Xms1g -Xmx1g"], + * deploymentStrategy: "rolling" + * ] + */ + DeploymentService(script, envConfig = [:]) { + // 初始化基础属性 + this.script = script + this.envConfig = envConfig + + // 初始化各个阶段服务(依赖注入模式) + // 通过分离关注点,提高代码的可测试性和可维护性 + this.buildService = new BuildService(script, envConfig) + this.deployService = new DeployService(script, envConfig) + + script.echo "🔧 DeploymentService 初始化完成" + } + + /** + * 执行构建阶段逻辑 + * 委托给BuildService处理Maven项目的编译和打包 + * 这是CI/CD流水线的第一个阶段,负责源代码到产物的转换 + * + * @param params 管道参数Map,包含构建所需的所有配置信息,必须包含: + * - MAVEN_SET: String - Maven设置文件ID + * - PROFILES: String - Maven构建环境profiles + * - jarFilePath: String - JAR文件输出路径 + * - jarFile: String - 生成的JAR文件名 + * - skipTests: Boolean - 是否跳过测试(默认true) + * - goals: String - Maven执行目标(默认"clean package") + * - pomFilePath: String - POM文件路径(默认pom.xml) + * + * @throws Exception 当Maven构建失败时抛出异常,中断流水线 + * + * @example 使用示例: + * def params = [ + * MAVEN_SET: "nexus-maven-dev", + * PROFILES: "dev", + * jarFilePath: "target", + * jarFile: "app.jar" + * ] + * deploymentService.executeBuildStage(params) + */ + def executeBuildStage(params) { + // ☕ 开始构建阶段... + buildService.executeBuildStage(params) + } + + /** + * 执行部署阶段逻辑 + * 委托给DeployService处理应用部署到目标服务器 + * 这是CI/CD流水线的第三个阶段,负责将容器部署到环境 + * + * @param params 管道参数Map,包含部署所需的所有配置信息,必须包含: + * - servers: List - 服务器列表数组 + * - deployServer: String - 部署服务器引用 + * - projectName: String - 项目名称 + * - PROFILES: String - 环境名称 + * 可选字段: + * - deployScript: String - 部署脚本文件名 + * - deployPath: String - 部署脚本路径 + * - envVars: Map - 环境变量配置 + * - deploymentStrategy: String - 部署策略 + * + * @throws Exception 当部署失败时抛出异常,支持自动回滚 + * + * @note 支持多种部署模式: + * - 单服务器部署:deployServer = "servers[0]" + * - 多服务器部署:deployServer = "servers[0,1,2]" + * - 自动选择:deployServer = "auto" + */ + def executeDeployStage(params) { + // 🚀 开始部署阶段... + deployService.executeDeployStage(params) + } + +} \ No newline at end of file diff --git a/src/com/node/ResolverService.groovy b/src/com/node/ResolverService.groovy new file mode 100644 index 0000000..45867d4 --- /dev/null +++ b/src/com/node/ResolverService.groovy @@ -0,0 +1,865 @@ +package com.node + +import com.utils.CommonUtils +import com.constants.ConfigConstants +import com.utils.ServerParser + +/** + * JDK API 服务类 + * 负责加载配置、管理分支和服务配置、设置环境参数 + * 适配新的配置文件格式,支持多环境、多服务配置 + * + * 主要功能: + * 1. 加载环境配置和主配置文件 + * 2. 解析分支和服务配置 + * 3. 设置Jenkins流水线参数 + * 4. 提供环境相关的配置获取方法 + * + * @version 2.0.0 + * @since 2025-09-09 + */ +class ResolverService implements Serializable { + + // Jenkins流水线脚本对象,用于执行Jenkins相关操作 + def script + + // 主配置文件内容(解析后的YAML数据) + def config + + // 组织名称 + def orgName + + // 环境配置文件内容 + def envConfig + + // 主配置文件路径 + def jobConfigFile + + /** + * 服务器解析器实例 + * 负责解析服务器引用配置和索引处理 + * 类型: ServerParser 自定义服务器解析工具类 + */ + def serverParser + + /** + * 构造函数 + * 初始化服务类实例 + * + * @param script Jenkins流水线脚本对象,提供echo、error等方法 + * @param orgName 组织名称,用于标识项目归属 + */ + ResolverService(script, orgName) { + this.script = script + this.orgName = orgName + this.config = [:] + this.envConfig = [:] + // 初始化服务器解析器,用于处理服务器引用和索引 + this.serverParser = new ServerParser(script) + } + + /** + * 加载所有配置文件 + * 按顺序加载环境配置和主配置文件 + * + * @throws Exception 如果配置文件加载失败 + */ + void loadConfigurations() { + script.echo "🚀 开始加载配置文件..." + + // 首先加载环境配置文件(.env.yml) + loadEnvConfig() + + // 然后加载主配置文件(根据环境配置中的路径) + loadMainConfig() + + script.echo "✅ 所有配置文件加载完成" + } + + /** + * 加载环境配置文件的方法 + * 从.jenkins/.env.yml读取环境配置,包含主配置文件路径等基本信息 + * + * @return 返回当前对象实例,支持链式调用 + * @throws Exception 如果文件存在但解析失败 + */ + def loadEnvConfig() { + try { + // 环境配置文件相对路径 + def envFilePath = '.jenkins/.env.yml' + script.echo "📁 环境配置文件路径: ${envFilePath}" + + if (script.fileExists(envFilePath)) { + // 读取并解析YAML格式的环境配置文件 + envConfig = script.readYaml(file: envFilePath) + + // 从环境配置中获取主配置文件路径 + jobConfigFile = ".pipeline-files/config-file/" + envConfig.jobConfigFile + script.echo "✅ 成功加载环境配置文件" + + // 打印所有环境配置项(调试用) + if (!envConfig.isEmpty()) { + script.echo "📋 环境配置内容:" + envConfig.each { key, value -> + script.echo " ${key}: ${value}" + } + } + } else { + script.echo "⚠️ 警告: 未找到环境配置文件 .env.yml,使用默认配置" + } + } catch (Exception e) { + script.echo "⚠️ 警告: .env.yml 文件无效,使用默认配置: ${e.message}" + // 不抛出异常,允许继续执行 + } + + return this + } + + /** + * 加载主配置文件的方法 + * 读取新的配置文件格式,包含common、branchConfig、serviceConfig等部分 + * + * @return 返回当前对象实例,支持链式调用 + * @throws Exception 如果文件不存在或解析失败 + */ + def loadMainConfig() { + try { + // 检查主配置文件是否存在 + if (!script.fileExists(jobConfigFile)) { + script.error("❌ 错误: 配置文件不存在: ${jobConfigFile},请检查文件路径或创建配置文件") + } + + // 读取并解析YAML格式的主配置文件 + this.config = script.readYaml(file: jobConfigFile) + script.echo "✅ 成功加载主配置文件: ${jobConfigFile}" + + // 打印配置详情(调试用) + printConfigSummary() + + } catch (Exception e) { + script.error("❌ 错误: 加载配置文件失败: ${jobConfigFile}. 错误详情: ${e.message}") + } + + return this + } + + /** + * 打印主配置文件的详细内容 + * 格式化输出所有配置信息,便于调试和验证 + */ + def printConfigSummary() { + script.echo "=" * 70 + script.echo "📊 主配置文件详细内容" + script.echo "=" * 70 + + if (!this.config) { + script.echo "⚠️ 配置文件内容为空" + return + } + + // 打印通用配置信息 + if (this.config.common) { + script.echo "🌐 通用配置:" + script.echo "-" * 50 + script.echo " 部署脚本: ${this.config.common.deployScript ?: '未设置'}" + script.echo " 部署路径: ${this.config.common.deployPath ?: '未设置'}" + + // 打印注册表配置 + if (this.config.common.registry) { + script.echo " 注册表配置:" + this.config.common.registry.each { registryName, registryConfig -> + script.echo " ${registryName}: ${registryConfig.registry}" + } + } + script.echo "" + } + + // 打印环境配置信息 + if (this.config.common?.profiles) { + script.echo "🏭 环境配置 (共 ${this.config.common.profiles.size()} 个环境):" + script.echo "-" * 50 + + this.config.common.profiles.each { envName, envConfig -> + script.echo " ${envName.toUpperCase()} 环境:" + script.echo " 服务器数量: ${envConfig.servers ? envConfig.servers.size() : 0}" + script.echo " 部署策略: ${envConfig.deploymentStrategy ?: 'rolling'}" + script.echo " JVM参数: ${envConfig.envVars?.JAVA_OPTS ?: '未设置'}" + script.echo " 健康检查: ${envConfig.healthCheckPath ?: '/actuator/health'}" + script.echo "" + } + } + + // 打印分支构建配置信息 + if (this.config.branchConfig) { + script.echo "🌿 分支构建配置 (共 ${this.config.branchConfig.size()} 个分支):" + script.echo "-" * 50 + + this.config.branchConfig.each { branchName, branchConfig -> + script.echo " ${branchName} 分支:" + script.echo " Jenkins代理: ${branchConfig.jenkinsAgent ?: '未设置'}" + script.echo " Maven配置集: ${branchConfig.maven?.maven ?: '未设置'}" + } + } + + // 打印服务配置信息 + if (this.config.serviceConfig) { + script.echo "🔧 服务配置 (共 ${this.config.serviceConfig.size()} 个服务):" + script.echo "-" * 50 + + this.config.serviceConfig.each { serviceName, serviceConfig -> + script.echo " ${serviceName} 服务:" + script.echo " 编程语言: ${serviceConfig.programLang ?: '未设置'}" + script.echo " 语言版本: ${serviceConfig.programLangVersion ?: '未设置'}" + script.echo " 容器名称: ${serviceConfig.containerName ?: '未设置'}" + script.echo " POM路径: ${serviceConfig.pomFilePath ?: '未设置'}" + script.echo " 服务路径: ${serviceConfig.servicePath ?: '未设置'}" + script.echo " 容器名称: ${serviceConfig.containerName ?: '未设置'}" + script.echo " JAR包名称: ${serviceConfig.jarName ?: '未设置'}" + script.echo " 服务端口: ${serviceConfig.port ?: '未设置'}" + } + } + + script.echo "=" * 70 + } + + /** + * 获取当前分支的配置 + * 根据当前Jenkins作业的分支名称查找对应的分支配置 + * + * @return 分支配置对象(Map类型) + * @throws Exception 如果分支配置不存在 + */ + def getBranchConfig() { + // 获取当前Jenkins作业的分支名称 + def branchName = script.env.BRANCH_NAME + + if (!branchName) { + script.error("❌ 错误: 无法获取分支名称,env.BRANCH_NAME 为空。请检查流水线配置") + } + + script.echo "🌿 正在查找分支配置: ${branchName}" + + // 检查分支配置是否存在 + if (!config.branchConfig || config.branchConfig.isEmpty()) { + script.error("❌ 错误: 分支配置列表为空或未定义。请检查配置文件结构") + } + + // 根据分支名称获取配置 + def branchConfig = config.branchConfig[branchName] + + if (branchConfig) { + script.echo "✅ 成功匹配分支配置:" + script.echo " - Jenkins代理: ${branchConfig.jenkinsAgent ?: '未设置'}" + script.echo " - Maven配置集: ${branchConfig.mavenSet ?: '未设置'}" + script.echo " - 构建参数: ${branchConfig.buildArgs ?: '默认参数'}" + } else { + // 构建友好的错误信息,列出所有可用分支 + def availableBranches = config.branchConfig.keySet().join(', ') + script.error("❌ 错误: 找不到分支 '${branchName}' 的配置。可用分支: ${availableBranches}") + } + + return branchConfig + } + + /** + * 获取指定服务的配置信息 + * 根据服务名称从serviceConfig中查找对应的服务配置 + * + * @param serviceName 服务名称,需要与配置文件中serviceConfig的键匹配 + * @return 服务配置对象(Map类型) + * @throws Exception 如果服务配置不存在 + */ + def getServiceConfig(serviceName) { + if (!serviceName) { + script.error("❌ 错误: 服务名称参数不能为空") + } + + script.echo "🔧 正在查找服务配置: ${serviceName}" + + // 检查服务配置是否存在 + if (!config.serviceConfig || config.serviceConfig.isEmpty()) { + script.error("❌ 错误: 服务配置列表为空或未定义,请检查配置文件结构") + } + + // 根据服务名称获取配置 + def serviceConfig = config.serviceConfig[serviceName] + + if (serviceConfig) { + script.echo "✅ 成功找到服务配置: ${serviceName}" + script.echo " - 容器名称: ${serviceConfig.containerName ?: '未设置'}" + script.echo " - 服务路径: ${serviceConfig.servicePath ?: '未设置'}" + script.echo " - 服务端口: ${serviceConfig.port ?: '未设置'}" + script.echo " - JAR包名称: ${serviceConfig.jarName ?: '未设置'}" + } else { + // 构建友好的错误信息,列出所有可用服务 + def availableServices = config.serviceConfig.keySet().join(', ') + script.error("❌ 错误: 找不到服务 '${serviceName}' 的配置。可用服务: ${availableServices}") + } + + return serviceConfig + } + + /** + * 设置管道参数 + * 整合分支配置、服务配置和环境配置,生成完整的流水线参数 + * + * @return 参数映射表,包含所有必要的部署参数 + */ + def setupParameters() { + script.echo "⚙️ 开始设置管道参数..." + + // 获取当前分支配置 + def branchConfig = getBranchConfig() + + // 从Jenkins作业名称中提取服务名称 + // 格式通常为: "组织名/服务名" 或 "服务名" + def serviceName = script.env.JOB_NAME?.tokenize('/')?.get(0) + script.echo "📦 服务名称: ${serviceName}" + + // 获取服务配置 + def serviceConfig = getServiceConfig(serviceName) + + // 根据分支类型获取环境配置 + def profiles = getBranchProfiles(branchConfig) + script.echo "🌍 环境配置: ${profiles}" + + // 生成唯一的镜像标签,格式: {环境}-{构建ID} + def imageTag = CommonUtils.generateImageTag(script.env.PROFILES, script.env.BUILD_ID) + script.echo "自定义镜像标签:${imageTag}" + + // 构建完整的参数映射表 + def params = [ + // 构建相关参数 + jenkinsAgent : branchConfig.jenkinsAgent, + MAVEN_SET : branchConfig.maven?.mavenSet, + skipTests : branchConfig.maven?.skipTests, + goals : branchConfig.maven?.goals, + dockerFile : branchConfig.docker?.dockerFile ?: '.jenkins/Dockerfile', + buildOptions : branchConfig.docker?.buildOptions, + + // 服务相关参数 + projectName : serviceConfig.containerName, + packageName : serviceConfig?.packageName, + servicePath : serviceConfig.servicePath, + jarName : serviceConfig.jarName, + jarFile : serviceConfig.jarName, + jarFilePath : "${serviceConfig.servicePath}/target", + port : serviceConfig.port, + pomFilePath : serviceConfig.pomFilePath, + deployServer : serviceConfig.deployServer, + + // 环境相关参数 + PROFILES : profiles.default_profile, + availableProfiles : profiles.available_profiles, + imageTags : [imageTag], + + + // Git配置 + gitUrl : config.common?.git?.url, + gitCredentialsId : config.common?.git?.credentialsId, + gitBranch : script.env.BRANCH_NAME, + + // 钉钉配置 + dingTalkCredentialsId: config.common?.dingtalk?.credentialsId, + + // 其他参数 + envConfig : this.envConfig, + orgName : this.orgName, + branchConfig : branchConfig, + serviceConfig : serviceConfig + ] + + // 获取分支环境变量配置 + getEnvironmentConfig(params) + + // 设置容器基础镜像 + params.baseImage = "${params.registry}/${params.registryProject}-base/${params.registryImage}" + + script.echo "基础参数:${params}" + + // 设置Jenkins参数(仅对标准流水线) + setupJenkinsParameters(profiles.available_profiles as List) + + script.echo "✅ 管道参数设置完成" + + // 返回参数 + return params + } + + /** + * 获取指定环境的全部配置参数 + * 从通用配置中提取指定环境的所有参数,包括服务器列表、环境变量、部署策略等 + * + * @param params 参数映射表(会被修改),用于存储提取的配置信息 + * @return 包含指定环境全部参数的Map + */ + def getEnvironmentConfig(params) { + try { + // 从参数中获取环境名称,如果没有设置则使用默认值"dev" + // 使用Elvis操作符 ?: 提供默认值,避免空指针异常 + def envName = params.PROFILES ?: "dev" + + // 将环境名称转换为小写,确保与配置中的key匹配(配置通常使用小写) + // 例如:将"DEV"转换为"dev","Prod"转换为"prod" + def envKey = envName.toLowerCase() + + script.echo "🔍 开始获取 ${envKey} 环境的全部配置参数..." + + // ========================================================================== + // 添加通知配置 + // ========================================================================== + if (config.common?.notification) { + params.notification = config.common.notification + script.echo "📧 通知配置已加载" + } + + // ========================================================================== + // 添加短信配置 - 详细配置信息 + // ========================================================================== + if (config.common?.sms) { + // 确保sms配置存在 + params.sms = config.common.sms + + script.echo "📱 短信服务配置详情:" + script.echo " - 服务商: ${params.sms.provider ?: '未设置'}" + script.echo " - 凭据ID: ${params.sms.credentialsId ?: '未设置'}" + script.echo " - 默认签名: ${params.sms.signName ?: '未设置'}" + + // 打印所有模板配置 + if (params.sms.templates) { + script.echo " - 可用模板:" + params.sms.templates.each { templateName, templateConfig -> + def templateCode = templateConfig.templateCode ?: '未设置' + def templateSign = templateConfig.signName ?: params.sms.signName ?: '未设置' + script.echo " 📋 ${templateName}: ${templateCode} (签名: ${templateSign})" + } + } else { + script.echo " - 模板: 未配置" + } + } else { + script.echo "📱 短信服务: 未配置" + } + + // ========================================================================== + // 添加构建策略配置 + // ========================================================================== + if (config.common?.buildStrategy) { + params.buildStrategy = config.common.buildStrategy + script.echo "⚡ 构建策略:" + script.echo " - 重试次数: ${params.buildStrategy.retryCount}" + script.echo " - 超时时间: ${params.buildStrategy.timeoutMinutes}分钟" + script.echo " - 启用缓存: ${params.buildStrategy.enableCaching}" + } + + // 安全地获取环境配置 - 使用Map方式访问 + // 使用安全导航操作符 ?. 避免空指针异常: + // - config.common?:如果config为null则返回null,不会抛出异常 + // - .profiles?:如果common为null则返回null + // - ."${envKey}":动态访问指定环境的配置 + def envConfig = config.common?.profiles?."${envKey}" + if (!envConfig) { + def availableProfiles = config?.common?.profiles?.keySet() ?: [] + throw new Exception("未找到 ${envKey} 环境的配置,可用环境: ${availableProfiles.join(', ')}") + } + + script.echo "✅ 找到 ${envKey} 环境配置,开始提取参数..." + + // ========================================================================== + // 提取服务器配置 + // ========================================================================== + + // 检查环境配置是否存在且包含服务器列表,并且服务器列表不为空 + // envConfig?.servers:安全访问servers属性 + // !envConfig.servers.empty:检查servers列表是否非空 + if (envConfig?.servers && !envConfig.servers.empty) { + // 使用环境配置中的服务器列表更新参数对象 + // 将找到的服务器列表赋值给params.servers,供后续部署使用 + params.servers = envConfig.servers + + // 输出日志信息,显示成功找到服务器配置 + // 使用表情符号增强可读性,显示服务器数量 + script.echo "🖥️ 服务器配置: 共 ${envConfig.servers.size()} 台服务器" + + // 打印服务器详情(用于调试和验证) + // 使用eachWithIndex迭代服务器列表,同时获取索引和服务器对象 + envConfig.servers.eachWithIndex { server, index -> + // 输出每台服务器的详细信息 + // ${index + 1}:显示序号(从1开始) + // ${server.serverName}:服务器名称标识 + // ${server.serverIP}:${server.serverPort}:服务器的IP地址和端口号 + script.echo " ${index + 1}. ${server.serverName} (${server.serverIP}:${server.serverPort})" + } + } else { + // 如果没有找到对应环境的服务器配置或服务器列表为空 + // 输出警告信息,提示用户检查配置 + script.echo "⚠️ 警告: 未找到 ${envName} 环境的服务器配置" + params.servers = [] + + // 注意:这里没有设置params.servers,调用方需要处理这种情况 + // 可能的处理方式:使用默认服务器、终止部署流程或提示用户输入 + } + + // ========================================================================== + // 提取目标服务器配置 + // ========================================================================== + + // 解析部署服务器引用,获取目标服务器列表 + // 使用ServerParser处理复杂的服务器引用表达式 + def targetServers = serverParser.parseTargetServers(params) + params.targetServers = targetServers + script.echo "🎯 部署目标服务器数量: ${targetServers.size()}" + + // ========================================================================== + // 提取环境变量配置 + // ========================================================================== + if (envConfig?.envVars && !envConfig.envVars.empty) { + params.envVars = envConfig.envVars + script.echo "🌡️ 环境变量: 共 ${envConfig.envVars.size()} 个" + envConfig.envVars.each { key, value -> + script.echo " ${key}=${value}" + } + } else { + script.echo "⚠️ 警告: ${envKey} 环境没有配置环境变量" + params.envVars = [:] + } + + // ========================================================================== + // 提取部署策略配置 + // ========================================================================== + if (envConfig?.deploymentStrategy) { + params.deploymentStrategy = envConfig.deploymentStrategy + script.echo "🎯 部署策略: ${envConfig.deploymentStrategy}" + } + + if (envConfig?.deploymentTimeout) { + params.deploymentTimeout = envConfig.deploymentTimeout + script.echo "⏱️ 部署超时: ${envConfig.deploymentTimeout} 秒" + } + + if (envConfig?.maxParallel) { + params.maxParallel = envConfig.maxParallel + script.echo "🔀 最大并行数: ${envConfig.maxParallel}" + } + + if (envConfig?.retryAttempts) { + params.retryAttempts = envConfig.retryAttempts + script.echo "🔄 重试次数: ${envConfig.retryAttempts}" + } + + if (envConfig?.canaryPercentage) { + params.canaryPercentage = envConfig.canaryPercentage + script.echo "📊 金丝雀比例: ${envConfig.canaryPercentage}%" + } + + // ========================================================================== + // 提取健康检查配置 + // ========================================================================== + if (envConfig?.healthCheckEnabled) { + params.healthCheckEnabled = envConfig.healthCheckEnabled + script.echo "❤️ 启用健康检查: ${envConfig.healthCheckEnabled}" + } + + if (envConfig?.healthCheckPath) { + params.healthCheckPath = envConfig.healthCheckPath + script.echo "❤️ 健康检查路径: ${envConfig.healthCheckPath}" + } + + if (envConfig?.healthCheckTimeout) { + params.healthCheckTimeout = envConfig.healthCheckTimeout + script.echo "⏱️ 健康检查超时: ${envConfig.healthCheckTimeout} 秒" + } + + if (envConfig?.healthCheckRetries) { + params.healthCheckRetries = envConfig.healthCheckRetries + script.echo "❤️ 健康检查重试次数: ${envConfig.healthCheckRetries}" + } + + if (envConfig?.healthCheckInterval) { + params.healthCheckInterval = envConfig.healthCheckInterval + script.echo "🔄 健康检查间隔: ${envConfig.healthCheckInterval} 秒" + } + + if (envConfig?.successThreshold) { + params.successThreshold = envConfig.successThreshold + script.echo "✅ 成功阈值: ${envConfig.successThreshold} 次" + } + + // ========================================================================== + // 提取回滚配置(生产环境特有) + // ========================================================================== + if (envConfig?.rollbackEnabled != null) { + params.rollbackEnabled = envConfig.rollbackEnabled + script.echo "↩️ 自动回滚: ${envConfig.rollbackEnabled}" + } + + if (envConfig?.rollbackOnFailure != null) { + params.rollbackOnFailure = envConfig.rollbackOnFailure + script.echo "❌ 失败时回滚: ${envConfig.rollbackOnFailure}" + } + + if (envConfig?.rollbackTimeout) { + params.rollbackTimeout = envConfig.rollbackTimeout + script.echo "⏱️ 回滚超时: ${envConfig.rollbackTimeout} 秒" + } + + // // ========================================================================== + // // 提取通知配置(生产环境特有) + // // ========================================================================== + // if (envConfig?.notifications) { + // params.notifications = envConfig.notifications + // script.echo "📧 通知配置:" + // if (envConfig.notifications.slackChannel) { + // script.echo " Slack频道: ${envConfig.notifications.slackChannel}" + // } + // if (envConfig.notifications.email) { + // script.echo " 邮件地址: ${envConfig.notifications.email}" + // } + // if (envConfig.notifications.recipients) { + // script.echo " 额外收件人: ${envConfig.notifications.recipients.join(', ')}" + // } + // script.echo " 通知时机: 开始=${envConfig.notifications.onStart}, 成功=${envConfig.notifications.onSuccess}, 失败=${envConfig.notifications.onFailure}" + // } + + // ========================================================================== + // 添加通用配置 + // ========================================================================== + params.deployScript = config.common?.deployScript + params.deployPath = config.common?.deployPath + + script.echo "📜 部署脚本: ${params.deployScript}" + script.echo "📁 部署路径: ${params.deployPath}" + + // ========================================================================== + // 添加镜像仓库配置 + // ========================================================================== + if (config.common?.registrys) { + params.registrys = config.common?.registrys + script.echo "🐳 镜像仓库配置:" + params.registrys.each { registryName, registryConfig -> + script.echo " ${registryName}: ${registryConfig.registry}" + } + } + + // ========================================================================== + // 添加环境分支镜像仓库配置 + // ========================================================================== + def registryConfig = config.common?.registrys?."${envConfig.registry}" + params.registry = registryConfig?.registry ?: "192.168.10.102:8001" + params.registryCredential = registryConfig?.registryCredential ?: "xiaomayi-hub-credentials" + params.registryProject = registryConfig?.registryProject ?: "xiaomayi" + params.registryImage = registryConfig?.registryImage ?: "jdk:17.0.16-centos8-dos" + params.registryType = registryConfig.registryType ?: "harbor" + params.imageFullName = "${params.registry}/${params.registryProject}/${params.projectName}" + + script.echo "✅ ${envKey} 环境全部参数获取完成" + + // 返回配置 + return params + } catch (Exception e) { + script.echo "❌ 获取环境配置失败: ${e.message}" + throw e + } + } + + /** + * 设置 Jenkins 作业参数 + * 为标准流水线作业创建环境选择参数和服务器选择参数 + * 根据可用环境配置动态生成下拉选择框,支持环境和服务器的两级选择 + * + * @param availableProfiles 可用的环境配置列表 + * - 类型: List 环境名称列表(如:['dev', 'test', 'prod']) + * - 来源: 通常从配置文件中解析得到的环境配置键值 + * - 要求: 列表不能为空,且必须与配置文件中的环境名称匹配 + * + * @note 在多分支流水线中会自动跳过参数设置(因为多分支通常根据分支自动确定环境) + * @note 在标准流水线中会创建用户可交互的选择参数 + * + * @example 生成的参数: + * - PROFILES: 环境选择下拉框(dev/test/prod) + * - SERVERS: 服务器选择下拉框(根据选择的环境动态显示对应服务器) + */ + private void setupJenkinsParameters(List availableProfiles) { + // 检查可用环境列表是否为空 + if (!availableProfiles || availableProfiles.isEmpty()) { + script.echo "⚠️ 可用环境配置列表为空,跳过参数设置" + return + } + + // 获取当前分支名称,用于判断流水线类型 + def branchName = script.env.BRANCH_NAME ?: '' + + // 判断是否为多分支流水线(分支名通常包含斜杠,如: feature/xxx) + boolean isMultibranchPipeline = branchName.contains('/') + + if (isMultibranchPipeline) { + script.echo "🔧 检测到多分支流水线,跳过参数设置(分支: ${branchName})" + script.echo "💡 多分支流水线通常根据分支名称自动确定目标环境" + return + } + + script.echo "🔧 标准流水线作业,设置部署环境参数" + script.echo "📋 可用环境选项: ${availableProfiles.join(', ')}" + + try { + // ========================================================================== + // 创建环境选择参数 + // ========================================================================== + def profileParameter = script.choice( + choices: availableProfiles.join('\n'), // 下拉选项,每行一个选择项 + description: '请选择要部署的目标环境', // 参数描述,显示在Jenkins界面上 + name: 'PROFILES' // 参数名称,后续通过params.PROFILES访问 + ) + + // ========================================================================== + // 创建服务器选择参数(动态生成所有环境的服务器选项) + // ========================================================================== + def allServers = getAllServersForParameters(availableProfiles) + def serverChoices = generateServerChoices(allServers) + + def serverParameter = script.choice( + choices: serverChoices.join('\n'), // 服务器下拉选项 + description: '请选择要部署的目标服务器(格式:环境-服务器名-IP)', // 参数描述 + name: 'SERVERS' // 参数名称 + ) + + // ========================================================================== + // 设置Jenkins作业参数 + // ========================================================================== + script.properties([ + script.parameters([ + profileParameter, // 环境选择参数 + serverParameter // 服务器选择参数 + ]) + ]) + + script.echo "✅ 成功设置部署参数:" + script.echo " - PROFILES: 环境选择(${availableProfiles.size()} 个选项)" + script.echo " - SERVERS: 服务器选择(${serverChoices.size()} 个选项)" + + } catch (Exception e) { + script.echo "⚠️ 设置 Jenkins 参数失败: ${e.message}" + script.echo "💡 这可能是因为没有权限修改作业配置,或者参数格式不正确" + // 不抛出异常,避免影响后续流程,参数设置失败通常不会阻断部署 + } + } + + /** + * 获取所有环境的服务器信息用于参数生成 + * 从配置文件中提取每个环境的服务器列表,用于构建服务器选择参数 + * 自动过滤空字符串和无效的环境名称 + * + * @param availableProfiles 可用环境列表,可能包含空字符串或无效值 + * @return Map 各环境的服务器列表映射(只包含有效环境) + */ + private Map getAllServersForParameters(List availableProfiles) { + def allServers = [:] + + // 过滤掉空字符串和null值,只处理有效的环境名称 + def validProfiles = availableProfiles.findAll { profile -> + profile != null && profile.trim() != '' + } + + script.echo "🔍 有效环境列表: ${validProfiles.join(', ')}" + script.echo "🗑️ 已过滤空环境: ${availableProfiles.size() - validProfiles.size()} 个" + + validProfiles.each { profile -> + try { + // 安全地获取环境配置中的服务器列表 + // 使用安全导航操作符避免空指针异常 + def envConfig = config?.common?.profiles?."${profile}" + + if (envConfig?.servers) { + allServers[profile] = envConfig.servers + script.echo " 📡 ${profile} 环境: ${envConfig.servers.size()} 台服务器" + + // 可选:输出服务器详情用于调试 + envConfig.servers.eachWithIndex { server, index -> + script.echo " ${index + 1}. ${server.serverName} (${server.serverIP}:${server.serverPort})" + } + } else { + script.echo " ⚠️ ${profile} 环境: 无服务器配置或服务器列表为空" + allServers[profile] = [] + } + } catch (Exception e) { + script.echo " ❌ 获取 ${profile} 环境服务器失败: ${e.message}" + script.echo " 🐛 错误详情: ${e.getStackTrace().find { it.contains('getAllServersForParameters') } ?: '未知位置'}" + allServers[profile] = [] + } + } + + // 检查是否有任何有效环境配置了服务器 + def environmentsWithServers = allServers.count { it.value && !it.value.empty } + script.echo "📊 统计: ${environmentsWithServers} 个环境配置了服务器,${allServers.size() - environmentsWithServers} 个环境无服务器" + + return allServers + } + + /** + * 生成服务器选择选项 + * 创建格式化的服务器选择项,便于用户识别和选择 + * + * @param allServers 所有环境的服务器映射 + * @return List 格式化后的服务器选项列表 + */ + private List generateServerChoices(Map allServers) { + def choices = [] + + // 添加默认选项 + choices.add("auto-自动选择(根据环境默认服务器)") + + // 为每个环境的每台服务器生成选项 + allServers.each { profile, servers -> + if (servers && !servers.empty) { + servers.each { server -> + // 格式化选项:环境-服务器名-IP:端口 + def displayName = "${profile}-${server.serverName}-${server.serverIP}:${server.serverPort}" + choices.add(displayName) + } + } else { + // 如果某个环境没有服务器,添加提示选项 + choices.add("${profile}-无可用服务器") + } + } + + return choices + } + + /** + * 根据分支配置获取对应的环境配置文件和默认配置文件 + * @param branchConfig 分支配置信息,包含分支类型等 + * @return Map 包含可用配置列表和默认配置 + */ + private def getBranchProfiles(branchConfig) { + // 获取当前分支名称 + def branchName = script.env.BRANCH_NAME ?: '' + + // 1. 优先从常量配置中获取该分支类型对应的可用配置 + def availableProfiles = ConfigConstants.BRANCH_PROFILES[branchName] + + // 2. 如果常量配置中没有找到,根据分支类型选择不同的默认配置源 + if (!availableProfiles) { + availableProfiles = (branchName == 'master') ? + envConfig.masterProfiles : // master分支使用特殊配置 + envConfig.Profiles // 其他分支使用通用配置 + } + + // 确保availableProfiles不为null,如果是null则转为空列表 + availableProfiles = availableProfiles ?: [] + + // 3. 获取默认配置,优先从常量配置中获取 + def defaultProfile = ConfigConstants.DEFAULT_PROFILES[branchName] + + // 4. 如果常量配置中没有默认配置,则从可用配置中选择 + if (!defaultProfile) { + defaultProfile = availableProfiles.find { it } ? // 查找第一个非空配置 + availableProfiles[0] : // 如果都为空则取第一个 + null // 如果没有可用配置则返回null + } + + return [ + available_profiles: availableProfiles, + default_profile : defaultProfile + ] + } + +} \ No newline at end of file diff --git a/src/com/node/stages/BuildService.groovy b/src/com/node/stages/BuildService.groovy new file mode 100644 index 0000000..b2c4393 --- /dev/null +++ b/src/com/node/stages/BuildService.groovy @@ -0,0 +1,353 @@ +package com.node.stages + +/** + * 前端构建服务 - 负责Vue项目的依赖安装和构建 + * + * 主要功能: + * - 验证Node.js运行环境和项目配置 + * - 执行yarn/npm依赖安装 + * - 执行Vue项目多环境构建 + * - 生成构建产物并验证完整性 + * - 提供详细的构建日志和错误处理 + * + * 设计原则: + * - 单一职责:专注于前端构建流程 + * - 可配置性:支持多环境构建配置 + * - 健壮性:完善的错误处理和验证机制 + * - 可重用性:适用于各种Vue/React前端项目 + * + * @version 1.0.0 + * @since 2025-09-20 + */ +class BuildService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + * 类型: Object (通常是Jenkins pipeline的script对象) + */ + def script + + /** + * 环境配置信息 + * 存储构建环境特定的配置参数 + * 类型: Map 键值对配置映射 + */ + def envConfig + + /** + * npm编译命令配置 + * 从.env.yml配置文件中读取的npm编译命令 + * 类型: String 多行命令字符串 + */ + def npmCompileCMD + + /** + * 构造函数 - 初始化前端构建服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * - 示例:在Pipeline中通过`this`传递当前脚本上下文 + * + * @param envConfig 环境配置信息(可选) + * - 类型:Map 键值对配置映射 + * - 默认值:空Map [:] + * - 内容:构建环境相关的配置参数 + * + * @param npmCompileCMD npm编译命令(可选) + * - 类型:String 多行命令字符串 + * - 默认值:null + * - 来源:从.env.yml配置文件读取 + */ + BuildService(script, envConfig = [:]) { + this.script = script + this.envConfig = envConfig + this.npmCompileCMD = envConfig.npmCompileCMD + script.echo "🔧 BuildService 初始化完成" + + if (npmCompileCMD) { + script.echo "📝 使用自定义npm编译命令配置" + } + } + + /** + * 执行前端构建阶段逻辑 + * 负责Vue项目的完整构建流程,是CI/CD流水线的核心构建阶段 + * + * 主要步骤: + * 1. 输出构建开始信息和配置详情 + * 2. 验证Node.js环境和项目配置 + * 3. 执行yarn依赖安装 + * 4. 执行多环境构建命令 + * 5. 验证构建产物完整性 + * 6. 处理构建结果和异常 + * + * @param params 管道参数Map,包含构建所需的所有配置信息,必须包含: + * - servicePath: String - 前端项目文件路径(相对或绝对路径) + * - PROFILES: String - 构建环境(dev/test/prod等) + * 可选字段: + * - jenkinsAgent: String - Jenkins代理标签 + * - programLangVersion: String - Node.js版本要求 + * - buildArgs: String - 额外的构建参数 + * + * @throws Exception 当构建失败时抛出异常,中断流水线 + * 异常信息包含详细的错误原因和建议解决方案 + * + * @example 使用示例: + * def buildParams = [ + * servicePath: "frontend", + * PROFILES: "prod", + * jenkinsAgent: "frontend-agent", + * programLangVersion: "16.x" + * ] + * BuildService.executeBuildStage(buildParams) + */ + def executeBuildStage(params) { + try { + script.echo "🏗️ 开始前端构建阶段..." + script.echo "📋 前端构建配置:" + script.echo " 📁 项目路径: ${params.servicePath}" + script.echo " 🌍 构建环境: ${params.PROFILES}" + script.echo " 🔧 Node版本: ${params.programLangVersion ?: '默认版本'}" + + if (this.npmCompileCMD) { + script.echo " ⚙️ 使用自定义npm编译命令" + } + + // 验证Node.js环境 - 确保构建环境符合要求 + validateNodeEnvironment(params) + + // 执行前端构建命令 - 包括依赖安装和构建 + script.echo "🔨 执行前端构建命令..." + executeBuildCommands(params) + + // 构建成功完成 + script.echo "✅ 前端构建完成" + script.echo "🎉 构建产物已生成到: ${params.servicePath}/dist 目录" + + } catch (Exception e) { + // 构建失败处理 - 提供详细的错误信息和调试建议 + script.echo "❌ 前端构建阶段失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - Node.js环境是否正常" + script.echo " - yarn/npm依赖是否正确" + script.echo " - 构建配置文件是否正确" + script.echo " - 网络连接是否正常" + + // 重新抛出异常,确保流水线标记为失败状态 + throw e + } + } + + /** + * 验证Node.js环境 + * 检查构建环境是否符合前端项目要求 + * + * 验证内容: + * - Node.js版本和可用性 + * - yarn/npm包管理器的可用性 + * - 项目目录结构和配置文件存在性 + * - 必要的构建工具和环境变量 + * + * @param params 参数Map,包含: + * - servicePath: String - 项目路径 + * - programLangVersion: String - 期望的Node版本(可选) + * + * @throws Exception 当环境验证失败时抛出,包含详细的错误信息 + */ + private def validateNodeEnvironment(params) { + script.echo "🔍 验证Node.js环境..." + + try { + // 检查Node.js版本 - 输出当前版本信息 + script.sh "node -v" + + // 检查包管理器可用性 - 优先使用yarn,fallback到npm + script.sh "yarn --version || npm --version" + + // 检查项目路径是否存在 - 确保项目目录可访问 + if (!script.fileExists(params.servicePath)) { + throw new Exception("项目路径不存在: ${params.servicePath}") + } + + // 检查package.json是否存在 - 项目配置文件必须存在 + def packageJsonPath = "${params.servicePath}/package.json" + if (!script.fileExists(packageJsonPath)) { + throw new Exception("package.json文件不存在: ${packageJsonPath}") + } + + // 可选:检查特定Node版本要求 + if (params.programLangVersion) { + script.echo "📋 要求Node版本: ${params.programLangVersion}" + // 这里可以添加版本检查逻辑 + } + + script.echo "✅ Node.js环境验证通过" + + } catch (Exception e) { + throw new Exception("Node.js环境验证失败: ${e.message}") + } + } + + /** + * 执行前端构建命令 + * 负责具体的构建命令执行,包括依赖安装和项目构建 + * + * 执行步骤: + * 1. 切换到项目目录 + * 2. 安装项目依赖(使用yarn) + * 3. 执行环境特定的构建命令 + * 4. 验证构建产物完整性 + * + * @param params 参数Map,包含: + * - servicePath: String - 项目路径 + * - PROFILES: String - 构建环境 + * - buildArgs: String - 额外构建参数(可选) + * + * @throws Exception 当构建命令执行失败时抛出 + */ + private def executeBuildCommands(params) { + // 切换到项目目录执行构建命令 + script.dir(params.servicePath) { + try { + if (this.npmCompileCMD) { + // 使用自定义的npm编译命令 + script.echo "📦 使用自定义npm编译命令安装依赖和构建..." + + // 替换命令中的环境变量占位符 + def processedCMD = this.npmCompileCMD.replace('${PROFILE}', params.PROFILES) + + script.echo "执行自定义编译命令:" + script.echo processedCMD + + script.sh processedCMD + } else { + // 使用默认的构建流程 - 包含镜像源设置和SSL配置 + script.echo "📦 开始默认构建流程..." + + // ========================================================================== + // 配置yarn镜像源和SSL设置 - 提高依赖下载速度和解决证书问题 + // ========================================================================== + script.echo "🌐 配置yarn镜像源和网络设置..." + script.sh """ + # 设置淘宝镜像源 - 国内访问速度更快 + yarn config set registry https://registry.npmmirror.com + + # 禁用SSL证书验证 - 解决企业内部网络证书问题 + yarn config set strict-ssl false + + # 设置网络超时和重试参数 - 提高网络稳定性 + yarn config set network-timeout 600000 + yarn config set fetch-retries 5 + yarn config set fetch-retry-factor 2 + + # 显示当前yarn配置(用于调试) + echo "✅ Yarn配置完成:" + yarn config get registry + yarn config get strict-ssl + yarn config get network-timeout + """ + + // ========================================================================== + // 清理缓存和旧依赖 - 确保依赖安装的干净性 + // ========================================================================== + script.echo "🧹 清理缓存和旧依赖..." + script.sh """ + # 清理yarn缓存 + yarn cache clean --all + + # 清理node_modules目录(如果存在) + if [ -d "node_modules" ]; then + echo "清理旧的node_modules目录..." + rm -rf node_modules + fi + + # 清理lock文件(可选,根据项目需要) + if [ -f "yarn.lock" ]; then + echo "备份yarn.lock文件..." + cp yarn.lock yarn.lock.backup + fi + + echo "✅ 清理完成" + """ + + // ========================================================================== + // 安装项目依赖 - 使用优化后的配置 + // ========================================================================== + script.echo "📦 安装项目依赖..." + script.sh """ + # 安装依赖,使用frozen-lockfile确保依赖一致性 + yarn install --frozen-lockfile --network-timeout 300000 + + # 验证依赖安装是否成功 + if [ \$? -eq 0 ]; then + echo "✅ 依赖安装成功" + echo "📊 依赖数量: \$(find node_modules -type d -maxdepth 1 | wc -l)" + else + echo "❌ 依赖安装失败" + exit 1 + fi + """ + + // ========================================================================== + // 执行构建命令 - 支持多环境构建 + // ========================================================================== + script.echo "🔨 执行构建命令: build:${params.PROFILES}" + + // 构建基础命令 + def buildCommand = "yarn run build:${params.PROFILES}" + + // 添加额外的构建参数(如果存在) + if (params.buildArgs) { + buildCommand += " ${params.buildArgs}" + } + + script.echo "执行命令:${buildCommand}" + + script.sh buildCommand + } + + // // 验证构建产物 - 确保dist目录已生成且包含必要文件 + // def distPath = "dist" + // if (!script.fileExists(distPath)) { + // throw new Exception("构建产物目录不存在: ${distPath}") + // } + + // // 输出构建产物信息 + // script.sh "ls -la ${distPath}" + // script.echo "✅ 构建命令执行成功" + + // ========================================================================== + // 验证构建产物 - 确保dist目录已生成且包含必要文件 + // ========================================================================== + script.echo "🔍 验证构建产物..." + def distPath = "dist" + if (!script.fileExists(distPath)) { + throw new Exception("构建产物目录不存在: ${distPath},请检查构建命令是否正确") + } + + // 检查dist目录是否为空 + def distFileCount = script.sh(script: "find ${distPath} -type f | wc -l", returnStdout: true).trim().toInteger() + if (distFileCount == 0) { + throw new Exception("构建产物目录为空: ${distPath},构建可能失败") + } + + // 输出构建产物详细信息 + script.echo "✅ 构建产物验证通过" + script.sh """ + echo "📊 构建产物详情:" + echo "文件数量: ${distFileCount}" + ls -la ${distPath} + echo "主要文件:" + find ${distPath} -name "*.js" -o -name "*.css" -o -name "*.html" | head -10 + """ + + script.echo "🎉 构建命令执行成功" + + } catch (Exception e) { + throw new Exception("构建命令执行失败: ${e.message}") + } + } + } + +} \ No newline at end of file diff --git a/src/com/node/stages/DeployService.groovy b/src/com/node/stages/DeployService.groovy new file mode 100644 index 0000000..a3e74f8 --- /dev/null +++ b/src/com/node/stages/DeployService.groovy @@ -0,0 +1,482 @@ +package com.node.stages + +import com.utils.SshUtils + +/** + * 前端部署服务 - 负责Vue项目构建产物的部署到目标服务器 + * + * 主要职责: + * - 传输构建产物到目标服务器 + * - 在服务器上解压和部署文件 + * - 支持多服务器部署和失败处理策略 + * - 验证部署结果和生成部署报告 + * + * 设计原则: + * - 单一职责:专注于前端部署相关逻辑 + * - 可配置性:支持多种部署配置和策略 + * - 健壮性:完善的错误处理和重试机制 + * - 可扩展性:易于添加新的部署功能 + * + * @version 1.0.0 + * @since 2025-09-20 + */ +class DeployService implements Serializable { + + /** + * Jenkins pipeline脚本对象 + * 用于访问Jenkins DSL方法,如sh、echo、error等 + */ + def script + + /** + * 环境配置信息 + * 存储部署环境特定的配置参数 + */ + def envConfig + + /** + * SSH工具类实例 + * 负责SSH连接和文件传输操作 + */ + def sshUtils + + /** + * 构造函数 - 初始化前端部署服务 + * + * @param script Jenkins pipeline脚本对象(必须) + * - 用途:用于在服务中访问Jenkins DSL方法 + * @param envConfig 环境配置信息(可选) + * - 默认值:空Map [:] + * - 内容:部署策略、超时设置等配置 + */ + DeployService(script, envConfig = [:]) { + this.script = script + this.envConfig = envConfig + this.sshUtils = new SshUtils(script) + script.echo "🔧 DeployService 初始化完成" + } + + /** + * 执行前端部署阶段逻辑 + * 负责将前端构建产物部署到目标服务器,支持单服务器和多服务器部署模式 + * + * 主要步骤: + * 1. 输出部署开始信息和配置验证 + * 2. 检查是否需要跳过部署(如UAT环境) + * 3. 验证打包文件的存在性和完整性 + * 4. 根据服务器数量选择部署模式 + * 5. 执行具体的部署操作 + * 6. 处理部署结果和异常情况 + * + * @param params 管道参数Map,包含部署所需的所有配置信息,必须包含: + * - servicePath: String - 本地服务路径(包含打包文件的目录) + * - projectName: String - 项目名称 + * - deployPath: String - 服务器部署路径 + * - packageName: String - 打包文件名 + * - PROFILES: String - 环境名称 + * - targetServers: List - 目标服务器列表 + * 可选字段: + * - stopOnFailure: Boolean - 失败时是否停止部署 + * + * @throws Exception 当部署失败时抛出异常,包含详细的错误信息 + * + * @example 使用示例: + * def deployParams = [ + * servicePath: "frontend", + * projectName: "my-app", + * deployPath: "/usr/local/nginx/html", + * packageName: "my-app-123.tar.gz", + * PROFILES: "prod", + * targetServers: servers, + * stopOnFailure: true + * ] + * DeployService.executeDeployStage(deployParams) + */ + def executeDeployStage(params) { + try { + // 输出部署开始信息和配置详情 + script.echo "🚀 开始前端部署阶段..." + script.echo "📋 部署配置详情:" + script.echo " 📁 项目路径: ${params.servicePath}" + script.echo " 📦 项目名称: ${params.projectName}" + script.echo " 📂 部署路径: ${params.deployPath}" + script.echo " 🌍 目标环境: ${params.PROFILES}" + script.echo " 📊 目标服务器: ${params.targetServers?.size() ?: 0} 台" + + // 检查是否需要跳过部署(UAT环境通常需要手动部署) + if (shouldSkipDeployment(params)) { + script.echo "⏭️ 跳过UAT环境的前端部署" + script.echo "💡 UAT环境通常需要手动验证和部署" + return + } + + // ========================================================================== + // 打包构建产物 - 将dist目录打包为压缩文件,便于传输和部署 + // ========================================================================== + script.echo "📦 开始打包构建产物..." + + // 生成打包文件名,格式:项目名-构建ID.tar.gz + // 如果参数中已指定包名则使用指定名称,否则自动生成 + def packageName = params.packageName ?: "${params.projectName}-${script.env.BUILD_ID ?: 'unknown'}.tar.gz" + + // 切换到项目目录执行打包操作 + script.dir(params.servicePath) { + try { + // 执行打包命令序列: + script.sh """ + echo "#### 开始打包构建产物 ####" + echo "当前工作目录: \$(pwd)" + + # 1. 显示当前文件列表 - 用于调试和验证构建结果 + echo "📁 当前目录文件列表:" + ls -lsh + + # 2. 清理旧的打包文件 - 避免文件名冲突和残留文件影响 + echo "🧹 清理旧的打包文件..." + rm -rf ${params.projectName} ${params.packageName} *.md5 + + # 3. 重命名dist目录为项目名称 - 便于在服务器上识别和管理 + echo "📛 重命名dist目录为项目名称..." + if [ -d "dist" ]; then + mv dist ${params.projectName} + echo "✅ 目录重命名成功" + else + echo "❌ 错误: dist目录不存在,请先执行构建阶段" + exit 1 + fi + + # 4. 创建tar.gz压缩包 - 压缩传输减少网络带宽占用 + echo "🗜️ 创建压缩包: ${params.packageName}" + tar zcf ${params.packageName} ${params.projectName} + + # 5. 生成MD5校验码 - 用于文件完整性验证,防止传输损坏 + echo "🔒 生成MD5校验码..." + md5sum ${params.packageName} > ${params.packageName}.md5 + + # 6. 显示打包结果 - 验证打包操作成功完成 + echo "📊 打包完成,文件详情:" + ls -lh ${params.packageName}* + echo "MD5校验码: \$(cat ${params.packageName}.md5)" + echo "文件大小: \$(du -h ${params.packageName})" + """ + + script.echo "✅ 打包完成: ${params.packageName}" + script.echo "📦 打包文件已生成,准备开始部署" + + } catch (Exception e) { + throw new Exception("打包构建产物失败: ${e.message}") + } + } + + // ========================================================================== + // 验证打包文件存在性 - 确保部署文件可用且完整 + // ========================================================================== + script.echo "🔍 验证打包文件..." + def packagePath = "${params.servicePath}/${params.packageName}" + def md5Path = "${packagePath}.md5" + + // 检查打包文件是否存在 + if (!script.fileExists(packagePath)) { + throw new Exception("打包文件不存在: ${packagePath},打包过程可能失败") + } + + // 检查MD5校验文件是否存在 + if (!script.fileExists(md5Path)) { + throw new Exception("MD5校验文件不存在: ${md5Path},文件完整性无法验证") + } + + // 验证文件大小(可选) + def fileSize = script.sh(script: "du -h ${packagePath} | cut -f1", returnStdout: true).trim() + script.echo "📏 打包文件大小: ${fileSize}" + + script.echo "✅ 打包文件验证通过: ${params.packageName}" + script.echo "🔒 MD5校验文件: ${params.packageName}.md5" + + // ========================================================================== + // 执行部署操作 - 根据服务器数量选择部署模式 + // ========================================================================== + script.echo "🎯 开始执行部署操作..." + def servers = params.targetServers ?: [] + + if (servers.size() > 1) { + script.echo "🔀 多服务器部署模式启动..." + script.echo "📊 失败处理策略: ${shouldStopOnDeploymentFailure(params) ? '失败停止' : '失败继续'}" + // 更新参数,包含生成的包名 + deployToMultipleServers(servers, params) + } else if (servers.size() == 1) { + script.echo "🔷 单服务器部署模式启动..." + // 更新参数,包含生成的包名 + deployToSingleServer(servers[0], params) + } else { + throw new Exception("未找到有效的部署目标服务器,请检查服务器配置") + } + + script.echo "✅ 前端部署完成" + script.echo "🎉 应用已成功部署到所有目标服务器" + script.echo "📦 部署包: ${params.packageName}" + script.echo "🔗 MD5校验: \$(cat ${md5Path})" + + } catch (Exception e) { + // 部署失败处理 - 提供详细的错误信息和调试建议 + script.echo "❌ 前端部署阶段失败: ${e.message}" + script.echo "💡 建议检查:" + script.echo " - 服务器SSH连接是否正常" + script.echo " - SSH密钥凭证是否有效" + script.echo " - 服务器目录权限是否正确" + script.echo " - 网络连接是否稳定" + script.echo " - 打包文件是否存在且完整" + script.echo " - 构建产物dist目录是否生成" + + // 重新抛出异常,确保流水线标记为失败状态 + throw e + } + } + + /** + * 部署到多个目标服务器 + * 支持失败继续或失败停止两种策略,适用于集群部署场景 + * + * @param servers 服务器列表,每台服务器包含: + * - serverIP: String - 服务器IP地址 + * - serverPort: Integer - SSH端口号 + * - serverName: String - 服务器名称标识 + * - sshKey: String - Jenkins SSH密钥凭证ID + * @param params 部署参数Map + * + * @throws Exception 当配置了失败停止且有服务器部署失败时抛出 + */ + private def deployToMultipleServers(servers, params) { + def deploymentResults = [:] // 部署结果跟踪Map + def failedServers = [] // 失败服务器列表 + + // 遍历所有服务器,逐台进行部署 + servers.each { server -> + try { + script.echo "➡️ 正在部署到服务器: ${server.serverName} (${server.serverIP}:${server.serverPort})" + + // 构建服务器配置对象,包含部署所需的所有参数 + def serverConfig = [ + serverIP : server.serverIP, + serverPort : server.serverPort, + serverName : server.serverName, + servicePath: params.servicePath, + projectName: params.projectName, + deployPath : params.deployPath, + packageName: params.packageName, + buildId : script.env.BUILD_ID ?: 'unknown', + profile : params.PROFILES, + sshKey : server.sshKey // Jenkins凭证ID + ] + + // 执行单台服务器部署 + deployToServer(serverConfig) + + script.echo "✅ 部署成功: ${server.serverName}" + deploymentResults[server.serverName] = "成功" + + } catch (Exception e) { + // 记录部署失败信息 + deploymentResults[server.serverName] = "失败: ${e.message}" + failedServers << server.serverName + script.echo "❌ 部署失败: ${server.serverName} - ${e.message}" + + // 根据失败处理策略决定是否继续部署 + if (shouldStopOnDeploymentFailure(params)) { + throw new Exception("部署中断,因为在 ${server.serverName} 上失败且配置了失败停止", e) + } else { + script.echo "⏭️ 继续部署其他服务器(配置了失败继续策略)" + } + } + } + + // 生成部署报告 - 汇总所有服务器的部署状态 + script.echo "=" * 60 + script.echo "📊 前端多服务器部署结果报告" + script.echo "=" * 60 + def successCount = deploymentResults.count { it.value == '成功' } + def failureCount = deploymentResults.size() - successCount + deploymentResults.each { serverName, status -> + def emoji = status.startsWith('成功') ? '✅' : '❌' + script.echo "${emoji} ${serverName}: ${status}" + } + script.echo "-" * 60 + script.echo "📈 统计: 成功 ${successCount}, 失败 ${failureCount}, 总计 ${deploymentResults.size()}" + if (failureCount == 0) { + script.echo "🎉 所有服务器部署成功!" + } else if (successCount == 0) { + script.echo "💥 所有服务器部署失败!" + } else { + script.echo "⚠️ 部分服务器部署失败,请检查日志" + } + script.echo "=" * 60 + + // 检查最终结果,如果有失败且需要通知,抛出异常 + if (failedServers) { + def errorMessage = "多服务器部署完成,但有 ${failedServers.size()} 台服务器失败: ${failedServers.join(', ')}" + script.echo "⚠️ ${errorMessage}" + throw new Exception(errorMessage) + } else { + script.echo "🎉 所有服务器前端部署成功!" + } + } + + /** + * 部署到单台服务器 + * 适用于单实例部署场景,包含完整的部署流程 + * + * @param server 服务器配置对象 + * @param params 部署参数Map + */ + private def deployToSingleServer(server, params) { + def serverConfig = [ + serverIP : server.serverIP, + serverPort : server.serverPort, + serverName : server.serverName, + servicePath: params.servicePath, + projectName: params.projectName, + deployPath : params.deployPath, + packageName: params.packageName, + buildId : script.env.BUILD_ID ?: 'unknown', + profile : params.PROFILES, + sshKey : server.sshKey + ] + + script.echo "🚀 开始部署到单服务器: ${serverConfig.serverName} (${serverConfig.serverIP}:${serverConfig.serverPort})" + + try { + deployToServer(serverConfig) + script.echo "✅ 单服务器部署完成: ${serverConfig.serverName}" + + } catch (Exception e) { + // 详细的错误信息处理 + script.echo "❌ 单服务器部署失败: ${serverConfig.serverName}" + script.echo " 💥 错误信息: ${e.message}" + def errorDetails = "部署到服务器 ${serverConfig.serverName} (${serverConfig.serverIP}) 失败" + if (e.message?.contains('Authentication')) { + errorDetails += " - SSH认证失败,请检查密钥凭证" + } else if (e.message?.contains('Connection')) { + errorDetails += " - 连接失败,请检查网络和端口" + } else if (e.message?.contains('Timeout')) { + errorDetails += " - 操作超时,请检查服务器状态" + } else if (e.message?.contains('Permission')) { + errorDetails += " - 权限不足,请检查文件权限" + } + throw new Exception(errorDetails, e) + } + } + + /** + * 部署到单台服务器的具体实现 + * 包含完整的部署流程:清理、传输、解压、验证 + * + * @param serverConfig 服务器配置对象 + */ + private def deployToServer(serverConfig) { + script.dir(serverConfig.servicePath) { + if (serverConfig.sshKey) { + // 使用SSH密钥认证部署 + script.withCredentials([script.sshUserPrivateKey( + credentialsId: serverConfig.sshKey, + keyFileVariable: 'SSH_KEY_FILE', + usernameVariable: 'SSH_USERNAME' + )]) { + executeDeploymentSteps(serverConfig) + } + } else { + // 不使用SSH密钥(使用密码或其他认证方式) + executeDeploymentSteps(serverConfig) + } + } + } + + /** + * 执行部署步骤序列 + * 1. 清理服务器旧文件 + * 2. 传输文件到服务器 + * 3. 解压文件并完成部署 + * 4. 验证部署结果 + * + * @param serverConfig 服务器配置对象 + */ + private def executeDeploymentSteps(serverConfig) { + // 1. 清理服务器旧文件 - 避免旧文件影响新部署 + script.echo "🧹 步骤1/4: 清理服务器旧文件..." + def cleanCommand = """ + cd ${serverConfig.deployPath} + rm -rf ${serverConfig.projectName} + rm -f ${serverConfig.packageName} + echo "清理完成: \$(date)" >> deployment.log + """ + def result = sshUtils.executeSshCommand(serverConfig, cleanCommand) + if (result != 0) { + throw new Exception("清理服务器文件失败,请检查目录权限") + } + + // 2. 传输文件到服务器 - 使用SCP安全传输 + script.echo "📤 步骤2/4: 传输文件到服务器..." + def remotePath = "${serverConfig.serverIP}:${serverConfig.deployPath}/${serverConfig.packageName}" + result = sshUtils.executeScpTransfer(serverConfig, serverConfig.packageName, remotePath) + if (result != 0) { + throw new Exception("文件传输失败,请检查网络连接和磁盘空间") + } + + // 3. 解压文件并完成部署 - 设置文件权限和记录部署信息 + script.echo "📦 步骤3/4: 解压文件并部署..." + def deployCommand = """ + cd ${serverConfig.deployPath} + tar zxf ${serverConfig.packageName} + chmod -R 755 ${serverConfig.projectName} + ls -la + echo "部署完成: \$(date)" >> deployment.log + echo "构建ID: ${serverConfig.buildId}" >> deployment.log + echo "环境: ${serverConfig.profile}" >> deployment.log + echo "文件大小: \$(du -sh ${serverConfig.projectName})" >> deployment.log + """ + result = sshUtils.executeSshCommand(serverConfig, deployCommand) + if (result != 0) { + throw new Exception("文件解压和部署失败,请检查磁盘空间和权限") + } + + // 4. 验证部署结果 - 确保部署成功且文件可访问 + script.echo "✅ 步骤4/4: 验证部署结果..." + def verifyCommand = """ + cd ${serverConfig.deployPath} + if [ -d "${serverConfig.projectName}" ] && [ -r "${serverConfig.projectName}" ]; then + echo "✅ 部署验证成功: 目录存在且可访问" + echo "验证成功: \$(date)" >> deployment.log + exit 0 + else + echo "❌ 部署验证失败: 目录不存在或不可访问" + exit 1 + fi + """ + result = sshUtils.executeSshCommand(serverConfig, verifyCommand) + if (result != 0) { + throw new Exception("部署验证失败,请检查服务器状态") + } + } + + /** + * 检查是否应该跳过部署 + * UAT环境通常需要手动部署和验证 + * + * @param params 部署参数Map + * @return boolean true表示跳过部署,false表示继续部署 + */ + private def shouldSkipDeployment(params) { + def profile = params.PROFILES?.toLowerCase() + return profile == "uat" // UAT环境跳过自动部署 + } + + /** + * 检查是否应该在部署失败时停止 + * 生产环境通常应该失败时停止,其他环境可以继续 + * + * @param params 部署参数Map + * @return boolean true表示失败时停止,false表示失败时继续 + */ + private def shouldStopOnDeploymentFailure(params) { + def isProduction = params.PROFILES?.toLowerCase() == 'prod' + return params.stopOnFailure ?: isProduction // 优先使用显式配置 + } +} \ No newline at end of file diff --git a/src/com/utils/CommonUtils.groovy b/src/com/utils/CommonUtils.groovy new file mode 100644 index 0000000..ee912c3 --- /dev/null +++ b/src/com/utils/CommonUtils.groovy @@ -0,0 +1,26 @@ +package com.utils + +class CommonUtils { + + + /** + * 生成Docker镜像标签 + * 根据环境profile和构建ID创建唯一的镜像标签,用于标识不同环境和版本的Docker镜像 + * 标签格式: {环境}-{构建ID} 或 {构建ID}(当环境为空时) + * + * @param profile 环境名称/配置文件(如:dev、test、uat、prod) + * - 用于区分不同环境的镜像 + * - 可以为null或空字符串,此时只使用构建ID + * @return 完整的Docker镜像标签字符串 + * @example + * - generateImageTag("dev") → "dev-123" + * - generateImageTag("prod") → "prod-123" + * - generateImageTag(null) → "123" + */ + static generateImageTag(profile, buildId) { + // 使用三元运算符判断profile是否存在 + // 如果profile存在且不为空,返回"profile-BUILD_ID"格式 + // 如果profile为空,只返回BUILD_ID + return profile ? "${profile}-${buildId}" : buildId + } +} \ No newline at end of file diff --git a/src/com/utils/DurationFormatter.groovy b/src/com/utils/DurationFormatter.groovy new file mode 100644 index 0000000..e0e2d39 --- /dev/null +++ b/src/com/utils/DurationFormatter.groovy @@ -0,0 +1,166 @@ +package com.utils + +/** + * 时间格式化工具类 - 静态方法 + * 提供耗时格式化功能,保留1位小数精度 + * 该类包含多种时间格式化方法,适用于不同的场景需求 + */ +class DurationFormatter { + + /** + * 私有构造函数,防止实例化 + * 这是一个工具类,只包含静态方法,不应该被实例化 + * @throws UnsupportedOperationException 如果尝试实例化该类 + */ + private DurationFormatter() { + throw new UnsupportedOperationException("这是一个工具类,不能实例化") + } + + /** + * 格式化耗时 - 精确保留1位小数版本(推荐使用) + * 该方法使用减法运算避免浮点数取模问题,确保精度准确 + * + * @param milliseconds 毫秒数,需要格式化的时间长度 + * @return 格式化后的时间字符串,格式如:"1小时 2分钟 5.0秒" + * + * @example + * formatDuration(5000) // 返回 "5.0秒" + * formatDuration(65000) // 返回 "1分钟 5.0秒" + * formatDuration(3725000) // 返回 "1小时 2分钟 5.0秒" + */ + static String formatDuration(long milliseconds) { + // 将毫秒转换为秒,使用浮点数确保小数精度 + double seconds = milliseconds / 1000.0 + + // 处理大于等于1小时的情况 + if (seconds >= 3600) { + // 计算小时数(整数部分) + int hours = (seconds / 3600) as int + // 计算剩余秒数(减去完整的小时数) + double remainingSeconds = seconds - (hours * 3600) + // 计算分钟数(整数部分) + int minutes = (remainingSeconds / 60) as int + // 计算剩余的秒数(减去完整的分钟数) + double secs = remainingSeconds - (minutes * 60) + // 格式化输出:小时 分钟 秒(保留1位小数) + return String.format("%d小时 %d分钟 %.1f秒", hours, minutes, secs) + } + // 处理大于等于1分钟但小于1小时的情况 + else if (seconds >= 60) { + // 计算分钟数(整数部分) + int minutes = (seconds / 60) as int + // 计算剩余的秒数(减去完整的分钟数) + double secs = seconds - (minutes * 60) + // 格式化输出:分钟 秒(保留1位小数) + return String.format("%d分钟 %.1f秒", minutes, secs) + } + // 处理小于1分钟的情况 + else { + // 直接格式化秒数(保留1位小数) + return String.format("%.1f秒", seconds) + } + } + + /** + * 格式化耗时 - 保留1位小数版本(使用取模运算,可能存在精度问题) + * 注意:此方法使用取模运算,在某些情况下可能存在浮点数精度问题 + * 推荐使用 formatDuration() 方法替代 + * + * @param milliseconds 毫秒数,需要格式化的时间长度 + * @return 格式化后的时间字符串 + * + * @deprecated 由于可能存在的浮点数精度问题,建议使用 formatDuration() 方法 + */ + private static String formatDuration2(long milliseconds) { + // 将毫秒转换为秒 + double totalSeconds = milliseconds / 1000.0 + // 使用取模运算计算秒数(可能导致精度问题) + double seconds = totalSeconds % 60 + // 计算总分钟数 + int totalMinutes = (totalSeconds / 60) as int + // 使用取模运算计算分钟数(可能导致精度问题) + double minutes = totalMinutes % 60 + // 计算小时数 + int hours = (totalMinutes / 60) as int + + // 根据时间长度选择不同的格式化方式 + if (hours >= 1) { + return String.format("%d小时 %d分钟 %.1f秒", hours, minutes, seconds) + } else if (totalMinutes >= 1) { + return String.format("%d分钟 %.1f秒", totalMinutes, seconds) + } else { + return String.format("%.1f秒", totalSeconds) + } + } + + /** + * 格式化耗时 - 高精度版本(使用整数运算避免浮点数问题) + * 该方法使用整数运算,完全避免浮点数精度问题 + * 精度为十分之一秒(0.1秒) + * + * @param milliseconds 毫秒数,需要格式化的时间长度 + * @return 格式化后的时间字符串,格式如:"1小时 2分钟 5.3秒" + * + * @example + * formatDurationPrecise(5333) // 返回 "5.3秒" + */ + static String formatDurationPrecise(long milliseconds) { + // 转换为十分之一秒精度(避免浮点数运算) + // 例如:5333ms → 53.33 → 53个十分之一秒 + long tenths = milliseconds / 100 // 十分之一秒 + + // 计算秒数:53个十分之一秒 → 5.3秒 → 5秒 + 0.3秒 + long seconds = tenths / 10 // 整数秒部分 + long tenthsRemainder = tenths % 10 // 十分之一秒部分(小数位) + + // 计算分钟数 + long minutes = seconds / 60 // 总分钟数 + long secondsRemainder = seconds % 60 // 剩余的秒数 + + // 计算小时数 + long hours = minutes / 60 // 总小时数 + long minutesRemainder = minutes % 60 // 剩余的分钟数 + + // 根据时间长度选择不同的格式化方式 + if (hours >= 1) { + // 格式:小时 分钟 秒.十分之一秒 + return String.format("%d小时 %d分钟 %d.%d秒", hours, minutesRemainder, secondsRemainder, tenthsRemainder) + } else if (minutes >= 1) { + // 格式:分钟 秒.十分之一秒 + return String.format("%d分钟 %d.%d秒", minutes, secondsRemainder, tenthsRemainder) + } else { + // 格式:秒.十分之一秒 + return String.format("%d.%d秒", seconds, tenthsRemainder) + } + } + + /** + * 格式化耗时 - 简化版本(只显示最大单位) + * 该方法将时间转换为最大的时间单位,便于快速阅读 + * + * @param milliseconds 毫秒数,需要格式化的时间长度 + * @return 简化格式的时间字符串 + * + * @example + * formatDurationSimple(3725000) // 返回 "1.0小时" + * formatDurationSimple(65000) // 返回 "1.1分钟" + * formatDurationSimple(5000) // 返回 "5.0秒" + */ + static String formatDurationSimple(long milliseconds) { + // 将毫秒转换为秒 + double seconds = milliseconds / 1000.0 + + // 根据时间长度选择最合适的时间单位 + if (seconds >= 3600) { + // 转换为小时并保留1位小数 + return String.format("%.1f小时", seconds / 3600) + } else if (seconds >= 60) { + // 转换为分钟并保留1位小数 + return String.format("%.1f分钟", seconds / 60) + } else { + // 保持秒数并保留1位小数 + return String.format("%.1f秒", seconds) + } + } + +} \ No newline at end of file diff --git a/src/com/utils/ServerParser.groovy b/src/com/utils/ServerParser.groovy new file mode 100644 index 0000000..74ee185 --- /dev/null +++ b/src/com/utils/ServerParser.groovy @@ -0,0 +1,481 @@ +package com.utils + +/** + * 服务器解析工具 - 负责解析服务器引用和索引处理 + */ +class ServerParser implements Serializable { + def script + + ServerParser(script) { + this.script = script + } + + /** + * 解析部署服务器引用并获取目标服务器列表 + * 根据配置中的服务器引用表达式(如"servers[0]"、"servers[0,2]"、"servers[0:2]"等) + * 从服务器列表中筛选出具体的服务器对象 + * + * 主要功能: + * 1. 解析服务器引用表达式 + * 2. 提取服务器索引或范围 + * 3. 根据索引从服务器列表中获取对应的服务器对象 + * 4. 返回筛选后的目标服务器列表 + * + * @param params 管道参数,包含servers列表和deployServer引用表达式 + * @return 目标服务器列表,根据引用表达式筛选后的服务器对象集合 + * + * 使用示例: + * - "servers[0]" -> 获取第一个服务器 + * - "servers[0,2]" -> 获取第1个和第3个服务器 + * - "servers[0:2]" -> 获取第1个到第3个服务器(索引0,1,2) + * - "servers[all]" -> 获取所有服务器 + * - "servers[primary]" -> 获取所有标记为isPrimary=true的服务器 + * + * @throws IllegalArgumentException 如果服务器引用格式无效或索引越界 + */ + private def parseTargetServers(params) { + // 获取部署服务器引用表达式,默认为"servers[0]"(第一个服务器) + def deployServerRef = params.deployServer ?: "servers[0]" + script.echo "📋 部署服务器引用: ${deployServerRef}" + + // 解析服务器引用表达式,提取索引或范围信息 + // 例如:"servers[0,2]" -> [0, 2], "servers[0:2]" -> [0, 1, 2] + def serverIndexes = parseServerReferences(deployServerRef) + script.echo "🔍 解析出的服务器索引: ${serverIndexes}" + + // 根据解析出的索引列表,从服务器列表中获取对应的服务器对象 + // params.servers 是完整的服务器列表,serverIndexes 是需要筛选的索引 + def targetServers = getServersByIndexes(params.servers, serverIndexes) + + // 打印目标服务器详细信息,便于调试和验证 + script.echo "🎯 目标服务器列表 (共 ${targetServers.size()} 台):" + targetServers.eachWithIndex { server, index -> + script.echo " ${index + 1}. ${server.serverName} (${server.serverIP}:${server.serverPort})" + } + + return targetServers + } + + // /** + // * 解析服务器引用字符串,支持多种格式 + // * 支持格式: "servers[0]", "0", "servers[0,1,2]", "0,1,2", "[0,2]" + // * @param serverRef 服务器引用字符串 + // * @return 服务器索引列表 + // */ + // private def parseServerReferences(serverRef) { + // if (!serverRef) { + // return [0] // 默认使用第一个服务器 + // } + + // def refStr = serverRef.toString().trim() + + // // 1. 处理单个数字 + // if (refStr.isNumber()) { + // return [refStr.toInteger()] + // } + + // // 2. 处理数组格式: "servers[0]", "server[1]" + // def singlePattern = ~/(?i)servers?\[(\d+)\]/ + // def singleMatcher = singlePattern.matcher(refStr) + // if (singleMatcher.find()) { + // script.echo "测试1:${singleMatcher.group(1).toInteger()}" + // return [singleMatcher.group(1).toInteger()] + // } + + // // 3. 处理多服务器格式: "servers[0,1,2]", "[0,1,2]" + // def multiPattern = ~/(?i)servers?\[([\d,\s]+)\]|\[([\d,\s]+)\]/ + // def multiMatcher = multiPattern.matcher(refStr) + // if (multiMatcher.find()) { + // def indexesStr = multiMatcher.group(1) ?: multiMatcher.group(2) + // return parseIndexesString(indexesStr) + // } + + // // 4. 处理逗号分隔格式: "0,1,2" + // if (refStr.contains(',')) { + // return parseIndexesString(refStr) + // } + + // // 5. 默认使用第一个服务器 + // script.echo "⚠️ 无法解析服务器引用 '${refStr}',使用默认服务器索引 0" + // return [0] + // } + + // /** + // * 解析索引字符串为整数列表 + // * @param indexesStr 索引字符串,如 "0,1,2" + // * @return 索引列表 + // */ + // private def parseIndexesString(indexesStr) { + // return indexesStr.split(',').collect { + // it.trim().toInteger() + // }.findAll { it != null } + // } + + /** + * 解析服务器引用字符串,支持多种格式 + * 负责将各种格式的服务器引用转换为统一的服务器索引列表 + * + * 支持格式: + * - 单个服务器: "servers[0]", "0", "server[1]", "[0]" + * - 多个服务器: "servers[0,1,2]", "0,1,2", "[0,1,2]", "servers[0],servers[1]" + * - 范围格式: "servers[0-2]", "0-2" + * - 混合格式: "servers[0,2-4,6]" + * - 特殊值: "all", "*" (选择所有服务器) + * + * @param serverRef 服务器引用字符串,可以是null、空字符串或各种格式的引用 + * @return List 服务器索引列表(去重且按升序排序) + * @throws NumberFormatException 当包含无法解析为数字的字符时 + * + * @example 使用示例: + * parseServerReferences("servers[0],servers[1]") // 返回 [0, 1] + * parseServerReferences("servers[0-2]") // 返回 [0, 1, 2] + * parseServerReferences("all") // 返回所有服务器索引 + */ + private def parseServerReferences(serverRef) { + // 处理空引用情况,提供默认值 + if (!serverRef) { + script.echo "ℹ️ 服务器引用为空,使用默认服务器索引 0" + return [0] // 默认使用第一个服务器 + } + + // 转换为字符串并去除前后空格 + def refStr = serverRef.toString().trim() + script.echo "🔍 开始解析服务器引用: '${refStr}'" + + // 使用Set集合存储索引,自动去重 + def indexes = [] as Set + + // ========================================================================== + // 0. 处理特殊值和全选情况 + // ========================================================================== + if (refStr == 'all' || refStr == '*') { + script.echo "🌐 检测到全选指令,选择所有服务器" + // 返回所有服务器的索引(需要外部提供服务器总数) + return getAllServerIndexes() // 需要实现这个方法 + } + + // ========================================================================== + // 1. 首先尝试解析逗号分隔的多个服务器引用 + // 处理格式: "servers[0],servers[1],servers[2]" + // ========================================================================== + if (refStr.contains(',') && refStr.contains('servers')) { + script.echo "📋 检测到逗号分隔的多服务器引用格式" + + // 按逗号分割字符串,并去除每个部分的空格 + def parts = refStr.split(',').collect { it.trim() } + script.echo "📊 分割后的部分: ${parts}" + + // 遍历每个部分并解析 + parts.each { part -> + if (part) { + script.echo " 🔍 解析部分: '${part}'" + def partIndexes = parseSingleServerReference(part) + if (partIndexes) { + indexes.addAll(partIndexes) + script.echo " ✅ 解析结果: ${partIndexes}" + } + } + } + + if (indexes) { + script.echo "✅ 多服务器引用解析完成: ${indexes.sort()}" + return indexes.sort() // 返回排序后的索引列表 + } + } + + // ========================================================================== + // 2. 处理数组格式: "servers[0,1,2]", "[0,1,2]", "server[0-2,4]" + // 使用正则表达式匹配方括号内的内容 + // ========================================================================== + def multiPattern = ~/(?i)(?:servers?)?\[([\d\s,\-]+)\]/ + def multiMatcher = multiPattern.matcher(refStr) + if (multiMatcher.find()) { + script.echo "📋 检测到数组格式的服务器引用" + + // 提取方括号内的内容(数字、逗号、空格、横杠) + def indexesStr = multiMatcher.group(1) + script.echo "📊 提取的索引字符串: '${indexesStr}'" + + // 解析索引字符串(支持逗号分隔和范围格式) + def parsedIndexes = parseIndexesString(indexesStr) + indexes.addAll(parsedIndexes) + + script.echo "✅ 数组格式解析完成: ${indexes.sort()}" + return indexes.sort() + } + + // ========================================================================== + // 3. 处理单个服务器引用: "servers[0]", "0", "server[1]" + // 支持带servers前缀或不带前缀的单个数字 + // ========================================================================== + def singlePattern = ~/(?i)servers?\[(\d+)\]|^(\d+)$/ + def singleMatcher = singlePattern.matcher(refStr) + if (singleMatcher.find()) { + script.echo "📋 检测到单个服务器引用格式" + + // 提取数字部分(可能在第1组或第2组) + def indexStr = singleMatcher.group(1) ?: singleMatcher.group(2) + if (indexStr) { + def index = indexStr.toInteger() + indexes.add(index) + script.echo "✅ 单个服务器解析完成: [${index}]" + return indexes.sort() + } + } + + // ========================================================================== + // 4. 处理逗号分隔的数字: "0,1,2", "1, 3, 5"(纯数字,无servers前缀) + // ========================================================================== + if (refStr.contains(',')) { + script.echo "📋 检测到逗号分隔的数字格式" + + def parsedIndexes = parseIndexesString(refStr) + indexes.addAll(parsedIndexes) + + script.echo "✅ 逗号分隔数字解析完成: ${indexes.sort()}" + return indexes.sort() + } + + // ========================================================================== + // 5. 如果所有解析尝试都失败,使用默认值并输出警告 + // ========================================================================== + script.echo "⚠️ 无法解析服务器引用 '${refStr}',使用默认服务器索引 0" + return [0] + } + + /** + * 解析单个服务器引用字符串 + * 专门处理单个服务器的各种表示格式 + * + * @param refStr 单个服务器引用字符串 + * @return List 解析得到的索引列表(通常只有一个元素) + * + * @example 使用示例: + * parseSingleServerReference("servers[0]") // 返回 [0] + * parseSingleServerReference("5") // 返回 [5] + * parseSingleServerReference("[2]") // 返回 [2] + */ + private def parseSingleServerReference(refStr) { + def indexes = [] as Set + + script.echo " 🔍 解析单个服务器引用: '${refStr}'" + + // 1. 处理格式: "servers[0]", "server[1]" + def singlePattern = ~/(?i)servers?\[(\d+)\]/ + def singleMatcher = singlePattern.matcher(refStr) + if (singleMatcher.find()) { + def index = singleMatcher.group(1).toInteger() + indexes.add(index) + script.echo " ✅ 解析为servers格式: ${index}" + return indexes + } + + // 2. 处理格式: "0", "1", "2" (纯数字) + if (refStr.isNumber()) { + def index = refStr.toInteger() + indexes.add(index) + script.echo " ✅ 解析为纯数字格式: ${index}" + return indexes + } + + // 3. 处理格式: "[0]", "[1]" (方括号包裹的数字) + def bracketPattern = ~/\[(\d+)\]/ + def bracketMatcher = bracketPattern.matcher(refStr) + if (bracketMatcher.find()) { + def index = bracketMatcher.group(1).toInteger() + indexes.add(index) + script.echo " ✅ 解析为方括号格式: ${index}" + return indexes + } + + script.echo " ⚠️ 无法解析单个服务器引用: '${refStr}'" + return indexes + } + + /** + * 解析索引字符串,支持多种复杂格式 + * 将字符串转换为数字索引列表,支持范围表达式和混合格式 + * + * @param indexesStr 索引字符串,格式如: "0,1,2", "0-2", "0,2-4,6" + * @return List 解析后的索引列表(去重且排序) + * + * @example 使用示例: + * parseIndexesString("0,1,2") // 返回 [0, 1, 2] + * parseIndexesString("0-2") // 返回 [0, 1, 2] + * parseIndexesString("0,2-4,6") // 返回 [0, 2, 3, 4, 6] + */ + private def parseIndexesString(indexesStr) { + def indexes = [] as Set + + script.echo " 解析索引字符串: '${indexesStr}'" + + // 按逗号分割字符串,处理每个部分 + indexesStr.split(',').each { part -> + def trimmedPart = part.trim() + script.echo " 处理部分: '${trimmedPart}'" + + if (trimmedPart.contains('-')) { + // 处理范围格式: "0-2", "1-3" + script.echo " 检测到范围格式" + + def rangeParts = trimmedPart.split('-') + if (rangeParts.size() == 2) { + def startStr = rangeParts[0].trim() + def endStr = rangeParts[1].trim() + + // 验证范围格式是否正确 + if (startStr.isNumber() && endStr.isNumber()) { + def start = startStr.toInteger() + def end = endStr.toInteger() + + if (start <= end) { + // 生成范围内的所有索引 + (start..end).each { index -> + indexes.add(index) + } + script.echo " ✅ 范围解析: ${start}-${end} -> ${(start..end).toList()}" + } else { + script.echo " ⚠️ 无效的范围格式: ${trimmedPart} (起始值大于结束值)" + } + } else { + script.echo " ⚠️ 范围包含非数字字符: ${trimmedPart}" + } + } else { + script.echo " ⚠️ 无效的范围格式: ${trimmedPart} (应包含一个横杠)" + } + } else if (trimmedPart.isNumber()) { + // 处理单个数字: "0", "1", "2" + def index = trimmedPart.toInteger() + indexes.add(index) + script.echo " ✅ 数字解析: ${index}" + } else if (trimmedPart) { + // 处理非数字内容(可能是空格或其他字符) + script.echo " ⚠️ 忽略非数字内容: '${trimmedPart}'" + } + } + + def result = indexes.sort() + script.echo " 索引字符串解析完成: ${result}" + return result + } + + /** + * 获取所有服务器的索引列表 + * 根据配置中的服务器数量生成所有可用的索引 + * + * @return List 所有服务器的索引列表 + * @throws IllegalStateException 当无法确定服务器数量时 + */ + private def getAllServerIndexes() { + // 从环境配置中获取服务器总数 + def serverCount = envConfig.servers?.size() ?: 0 + + if (serverCount <= 0) { + script.echo "⚠️ 无法确定服务器数量,使用默认索引 0" + return [0] + } + + def allIndexes = (0..= servers.size() + * + * 使用示例: + * servers = [server1, server2, server3] + * indexes = [0, 2] -> 返回 [server1, server3] + * indexes = [1, 5] -> 返回 [server2],并警告索引5无效 + * indexes = [10] -> 抛出异常,没有有效服务器 + */ + private def getServersByIndexes(servers, indexes) { + // 初始化目标服务器列表,用于存储有效索引对应的服务器 + def targetServers = [] + + // 初始化无效索引列表,用于收集和报告超出范围的索引 + def invalidIndexes = [] + + // 遍历所有提供的索引,进行有效性验证和服务器获取 + indexes.each { index -> + // 检查索引是否在有效范围内(0 到 服务器数量-1) + if (index >= 0 && index < servers.size()) { + // 索引有效,将对应的服务器对象添加到目标列表 + targetServers << servers[index] + } else { + // 索引无效,添加到无效索引列表用于后续报告 + invalidIndexes << index + } + } + + // 如果存在无效索引,输出警告信息帮助用户调试 + if (invalidIndexes) { + script.echo "⚠️ 警告: 忽略无效的服务器索引: ${invalidIndexes.join(', ')}" + script.echo " 有效索引范围: 0 - ${servers.size() - 1}" + script.echo " 当前服务器总数: ${servers.size()}" + } + + // 检查是否成功获取到任何有效的服务器 + if (targetServers.empty) { + // 如果没有找到任何有效的服务器,抛出异常终止流程 + // 这通常表示配置错误或索引计算有问题 + throw new IllegalArgumentException("没有找到有效的目标服务器,请检查服务器索引。有效索引范围: 0 - ${servers.size() - 1}") + } + + // 返回筛选后的目标服务器列表 + return targetServers + } + + // /** + // * 测试服务器引用解析功能 + // * 用于验证各种格式的解析是否正确 + // */ + // private def testParseServerReferences() { + // script.echo "🧪 开始测试服务器引用解析功能" + + // def testCases = [ + // "servers[0],servers[1]": [0, 1], + // "servers[0,1]": [0, 1], + // "0,1": [0, 1], + // "servers[0]": [0], + // "0": [0], + // "servers[0-2]": [0, 1, 2], + // "servers[0,2-4,6]": [0, 2, 3, 4, 6], + // "all": [0, 1, 2, 3, 4], // 假设有5台服务器 + // "": [0] + // ] + + // testCases.each { input, expected -> + // script.echo "---" + // script.echo "测试输入: '${input}'" + // def result = parseServerReferences(input) + // script.echo "测试结果: ${result} (期望: ${expected})" + + // if (result == expected) { + // script.echo "✅ 测试通过" + // } else { + // script.echo "❌ 测试失败" + // } + // } + + // script.echo "🧪 测试完成" + // } + +} \ No newline at end of file diff --git a/src/com/utils/SshUtils.groovy b/src/com/utils/SshUtils.groovy new file mode 100644 index 0000000..0aa7425 --- /dev/null +++ b/src/com/utils/SshUtils.groovy @@ -0,0 +1,196 @@ +package com.utils + +/** + * SSH工具类 - 负责SSH连接和文件传输操作 + * 提供统一的SSH命令构建和执行方法 + * + * 主要功能: + * 1. SSH命令执行:通过SSH连接到远程服务器执行命令 + * 2. SCP文件传输:使用SCP协议在本地和远程服务器之间传输文件 + * 3. 连接测试:检查服务器SSH连接是否正常 + * 4. 灵活的配置选项:支持自定义SSH连接参数和超时设置 + * + * @version 1.0.0 + * @since 2024 + */ +class SshUtils implements Serializable { + + // Jenkins流水线脚本对象,用于执行shell命令和流程控制 + def script + + /** + * 构造函数 + * 初始化SSH工具类实例 + * + * @param script Jenkins流水线脚本对象,提供sh、echo等方法 + */ + SshUtils(script) { + this.script = script + } + + /** + * 执行SSH命令 + * 通过SSH连接到远程服务器并执行指定的命令 + * + * @param serverConfig 服务器配置信息,包含连接参数 + * - serverIP: 服务器IP地址(必需) + * - serverPort: SSH端口号(可选,默认22) + * - username: 用户名(可选) + * - sshKey: SSH密钥文件路径或凭证ID(可选) + * - deployPath: 部署路径(用于上下文) + * @param command 要在远程服务器上执行的Shell命令 + * @param options SSH连接选项(可选) + * - ConnectTimeout: 连接超时时间(秒) + * - StrictHostKeyChecking: 是否严格检查主机密钥 + * - 其他SSH选项参数 + * @return 命令执行的状态码,0表示成功,非0表示失败 + */ + def executeSshCommand(serverConfig, command, options = [:]) { + // 构建完整的SSH命令字符串 + def sshCommand = buildSshCommand(serverConfig, options) + // 执行SSH命令并返回状态码 + return script.sh(script: "${sshCommand} '${command}'", returnStatus: true) + } + + /** + * 执行SCP文件传输 + * 使用SCP协议将本地文件传输到远程服务器 + * + * @param serverConfig 服务器配置信息 + * @param localFile 本地文件路径(要传输的文件) + * @param remotePath 远程服务器目标路径 + * @param options SCP传输选项(可选) + * @return 文件传输的状态码,0表示成功,非0表示失败 + */ + def executeScpTransfer(serverConfig, localFile, remotePath, options = [:]) { + // 构建完整的SCP命令字符串 + def scpCommand = buildScpCommand(serverConfig, options) + // 执行SCP文件传输并返回状态码 + return script.sh(script: "${scpCommand} ${localFile} ${remotePath}", returnStatus: true) + } + + /** + * 构建SSH命令 + * 根据服务器配置和选项构建完整的SSH命令字符串 + * + * @param serverConfig 服务器配置信息 + * @param options SSH连接选项 + * @return 完整的SSH命令字符串 + */ + private def buildSshCommand(serverConfig, options = [:]) { + // 初始化SSH命令基础部分 + def cmd = "ssh" + + // 添加认证信息 - 使用SSH密钥认证 + if (serverConfig.sshKey) { + // 使用环境变量中的SSH密钥文件路径 + cmd += " -i \${SSH_KEY_FILE}" + } + // 添加用户名认证 + if (serverConfig.username) { + cmd += " -l ${serverConfig.username}" + } + + // 添加连接信息 - 服务器IP地址 + cmd += " ${serverConfig.serverIP}" + // 添加端口号(如果指定) + if (serverConfig.serverPort) { + cmd += " -p ${serverConfig.serverPort}" + } + + // 添加SSH连接选项(超时、主机密钥检查等) + cmd += buildSshOptions(options) + + return cmd + } + + /** + * 构建SCP命令 + * 根据服务器配置和选项构建完整的SCP命令字符串 + * + * @param serverConfig 服务器配置信息 + * @param options SCP传输选项 + * @return 完整的SCP命令字符串 + */ + private def buildScpCommand(serverConfig, options = [:]) { + // 初始化SCP命令基础部分 + def cmd = "scp" + + // 添加认证信息 - 使用SSH密钥认证 + if (serverConfig.sshKey) { + // 使用环境变量中的SSH密钥文件路径 + cmd += " -i \${SSH_KEY_FILE}" + } + + // 添加端口号(如果指定) + if (serverConfig.serverPort) { + // SCP使用-P参数指定端口(注意是大写P) + cmd += " -P ${serverConfig.serverPort}" + } + + // 添加SCP连接选项 + cmd += buildSshOptions(options) + + // 构建目标地址字符串 + def target = serverConfig.serverIP + // 添加用户名(如果指定) + if (serverConfig.username) { + target = "${serverConfig.username}@${target}" + } + // 添加部署路径(如果指定) + if (serverConfig.deployPath) { + target += ":${serverConfig.deployPath}" + } + + return cmd + } + + /** + * 构建SSH选项字符串 + * 将SSH选项Map转换为命令行参数格式 + * + * @param options SSH选项Map + * - ConnectTimeout: 连接超时时间(秒) + * - StrictHostKeyChecking: 是否严格检查主机密钥(yes/no) + * - ServerAliveInterval: 服务器存活检测间隔(秒) + * - LogLevel: 日志级别 + * - BatchMode: 是否启用批处理模式 + * @return SSH选项参数字符串 + */ + private def buildSshOptions(options) { + def opts = "" + // 默认SSH选项配置 + def defaultOptions = [ + 'ConnectTimeout' : '30', // 连接超时30秒 + 'StrictHostKeyChecking': 'no', // 不严格检查主机密钥(适用于自动化场景) + 'ServerAliveInterval' : '60', // 每60秒发送一次存活检测 + 'LogLevel' : 'ERROR', // 只记录错误日志 + 'BatchMode' : 'yes' // 启用批处理模式(禁止交互式提示) + ] + + // 合并默认选项和用户自定义选项,用户选项优先 + def finalOptions = defaultOptions + (options ?: [:]) + + // 将选项Map转换为命令行参数 + finalOptions.each { key, value -> + // 格式:-o Key=Value + opts += " -o ${key}=${value}" + } + + return opts + } + + /** + * 检查服务器连接 + * 测试与远程服务器的SSH连接是否正常 + * + * @param serverConfig 服务器配置信息 + * @return boolean 连接是否成功,true表示连接正常,false表示连接失败 + */ + def checkServerConnection(serverConfig) { + // 执行简单的echo命令测试连接 + def result = executeSshCommand(serverConfig, "echo '连接测试成功'", [ConnectTimeout: '10']) + // 返回状态码为0表示连接成功 + return result == 0 + } +} \ No newline at end of file diff --git a/vars/pipelineDockerService.groovy b/vars/pipelineDockerService.groovy new file mode 100644 index 0000000..ac5053f --- /dev/null +++ b/vars/pipelineDockerService.groovy @@ -0,0 +1,265 @@ +// 导入在 Pipeline 中需要使用的共享库类 +// 这些类通常在 Jenkins 的共享库 (Shared Libraries) 中定义,提供了可重用的功能 +import com.docker.ResolverService +import com.docker.DeploymentService +import com.common.StageService +import com.common.NotificationService + +/** + * 主管道入口点 + * 提供完整的CI/CD流水线功能,实现从配置加载到部署的全流程自动化 + * 这是一个 Jenkins 共享库 (Shared Library) 中的全局变量 (Global Variable) 或直接在 Pipeline 脚本中定义的方法 + * + * @param orgName 组织名称,用于标识不同的组织配置和环境。这是调用此流水线时必须传入的主要参数。 + * + * 功能特性: + * - 动态配置加载:支持根据 orgName 加载多环境配置管理 + * - 多阶段流水线:清晰的构建、测试、部署、验证流程 + * - 错误恢复机制:完善的异常处理和回滚功能 (虽然在当前代码示例中未完全展示,但框架支持) + * - 资源清理:自动清理工作空间和临时资源,避免磁盘空间耗尽 + * - 通知机制:构建结果通知(成功、失败、中止、不稳定) + * + * 示例用法: + * @Library ('share-library@master')_ + * orgName = 'xiaomayi' + * pipelineImagesService(orgName) + */ +def call(String orgName) { // Jenkins Pipeline 的入口方法通常命名为 call + // 定义管道参数变量,用于在不同阶段间传递配置信息。初始化为 null。 + def pipelineParams = null + // 声明服务类变量,将在后续阶段初始化 + def resolverService = null // 配置解析服务实例 + def deploymentService = null // 部署服务实例 + def stageService = null // 阶段服务实例 + def notificationService = null // 通知服务实例 + // Jenkins代理标签,指定流水线特定阶段在哪个代理节点上运行 + def jenkinsAgent = '' + + // 启动流水线定义。这是 Declarative Pipeline 的语法开始 + pipeline { + // 指定流水线执行代理,any表示可在任何可用代理上运行。 + // 注意:这里定义的 agent 会被后面 stage 中定义的 agent 覆盖。 + agent any + + // 流水线全局配置选项块 + options { + // 构建历史保留策略,用于管理 Jenkins 服务器磁盘空间 + buildDiscarder(logRotator( + artifactDaysToKeepStr: '', // 构建产物保留天数(空字符串表示不按天数限制,可能使用数量限制) + artifactNumToKeepStr: '2', // 保留最近2个构建的产物(如生成的jar包、docker镜像等) + daysToKeepStr: '7', // 构建记录元数据(日志、状态等)保留7天 + numToKeepStr: '3' // 保留最近3个构建记录(无论天数) + )) + // 全局超时设置:整个流水线最多运行2小时,超时则自动终止,防止无限运行 + timeout(time: 2, unit: 'HOURS') + // 还可以添加其他选项,例如: disableConcurrentBuilds() 防止并行运行等 + } + + // 流水线阶段定义块,包含按顺序执行的一系列阶段 (stages) + stages { + /** + * 第一阶段:初始化配置和环境 + * 加载环境配置、解析参数、初始化服务实例。 + * 此阶段仍在 pipeline 顶部定义的 agent any 上运行。 + */ + stage('Setup Parameters & ENV') { + steps { + // 执行Git检出操作 (checkout SCM),获取流水线配置文件或所需的配置仓库 + checkout scmGit([ // scmGit 是一种 checkout 方式 + // 指定要检出的分支,这里检出 master 分支 + branches: [[name: '*/master']], + // 配置远程仓库信息,包括认证和URL + userRemoteConfigs: [[ + credentialsId: 'c9b5eae2-5df5-485f-bebd-9cd5393b03e1', // 存储在 Jenkins 中的 Git 认证凭据ID + url: 'http://192.168.10.102:3000/cicd/jenkins-pipeline-files.git' // 配置仓库的URL地址 + ]], + // 扩展配置:指定检出到非默认工作目录的子目录中,避免与后续可能检出的应用代码冲突 + extensions: [[ + $class: 'RelativeTargetDirectory', // Jenkins checkout 扩展的类名,用于设置相对目标目录 + relativeTargetDir: '.pipeline-files' // 将配置仓库检出到工作空间下的 .pipeline-files 目录 + ]], + ]) + script { + echo "=== 开始初始化配置 ===" + echo "组织名称: ${orgName}" // 打印传入的组织名称参数 + try { // 使用 try-catch 进行异常处理,确保配置失败时能妥善处理 + // 初始化阶段服务实例,this 代表当前的流水线脚本对象,用于访问 Jenkins 步骤 + stageService = new StageService(this) + // 记录 '初始化配置' 阶段的开始时间 + stageService.recordStageStart('初始化配置') + + // 初始化配置解析服务实例,传入当前脚本对象和组织名称 + resolverService = new ResolverService(this, orgName) + // 调用解析服务的方法,加载所有配置文件(如环境相关的配置和主配置) + resolverService.loadConfigurations() + // 调用解析服务的方法,解析加载的配置并设置管道运行所需的参数集合 + // 返回的 pipelineParams 可能包含 Maven 设置、镜像仓库地址、目标服务器配置、环境变量等 + pipelineParams = resolverService.setupParameters() + + // 处理环境 profile 选择逻辑 + // params.PROFILES 可能是一个通过 Jenkins 输入参数(parameters {} 块)传入的值 + // 如果用户选择了 profile,则使用它;否则使用配置文件中定义的默认 profile (pipelineParams.PROFILES) + def selectedProfile = params.PROFILES ?: pipelineParams.PROFILES + pipelineParams.PROFILES = selectedProfile // 将最终确定的 profile 设置回参数Map中 + + // 设置后续构建和部署阶段使用的 Jenkins 代理标签 + // 优先使用配置文件中指定的代理标签,如果未配置则默认使用 'master' 节点(Jenkins Controller) + jenkinsAgent = pipelineParams?.jenkinsAgent ?: 'master' + echo "本次使用代理节点: ${jenkinsAgent}" + + // 初始化部署服务实例,传入当前脚本对象和从配置中获取的环境配置 + deploymentService = new DeploymentService(this, pipelineParams.envConfig) + + // 初始化消息通知服务实例,传入当前脚本对象、管道参数和阶段服务实例 + notificationService = new NotificationService(this, pipelineParams, stageService) + + // 记录 '初始化配置' 阶段的成功结束 + stageService.recordStageEnd('初始化配置', 'SUCCESS') + + } catch (Exception e) { // 捕获初始化过程中可能抛出的任何异常 + // 打印错误信息到流水线日志 + echo "配置初始化失败: ${e.message}" + // 记录 '初始化配置' 阶段的失败结束 + stageService.recordStageEnd('初始化配置', 'FAILURE') + // 抛出错误,终止整个流水线的执行,并标记为 FAILURE 状态 + error("管道初始化失败: " + e.getMessage()) + } + + echo "=== 结束初始化配置 ===" + } + } + } + + /** + * 第二阶段:代码构建 + * 执行Maven构建(编译、测试、打包),生成可部署的JAR包和Docker镜像。 + * 此阶段在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行,以分担主节点负载或使用特定环境。 + */ + stage('Build') { + // 指定在此阶段使用的 Jenkins 代理节点标签 + agent { + label jenkinsAgent // 使用前面计算得到的代理标签 + } + steps { + script { + echo "=== 🔨 开始代码构建 ===" + // 记录 '构建镜像' 阶段的开始时间 + stageService.recordStageStart('构建镜像') + // 调用部署服务的方法,执行具体的构建逻辑 + // 该方法内部可能包含:mvn clean compile/test/package、docker build/push 等步骤 + deploymentService.executeBuildStage(pipelineParams) + // 记录 '构建镜像' 阶段的成功结束 + stageService.recordStageEnd('构建镜像', 'SUCCESS') + echo "=== 🔨 完成代码构建 ===" + } + } + } + + /** + * 第三阶段:应用部署 + * 将Docker镜像部署到目标服务器(如测试、预生产、生产环境)。 + * 此阶段同样在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行。 + */ + stage('Deploy') { + // 指定在此阶段使用的 Jenkins 代理节点标签 + agent { + label jenkinsAgent + } + steps { + script { + echo "=== 🚀 开始应用部署 ===" + // 记录 '部署镜像' 阶段的开始时间 + stageService.recordStageStart('部署镜像') + // 调用部署服务的方法,执行具体的部署逻辑 + // 该方法内部可能包含:通过 SSH 连接到目标服务器、执行部署脚本、更新容器(docker compose/k8s)等 + deploymentService.executeDeployStage(pipelineParams) + // 记录 '部署镜像' 阶段的成功结束 + stageService.recordStageEnd('部署镜像', 'SUCCESS') + echo "=== 🚀 完成应用部署 ===" + } + } + } + // 可以在此处添加更多阶段,例如:自动化测试、集成测试、性能测试、安全扫描等 + } + + /** + * 流水线后处理块 (post section) + * 根据流水线的最终状态执行不同的操作,无论构建成功与否都会执行的处理逻辑。 + * 此部分在 pipeline 顶部定义的 agent any 上运行。 + */ + post { + /** + * always:无论构建结果如何(成功、失败、中止、不稳定)都会执行 + * 主要用于资源清理和状态报告等必须完成的操作 + */ + always { + echo "=== 管道执行完成 ===" + echo "完成时间: ${new Date()}" // 打印流水线结束的时间 + // 清理工作空间,删除当前流水线运行产生的所有文件,释放磁盘空间 + // cleanWs() + script { + echo "构建完成,状态: ${currentBuild.currentResult}" // 打印最终构建状态 + // 调用阶段服务的方法,收集所有阶段的执行信息(开始时间、持续时间、状态),可能用于通知或报告 + stageService.collectAllStagesInfo() + } + } + + /** + * success:只有当整个流水线的状态为 SUCCESS 时执行 + * 主要用于成功通知和结果报告 + */ + success { + echo "✅ 部署成功!" + script { + // 调用通知服务的方法,发送构建成功的通知(如邮件、钉钉、Slack等) + notificationService.sendBuildSuccess(extraInfo: [:]) + } + } + + /** + * failure:只有当整个流水线的状态为 FAILURE 时执行 + * 主要用于错误报告、通知和可能的清理或回滚操作(虽然此处未展示回滚) + */ + failure { + echo "❌ 部署失败!" + script { + // 调用通知服务的方法,发送构建失败的通知,并传递错误信息 + // 这里的 errorMsg 和 extraInfo 是示例,实际可能从捕获的异常中获取 + notificationService.sendBuildFailure( + errorMsg: "单元测试失败", // 示例错误消息 + extraInfo: [stage: "test", errorDetails: "具体错误信息"] // 示例额外信息 + ) + } + } + + /** + * aborted:只有当整个流水线的状态为 ABORTED 时执行(通常由用户手动中止) + * 用于通知构建被中止的情况 + */ + aborted { + echo "❌ 构建中止!" + script { + // 调用通知服务的方法,发送构建中止的通知 + notificationService.sendBuildAborted( + // 传递一些额外信息,例如中止原因和操作人(如果环境变量可用) + extraInfo: [中止原因: '手动中止', 操作人: env.USER_ID] + ) + } + } + + /** + * unstable:只有当整个流水线的状态为 UNSTABLE 时执行(通常由测试失败或有警告导致) + * 主要用于警告通知 + */ + unstable { + echo "⚠️ 管道执行不稳定" + script { + // 调用通知服务的方法,发送构建不稳定的通知 + notificationService.sendBuildUnstable( + extraInfo: ["不稳定原因": "测试未通过或存在警告"] // 示例原因 + ) + } + } + } + } +} \ No newline at end of file diff --git a/vars/pipelineGolangService.groovy b/vars/pipelineGolangService.groovy new file mode 100644 index 0000000..e69de29 diff --git a/vars/pipelineJDKService.groovy b/vars/pipelineJDKService.groovy new file mode 100644 index 0000000..fb9f8ad --- /dev/null +++ b/vars/pipelineJDKService.groovy @@ -0,0 +1,364 @@ +// 导入在 Pipeline 中需要使用的共享库类 +// 这些类通常在 Jenkins 的共享库 (Shared Libraries) 中定义,提供了可重用的功能 +import com.jdk.ResolverService +import com.jdk.DeploymentService +import com.common.StageService +import com.common.NotificationService + +/** + * 主管道入口点 + * 提供完整的CI/CD流水线功能,实现从配置加载到部署的全流程自动化 + * 这是一个 Jenkins 共享库 (Shared Library) 中的全局变量 (Global Variable) 或直接在 Pipeline 脚本中定义的方法 + * + * @param orgName 组织名称,用于标识不同的组织配置和环境。这是调用此流水线时必须传入的主要参数。 + * + * 功能特性: + * - 动态配置加载:支持根据 orgName 加载多环境配置管理 + * - 多阶段流水线:清晰的构建、测试、部署、验证流程 + * - 错误恢复机制:完善的异常处理和回滚功能 (虽然在当前代码示例中未完全展示,但框架支持) + * - 资源清理:自动清理工作空间和临时资源,避免磁盘空间耗尽 + * - 通知机制:构建结果通知(成功、失败、中止、不稳定) + * + * 示例用法: + * @Library ('share-library@master')_ + * orgName = 'xiaomayi' + * pipelineJDKService(orgName) + */ +def call(String orgName) { // Jenkins Pipeline 的入口方法通常命名为 call + // 定义管道参数变量,用于在不同阶段间传递配置信息。初始化为 null。 + def pipelineParams = null + // 声明服务类变量,将在后续阶段初始化 + def resolverService = null // 配置解析服务实例 + def deploymentService = null // 部署服务实例 + def stageService = null // 阶段服务实例 + def notificationService = null // 通知服务实例 + // Jenkins代理标签,指定流水线特定阶段在哪个代理节点上运行 + def jenkinsAgent = '' + + // 启动流水线定义。这是 Declarative Pipeline 的语法开始 + pipeline { + // 指定流水线执行代理,any表示可在任何可用代理上运行。 + // 注意:这里定义的 agent 会被后面 stage 中定义的 agent 覆盖。 + agent any + + // 流水线全局配置选项块 + options { + // 构建历史保留策略,用于管理 Jenkins 服务器磁盘空间 + buildDiscarder(logRotator( + artifactDaysToKeepStr: '', // 构建产物保留天数(空字符串表示不按天数限制,可能使用数量限制) + artifactNumToKeepStr: '2', // 保留最近2个构建的产物(如生成的jar包、docker镜像等) + daysToKeepStr: '7', // 构建记录元数据(日志、状态等)保留7天 + numToKeepStr: '3' // 保留最近3个构建记录(无论天数) + )) + // 全局超时设置:整个流水线最多运行2小时,超时则自动终止,防止无限运行 + timeout(time: 2, unit: 'HOURS') + // 还可以添加其他选项,例如: disableConcurrentBuilds() 防止并行运行等 + } + + // 流水线阶段定义块,包含按顺序执行的一系列阶段 (stages) + stages { + /** + * 第一阶段:初始化配置和环境 + * 加载环境配置、解析参数、初始化服务实例。 + * 此阶段仍在 pipeline 顶部定义的 agent any 上运行。 + */ + stage('Setup Parameters & ENV') { + steps { + // 执行Git检出操作 (checkout SCM),获取流水线配置文件或所需的配置仓库 + checkout scmGit([ // scmGit 是一种 checkout 方式 + // 指定要检出的分支,这里检出 master 分支 + branches: [[name: '*/master']], + // 配置远程仓库信息,包括认证和URL + userRemoteConfigs: [[ + credentialsId: 'c9b5eae2-5df5-485f-bebd-9cd5393b03e1', // 存储在 Jenkins 中的 Git 认证凭据ID + url: 'http://192.168.10.102:3000/cicd/jenkins-pipeline-files.git' // 配置仓库的URL地址 + ]], + // 扩展配置:指定检出到非默认工作目录的子目录中,避免与后续可能检出的应用代码冲突 + extensions: [[ + $class: 'RelativeTargetDirectory', // Jenkins checkout 扩展的类名,用于设置相对目标目录 + relativeTargetDir: '.pipeline-files' // 将配置仓库检出到工作空间下的 .pipeline-files 目录 + ]], + ]) + script { + echo "=== 开始初始化配置 ===" + echo "组织名称: ${orgName}" // 打印传入的组织名称参数 + try { // 使用 try-catch 进行异常处理,确保配置失败时能妥善处理 + // 初始化阶段服务实例,this 代表当前的流水线脚本对象,用于访问 Jenkins 步骤 + stageService = new StageService(this) + // 记录 '初始化配置' 阶段的开始时间 + stageService.recordStageStart('初始化配置') + + // 初始化配置解析服务实例,传入当前脚本对象和组织名称 + resolverService = new ResolverService(this, orgName) + // 调用解析服务的方法,加载所有配置文件(如环境相关的配置和主配置) + resolverService.loadConfigurations() + // 调用解析服务的方法,解析加载的配置并设置管道运行所需的参数集合 + // 返回的 pipelineParams 可能包含 Maven 设置、镜像仓库地址、目标服务器配置、环境变量等 + pipelineParams = resolverService.setupParameters() + + // 处理环境 profile 选择逻辑 + // params.PROFILES 可能是一个通过 Jenkins 输入参数(parameters {} 块)传入的值 + // 如果用户选择了 profile,则使用它;否则使用配置文件中定义的默认 profile (pipelineParams.PROFILES) + def selectedProfile = params.PROFILES ?: pipelineParams.PROFILES + pipelineParams.PROFILES = selectedProfile // 将最终确定的 profile 设置回参数Map中 + + // 设置后续构建和部署阶段使用的 Jenkins 代理标签 + // 优先使用配置文件中指定的代理标签,如果未配置则默认使用 'master' 节点(Jenkins Controller) + jenkinsAgent = pipelineParams?.jenkinsAgent ?: 'master' + echo "本次使用代理节点: ${jenkinsAgent}" + + // 初始化部署服务实例,传入当前脚本对象和从配置中获取的环境配置 + deploymentService = new DeploymentService(this, pipelineParams.envConfig) + + // 初始化消息通知服务实例,传入当前脚本对象、管道参数和阶段服务实例 + notificationService = new NotificationService(this, pipelineParams, stageService) + + // ✅ 关键步骤:将配置文件 stash 起来 + stash name: 'pipeline-files', includes: '.pipeline-files/**' + + // 记录 '初始化配置' 阶段的成功结束 + stageService.recordStageEnd('初始化配置', 'SUCCESS') + + } catch (Exception e) { // 捕获初始化过程中可能抛出的任何异常 + // 打印错误信息到流水线日志 + echo "配置初始化失败: ${e.message}" + // 记录 '初始化配置' 阶段的失败结束 + stageService.recordStageEnd('初始化配置', 'FAILURE') + // 抛出错误,终止整个流水线的执行,并标记为 FAILURE 状态 + error("管道初始化失败: " + e.getMessage()) + } + + echo "=== 结束初始化配置 ===" + } + } + } + + /** + * 第二阶段:代码构建 + * 执行Maven构建(编译、测试、打包),生成可部署的JAR包和Docker镜像。 + * 此阶段在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行,以分担主节点负载或使用特定环境。 + */ + stage('Build') { + // 指定在此阶段使用的 Jenkins 代理节点标签 + agent { + label jenkinsAgent // 使用前面计算得到的代理标签 + } + steps { + script { + echo "=== 🔨 开始代码构建 ===" + // ✅ 关键步骤:在节点上 unstash 配置文件 + unstash 'pipeline-files' + // 记录 '构建镜像' 阶段的开始时间 + stageService.recordStageStart('构建镜像') + // 调用部署服务的方法,执行具体的构建逻辑 + // 该方法内部可能包含:mvn clean compile/test/package、docker build/push 等步骤 + deploymentService.executeBuildStage(pipelineParams) + // 记录 '构建镜像' 阶段的成功结束 + stageService.recordStageEnd('构建镜像', 'SUCCESS') + echo "=== 🔨 完成代码构建 ===" + } + } + } + + /** + * 第三阶段:容器构建阶段 + * 负责将JAR包构建为Docker镜像,并推送到镜像仓库 + * 此阶段同样在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行。 + */ + stage('Container Build') { + agent { + label jenkinsAgent + } + steps { + script { + echo "=== 🐳 开始容器构建 ===" + // ✅ 关键步骤:在节点上 unstash 配置文件 + unstash 'pipeline-files' + // 记录 '应用部署' 阶段的开始时间 + stageService.recordStageStart('容器构建') + // 调用部署服务的方法,执行具体的容器构建逻辑 + deploymentService.executeContainerBuildStage(pipelineParams) + // 记录 '应用部署' 阶段的成功结束 + stageService.recordStageEnd('容器构建', 'SUCCESS') + echo "=== 🚀 完成容器构建 ===" + } + } + } + + /** + * 第四阶段:应用部署 + * 将Docker镜像部署到目标服务器(如测试、预生产、生产环境)。 + * 此阶段同样在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行。 + */ + stage('Deploy') { + // 指定在此阶段使用的 Jenkins 代理节点标签 + agent { + label jenkinsAgent + } + steps { + script { + echo "=== 🚀 开始应用部署 ===" + // ✅ 关键步骤:在节点上 unstash 配置文件 + unstash 'pipeline-files' + // 记录 '应用部署' 阶段的开始时间 + stageService.recordStageStart('应用部署') + // 调用部署服务的方法,执行具体的部署逻辑 + // 该方法内部可能包含:通过 SSH 连接到目标服务器、执行部署脚本、更新容器(docker compose/k8s)等 + deploymentService.executeDeployStage(pipelineParams) + // 记录 '应用部署' 阶段的成功结束 + stageService.recordStageEnd('应用部署', 'SUCCESS') + echo "=== 🚀 完成应用部署 ===" + } + } + } + + /** + * 第五阶段:健康检查 + * 负责验证部署的应用是否正常运行,确保部署的服务的可用性和功能性 + * 这是CI/CD流程中的重要质量关卡,防止有缺陷的部署进入生产环境 + */ + stage('Health Check') { + agent { + // 使用配置文件中指定的代理标签,如果未配置则默认使用'master'节点 + // 健康检查可能需要访问部署环境网络或使用特定工具 + label jenkinsAgent + } + steps { + script { + echo "=== 🏥 开始健康检查 ===" + // ✅ 关键步骤:在节点上 unstash 配置文件 + unstash 'pipeline-files' + // 记录健康检查阶段的开始时间和状态 + stageService.recordStageStart('健康检查') + + try { + // 调用部署服务的健康检查方法,执行具体的验证逻辑 + // deploymentService.executeHealthCheckStage(pipelineParams) + + // 🏗️ 以下是健康检查可能包含的具体操作(示例): + + // 1. 🔗 服务端点连通性测试 + // - 检查应用的健康检查端点(如:/actuator/health)是否返回200状态码 + // - 验证关键API接口是否可访问 + + // 2. 📊 应用状态验证 + // - 检查应用日志是否有启动错误 + // - 验证数据库连接是否正常建立 + // - 确认必要的服务依赖(如Redis、MQ等)是否连接成功 + + // 3. 🔍 功能冒烟测试 + // - 执行一组最基本的业务操作测试,确保核心功能正常 + // - 验证配置是否正确加载(如配置文件、环境变量等) + + // 4. 📈 性能基线检查 + // - 检查应用启动时间是否在可接受范围内 + // - 验证内存占用、线程数等指标是否正常 + + // 5. 🐳 容器运行状态检查 + // - 确认Docker容器处于Running状态 + // - 检查容器资源使用情况(CPU、内存) + + // 调用部署服务的健康检查阶段逻辑 + // deploymentService.executeHealthCheckStage(pipelineParams) + + // 记录健康检查阶段的成功结束 + stageService.recordStageEnd('健康检查', 'SUCCESS') + + } catch (Exception e) { + // 健康检查失败时的处理 + echo "健康检查失败: ${e.message}" + // 记录健康检查阶段的失败状态 + stageService.recordStageEnd('健康检查', 'FAILURE') + // 可以选择抛出错误使整个流水线失败,或者标记为UNSTABLE + error("健康检查未通过,部署可能存在风险: " + e.getMessage()) + } + + echo "=== 🏥 完成健康检查 ===" + } + } + } + + // 可以在此处添加更多阶段,例如:自动化测试、集成测试、性能测试、安全扫描等 + } + + /** + * 流水线后处理块 (post section) + * 根据流水线的最终状态执行不同的操作,无论构建成功与否都会执行的处理逻辑。 + * 此部分在 pipeline 顶部定义的 agent any 上运行。 + */ + post { + /** + * always:无论构建结果如何(成功、失败、中止、不稳定)都会执行 + * 主要用于资源清理和状态报告等必须完成的操作 + */ + always { + echo "=== 管道执行完成 ===" + echo "完成时间: ${new Date()}" // 打印流水线结束的时间 + // 清理工作空间,删除当前流水线运行产生的所有文件,释放磁盘空间 + cleanWs() + script { + echo "构建完成,状态: ${currentBuild.currentResult}" // 打印最终构建状态 + // 调用阶段服务的方法,收集所有阶段的执行信息(开始时间、持续时间、状态),可能用于通知或报告 + stageService.collectAllStagesInfo() + } + } + + /** + * success:只有当整个流水线的状态为 SUCCESS 时执行 + * 主要用于成功通知和结果报告 + */ + success { + echo "✅ 部署成功!" + script { + // 调用通知服务的方法,发送构建成功的通知(如邮件、钉钉、Slack等) + notificationService.sendBuildSuccess() + } + } + + /** + * failure:只有当整个流水线的状态为 FAILURE 时执行 + * 主要用于错误报告、通知和可能的清理或回滚操作(虽然此处未展示回滚) + */ + failure { + echo "❌ 部署失败!" + script { + // 调用通知服务的方法,发送构建失败的通知,并传递错误信息 + // 这里的 errorMsg 和 extraInfo 是示例,实际可能从捕获的异常中获取 + notificationService.sendBuildFailure( + errorMsg: "单元测试失败", // 示例错误消息 + extraInfo: [stage: "test", errorDetails: "具体错误信息"] // 示例额外信息 + ) + } + } + + /** + * aborted:只有当整个流水线的状态为 ABORTED 时执行(通常由用户手动中止) + * 用于通知构建被中止的情况 + */ + aborted { + echo "❌ 构建中止!" + script { + // 调用通知服务的方法,发送构建中止的通知 + notificationService.sendBuildAborted( + // 传递一些额外信息,例如中止原因和操作人(如果环境变量可用) + extraInfo: [中止原因: '手动中止', 操作人: env.USER_ID] + ) + } + } + + /** + * unstable:只有当整个流水线的状态为 UNSTABLE 时执行(通常由测试失败或有警告导致) + * 主要用于警告通知 + */ + unstable { + echo "⚠️ 管道执行不稳定" + script { + // 调用通知服务的方法,发送构建不稳定的通知 + notificationService.sendBuildUnstable( + extraInfo: ["不稳定原因": "测试未通过或存在警告"] // 示例原因 + ) + } + } + } + } +} \ No newline at end of file diff --git a/vars/pipelineK8SService.groovy b/vars/pipelineK8SService.groovy new file mode 100644 index 0000000..e69de29 diff --git a/vars/pipelineNexusService.groovy b/vars/pipelineNexusService.groovy new file mode 100644 index 0000000..e69de29 diff --git a/vars/pipelineNodeService.groovy b/vars/pipelineNodeService.groovy new file mode 100644 index 0000000..ed94a07 --- /dev/null +++ b/vars/pipelineNodeService.groovy @@ -0,0 +1,354 @@ +// 导入在 Pipeline 中需要使用的共享库类 +// 这些类通常在 Jenkins 的共享库 (Shared Libraries) 中定义,提供了可重用的功能 +import com.node.ResolverService +import com.node.DeploymentService +import com.common.StageService +import com.common.NotificationService + +/** + * 主管道入口点 + * 提供完整的CI/CD流水线功能,实现从配置加载到部署的全流程自动化 + * 这是一个 Jenkins 共享库 (Shared Library) 中的全局变量 (Global Variable) 或直接在 Pipeline 脚本中定义的方法 + * + * @param orgName 组织名称,用于标识不同的组织配置和环境。这是调用此流水线时必须传入的主要参数。 + * + * 功能特性: + * - 动态配置加载:支持根据 orgName 加载多环境配置管理 + * - 多阶段流水线:清晰的构建、测试、部署、验证流程 + * - 错误恢复机制:完善的异常处理和回滚功能 (虽然在当前代码示例中未完全展示,但框架支持) + * - 资源清理:自动清理工作空间和临时资源,避免磁盘空间耗尽 + * - 通知机制:构建结果通知(成功、失败、中止、不稳定) + * + * 示例用法: + * @Library ('share-library@master')_ + * orgName = 'xiaomayi' + * pipelineJDKService(orgName) + */ +def call(String orgName) { // Jenkins Pipeline 的入口方法通常命名为 call + // 定义管道参数变量,用于在不同阶段间传递配置信息。初始化为 null。 + def pipelineParams = null + // 声明服务类变量,将在后续阶段初始化 + def resolverService = null // 配置解析服务实例 + def deploymentService = null // 部署服务实例 + def stageService = null // 阶段服务实例 + def notificationService = null // 通知服务实例 + // Jenkins代理标签,指定流水线特定阶段在哪个代理节点上运行 + def jenkinsAgent = '' + + // 启动流水线定义。这是 Declarative Pipeline 的语法开始 + pipeline { + // 指定流水线执行代理,any表示可在任何可用代理上运行。 + // 注意:这里定义的 agent 会被后面 stage 中定义的 agent 覆盖。 + agent any + + // 流水线全局配置选项块 + options { + // 构建历史保留策略,用于管理 Jenkins 服务器磁盘空间 + buildDiscarder(logRotator( + artifactDaysToKeepStr: '', // 构建产物保留天数(空字符串表示不按天数限制,可能使用数量限制) + artifactNumToKeepStr: '2', // 保留最近2个构建的产物(如生成的jar包、docker镜像等) + daysToKeepStr: '7', // 构建记录元数据(日志、状态等)保留7天 + numToKeepStr: '3' // 保留最近3个构建记录(无论天数) + )) + // 全局超时设置:整个流水线最多运行2小时,超时则自动终止,防止无限运行 + timeout(time: 2, unit: 'HOURS') + // 还可以添加其他选项,例如: disableConcurrentBuilds() 防止并行运行等 + } + + // 流水线阶段定义块,包含按顺序执行的一系列阶段 (stages) + stages { + /** + * 第一阶段:初始化配置和环境 + * 加载环境配置、解析参数、初始化服务实例。 + * 此阶段仍在 pipeline 顶部定义的 agent any 上运行。 + */ + stage('Setup Parameters & ENV') { + steps { + // 执行Git检出操作 (checkout SCM),获取流水线配置文件或所需的配置仓库 + checkout scmGit([ // scmGit 是一种 checkout 方式 + // 指定要检出的分支,这里检出 master 分支 + branches: [[name: '*/master']], + // 配置远程仓库信息,包括认证和URL + userRemoteConfigs: [[ + credentialsId: 'c9b5eae2-5df5-485f-bebd-9cd5393b03e1', // 存储在 Jenkins 中的 Git 认证凭据ID + url: 'http://192.168.10.102:3000/cicd/jenkins-pipeline-files.git' // 配置仓库的URL地址 + ]], + // 扩展配置:指定检出到非默认工作目录的子目录中,避免与后续可能检出的应用代码冲突 + extensions: [[ + $class: 'RelativeTargetDirectory', // Jenkins checkout 扩展的类名,用于设置相对目标目录 + relativeTargetDir: '.pipeline-files' // 将配置仓库检出到工作空间下的 .pipeline-files 目录 + ]], + ]) + script { + echo "=== 开始初始化配置 ===" + echo "组织名称: ${orgName}" // 打印传入的组织名称参数 + try { // 使用 try-catch 进行异常处理,确保配置失败时能妥善处理 + // 初始化阶段服务实例,this 代表当前的流水线脚本对象,用于访问 Jenkins 步骤 + stageService = new StageService(this) + // 记录 '初始化配置' 阶段的开始时间 + stageService.recordStageStart('初始化配置') + + // 初始化配置解析服务实例,传入当前脚本对象和组织名称 + resolverService = new ResolverService(this, orgName) + // 调用解析服务的方法,加载所有配置文件(如环境相关的配置和主配置) + resolverService.loadConfigurations() + // 调用解析服务的方法,解析加载的配置并设置管道运行所需的参数集合 + // 返回的 pipelineParams 可能包含 Maven 设置、镜像仓库地址、目标服务器配置、环境变量等 + pipelineParams = resolverService.setupParameters() + + // 处理环境 profile 选择逻辑 + // params.PROFILES 可能是一个通过 Jenkins 输入参数(parameters {} 块)传入的值 + // 如果用户选择了 profile,则使用它;否则使用配置文件中定义的默认 profile (pipelineParams.PROFILES) + def selectedProfile = params.PROFILES ?: pipelineParams.PROFILES + pipelineParams.PROFILES = selectedProfile // 将最终确定的 profile 设置回参数Map中 + + // 设置后续构建和部署阶段使用的 Jenkins 代理标签 + // 优先使用配置文件中指定的代理标签,如果未配置则默认使用 'master' 节点(Jenkins Controller) + jenkinsAgent = pipelineParams?.jenkinsAgent ?: 'master' + echo "本次使用代理节点: ${jenkinsAgent}" + + // 初始化部署服务实例,传入当前脚本对象和从配置中获取的环境配置 + deploymentService = new DeploymentService(this, pipelineParams.envConfig) + + // 初始化消息通知服务实例,传入当前脚本对象、管道参数和阶段服务实例 + notificationService = new NotificationService(this, pipelineParams, stageService) + + // 记录 '初始化配置' 阶段的成功结束 + stageService.recordStageEnd('初始化配置', 'SUCCESS') + + } catch (Exception e) { // 捕获初始化过程中可能抛出的任何异常 + // 打印错误信息到流水线日志 + echo "配置初始化失败: ${e.message}" + // 记录 '初始化配置' 阶段的失败结束 + stageService.recordStageEnd('初始化配置', 'FAILURE') + // 抛出错误,终止整个流水线的执行,并标记为 FAILURE 状态 + error("管道初始化失败: " + e.getMessage()) + } + + echo "=== 结束初始化配置 ===" + } + } + } + + /** + * 第二阶段:代码构建 + * 执行Maven构建(编译、测试、打包),生成可部署的JAR包和Docker镜像。 + * 此阶段在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行,以分担主节点负载或使用特定环境。 + */ + stage('Build') { + // 指定在此阶段使用的 Jenkins 代理节点标签 + agent { + label jenkinsAgent // 使用前面计算得到的代理标签 + } + steps { + script { + echo "=== 🔨 开始代码构建 ===" + // 记录 '构建镜像' 阶段的开始时间 + stageService.recordStageStart('构建镜像') + // 调用部署服务的方法,执行具体的构建逻辑 + // 该方法内部可能包含:mvn clean compile/test/package、docker build/push 等步骤 + deploymentService.executeBuildStage(pipelineParams) + // 记录 '构建镜像' 阶段的成功结束 + stageService.recordStageEnd('构建镜像', 'SUCCESS') + echo "=== 🔨 完成代码构建 ===" + } + } + } + + // /** + // * 第三阶段:容器构建阶段 + // * 负责将JAR包构建为Docker镜像,并推送到镜像仓库 + // * 此阶段同样在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行。 + // */ + // stage('Container Build') { + // agent { + // label jenkinsAgent + // } + // steps { + // script { + // echo "=== 🐳 开始容器构建 ===" + // // 记录 '应用部署' 阶段的开始时间 + // stageService.recordStageStart('容器构建') + // // 调用部署服务的方法,执行具体的容器构建逻辑 + // deploymentService.executeContainerBuildStage(pipelineParams) + // // 记录 '应用部署' 阶段的成功结束 + // stageService.recordStageEnd('容器构建', 'SUCCESS') + // echo "=== 🚀 完成容器构建 ===" + // } + // } + // } + + /** + * 第四阶段:应用部署 + * 将Docker镜像部署到目标服务器(如测试、预生产、生产环境)。 + * 此阶段同样在指定的 Jenkins 代理节点 (jenkinsAgent) 上运行。 + */ + stage('Deploy') { + // 指定在此阶段使用的 Jenkins 代理节点标签 + agent { + label jenkinsAgent + } + steps { + script { + echo "=== 🚀 开始应用部署 ===" + // 记录 '应用部署' 阶段的开始时间 + stageService.recordStageStart('应用部署') + // 调用部署服务的方法,执行具体的部署逻辑 + // 该方法内部可能包含:通过 SSH 连接到目标服务器、执行部署脚本、更新容器(docker compose/k8s)等 + deploymentService.executeDeployStage(pipelineParams) + // 记录 '应用部署' 阶段的成功结束 + stageService.recordStageEnd('应用部署', 'SUCCESS') + echo "=== 🚀 完成应用部署 ===" + } + } + } + + /** + * 第五阶段:健康检查 + * 负责验证部署的应用是否正常运行,确保部署的服务的可用性和功能性 + * 这是CI/CD流程中的重要质量关卡,防止有缺陷的部署进入生产环境 + */ + stage('Health Check') { + agent { + // 使用配置文件中指定的代理标签,如果未配置则默认使用'master'节点 + // 健康检查可能需要访问部署环境网络或使用特定工具 + label jenkinsAgent + } + steps { + script { + echo "=== 🏥 开始健康检查 ===" + + // 记录健康检查阶段的开始时间和状态 + stageService.recordStageStart('健康检查') + + try { + // 调用部署服务的健康检查方法,执行具体的验证逻辑 + // deploymentService.executeHealthCheckStage(pipelineParams) + + // 🏗️ 以下是健康检查可能包含的具体操作(示例): + + // 1. 🔗 服务端点连通性测试 + // - 检查应用的健康检查端点(如:/actuator/health)是否返回200状态码 + // - 验证关键API接口是否可访问 + + // 2. 📊 应用状态验证 + // - 检查应用日志是否有启动错误 + // - 验证数据库连接是否正常建立 + // - 确认必要的服务依赖(如Redis、MQ等)是否连接成功 + + // 3. 🔍 功能冒烟测试 + // - 执行一组最基本的业务操作测试,确保核心功能正常 + // - 验证配置是否正确加载(如配置文件、环境变量等) + + // 4. 📈 性能基线检查 + // - 检查应用启动时间是否在可接受范围内 + // - 验证内存占用、线程数等指标是否正常 + + // 5. 🐳 容器运行状态检查 + // - 确认Docker容器处于Running状态 + // - 检查容器资源使用情况(CPU、内存) + + // 调用部署服务的健康检查阶段逻辑 + // deploymentService.executeHealthCheckStage(pipelineParams) + + // 记录健康检查阶段的成功结束 + stageService.recordStageEnd('健康检查', 'SUCCESS') + + } catch (Exception e) { + // 健康检查失败时的处理 + echo "健康检查失败: ${e.message}" + // 记录健康检查阶段的失败状态 + stageService.recordStageEnd('健康检查', 'FAILURE') + // 可以选择抛出错误使整个流水线失败,或者标记为UNSTABLE + error("健康检查未通过,部署可能存在风险: " + e.getMessage()) + } + + echo "=== 🏥 完成健康检查 ===" + } + } + } + + // 可以在此处添加更多阶段,例如:自动化测试、集成测试、性能测试、安全扫描等 + } + + /** + * 流水线后处理块 (post section) + * 根据流水线的最终状态执行不同的操作,无论构建成功与否都会执行的处理逻辑。 + * 此部分在 pipeline 顶部定义的 agent any 上运行。 + */ + post { + /** + * always:无论构建结果如何(成功、失败、中止、不稳定)都会执行 + * 主要用于资源清理和状态报告等必须完成的操作 + */ + always { + echo "=== 管道执行完成 ===" + echo "完成时间: ${new Date()}" // 打印流水线结束的时间 + // 清理工作空间,删除当前流水线运行产生的所有文件,释放磁盘空间 + cleanWs() + script { + echo "构建完成,状态: ${currentBuild.currentResult}" // 打印最终构建状态 + // 调用阶段服务的方法,收集所有阶段的执行信息(开始时间、持续时间、状态),可能用于通知或报告 + stageService.collectAllStagesInfo() + } + } + + /** + * success:只有当整个流水线的状态为 SUCCESS 时执行 + * 主要用于成功通知和结果报告 + */ + success { + echo "✅ 部署成功!" + script { + // 调用通知服务的方法,发送构建成功的通知(如邮件、钉钉、Slack等) + notificationService.sendBuildSuccess() + } + } + + /** + * failure:只有当整个流水线的状态为 FAILURE 时执行 + * 主要用于错误报告、通知和可能的清理或回滚操作(虽然此处未展示回滚) + */ + failure { + echo "❌ 部署失败!" + script { + // 调用通知服务的方法,发送构建失败的通知,并传递错误信息 + // 这里的 errorMsg 和 extraInfo 是示例,实际可能从捕获的异常中获取 + notificationService.sendBuildFailure( + errorMsg: "单元测试失败", // 示例错误消息 + extraInfo: [stage: "test", errorDetails: "具体错误信息"] // 示例额外信息 + ) + } + } + + /** + * aborted:只有当整个流水线的状态为 ABORTED 时执行(通常由用户手动中止) + * 用于通知构建被中止的情况 + */ + aborted { + echo "❌ 构建中止!" + script { + // 调用通知服务的方法,发送构建中止的通知 + notificationService.sendBuildAborted( + // 传递一些额外信息,例如中止原因和操作人(如果环境变量可用) + extraInfo: [中止原因: '手动中止', 操作人: env.USER_ID] + ) + } + } + + /** + * unstable:只有当整个流水线的状态为 UNSTABLE 时执行(通常由测试失败或有警告导致) + * 主要用于警告通知 + */ + unstable { + echo "⚠️ 管道执行不稳定" + script { + // 调用通知服务的方法,发送构建不稳定的通知 + notificationService.sendBuildUnstable( + extraInfo: ["不稳定原因": "测试未通过或存在警告"] // 示例原因 + ) + } + } + } + } +} \ No newline at end of file diff --git a/vars/pipelinePythonService.groovy b/vars/pipelinePythonService.groovy new file mode 100644 index 0000000..e69de29