Add Jenkins database migration pipelines
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
Codex
2026-04-29 11:20:56 +08:00
parent e191619ab3
commit 01ec7e132c
3 changed files with 289 additions and 0 deletions

View File

@@ -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-<BUILD_NUMBER>.json`
导出成功后Jenkins 归档:
```text
<OUTPUT_DIRECTORY>/<EXPORT_NAME>
```
### 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
```

View File

@@ -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}"
}
}
}

View File

@@ -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}"
}
}
}