From 01ec7e132c95cc78078d1fbfcd3995e754db55ba Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 29 Apr 2026 11:20:56 +0800 Subject: [PATCH] Add Jenkins database migration pipelines --- ...DATABASE_MIGRATION_PIPELINES_2026-04-29.md | 94 +++++++++++++++ jenkins/Jenkinsfile.database-export | 86 ++++++++++++++ jenkins/Jenkinsfile.database-import | 109 ++++++++++++++++++ 3 files changed, 289 insertions(+) create mode 100644 docs/technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md create mode 100644 jenkins/Jenkinsfile.database-export create mode 100644 jenkins/Jenkinsfile.database-import diff --git a/docs/technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md b/docs/technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md new file mode 100644 index 00000000..05d33663 --- /dev/null +++ b/docs/technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md @@ -0,0 +1,94 @@ +# Jenkins SpacetimeDB 数据库导入导出流水线方案 + +日期:`2026-04-29` + +## 1. 目标 + +为 Jenkins 增加两条人工触发的数据库迁移流水线: + +1. `Genarrative-Database-Export`:调用仓库内 `scripts/spacetime-export-migration-json.mjs`,通过 SpacetimeDB 迁移导出 procedure 生成迁移 JSON,并归档为 Jenkins 产物。 +2. `Genarrative-Database-Import`:调用仓库内 `scripts/spacetime-import-migration-json.mjs`,通过 SpacetimeDB 迁移导入 procedure 导入迁移 JSON,默认只执行 `dry-run`。 + +本方案只编排已有迁移脚本,不在 Jenkinsfile 中重新实现表结构枚举、JSON 解析或 SQL 拼接逻辑。 + +## 2. 执行依据 + +1. SpacetimeDB CLI 调用按仓库技能 `spacetimedb-cli` 执行,数据库调用通过 `spacetime call` 或 HTTP procedure API 完成。 +2. SpacetimeDB 读写语义按 `spacetimedb-concepts` 执行:导入导出能力由模块内 procedure/reducer 负责校验和事务处理,Jenkins 不直接改表。 +3. 迁移脚本复用当前仓库的参数解析与错误处理: + - `scripts/spacetime-export-migration-json.mjs` + - `scripts/spacetime-import-migration-json.mjs` + - `scripts/spacetime-migration-common.mjs` + +## 3. Jenkins 作业 + +### 3.1 数据库导出 + +脚本路径: + +```text +jenkins/Jenkinsfile.database-export +``` + +推荐作业名: + +```text +Genarrative-Database-Export +``` + +关键参数: + +1. `DATABASE`:目标 SpacetimeDB 数据库名;留空时读取仓库环境变量。 +2. `SERVER`:SpacetimeDB server 别名,默认 `maincloud`。 +3. `SERVER_URL`:显式服务地址;填写后优先于 `SERVER`。 +4. `ROOT_DIR`:可选,透传给 `spacetime --root-dir`。 +5. `INCLUDE_TABLES`:可选,逗号分隔的表名白名单。 +6. `OUTPUT_DIRECTORY`:导出文件目录,默认 `database-exports`。 +7. `EXPORT_NAME`:导出文件名;留空时使用 `spacetime-migration-.json`。 + +导出成功后,Jenkins 归档: + +```text +/ +``` + +### 3.2 数据库导入 + +脚本路径: + +```text +jenkins/Jenkinsfile.database-import +``` + +推荐作业名: + +```text +Genarrative-Database-Import +``` + +关键参数: + +1. `INPUT_FILE`:必填,迁移 JSON 文件路径。 +2. `DATABASE`、`SERVER`、`SERVER_URL`、`ROOT_DIR`:与导出流水线一致。 +3. `INCLUDE_TABLES`:可选,只导入指定表。 +4. `DRY_RUN`:默认 `true`,只校验不写入。 +5. `INCREMENTAL`:默认 `true`,跳过已存在或冲突的行。 +6. `REPLACE_EXISTING`:默认 `false`,只覆盖本次迁移文件中涉及的表;不可与 `INCREMENTAL` 同时启用。 +7. `BOOTSTRAP_SECRET`:可选,用于授权临时 Web API identity。 +8. `TOKEN`:可选,SpacetimeDB 客户端连接 token;留空时脚本会自动创建临时 identity 并在结束后撤销。 +9. `NOTE`:迁移授权备注。 + +## 4. 安全边界 + +1. 导入流水线默认 `DRY_RUN=true`,需要人工明确关闭才会写入数据。 +2. `INCREMENTAL` 与 `REPLACE_EXISTING` 互斥,Jenkinsfile 会在执行前阻止同时启用。 +3. Jenkinsfile 不打印 token;生产环境应通过 Jenkins 凭据或目标机器环境变量传入敏感值。 +4. 如果不传 `TOKEN`,导入脚本会创建临时 Web API identity,并调用迁移授权/撤销 procedure 收敛权限窗口。 + +## 5. 文件清单 + +```text +jenkins/Jenkinsfile.database-export +jenkins/Jenkinsfile.database-import +docs/technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md +``` diff --git a/jenkins/Jenkinsfile.database-export b/jenkins/Jenkinsfile.database-export new file mode 100644 index 00000000..ee3ebc48 --- /dev/null +++ b/jenkins/Jenkinsfile.database-export @@ -0,0 +1,86 @@ +pipeline { + agent none + + options { + disableConcurrentBuilds() + timestamps() + } + + parameters { + string(name: 'AGENT_LABEL', defaultValue: 'built-in', description: '执行节点标签') + string(name: 'GENARRATIVE_WORKSPACE_ROOT', defaultValue: '', description: '源码根目录,留空则使用当前 Jenkins 工作区') + string(name: 'DATABASE', defaultValue: '', description: 'SpacetimeDB 数据库名,留空则读取环境变量') + string(name: 'SERVER', defaultValue: 'maincloud', description: 'SpacetimeDB server 别名,例如 maincloud/local/dev') + string(name: 'SERVER_URL', defaultValue: '', description: 'SpacetimeDB server URL,填写后优先于 SERVER') + string(name: 'ROOT_DIR', defaultValue: '', description: 'spacetime CLI root-dir,可选') + string(name: 'INCLUDE_TABLES', defaultValue: '', description: '可选,逗号分隔的表名白名单') + string(name: 'OUTPUT_DIRECTORY', defaultValue: 'database-exports', description: '导出文件目录,相对源码根目录或绝对路径') + string(name: 'EXPORT_NAME', defaultValue: '', description: '导出文件名,留空则自动使用构建号') + } + + stages { + stage('导出数据库') { + agent { + label "${params.AGENT_LABEL}" + } + + steps { + script { + // 允许 Jenkins Job 指定固定源码目录;未指定时使用当前工作区,方便临时手工执行。 + env.WORKSPACE_ROOT = params.GENARRATIVE_WORKSPACE_ROOT?.trim() ? params.GENARRATIVE_WORKSPACE_ROOT.trim() : pwd() + def exportName = params.EXPORT_NAME?.trim() + if (!exportName) { + exportName = "spacetime-migration-${env.BUILD_NUMBER}.json" + } + if (!(exportName ==~ /^[A-Za-z0-9._-]+$/)) { + error("EXPORT_NAME 只能包含字母、数字、点、下划线和短横线,当前值: ${exportName}") + } + env.EFFECTIVE_EXPORT_NAME = exportName + } + + dir("${env.WORKSPACE_ROOT}") { + checkout scm + + sh """ + bash -lc ' + set -euo pipefail + export_dir="${params.OUTPUT_DIRECTORY}" + if [[ -z "\${export_dir}" ]]; then + export_dir="database-exports" + fi + mkdir -p "\${export_dir}" + output_path="\${export_dir}/${env.EFFECTIVE_EXPORT_NAME}" + args=(scripts/spacetime-export-migration-json.mjs --out "\${output_path}") + if [[ -n "${params.DATABASE}" ]]; then + args+=(--database "${params.DATABASE}") + fi + if [[ -n "${params.SERVER}" ]]; then + args+=(--server "${params.SERVER}") + fi + if [[ -n "${params.SERVER_URL}" ]]; then + args+=(--server-url "${params.SERVER_URL}") + fi + if [[ -n "${params.ROOT_DIR}" ]]; then + args+=(--root-dir "${params.ROOT_DIR}") + fi + if [[ -n "${params.INCLUDE_TABLES}" ]]; then + args+=(--include "${params.INCLUDE_TABLES}") + fi + # 复用后端迁移 procedure 导出 JSON,避免 Jenkins 直接拼接表结构和 SQL。 + node "\${args[@]}" + test -s "\${output_path}" + ' + """ + + archiveArtifacts artifacts: "${params.OUTPUT_DIRECTORY ?: 'database-exports'}/${env.EFFECTIVE_EXPORT_NAME}", fingerprint: true + } + } + } + } + + post { + success { + echo "数据库导出完成: ${env.EFFECTIVE_EXPORT_NAME}" + } + } +} diff --git a/jenkins/Jenkinsfile.database-import b/jenkins/Jenkinsfile.database-import new file mode 100644 index 00000000..80347d88 --- /dev/null +++ b/jenkins/Jenkinsfile.database-import @@ -0,0 +1,109 @@ +pipeline { + agent none + + options { + disableConcurrentBuilds() + timestamps() + } + + parameters { + string(name: 'AGENT_LABEL', defaultValue: 'built-in', description: '执行节点标签') + string(name: 'GENARRATIVE_WORKSPACE_ROOT', defaultValue: '', description: '源码根目录,留空则使用当前 Jenkins 工作区') + string(name: 'DATABASE', defaultValue: '', description: 'SpacetimeDB 数据库名,留空则读取环境变量') + string(name: 'SERVER', defaultValue: 'maincloud', description: 'SpacetimeDB server 别名,例如 maincloud/local/dev') + string(name: 'SERVER_URL', defaultValue: '', description: 'SpacetimeDB server URL,填写后优先于 SERVER') + string(name: 'ROOT_DIR', defaultValue: '', description: 'spacetime CLI root-dir,可选') + string(name: 'INPUT_FILE', defaultValue: '', description: '必填,迁移 JSON 文件路径,相对源码根目录或绝对路径') + string(name: 'INCLUDE_TABLES', defaultValue: '', description: '可选,逗号分隔的表名白名单') + booleanParam(name: 'DRY_RUN', defaultValue: true, description: '仅校验导入,不写入数据') + booleanParam(name: 'INCREMENTAL', defaultValue: true, description: '增量导入,跳过已存在或冲突的行') + booleanParam(name: 'REPLACE_EXISTING', defaultValue: false, description: '覆盖本次文件内涉及的表,不可与 INCREMENTAL 同时启用') + string(name: 'BOOTSTRAP_SECRET', defaultValue: '', description: '可选,授权临时导入 identity 的迁移引导密钥') + string(name: 'TOKEN', defaultValue: '', description: '可选,SpacetimeDB 客户端连接 token;留空则自动创建临时 identity') + string(name: 'NOTE', defaultValue: 'jenkins database import', description: '迁移授权备注') + } + + stages { + stage('校验参数') { + agent { + label 'built-in' + } + + steps { + script { + if (!params.INPUT_FILE?.trim()) { + error('INPUT_FILE 不能为空。') + } + if (params.INCREMENTAL && params.REPLACE_EXISTING) { + error('INCREMENTAL 不能和 REPLACE_EXISTING 同时启用。') + } + } + } + } + + stage('导入数据库') { + agent { + label "${params.AGENT_LABEL}" + } + + steps { + script { + // 固定源码目录可复用 Jenkins Agent 上的脚本和依赖,未指定时回退到当前工作区。 + env.WORKSPACE_ROOT = params.GENARRATIVE_WORKSPACE_ROOT?.trim() ? params.GENARRATIVE_WORKSPACE_ROOT.trim() : pwd() + } + + dir("${env.WORKSPACE_ROOT}") { + checkout scm + + sh """ + bash -lc ' + set -euo pipefail + args=(scripts/spacetime-import-migration-json.mjs --in "${params.INPUT_FILE}") + if [[ -n "${params.DATABASE}" ]]; then + args+=(--database "${params.DATABASE}") + fi + if [[ -n "${params.SERVER}" ]]; then + args+=(--server "${params.SERVER}") + fi + if [[ -n "${params.SERVER_URL}" ]]; then + args+=(--server-url "${params.SERVER_URL}") + fi + if [[ -n "${params.ROOT_DIR}" ]]; then + args+=(--root-dir "${params.ROOT_DIR}") + fi + if [[ -n "${params.INCLUDE_TABLES}" ]]; then + args+=(--include "${params.INCLUDE_TABLES}") + fi + if [[ "${params.DRY_RUN}" == "true" ]]; then + args+=(--dry-run) + fi + if [[ "${params.INCREMENTAL}" == "true" ]]; then + args+=(--incremental) + fi + if [[ "${params.REPLACE_EXISTING}" == "true" ]]; then + args+=(--replace-existing) + fi + if [[ -n "${params.BOOTSTRAP_SECRET}" ]]; then + args+=(--bootstrap-secret "${params.BOOTSTRAP_SECRET}") + fi + if [[ -n "${params.TOKEN}" ]]; then + args+=(--token "${params.TOKEN}") + fi + if [[ -n "${params.NOTE}" ]]; then + args+=(--note "${params.NOTE}") + fi + # 导入默认 dry-run,并通过迁移 procedure 写入,保持权限校验和表级统计一致。 + node "\${args[@]}" + ' + """ + } + } + } + } + + post { + success { + echo "数据库导入流水线完成,dry-run: ${params.DRY_RUN}" + } + } +}