Files
Genarrative/jenkins/Jenkinsfile.production-stdb-module-publish
T
kdletters b49bf9c984 结构化图片画布持久化并补充历史资源修复
将旧布局上限统一提升至2MiB并接入revision CAS保存
新增画布图层、生成对话框和迁移状态结构化存储
新增migration operator保护的回填、激活和回滚脚本
新增历史图片映射及音频资源恢复的定向dry-run/apply流程
补齐前端保存队列、OpenAPI、Bindings、发布归档与回归测试
2026-07-20 20:45:15 +08:00

193 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
pipeline {
agent none
options {
disableConcurrentBuilds()
skipDefaultCheckout(true)
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
}
parameters {
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
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 Secret Text 凭据 genarrative-notification-emails 合并发送')
string(name: 'MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID', defaultValue: 'genarrative-spacetime-bootstrap-secret-dev-file', description: '必填:与目标 wasm 一致的 Jenkins Secret File 凭据 ID;仅在发布时受保护挂载')
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: '要复制归档产物的上游构建号')
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: 'http://127.0.0.1:3101', description: '显式 SpacetimeDB server URL,填写后优先于 SPACETIME_SERVER')
string(name: 'SPACETIME_ROOT_DIR', defaultValue: '/stdb', description: 'spacetime CLI root-dir;需与自托管 spacetimedb.service 一致')
string(name: 'SPACETIME_RUN_AS_USER', defaultValue: 'spacetimedb', description: '执行 spacetime publish 的本机用户,默认使用自托管服务用户')
string(name: 'API_ENV_FILE', defaultValue: '/etc/genarrative/api-server.env', description: '需补齐 runtime bootstrap secret FILE 的 api-server 环境文件')
string(name: 'WORKER_ENV_FILE', defaultValue: '/etc/genarrative/external-generation-worker.env', description: '需补齐 runtime bootstrap secret FILE 的 worker 环境文件;文件不存在时跳过')
booleanParam(name: 'KEEP_MAINTENANCE_MODE', defaultValue: false, description: '发布 module 后保持维护模式并停止旧 API/controller/worker,等待受控维护和后续 API deploy')
choice(name: 'DATABASE_BACKUP_MODE', choices: ['async', 'sync', 'skip'], description: '数据库备份策略:async 在 publish 前生成本地冷备份、后台上传 OSS;sync 在 publish 前等待上传完成且失败阻断;skip 跳过')
}
stages {
stage('Prepare') {
agent {
label 'linux && genarrative-build'
}
steps {
script {
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
error('release 部署需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。当前 Linux 开发/构建/开发部署 agent 不能执行 release 部署。')
}
if (!params.BUILD_VERSION?.trim()) {
error('BUILD_VERSION 不能为空。')
}
if (params.BUILD_VERSION != params.BUILD_VERSION.trim() || !(params.BUILD_VERSION ==~ /^[A-Za-z0-9][A-Za-z0-9._-]*$/)) {
error("BUILD_VERSION 只能包含字母、数字、点、下划线和短横线,且必须以字母或数字开头: ${params.BUILD_VERSION}")
}
if (!params.BUILD_JOB_NAME?.trim()) {
error('BUILD_JOB_NAME 不能为空。')
}
if (!params.BUILD_NUMBER_TO_DEPLOY?.trim()) {
error('BUILD_NUMBER_TO_DEPLOY 不能为空。')
}
if (!params.MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID?.trim()) {
error('MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID 必须引用受保护的 Jenkins Secret File 凭据。')
}
if (!params.DATABASE?.trim()) {
error('DATABASE 不能为空。')
}
if (!params.SPACETIME_SERVER?.trim() && !params.SPACETIME_SERVER_URL?.trim()) {
error('SPACETIME_SERVER 与 SPACETIME_SERVER_URL 不能同时为空。')
}
def spacetimeRootDir = params.SPACETIME_ROOT_DIR?.trim() ? params.SPACETIME_ROOT_DIR.trim() : '/stdb'
if (!(spacetimeRootDir ==~ /^\/(?!.*\.\.)[A-Za-z0-9._\/-]+$/)) {
error("SPACETIME_ROOT_DIR 必须是 Linux 绝对路径且不能包含 ..: ${spacetimeRootDir}")
}
def spacetimeRunAsUser = params.SPACETIME_RUN_AS_USER?.trim()
if (spacetimeRunAsUser && !(spacetimeRunAsUser ==~ /^[A-Za-z_][A-Za-z0-9_-]*$/)) {
error("SPACETIME_RUN_AS_USER 只能是本机用户名: ${spacetimeRunAsUser}")
}
[API_ENV_FILE: params.API_ENV_FILE, WORKER_ENV_FILE: params.WORKER_ENV_FILE].each { name, value ->
def envFile = value?.trim()
if (!envFile || envFile == '/' || !(envFile ==~ /^\/(?!.*\.\.)[A-Za-z0-9._\/-]+$/)) {
error("${name} 必须是 Linux 绝对路径且不能包含 ..: ${envFile}")
}
}
def spacetimeServerUrl = params.SPACETIME_SERVER_URL?.trim()
if (spacetimeServerUrl && !(spacetimeServerUrl ==~ /^https?:\/\/[A-Za-z0-9._:-]+$/)) {
error("SPACETIME_SERVER_URL 只能是 http(s) URL,且不能包含路径或 shell 特殊字符: ${spacetimeServerUrl}")
}
def spacetimeServer = params.SPACETIME_SERVER?.trim()
if (!spacetimeServerUrl && spacetimeServer && !(spacetimeServer ==~ /^[A-Za-z0-9._:-]+$/)) {
error("SPACETIME_SERVER 只能包含字母、数字、点、下划线、冒号和短横线: ${spacetimeServer}")
}
}
}
}
stage('Fetch Artifact') {
agent {
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-dev-deploy' : 'linux && genarrative-release-deploy'}"
}
steps {
copyArtifacts(
projectName: params.BUILD_JOB_NAME,
selector: specific(params.BUILD_NUMBER_TO_DEPLOY),
filter: "build/${params.BUILD_VERSION}/spacetime_module.wasm,build/${params.BUILD_VERSION}/spacetime_module.wasm.sha256,build/${params.BUILD_VERSION}/release-manifest.json,scripts/deploy/production-stdb-publish.sh,scripts/deploy/production-runtime-writer-identity-rotate.mjs,scripts/deploy/maintenance-on.sh,scripts/deploy/maintenance-off.sh,scripts/spacetime-migration-common.mjs,scripts/spacetime-maintain-external-generation-jobs.mjs,scripts/spacetime-migrate-editor-canvas-layout.mjs,scripts/spacetime-repair-editor-canvas-resources.mjs,scripts/database-backup-to-oss.mjs",
target: '.',
fingerprintArtifacts: true
)
}
}
stage('Publish Stdb Module') {
agent {
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-dev-deploy' : 'linux && genarrative-release-deploy'}"
}
steps {
script {
def keepMaintenanceArg = params.KEEP_MAINTENANCE_MODE ? '--keep-maintenance-mode' : ''
def backupMode = params.DATABASE_BACKUP_MODE?.trim() ? params.DATABASE_BACKUP_MODE.trim() : 'async'
if (!(backupMode in ['async', 'sync', 'skip'])) {
error("DATABASE_BACKUP_MODE 只能是 async、sync 或 skip: ${backupMode}")
}
def publishScriptPath = 'scripts/deploy/production-stdb-publish.sh'
def publishScriptText = readFile(file: publishScriptPath, encoding: 'UTF-8')
def publishScriptSupportsBackupMode = publishScriptText.contains('--backup-mode')
def backupArg = ''
if (publishScriptSupportsBackupMode) {
backupArg = "--backup-mode \"${backupMode}\""
} else if (backupMode == 'sync') {
error('当前工作区的 scripts/deploy/production-stdb-publish.sh 还不支持 --backup-mode,无法保证 sync 语义。请先更新工作区脚本后再运行。')
} else {
echo "[Jenkins] 当前工作区脚本还不支持 --backup-modeasync/skip 自动降级为 --skip-backup,避免参数不兼容导致发布失败。"
backupArg = '--skip-backup'
}
def rootArg = "--root-dir \"${params.SPACETIME_ROOT_DIR?.trim() ? params.SPACETIME_ROOT_DIR.trim() : '/stdb'}\""
def runAsArg = params.SPACETIME_RUN_AS_USER?.trim()
? "--run-as-user \"${params.SPACETIME_RUN_AS_USER.trim()}\""
: ''
def serverArg = params.SPACETIME_SERVER_URL?.trim()
? "--server-url \"${params.SPACETIME_SERVER_URL.trim()}\""
: "--server \"${params.SPACETIME_SERVER}\""
withCredentials([
file(credentialsId: params.MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID.trim(), variable: 'MIGRATION_BOOTSTRAP_SECRET_FILE')
]) {
sh """
bash -lc '
set -euo pipefail
chmod +x scripts/deploy/production-stdb-publish.sh scripts/deploy/production-runtime-writer-identity-rotate.mjs scripts/deploy/maintenance-on.sh scripts/deploy/maintenance-off.sh
scripts/deploy/production-stdb-publish.sh \\
--source-dir "build/${params.BUILD_VERSION}" \\
--database "${params.DATABASE}" \\
${rootArg} \\
${runAsArg} \\
${serverArg} \\
--migration-bootstrap-secret-file "\${MIGRATION_BOOTSTRAP_SECRET_FILE:?MIGRATION_BOOTSTRAP_SECRET_FILE 不能为空}" \\
--api-env-file "${params.API_ENV_FILE}" \\
--worker-env-file "${params.WORKER_ENV_FILE}" \\
${keepMaintenanceArg} \\
${backupArg}
'
"""
}
}
}
}
}
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}"
}
}
}