diff --git a/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md b/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md index a1de6b16..79b1221e 100644 --- a/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md +++ b/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md @@ -30,6 +30,7 @@ - `jenkins/Jenkinsfile.production-server-provision` - `jenkins/Jenkinsfile.production-database-export` - `jenkins/Jenkinsfile.production-database-import` +- `jenkins/Jenkinsfile.production-notify-email` - `npm run build:production-release` 旧 Jenkins 一体化发布链对应的 Jenkinsfile 已从仓库移除,生产构建和发布入口统一切到 `jenkins/Jenkinsfile.production-*`。`scripts/deploy-rust-remote.sh` 等旧发布包脚本暂保留为历史迁移参考,不再作为生产 Jenkins Job 的入口。 @@ -237,6 +238,7 @@ Jenkins controller 与 Linux agent 看到的 Git 服务地址不同,必须拆 8. `Genarrative-Database-Export` 9. `Genarrative-Database-Import` 10. `Genarrative-Full-Build-And-Deploy` +11. `Genarrative-Notify-Email` 已落地的生产流水线脚本文件: @@ -250,12 +252,17 @@ Jenkins controller 与 Linux agent 看到的 Git 服务地址不同,必须拆 - `jenkins/Jenkinsfile.production-server-provision` - `jenkins/Jenkinsfile.production-database-export` - `jenkins/Jenkinsfile.production-database-import` +- `jenkins/Jenkinsfile.production-notify-email` `Genarrative-Database-Export`、`Genarrative-Database-Import` 的生产版 Jenkinsfile 已落地;旧的数据库导入导出 Jenkinsfile 已删除,避免继续沿用旧部署目录和旧一体化发布链假设。 构建流水线运行在当前 Linux agent 的脱敏 label expression `linux && genarrative-build`。发布、导入导出和服务器配置流水线通过 `DEPLOY_TARGET` 映射到 Linux-only 脱敏部署表达式;其中 `development` 映射到当前 Linux 开发/构建/开发部署 agent 的 `linux && genarrative-build`,`release` 映射到独立 Linux 生产部署 agent 的 `linux && genarrative-release-deploy`,不能复用当前开发/构建/开发部署 agent。真实机器名、IP 和带 IP 的 Jenkins label 只允许留在 Jenkins 节点连接配置中,不能暴露为 Job 参数默认值、调度标签或文档推荐值。 -发布流水线通过 Jenkins `copyArtifacts(...)` 从对应构建 Job 获取归档产物,因此 Jenkins 需要安装并启用 Copy Artifact 插件。数据库导入流水线的手动上传模式使用 `stashedFile` 文件参数,因此 Jenkins 还需要安装并启用 File Parameter 插件。所有生产 Pipeline 日志必须带时间戳以便审计,Jenkins 需要安装 Timestamper 插件,并在全局配置中启用 `Enabled for all Pipeline builds`。生产发布不能退回到读取构建 workspace 本地目录的旧模式。 +发布流水线通过 Jenkins `copyArtifacts(...)` 从对应构建 Job 获取归档产物,因此 Jenkins 需要安装并启用 Copy Artifact 插件。数据库导入流水线的手动上传模式使用 `stashedFile` 文件参数,因此 Jenkins 还需要安装并启用 File Parameter 插件。所有生产 Pipeline 日志必须带时间戳以便审计,Jenkins 需要安装 Timestamper 插件,并在全局配置中启用 `Enabled for all Pipeline builds`。邮件通知流水线使用 Jenkins Pipeline `mail` step,Jenkins 需要安装/启用 Mailer 能力,并在系统配置中配置 SMTP。生产发布不能退回到读取构建 workspace 本地目录的旧模式。 + +邮件通知的持久收件人不写入 Git,由 Jenkins 全局环境变量 `GENARRATIVE_NOTIFICATION_EMAILS` 保存,多个邮箱用逗号分隔。所有生产流水线仍提供 `NOTIFICATION_EMAILS` 参数作为本次运行的追加收件人;通知 Job 会把 `GENARRATIVE_NOTIFICATION_EMAILS` 与本次 `NOTIFICATION_EMAILS` 合并去重后发送,参数留空时只发送给全局持久收件人。流水线结束时在 `post { always { ... } }` 中异步触发 `Genarrative-Notify-Email`,把来源 Job、构建号、构建 URL、结果、源码分支、源码 commit、发布版本、部署目标和数据库名传给通知 Job。通知 Job 失败不能反向改变业务流水线结果,只在来源流水线日志中记录触发失败。 + +`GENARRATIVE_NOTIFICATION_EMAILS` 在 Jenkins controller 的 `Manage Jenkins` -> `System` -> `Global properties` -> `Environment variables` 中配置,例如 `ops@example.com,dev@example.com`。SMTP 服务器在同一页面的 `E-mail Notification` 区域配置。该全局变量属于 Jenkins 持久化配置,不作为仓库文件提交。 所有发布流水线必须提供 `DEPLOY_TARGET` 参数,用于选择逻辑部署目标: @@ -547,6 +554,8 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module - [x] 更新旧部署文档,标记旧一体化脚本为废弃或迁移对象。 - [x] `jenkins/Jenkinsfile.production-database-export` - [x] `jenkins/Jenkinsfile.production-database-import` +- [x] `jenkins/Jenkinsfile.production-notify-email` +- [x] 所有生产流水线通过 `NOTIFICATION_EMAILS` 参数在结束时触发 `Genarrative-Notify-Email` ## 参考 diff --git a/jenkins/Jenkinsfile.production-api-build b/jenkins/Jenkinsfile.production-api-build index 51be065e..814e88c6 100644 --- a/jenkins/Jenkinsfile.production-api-build +++ b/jenkins/Jenkinsfile.production-api-build @@ -23,6 +23,7 @@ pipeline { string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支,默认 master 最新提交') string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit') string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins 全局环境变量 GENARRATIVE_NOTIFICATION_EMAILS 合并发送') booleanParam(name: 'PUBLISH_AFTER_BUILD', defaultValue: false, description: '构建成功后是否触发 API 发布') string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Api-Deploy', description: 'API 发布流水线作业名') choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: 'PUBLISH_AFTER_BUILD=true 时的逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent') @@ -97,6 +98,7 @@ pipeline { string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH), string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT), string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION), + string(name: 'NOTIFICATION_EMAILS', value: params.NOTIFICATION_EMAILS ?: ''), string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET), booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT), string(name: 'BUILD_JOB_NAME', value: env.JOB_NAME), @@ -107,6 +109,34 @@ pipeline { } post { + always { + script { + def notificationParameters = [ + string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME), + string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER), + string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''), + string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'), + string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''), + string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT ?: (params.COMMIT_HASH ?: '')), + string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION ?: (params.BUILD_VERSION ?: '')), + string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET ?: ''), + string(name: 'DATABASE', value: params.DATABASE ?: ''), + string(name: 'SUMMARY', value: 'API 构建流水线结束'), + ] + def notificationRecipients = params.NOTIFICATION_EMAILS?.trim() + if (notificationRecipients) { + notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients)) + } + try { + build job: 'Genarrative-Notify-Email', + wait: false, + propagate: false, + parameters: notificationParameters + } catch (error) { + echo "邮件通知触发失败: ${error.message}" + } + } + } success { echo "API 构建完成: version=${env.EFFECTIVE_BUILD_VERSION}, commit=${env.SOURCE_COMMIT}" } diff --git a/jenkins/Jenkinsfile.production-api-deploy b/jenkins/Jenkinsfile.production-api-deploy index d9310fd1..ddaa74d8 100644 --- a/jenkins/Jenkinsfile.production-api-deploy +++ b/jenkins/Jenkinsfile.production-api-deploy @@ -16,6 +16,7 @@ pipeline { booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机') string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '部署脚本来源分支') string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit;上游触发时传实际构建 commit') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins 全局环境变量 GENARRATIVE_NOTIFICATION_EMAILS 合并发送') string(name: 'BUILD_VERSION', defaultValue: '', description: '待发布版本号') string(name: 'BUILD_JOB_NAME', defaultValue: 'Genarrative-Api-Build', description: 'API 构建流水线作业名') string(name: 'BUILD_NUMBER_TO_DEPLOY', defaultValue: '', description: '要复制归档产物的上游构建号') @@ -112,6 +113,34 @@ pipeline { } post { + always { + script { + def notificationParameters = [ + string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME), + string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER), + string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''), + string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'), + string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''), + string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT ?: (params.COMMIT_HASH ?: '')), + string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION ?: (params.BUILD_VERSION ?: '')), + string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET ?: ''), + string(name: 'DATABASE', value: params.DATABASE ?: ''), + string(name: 'SUMMARY', value: 'API 发布流水线结束'), + ] + def notificationRecipients = params.NOTIFICATION_EMAILS?.trim() + if (notificationRecipients) { + notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients)) + } + try { + build job: 'Genarrative-Notify-Email', + wait: false, + propagate: false, + parameters: notificationParameters + } catch (error) { + echo "邮件通知触发失败: ${error.message}" + } + } + } success { echo "API 发布完成: version=${params.BUILD_VERSION}" } diff --git a/jenkins/Jenkinsfile.production-database-export b/jenkins/Jenkinsfile.production-database-export index 2a54712b..e7298075 100644 --- a/jenkins/Jenkinsfile.production-database-export +++ b/jenkins/Jenkinsfile.production-database-export @@ -16,6 +16,7 @@ pipeline { booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机') string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '导出脚本来源分支') string(name: 'COMMIT_HASH', defaultValue: '', description: '导出脚本来源 commit') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins 全局环境变量 GENARRATIVE_NOTIFICATION_EMAILS 合并发送') string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: 'SpacetimeDB database') string(name: 'SPACETIME_SERVER', defaultValue: 'local', description: 'SpacetimeDB server alias') string(name: 'SPACETIME_SERVER_URL', defaultValue: '', description: '显式 SpacetimeDB server URL,填写后优先于 SPACETIME_SERVER') @@ -191,6 +192,34 @@ pipeline { } post { + always { + script { + def notificationParameters = [ + string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME), + string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER), + string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''), + string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'), + string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''), + string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT ?: (params.COMMIT_HASH ?: '')), + string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION ?: (params.BUILD_VERSION ?: '')), + string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET ?: ''), + string(name: 'DATABASE', value: params.DATABASE ?: ''), + string(name: 'SUMMARY', value: '数据库导出流水线结束'), + ] + def notificationRecipients = params.NOTIFICATION_EMAILS?.trim() + if (notificationRecipients) { + notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients)) + } + try { + build job: 'Genarrative-Notify-Email', + wait: false, + propagate: false, + parameters: notificationParameters + } catch (error) { + echo "邮件通知触发失败: ${error.message}" + } + } + } success { echo "数据库导出完成: target=${params.DEPLOY_TARGET}, database=${params.DATABASE}, file=${env.EFFECTIVE_EXPORT_NAME}" } diff --git a/jenkins/Jenkinsfile.production-database-import b/jenkins/Jenkinsfile.production-database-import index 1e7d3c43..f130a2d3 100644 --- a/jenkins/Jenkinsfile.production-database-import +++ b/jenkins/Jenkinsfile.production-database-import @@ -16,6 +16,7 @@ pipeline { booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机') string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '导入脚本来源分支') string(name: 'COMMIT_HASH', defaultValue: '', description: '导入脚本来源 commit') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins 全局环境变量 GENARRATIVE_NOTIFICATION_EMAILS 合并发送') string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: 'SpacetimeDB database') string(name: 'SPACETIME_SERVER', defaultValue: 'local', description: 'SpacetimeDB server alias') string(name: 'SPACETIME_SERVER_URL', defaultValue: '', description: '显式 SpacetimeDB server URL,填写后优先于 SPACETIME_SERVER') @@ -308,6 +309,34 @@ pipeline { } post { + always { + script { + def notificationParameters = [ + string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME), + string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER), + string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''), + string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'), + string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''), + string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT ?: (params.COMMIT_HASH ?: '')), + string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION ?: (params.BUILD_VERSION ?: '')), + string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET ?: ''), + string(name: 'DATABASE', value: params.DATABASE ?: ''), + string(name: 'SUMMARY', value: '数据库导入流水线结束'), + ] + def notificationRecipients = params.NOTIFICATION_EMAILS?.trim() + if (notificationRecipients) { + notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients)) + } + try { + build job: 'Genarrative-Notify-Email', + wait: false, + propagate: false, + parameters: notificationParameters + } catch (error) { + echo "邮件通知触发失败: ${error.message}" + } + } + } success { echo "数据库导入流水线完成: target=${params.DEPLOY_TARGET}, database=${params.DATABASE}, dryRun=${params.DRY_RUN}" } diff --git a/jenkins/Jenkinsfile.production-full-build-and-deploy b/jenkins/Jenkinsfile.production-full-build-and-deploy index cb369b4d..a2789212 100644 --- a/jenkins/Jenkinsfile.production-full-build-and-deploy +++ b/jenkins/Jenkinsfile.production-full-build-and-deploy @@ -20,6 +20,7 @@ pipeline { string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit') string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER') booleanParam(name: 'RUN_NPM_CI', defaultValue: true, description: 'Web 构建前是否执行 npm ci') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins 全局环境变量 GENARRATIVE_NOTIFICATION_EMAILS 合并发送') string(name: 'WEB_BUILD_JOB_NAME', defaultValue: 'Genarrative-Web-Build', description: 'Web 构建流水线作业名') string(name: 'API_BUILD_JOB_NAME', defaultValue: 'Genarrative-Api-Build', description: 'API 构建流水线作业名') string(name: 'STDB_BUILD_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Build', description: 'Stdb 构建流水线作业名') @@ -79,6 +80,7 @@ pipeline { string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH), string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT), string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION), + string(name: 'NOTIFICATION_EMAILS', value: params.NOTIFICATION_EMAILS ?: ''), booleanParam(name: 'RUN_NPM_CI', value: params.RUN_NPM_CI), ] env.WEB_BUILD_NUMBER = webRun.number.toString() @@ -95,6 +97,7 @@ pipeline { string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH), string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT), string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION), + string(name: 'NOTIFICATION_EMAILS', value: params.NOTIFICATION_EMAILS ?: ''), ] env.API_BUILD_NUMBER = apiRun.number.toString() } @@ -110,6 +113,7 @@ pipeline { string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH), string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT), string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION), + string(name: 'NOTIFICATION_EMAILS', value: params.NOTIFICATION_EMAILS ?: ''), string(name: 'DATABASE', value: params.DATABASE), ] env.STDB_BUILD_NUMBER = stdbRun.number.toString() @@ -128,6 +132,7 @@ pipeline { string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH), string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT), string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION), + string(name: 'NOTIFICATION_EMAILS', value: params.NOTIFICATION_EMAILS ?: ''), string(name: 'DATABASE', value: params.DATABASE), string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET), booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT), @@ -146,6 +151,7 @@ pipeline { string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH), string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT), string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION), + string(name: 'NOTIFICATION_EMAILS', value: params.NOTIFICATION_EMAILS ?: ''), string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET), booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT), string(name: 'BUILD_JOB_NAME', value: params.API_BUILD_JOB_NAME), @@ -163,6 +169,7 @@ pipeline { string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH), string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT), string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION), + string(name: 'NOTIFICATION_EMAILS', value: params.NOTIFICATION_EMAILS ?: ''), string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET), booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT), string(name: 'BUILD_JOB_NAME', value: params.WEB_BUILD_JOB_NAME), @@ -173,6 +180,34 @@ pipeline { } post { + always { + script { + def notificationParameters = [ + string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME), + string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER), + string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''), + string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'), + string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''), + string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT ?: (params.COMMIT_HASH ?: '')), + string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION ?: (params.BUILD_VERSION ?: '')), + string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET ?: ''), + string(name: 'DATABASE', value: params.DATABASE ?: ''), + string(name: 'SUMMARY', value: '全量构建发布编排结束'), + ] + def notificationRecipients = params.NOTIFICATION_EMAILS?.trim() + if (notificationRecipients) { + notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients)) + } + try { + build job: 'Genarrative-Notify-Email', + wait: false, + propagate: false, + parameters: notificationParameters + } catch (error) { + echo "邮件通知触发失败: ${error.message}" + } + } + } success { echo "Full Build-And-Deploy 完成: version=${env.EFFECTIVE_BUILD_VERSION}, commit=${env.SOURCE_COMMIT}" } diff --git a/jenkins/Jenkinsfile.production-notify-email b/jenkins/Jenkinsfile.production-notify-email new file mode 100644 index 00000000..b1d535de --- /dev/null +++ b/jenkins/Jenkinsfile.production-notify-email @@ -0,0 +1,69 @@ +pipeline { + agent { + label 'linux && genarrative-build' + } + + options { + disableConcurrentBuilds() + skipDefaultCheckout(true) + buildDiscarder(logRotator(numToKeepStr: '50', artifactNumToKeepStr: '0')) + } + + parameters { + string(name: 'EMAIL_RECIPIENTS', defaultValue: '', description: '本次运行追加邮件通知收件人;会与 Jenkins 全局环境变量 GENARRATIVE_NOTIFICATION_EMAILS 合并发送') + string(name: 'SOURCE_JOB_NAME', defaultValue: '', description: '来源流水线名称') + string(name: 'SOURCE_BUILD_NUMBER', defaultValue: '', description: '来源构建号') + string(name: 'SOURCE_BUILD_URL', defaultValue: '', description: '来源构建 URL') + string(name: 'SOURCE_RESULT', defaultValue: 'UNKNOWN', description: '来源流水线结果') + string(name: 'SOURCE_BRANCH', defaultValue: '', description: '源码分支') + string(name: 'SOURCE_COMMIT', defaultValue: '', description: '源码 commit') + string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号') + string(name: 'DEPLOY_TARGET', defaultValue: '', description: '部署目标') + string(name: 'DATABASE', defaultValue: '', description: 'SpacetimeDB database') + string(name: 'SUMMARY', defaultValue: '', description: '补充摘要') + } + + stages { + stage('Send Email') { + steps { + script { + def recipientList = [] + [env.GENARRATIVE_NOTIFICATION_EMAILS, params.EMAIL_RECIPIENTS].each { rawRecipients -> + rawRecipients?.split(',')?.each { recipient -> + def normalized = recipient.trim() + if (normalized && !recipientList.contains(normalized)) { + recipientList.add(normalized) + } + } + } + def recipients = recipientList.join(',') + if (!recipients) { + echo '[notify-email] EMAIL_RECIPIENTS 与 GENARRATIVE_NOTIFICATION_EMAILS 均未配置,跳过邮件发送。' + return + } + + def result = params.SOURCE_RESULT?.trim() ?: 'UNKNOWN' + def jobName = params.SOURCE_JOB_NAME?.trim() ?: 'unknown-job' + def buildNumber = params.SOURCE_BUILD_NUMBER?.trim() ?: 'unknown-build' + def subject = "[Genarrative][${result}] ${jobName} #${buildNumber}" + def body = """Genarrative Jenkins 流水线执行结果 + +结果: ${result} +流水线: ${jobName} +构建号: ${buildNumber} +构建 URL: ${params.SOURCE_BUILD_URL ?: ''} +源码分支: ${params.SOURCE_BRANCH ?: ''} +源码 commit: ${params.SOURCE_COMMIT ?: ''} +发布版本: ${params.BUILD_VERSION ?: ''} +部署目标: ${params.DEPLOY_TARGET ?: ''} +数据库: ${params.DATABASE ?: ''} +摘要: ${params.SUMMARY ?: ''} +""" + + mail to: recipients, subject: subject, body: body + echo "[notify-email] 已发送邮件: recipients=${recipients}, source=${jobName} #${buildNumber}, result=${result}" + } + } + } + } +} diff --git a/jenkins/Jenkinsfile.production-server-provision b/jenkins/Jenkinsfile.production-server-provision index 54690824..e2bac04e 100644 --- a/jenkins/Jenkinsfile.production-server-provision +++ b/jenkins/Jenkinsfile.production-server-provision @@ -14,6 +14,7 @@ pipeline { parameters { choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent') booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins 全局环境变量 GENARRATIVE_NOTIFICATION_EMAILS 合并发送') booleanParam(name: 'CONFIRM_PROVISION', defaultValue: false, description: '确认执行服务器初始化;未勾选时只允许 dry-run') booleanParam(name: 'DRY_RUN', defaultValue: true, description: '只打印将执行的服务器初始化命令,不写入系统配置') string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '部署脚本来源分支') @@ -372,6 +373,34 @@ pipeline { } post { + always { + script { + def notificationParameters = [ + string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME), + string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER), + string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''), + string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'), + string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''), + string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT ?: (params.COMMIT_HASH ?: '')), + string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION ?: (params.BUILD_VERSION ?: '')), + string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET ?: ''), + string(name: 'DATABASE', value: params.DATABASE ?: ''), + string(name: 'SUMMARY', value: '服务器初始化流水线结束'), + ] + def notificationRecipients = params.NOTIFICATION_EMAILS?.trim() + if (notificationRecipients) { + notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients)) + } + try { + build job: 'Genarrative-Notify-Email', + wait: false, + propagate: false, + parameters: notificationParameters + } catch (error) { + echo "邮件通知触发失败: ${error.message}" + } + } + } success { echo "Server provision 完成: target=${params.DEPLOY_TARGET}, dryRun=${params.DRY_RUN}, nginxConfigMode=${params.NGINX_CONFIG_MODE}" } diff --git a/jenkins/Jenkinsfile.production-stdb-module-build b/jenkins/Jenkinsfile.production-stdb-module-build index d2c72113..d8f1e4a7 100644 --- a/jenkins/Jenkinsfile.production-stdb-module-build +++ b/jenkins/Jenkinsfile.production-stdb-module-build @@ -23,6 +23,7 @@ pipeline { string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支,默认 master 最新提交') string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit') string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins 全局环境变量 GENARRATIVE_NOTIFICATION_EMAILS 合并发送') booleanParam(name: 'PUBLISH_AFTER_BUILD', defaultValue: false, description: '构建成功后是否触发 Stdb module 发布') string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Publish', description: 'Stdb module 发布流水线作业名') choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: 'PUBLISH_AFTER_BUILD=true 时的逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent') @@ -98,6 +99,7 @@ pipeline { string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH), string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT), string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION), + string(name: 'NOTIFICATION_EMAILS', value: params.NOTIFICATION_EMAILS ?: ''), string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET), booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT), string(name: 'BUILD_JOB_NAME', value: env.JOB_NAME), @@ -109,6 +111,34 @@ pipeline { } post { + always { + script { + def notificationParameters = [ + string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME), + string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER), + string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''), + string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'), + string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''), + string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT ?: (params.COMMIT_HASH ?: '')), + string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION ?: (params.BUILD_VERSION ?: '')), + string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET ?: ''), + string(name: 'DATABASE', value: params.DATABASE ?: ''), + string(name: 'SUMMARY', value: 'Stdb module 构建流水线结束'), + ] + def notificationRecipients = params.NOTIFICATION_EMAILS?.trim() + if (notificationRecipients) { + notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients)) + } + try { + build job: 'Genarrative-Notify-Email', + wait: false, + propagate: false, + parameters: notificationParameters + } catch (error) { + echo "邮件通知触发失败: ${error.message}" + } + } + } success { echo "Stdb module 构建完成: version=${env.EFFECTIVE_BUILD_VERSION}, commit=${env.SOURCE_COMMIT}" } diff --git a/jenkins/Jenkinsfile.production-stdb-module-publish b/jenkins/Jenkinsfile.production-stdb-module-publish index 4465aea5..5d5721d8 100644 --- a/jenkins/Jenkinsfile.production-stdb-module-publish +++ b/jenkins/Jenkinsfile.production-stdb-module-publish @@ -16,6 +16,7 @@ pipeline { booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机') string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '部署脚本来源分支') string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit;上游触发时传实际构建 commit') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins 全局环境变量 GENARRATIVE_NOTIFICATION_EMAILS 合并发送') string(name: 'BUILD_VERSION', defaultValue: '', description: '待发布版本号') string(name: 'BUILD_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Build', description: 'Stdb module 构建流水线作业名') string(name: 'BUILD_NUMBER_TO_DEPLOY', defaultValue: '', description: '要复制归档产物的上游构建号') @@ -115,6 +116,34 @@ pipeline { } post { + always { + script { + def notificationParameters = [ + string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME), + string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER), + string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''), + string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'), + string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''), + string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT ?: (params.COMMIT_HASH ?: '')), + string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION ?: (params.BUILD_VERSION ?: '')), + string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET ?: ''), + string(name: 'DATABASE', value: params.DATABASE ?: ''), + string(name: 'SUMMARY', value: 'Stdb module 发布流水线结束'), + ] + def notificationRecipients = params.NOTIFICATION_EMAILS?.trim() + if (notificationRecipients) { + notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients)) + } + try { + build job: 'Genarrative-Notify-Email', + wait: false, + propagate: false, + parameters: notificationParameters + } catch (error) { + echo "邮件通知触发失败: ${error.message}" + } + } + } success { echo "Stdb module 发布完成: version=${params.BUILD_VERSION}, database=${params.DATABASE}" } diff --git a/jenkins/Jenkinsfile.production-web-build b/jenkins/Jenkinsfile.production-web-build index b6452fe0..0cf66b92 100644 --- a/jenkins/Jenkinsfile.production-web-build +++ b/jenkins/Jenkinsfile.production-web-build @@ -17,6 +17,7 @@ pipeline { string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支,默认 master 最新提交') string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit') string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins 全局环境变量 GENARRATIVE_NOTIFICATION_EMAILS 合并发送') booleanParam(name: 'RUN_NPM_CI', defaultValue: true, description: '构建前是否执行 npm ci') booleanParam(name: 'PUBLISH_AFTER_BUILD', defaultValue: false, description: '构建成功后是否触发 Web 发布') string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Web-Deploy', description: 'Web 发布流水线作业名') @@ -87,6 +88,7 @@ pipeline { string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH), string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT), string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION), + string(name: 'NOTIFICATION_EMAILS', value: params.NOTIFICATION_EMAILS ?: ''), string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET), booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT), string(name: 'BUILD_JOB_NAME', value: env.JOB_NAME), @@ -97,6 +99,34 @@ pipeline { } post { + always { + script { + def notificationParameters = [ + string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME), + string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER), + string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''), + string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'), + string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''), + string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT ?: (params.COMMIT_HASH ?: '')), + string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION ?: (params.BUILD_VERSION ?: '')), + string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET ?: ''), + string(name: 'DATABASE', value: params.DATABASE ?: ''), + string(name: 'SUMMARY', value: 'Web 构建流水线结束'), + ] + def notificationRecipients = params.NOTIFICATION_EMAILS?.trim() + if (notificationRecipients) { + notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients)) + } + try { + build job: 'Genarrative-Notify-Email', + wait: false, + propagate: false, + parameters: notificationParameters + } catch (error) { + echo "邮件通知触发失败: ${error.message}" + } + } + } success { echo "Web 构建完成: version=${env.EFFECTIVE_BUILD_VERSION}, commit=${env.SOURCE_COMMIT}" } diff --git a/jenkins/Jenkinsfile.production-web-deploy b/jenkins/Jenkinsfile.production-web-deploy index 8cb750b5..146ee6cd 100644 --- a/jenkins/Jenkinsfile.production-web-deploy +++ b/jenkins/Jenkinsfile.production-web-deploy @@ -16,6 +16,7 @@ pipeline { booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机') string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '部署脚本来源分支') string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit;上游触发时传实际构建 commit') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins 全局环境变量 GENARRATIVE_NOTIFICATION_EMAILS 合并发送') string(name: 'BUILD_VERSION', defaultValue: '', description: '待发布版本号') string(name: 'BUILD_JOB_NAME', defaultValue: 'Genarrative-Web-Build', description: 'Web 构建流水线作业名') string(name: 'BUILD_NUMBER_TO_DEPLOY', defaultValue: '', description: '要复制归档产物的上游构建号') @@ -110,6 +111,34 @@ pipeline { } post { + always { + script { + def notificationParameters = [ + string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME), + string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER), + string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''), + string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'), + string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''), + string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT ?: (params.COMMIT_HASH ?: '')), + string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION ?: (params.BUILD_VERSION ?: '')), + string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET ?: ''), + string(name: 'DATABASE', value: params.DATABASE ?: ''), + string(name: 'SUMMARY', value: 'Web 发布流水线结束'), + ] + def notificationRecipients = params.NOTIFICATION_EMAILS?.trim() + if (notificationRecipients) { + notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients)) + } + try { + build job: 'Genarrative-Notify-Email', + wait: false, + propagate: false, + parameters: notificationParameters + } catch (error) { + echo "邮件通知触发失败: ${error.message}" + } + } + } success { echo "Web 发布完成: version=${params.BUILD_VERSION}" }