Files
Genarrative/jenkins/Jenkinsfile.production-stdb-module-publish
T
k88936 a03f13d0a5
Project CI / Repository checks (push) Successful in 1m28s
Project CI / Frontend tests (push) Successful in 3m19s
Project CI / Backend tests (push) Successful in 4m3s
Project CI / Native shell tests (push) Successful in 13m58s
Fix/动作 前端展示下载+后端数据结构+精选 的问题 (#117)
原问题

```
历史角色动作是一个兼容例外:
后端允许它没有 editor_project_resource 资源行,只要布局自身保存了完整图片序列帧。
```

- 把生成动作的原视频asstetkind改为 video, 只把图片序列作为action
- db asset新增字段image_sequence_frames_json image_seq_duration_ms等, (原来动作的这些数据存在generation-input中,并不合适)
修改了externaljob,把这部分数据正确填写到数据库中。
- 为避免到处fallback,做了数据库迁移, 脚本: `scripts/spacetime-normalize-editor-character-actions.mjs`   手动进行过画布+素材库中动作序列帧+原始视频 迁移的测试

- 移除preview_video_path字段,  reason:
>preview video
    → separate resource/asset with assetKind="video"
  final transparent action
    → assetKind="character-animation"
    → source_resource_id points to the preview-video resource
    → image_sequence_frames_json contains the actual playable result
>
> So preview_video_path on the final action duplicates the source video resource’s image_src.
- 移除每个frame的index字段 原因: 这个只用于后端内部处理时有一个并发请求, 每个赋一个index方便收集, 后续没有再用到,且与数组本身重复

- 清除副产品preview video的generation_input_json,  因为会影响改造功能, 迁移后预览视频不提供改造(参数), 只有序列帧动作有改造

仍存在的共性问题: #134

- 导出:
下载改为完整序列帧, 封面不再作为fallback, 部分帧读取失败时行为:仍生成 ZIP,并记录失败帧, 不变

画布放置:
before:
![shotmd-1785207959-compressed.webp](/attachments/e56454f0-49a3-4a8a-951f-c21960975f99)
after:
![shotmd-1785231365-compressed.webp](/attachments/886f07df-c25b-4ecc-acc9-160e1795a991)
精选模块: 主页展示, 审核部分UI:
![shotmd-1785500046-compressed.webp](/attachments/9ec3b679-0045-494a-9f20-134f7e73fecf)
![shotmd-1785499780-compressed.webp](/attachments/42396e63-5f26-4853-a367-0f3c15111add)
这两处序列帧们的加载设计为惰式的, 只有hover和单独preview才会全部加载

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/117
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-08-06 16:57:38 +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-normalize-editor-character-actions.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-mode,async/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}"
}
}
}