From 813dbf1fdd9cd9282c2d2551fc2fe5a87bf05dd0 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 2 May 2026 22:47:55 +0800 Subject: [PATCH] Persist notification recipients in Jenkins credentials --- .../PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md | 4 +- jenkins/Jenkinsfile.production-api-build | 2 +- jenkins/Jenkinsfile.production-api-deploy | 2 +- .../Jenkinsfile.production-database-export | 2 +- .../Jenkinsfile.production-database-import | 2 +- ...nkinsfile.production-full-build-and-deploy | 2 +- jenkins/Jenkinsfile.production-notify-email | 52 ++++++++++++------- .../Jenkinsfile.production-server-provision | 2 +- .../Jenkinsfile.production-stdb-module-build | 2 +- ...Jenkinsfile.production-stdb-module-publish | 2 +- jenkins/Jenkinsfile.production-web-build | 2 +- jenkins/Jenkinsfile.production-web-deploy | 2 +- 12 files changed, 44 insertions(+), 32 deletions(-) diff --git a/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md b/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md index 79b1221e..5db4d93b 100644 --- a/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md +++ b/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md @@ -260,9 +260,9 @@ Jenkins controller 与 Linux agent 看到的 Git 服务地址不同,必须拆 发布流水线通过 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 失败不能反向改变业务流水线结果,只在来源流水线日志中记录触发失败。 +邮件通知的持久收件人不写入 Git,由 Jenkins `Secret text` 凭据 `genarrative-notification-emails` 保存,凭据内容为逗号分隔邮箱。所有生产流水线仍提供 `NOTIFICATION_EMAILS` 参数作为本次运行的追加收件人;通知 Job 会把持久收件人凭据与本次 `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 持久化配置,不作为仓库文件提交。 +持久收件人在 Jenkins controller 的 `Manage Jenkins` -> `Credentials` -> `System` -> `Global credentials` 中新增 `Secret text` 凭据,`ID` 固定为 `genarrative-notification-emails`,`Secret` 填 `ops@example.com,dev@example.com` 这类逗号分隔邮箱。SMTP 服务器在 `Manage Jenkins` -> `System` 的 `E-mail Notification` 区域配置。邮件地址属于 Jenkins 持久化配置,不作为仓库文件提交。 所有发布流水线必须提供 `DEPLOY_TARGET` 参数,用于选择逻辑部署目标: diff --git a/jenkins/Jenkinsfile.production-api-build b/jenkins/Jenkinsfile.production-api-build index 814e88c6..a76f354a 100644 --- a/jenkins/Jenkinsfile.production-api-build +++ b/jenkins/Jenkinsfile.production-api-build @@ -23,7 +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 合并发送') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 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') diff --git a/jenkins/Jenkinsfile.production-api-deploy b/jenkins/Jenkinsfile.production-api-deploy index ddaa74d8..7d227b6d 100644 --- a/jenkins/Jenkinsfile.production-api-deploy +++ b/jenkins/Jenkinsfile.production-api-deploy @@ -16,7 +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: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 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: '要复制归档产物的上游构建号') diff --git a/jenkins/Jenkinsfile.production-database-export b/jenkins/Jenkinsfile.production-database-export index e7298075..cda064f7 100644 --- a/jenkins/Jenkinsfile.production-database-export +++ b/jenkins/Jenkinsfile.production-database-export @@ -16,7 +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: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 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') diff --git a/jenkins/Jenkinsfile.production-database-import b/jenkins/Jenkinsfile.production-database-import index f130a2d3..d866f5ec 100644 --- a/jenkins/Jenkinsfile.production-database-import +++ b/jenkins/Jenkinsfile.production-database-import @@ -16,7 +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: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 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') diff --git a/jenkins/Jenkinsfile.production-full-build-and-deploy b/jenkins/Jenkinsfile.production-full-build-and-deploy index a2789212..a8139dc4 100644 --- a/jenkins/Jenkinsfile.production-full-build-and-deploy +++ b/jenkins/Jenkinsfile.production-full-build-and-deploy @@ -20,7 +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: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 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 构建流水线作业名') diff --git a/jenkins/Jenkinsfile.production-notify-email b/jenkins/Jenkinsfile.production-notify-email index b1d535de..9a6ee36c 100644 --- a/jenkins/Jenkinsfile.production-notify-email +++ b/jenkins/Jenkinsfile.production-notify-email @@ -10,7 +10,8 @@ pipeline { } parameters { - string(name: 'EMAIL_RECIPIENTS', defaultValue: '', description: '本次运行追加邮件通知收件人;会与 Jenkins 全局环境变量 GENARRATIVE_NOTIFICATION_EMAILS 合并发送') + string(name: 'EMAIL_RECIPIENTS_CREDENTIAL_ID', defaultValue: 'genarrative-notification-emails', description: '持久收件人 Secret Text 凭据 ID,凭据内容为逗号分隔邮箱;留空则只使用本次追加收件人') + string(name: 'EMAIL_RECIPIENTS', defaultValue: '', description: '本次运行追加邮件通知收件人;会与持久收件人凭据合并发送') string(name: 'SOURCE_JOB_NAME', defaultValue: '', description: '来源流水线名称') string(name: 'SOURCE_BUILD_NUMBER', defaultValue: '', description: '来源构建号') string(name: 'SOURCE_BUILD_URL', defaultValue: '', description: '来源构建 URL') @@ -27,26 +28,27 @@ pipeline { 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 sendNotification = { persistedRecipients -> + def recipientList = [] + [persistedRecipients, 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 recipients = recipientList.join(',') + if (!recipients) { + echo '[notify-email] 持久收件人凭据与 EMAIL_RECIPIENTS 均未配置,跳过邮件发送。' + 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 流水线执行结果 + 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} @@ -60,8 +62,18 @@ pipeline { 摘要: ${params.SUMMARY ?: ''} """ - mail to: recipients, subject: subject, body: body - echo "[notify-email] 已发送邮件: recipients=${recipients}, source=${jobName} #${buildNumber}, result=${result}" + mail to: recipients, subject: subject, body: body + echo "[notify-email] 已发送邮件: recipients=${recipients}, source=${jobName} #${buildNumber}, result=${result}" + } + + def credentialId = params.EMAIL_RECIPIENTS_CREDENTIAL_ID?.trim() + if (credentialId) { + withCredentials([string(credentialsId: credentialId, variable: 'PERSISTED_EMAIL_RECIPIENTS')]) { + sendNotification(env.PERSISTED_EMAIL_RECIPIENTS) + } + } else { + sendNotification('') + } } } } diff --git a/jenkins/Jenkinsfile.production-server-provision b/jenkins/Jenkinsfile.production-server-provision index e2bac04e..e6bfc81a 100644 --- a/jenkins/Jenkinsfile.production-server-provision +++ b/jenkins/Jenkinsfile.production-server-provision @@ -14,7 +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 合并发送') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 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: '部署脚本来源分支') diff --git a/jenkins/Jenkinsfile.production-stdb-module-build b/jenkins/Jenkinsfile.production-stdb-module-build index d8f1e4a7..17aec0c2 100644 --- a/jenkins/Jenkinsfile.production-stdb-module-build +++ b/jenkins/Jenkinsfile.production-stdb-module-build @@ -23,7 +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 合并发送') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 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') diff --git a/jenkins/Jenkinsfile.production-stdb-module-publish b/jenkins/Jenkinsfile.production-stdb-module-publish index 5d5721d8..da51c059 100644 --- a/jenkins/Jenkinsfile.production-stdb-module-publish +++ b/jenkins/Jenkinsfile.production-stdb-module-publish @@ -16,7 +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: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 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: '要复制归档产物的上游构建号') diff --git a/jenkins/Jenkinsfile.production-web-build b/jenkins/Jenkinsfile.production-web-build index 0cf66b92..ff5aa5c1 100644 --- a/jenkins/Jenkinsfile.production-web-build +++ b/jenkins/Jenkinsfile.production-web-build @@ -17,7 +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 合并发送') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 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 发布流水线作业名') diff --git a/jenkins/Jenkinsfile.production-web-deploy b/jenkins/Jenkinsfile.production-web-deploy index 146ee6cd..c2d9db86 100644 --- a/jenkins/Jenkinsfile.production-web-deploy +++ b/jenkins/Jenkinsfile.production-web-deploy @@ -16,7 +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: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 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: '要复制归档产物的上游构建号')