From 4a0faf5f5169abd8aec8ab52d7065fc354ebcde4 Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 29 Apr 2026 18:52:09 +0800 Subject: [PATCH] Add deploy pipeline SpacetimeDB auto migration --- ..._RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md | 30 ++- docs/technical/README.md | 4 +- ...ND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md | 21 +- ...N_STRING_MIGRATION_PROCEDURE_2026-04-27.md | 25 +++ jenkins/Jenkinsfile.build-and-deploy | 4 + jenkins/Jenkinsfile.deploy | 10 + scripts/deploy-rust-remote.sh | 205 +++++++++++++++++- scripts/jenkins-deploy-release.sh | 35 ++- 8 files changed, 304 insertions(+), 30 deletions(-) diff --git a/docs/technical/JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md b/docs/technical/JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md index 51f10fbd..522850fa 100644 --- a/docs/technical/JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md +++ b/docs/technical/JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md @@ -88,20 +88,24 @@ scripts/jenkins-deploy-release.sh \ --deploy-dir /var/lib/jenkins/deploy/Genarrative \ --web-port \ [--clear-database] \ + [--no-migrate-on-conflict] \ + [--migration-dir ] \ --hook-with-sudo ``` 脚本语义: 1. 若部署目录已有旧版本且存在 `stop.sh`,先执行旧版本 `stop.sh`。 -2. 只删除发布产物白名单中的旧文件,例如 `web/`、`api-server`、`spacetime_module.wasm`、`.env*`、`start.sh`、`stop.sh`、`web-server.mjs`、`README.md`。 -3. 将指定版本目录中的同名发布产物复制到部署目录;文件产物使用普通复制,`web/` 等目录产物必须递归复制。 -4. 如果 `CLEAR_DATABASE=true`,部署脚本会以 `./start.sh --clear-database` 启动新版本;这样发布阶段的 `spacetime publish` 会追加 `-c=on-conflict`。 -5. 执行新版本 `start.sh`。 +2. 覆盖前如果旧部署目录存在 `migration-bootstrap-secret.txt`,先复制到 `run/migration-bootstrap-secret.previous.txt`,供新版本 `start.sh` 在 schema 冲突自动迁移时授权导出旧库。 +3. 只删除发布产物白名单中的旧文件,例如 `web/`、`api-server`、`spacetime_module.wasm`、`migration-bootstrap-secret.txt`、`scripts/`、`.env*`、`start.sh`、`stop.sh`、`web-server.mjs`、`README.md`。 +4. 将指定版本目录中的同名发布产物复制到部署目录;文件产物使用普通复制,`web/`、`scripts/` 等目录产物必须递归复制。 +5. 把 `WEB_PORT`、`MIGRATE_ON_CONFLICT`、`MIGRATION_DIRECTORY` 写入部署目录 `.env.local`,确保通过 sudo 执行 `start.sh` 时仍能读取 Jenkins 参数。 +6. 如果 `CLEAR_DATABASE=true`,部署脚本会以 `./start.sh --clear-database` 启动新版本;这样发布阶段的 `spacetime publish` 会追加 `-c=on-conflict`,代表人工确认清库,不进入自动导出和回灌。 +7. 执行新版本 `start.sh`;普通发布遇到 schema 冲突时,默认由发布包内迁移脚本自动导出旧库、清库发布新 wasm、导入回灌。 -如果 `RUN_DEPLOY_HOOKS_WITH_SUDO=true`,第 1 步和第 4 步会改为 `sudo -n` 调用;这要求 Jenkins 运行用户提前配置免密 sudo,否则部署会直接失败,不会进入交互式密码提示。 +如果 `RUN_DEPLOY_HOOKS_WITH_SUDO=true`,旧版本 `stop.sh` 和新版本 `start.sh` 会改为 `sudo -n` 调用;这要求 Jenkins 运行用户提前配置免密 sudo,否则部署会直接失败,不会进入交互式密码提示。 -这样可以满足“发布文件直接覆盖”的要求,同时保留部署目录里像 `.spacetimedb/`、`logs/`、`run/` 这类运行态目录,不会因为部署被整体删除。发布白名单内的 `.env`、`.env.local` 会先以构建产物中的文件为准;部署脚本会在启动 hook 前移除这些环境文件中的 UTF-8 BOM 与 CRLF,并把 Jenkins 部署参数 `WEB_PORT` 写入 `.env.local` 的 `GENARRATIVE_WEB_PORT`,避免 `start.sh` 在 Bash 下把首行变量名误解析成命令,也避免端口配置只停留在上游构建阶段。`start.sh` 会先执行 Ubuntu 专用 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin//spacetimedb-cli` 或 `$HOME/.local/share/spacetime/bin//spacetimedb-cli` 同步到部署目录 `.spacetimedb/bin/current/spacetimedb-cli`,后续启动、探活和 root-dir 占用判定都使用部署目录内 `.spacetimedb/`,且不再额外设置 `--data-dir`,避免 Jenkins 机器全局 `spacetime login` 变化影响本地库更新;如遇 `403 Forbidden`,按 `SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md` 排查数据库所有者与 CLI 身份。 +这样可以满足“发布文件直接覆盖”的要求,同时保留部署目录里像 `.spacetimedb/`、`logs/`、`run/`、`database-migrations/` 这类运行态目录,不会因为部署被整体删除。发布白名单内的 `.env`、`.env.local` 会先以构建产物中的文件为准;部署脚本会在启动 hook 前移除这些环境文件中的 UTF-8 BOM 与 CRLF,并把 Jenkins 部署参数 `WEB_PORT` 写入 `.env.local` 的 `GENARRATIVE_WEB_PORT`,把 `MIGRATE_ON_CONFLICT` 写入 `GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT`,把 `MIGRATION_DIRECTORY` 写入 `GENARRATIVE_SPACETIME_MIGRATION_DIR`,避免 `start.sh` 在 Bash 下把首行变量名误解析成命令,也避免端口和迁移配置只停留在上游构建阶段。`start.sh` 会先执行 Ubuntu 专用 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin//spacetimedb-cli` 或 `$HOME/.local/share/spacetime/bin//spacetimedb-cli` 同步到部署目录 `.spacetimedb/bin/current/spacetimedb-cli`,后续启动、探活和 root-dir 占用判定都使用部署目录内 `.spacetimedb/`,且不再额外设置 `--data-dir`,避免 Jenkins 机器全局 `spacetime login` 变化影响本地库更新;如遇 `403 Forbidden`,按 `SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md` 排查数据库所有者与 CLI 身份。 ### 4.3 构建并部署 @@ -125,6 +129,8 @@ jenkins/Jenkinsfile.build-and-deploy - `DEPLOY_DIRECTORY` - `WEB_PORT` - `CLEAR_DATABASE` + - `MIGRATE_ON_CONFLICT` + - `MIGRATION_DIRECTORY` - `EXPECTED_UPSTREAM_JOB` ## 5. Jenkins 参数建议 @@ -137,6 +143,8 @@ jenkins/Jenkinsfile.build-and-deploy 4. `RUN_NPM_CI`:是否在构建前执行 `npm ci`。 5. `WEB_PORT`:静态网站监听端口;`构建并部署` 默认值为 `25001`,并通过下游 `部署` 同名参数作为最终启动端口。 6. `CLEAR_DATABASE`:部署阶段是否清空 SpacetimeDB 数据后再发布 wasm;默认 `false`。 +7. `MIGRATE_ON_CONFLICT`:普通部署遇到 SpacetimeDB schema 冲突时是否自动导出、清库发布、导入回灌;默认 `true`。 +8. `MIGRATION_DIRECTORY`:自动迁移 JSON 输出目录;留空时使用部署目录内 `database-migrations/`。 如果当前 Jenkins 没有额外配置独立 Agent,而是直接在控制器自身执行任务,`AGENT_LABEL` 应填写 `built-in`。 如果 Jenkins 进程以默认 `jenkins` 用户运行,部署目录建议直接放在 `/var/lib/jenkins/deploy/Genarrative` 这类 Jenkins 自有目录下,避免再依赖 `/home/ubuntu/*` 的额外写权限。 @@ -148,9 +156,11 @@ jenkins/Jenkinsfile.build-and-deploy 2. `SOURCE_NODE_NAME` 3. `DEPLOY_DIRECTORY` 4. `CLEAR_DATABASE` -5. `RUN_DEPLOY_HOOKS_WITH_SUDO` -6. `EXPECTED_UPSTREAM_JOB` -7. `WEB_PORT` +5. `MIGRATE_ON_CONFLICT` +6. `MIGRATION_DIRECTORY` +7. `RUN_DEPLOY_HOOKS_WITH_SUDO` +8. `EXPECTED_UPSTREAM_JOB` +9. `WEB_PORT` 其中仅 `构建并部署` 流水线还需要: @@ -158,6 +168,8 @@ jenkins/Jenkinsfile.build-and-deploy 2. `RUN_DEPLOY_HOOKS_WITH_SUDO` 3. `WEB_PORT` 4. `CLEAR_DATABASE` +5. `MIGRATE_ON_CONFLICT` +6. `MIGRATION_DIRECTORY` 如果你选择启用 `RUN_DEPLOY_HOOKS_WITH_SUDO=true`,推荐提前在服务器上增加一份最小 sudoers 配置,例如: diff --git a/docs/technical/README.md b/docs/technical/README.md index f1e92a4c..b96748e2 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -57,9 +57,9 @@ - [CREATION_AGENT_PUBLISH_GATE_SCHEMA_ALIGNMENT_FIX_2026-04-23.md](./CREATION_AGENT_PUBLISH_GATE_SCHEMA_ALIGNMENT_FIX_2026-04-23.md):记录发布阻断项仍按旧 `worldHook / playerPremise / sceneChapters` schema 校验的问题,以及将 Rust `publish gate` 对齐到 `anchorContent / creatorIntent / sceneChapterBlueprints` 当前主链结构的修复口径。 - [CREATION_HUB_CARD_ACTIONS_2026-04-22.md](./CREATION_HUB_CARD_ACTIONS_2026-04-22.md):冻结创作中心作品卡“体验 / 删除”入口的最小落地语义,明确 RPG 已发布作品软删除、卡片直达运行时,以及暂不扩草稿 / 拼图删除契约。 - [CREATION_CATEGORY_OPENING_TIMEOUT_GUARD_FIX_2026-04-22.md](./CREATION_CATEGORY_OPENING_TIMEOUT_GUARD_FIX_2026-04-22.md):记录创作中心点击类别后长时间停留在“正在开启”的根因与修复口径,收口前端创建会话启动超时、中文错误提示以及 Big Fish / 拼图代理上游超时兜底。 -- [JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md](./JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md):冻结 Jenkins `构建 / 部署 / 构建并部署` 三条流水线的职责、版本号传递、上游触发门禁、本地目录部署脚本与 `/home/ubuntu/Genarrative-deploy/` 覆盖策略。 +- [JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md](./JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md):冻结 Jenkins `构建 / 部署 / 构建并部署` 三条流水线的职责、版本号传递、上游触发门禁、本地目录部署脚本、发布包覆盖策略,以及部署阶段 SpacetimeDB schema 冲突自动导出、清库发布、导入回灌能力。 - [JENKINS_DEPLOY_ENV_BOM_FIX_2026-04-25.md](./JENKINS_DEPLOY_ENV_BOM_FIX_2026-04-25.md):记录 Jenkins 部署时 `.env.local` 首行 UTF-8 BOM 导致 `start.sh` 加载失败的根因,并冻结发布包构建、部署脚本和启动脚本的环境文件净化规则。 -- [RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md](./RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md):冻结 Rust 本地一键联调脚本与 Ubuntu 发布包构建脚本的执行口径,覆盖 `npm run dev:rust`、`npm run build:rust:ubuntu`、Vite release、Linux `api-server`、SpacetimeDB wasm、启动停止脚本、默认 scp 上传和安全清库开关。 +- [RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md](./RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md):冻结 Rust 本地一键联调脚本与 Ubuntu 发布包构建脚本的执行口径,覆盖 `npm run dev:rust`、`npm run build:rust:ubuntu`、Vite release、Linux `api-server`、SpacetimeDB wasm、启动停止脚本、默认 scp 上传、安全清库开关,以及发布包内 schema 冲突自动迁移脚本。 - [RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](./RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md):记录当前 Rust `api-server` 已挂载的 101 条 Axum 路由,并补充管理后台入口与管理接口索引,按 auth、assets、runtime、custom world、story、generated path 等挂载面归类,用于对照 Node 能力基线与切流 smoke 清单。 - [BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md](./BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md):冻结后端重写收口阶段的横向治理规则,覆盖 TypeScript contract 到 Rust DTO 映射、SpacetimeDB schema 演进、大对象 / workflow cache 存储边界和文档维护门禁。 - [PLATFORM_LLM_TEXT_GATEWAY_DESIGN_2026-04-21.md](./PLATFORM_LLM_TEXT_GATEWAY_DESIGN_2026-04-21.md):`platform-llm` 文本模型网关首版设计,冻结 OpenAI 兼容 `/chat/completions`、SSE 增量解析、错误模型与重试边界。 diff --git a/docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md b/docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md index db0442d3..f8d46700 100644 --- a/docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md +++ b/docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md @@ -142,7 +142,7 @@ npm run deploy:rust:remote 5. 执行 `cargo build -p spacetime-module --release --target wasm32-unknown-unknown --manifest-path server-rs/Cargo.toml`,并把 `spacetime_module.wasm` 复制到目标目录。 6. 把仓库根目录的 `.env` 与 `.env.local` 分别复制到目标目录根部和目标目录的 `web/` 下;复制后统一移除 UTF-8 BOM 与 CRLF,避免目标服务器 Bash 加载环境文件失败。 7. 在目标目录写入 `web-server.mjs`,用于托管 `web/` 并把 `/api/*`、`/generated-*`、`/healthz` 反代到本包内的 `api-server`。 -8. 在目标目录写入 `start.sh` 与 `stop.sh`;`start.sh` 会先按 `KEY=value` 子集加载发布目录根部的 `.env`、`.env.local`,兼容 UTF-8 BOM 与 CRLF,再回退到构建时通过 `--database`、`--api-port`、`--web-port`、`--spacetime-host`、`--spacetime-port` 写入的默认值,并默认导出 `NO_COLOR=1` 与 `CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件;同时按 Ubuntu 发布环境使用发布目录内 `.spacetimedb/` 作为 root-dir,不再额外设置 `--data-dir`,启动前先执行 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin//spacetimedb-cli` 或 `$HOME/.local/share/spacetime/bin//spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`,当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`;启动参数为 `spacetime --root-dir ./.spacetimedb start --edition standalone --listen-addr :`,探活必须确认 `server ping` 输出包含 `Server is online:`;如果以 `--clear-database` 启动,则内部 `spacetime publish` 会追加 `-c=on-conflict`,仅在 schema 冲突时删除旧模块数据。 +8. 在目标目录写入 `start.sh` 与 `stop.sh`;`start.sh` 会先按 `KEY=value` 子集加载发布目录根部的 `.env`、`.env.local`,兼容 UTF-8 BOM 与 CRLF,再回退到构建时通过 `--database`、`--api-port`、`--web-port`、`--spacetime-host`、`--spacetime-port` 写入的默认值,并默认导出 `NO_COLOR=1` 与 `CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件;同时按 Ubuntu 发布环境使用发布目录内 `.spacetimedb/` 作为 root-dir,不再额外设置 `--data-dir`,启动前先执行 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin//spacetimedb-cli` 或 `$HOME/.local/share/spacetime/bin//spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`,当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`;启动参数为 `spacetime --root-dir ./.spacetimedb start --edition standalone --listen-addr :`,探活必须确认 `server ping` 输出包含 `Server is online:`;普通启动先无清库发布,若 publish 输出可判定为 schema 冲突,则自动导出旧库、清库发布新 wasm、导入回灌;如果以 `--clear-database` 启动,则内部 `spacetime publish` 会追加 `-c=on-conflict`,代表人工确认清库,不触发自动回灌。 9. 默认执行 `scp -r -i ~\.ssh\dsk.pem build/ ubuntu@82.157.175.59:/home/ubuntu/genarrative/` 上传发布包。 发布包结构: @@ -156,6 +156,9 @@ build// │ └─ .env.local ├─ api-server ├─ spacetime_module.wasm +├─ migration-bootstrap-secret.txt +├─ scripts/ +│ └─ spacetime-*.mjs ├─ web-server.mjs ├─ start.sh ├─ stop.sh @@ -178,19 +181,21 @@ cd build/ ./stop.sh ``` -如果后续通过 Jenkins 的部署脚本把发布包覆盖到固定部署目录,部署阶段默认只替换 `web/`、`api-server`、`spacetime_module.wasm`、`.env*`、`start.sh`、`stop.sh`、`web-server.mjs`、`README.md` 等发布产物;文件产物使用普通复制,`web/` 等目录产物递归复制,不会删除部署目录中的 `.spacetimedb/`、`logs/`、`run/` 这类运行态目录。 +如果后续通过 Jenkins 的部署脚本把发布包覆盖到固定部署目录,部署阶段默认只替换 `web/`、`api-server`、`spacetime_module.wasm`、`migration-bootstrap-secret.txt`、`scripts/`、`.env*`、`start.sh`、`stop.sh`、`web-server.mjs`、`README.md` 等发布产物;文件产物使用普通复制,`web/`、`scripts/` 等目录产物递归复制,不会删除部署目录中的 `.spacetimedb/`、`logs/`、`run/`、`database-migrations/` 这类运行态目录。 安全边界: 1. 构建脚本会把仓库根目录已有的 `.env`、`.env.local` 一并复制进发布包,因此运行前必须确认这些文件内容适合被带入目标环境。 2. 如果仓库根目录不存在 `.env` 或 `.env.local`,脚本会打印跳过日志,但不会因此失败;此时 `start.sh` 仅使用构建时写入的默认值与运行时显式传入的环境变量。 3. `start.sh` 只解析合法 `KEY=value` 环境行,支持不加引号、双引号和单引号;不执行复杂 shell 表达式,避免把环境文件变成脚本入口。 -4. `start.sh` 默认不追加清理参数;只有显式执行 `./start.sh --clear-database` 才追加 `-c=on-conflict`,在 schema 冲突时清理旧模块数据后重发。 -5. `start.sh` 使用 `spacetime publish --bin-path spacetime_module.wasm --yes` 发布当前包内 wasm;清库模式下会追加 `-c=on-conflict`,仅在 schema 冲突时删除旧模块数据。 -6. `start.sh` 会先复用已经按目标地址就绪的 SpacetimeDB;如果同一个 `.spacetimedb/` root-dir 已被其他未就绪实例占用,则只输出命令名为 `spacetime` 或 `spacetimedb-cli` 且命令行包含当前 root-dir 的真实占用进程并失败,避免把排查用的 `grep` / `awk` 误判为 SpacetimeDB 实例。 -7. 如果 `spacetime publish` 报 `403 Forbidden`,优先确认 `spacetime --root-dir ./.spacetimedb login show` 输出的身份是否有权更新目标库;`--clear-database` 不能绕过身份授权。 -8. 当前脚本是单目录进程启动方案,不替代生产 systemd、Nginx、TLS、日志轮转与守护进程配置。 -9. 如只需要本地生成发布包,可传 `--skip-upload` 跳过默认 scp 上传。 +4. `start.sh` 默认不追加清理参数;普通发布如果遇到 SpacetimeDB schema 冲突,会调用发布包内的 `scripts/spacetime-export-migration-json.mjs` 导出旧库,再清库发布新 wasm,并调用 `scripts/spacetime-import-migration-json.mjs --replace-existing` 回灌。可通过 `GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT=false` 禁用该行为。 +5. 自动迁移导出旧库时优先读取 `run/migration-bootstrap-secret.previous.txt`,导入新库时读取当前发布包 `migration-bootstrap-secret.txt`;Jenkins 部署脚本会在覆盖发布包前保存旧密钥。手工覆盖发布包时,也应在覆盖前保留旧模块的引导密钥,否则旧库导出可能无法授权。 +6. 自动迁移 JSON 默认写入发布目录下 `database-migrations//`;可通过 `GENARRATIVE_SPACETIME_MIGRATION_DIR` 改写。该目录属于运行态,不应被 Jenkins 覆盖部署删除。 +7. 只有显式执行 `./start.sh --clear-database` 才追加 `-c=on-conflict`,该模式代表人工确认清库,不执行导出和回灌。 +8. `start.sh` 会先复用已经按目标地址就绪的 SpacetimeDB;如果同一个 `.spacetimedb/` root-dir 已被其他未就绪实例占用,则只输出命令名为 `spacetime` 或 `spacetimedb-cli` 且命令行包含当前 root-dir 的真实占用进程并失败,避免把排查用的 `grep` / `awk` 误判为 SpacetimeDB 实例。 +9. 如果 `spacetime publish` 报 `403 Forbidden`,优先确认 `spacetime --root-dir ./.spacetimedb login show` 输出的身份是否有权更新目标库;`--clear-database` 不能绕过身份授权。 +10. 当前脚本是单目录进程启动方案,不替代生产 systemd、Nginx、TLS、日志轮转与守护进程配置。 +11. 如只需要本地生成发布包,可传 `--skip-upload` 跳过默认 scp 上传。 目标服务器最小要求: diff --git a/docs/technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md b/docs/technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md index aa35ecdf..aa1c65af 100644 --- a/docs/technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md +++ b/docs/technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md @@ -146,6 +146,31 @@ npm run spacetime:publish:maincloud -- --database xushi-p4wfr 冲突自动迁移需要发布脚本本次生成的 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET`。因此不要和 `--no-migration-bootstrap-secret` 同时使用。 +### 部署流水线自动迁移 + +Ubuntu 发布包的 `start.sh` 与 Jenkins `Genarrative-Deploy` 也采用同一套迁移 procedure,但迁移触发点在部署目录内: + +1. Jenkins 覆盖部署前,如果旧部署目录存在 `migration-bootstrap-secret.txt`,先保存到 `run/migration-bootstrap-secret.previous.txt`。 +2. Jenkins 复制新发布包,包含新 wasm、新 `migration-bootstrap-secret.txt` 和 `scripts/spacetime-*.mjs` 迁移脚本。 +3. 新 `start.sh` 先不清库发布当前包内 `spacetime_module.wasm`。 +4. 如果发布成功,流程结束。 +5. 如果发布失败且输出可判定为 schema 冲突,`start.sh` 用旧密钥授权导出旧库 JSON。 +6. 导出成功后,`start.sh` 清库发布新 wasm。 +7. 新 wasm 发布成功后,`start.sh` 用新密钥授权导入,并以 `--replace-existing` 回灌迁移 JSON。 + +自动迁移 JSON 默认保存到部署目录的 `database-migrations//.json`。可通过 Jenkins 参数 `MIGRATION_DIRECTORY` 或环境变量 `GENARRATIVE_SPACETIME_MIGRATION_DIR` 覆盖。该目录属于运行态数据,部署脚本不会删除。 + +Jenkins 参数 `MIGRATE_ON_CONFLICT` 默认 `true`。如果设为 `false`,普通发布遇到 schema 冲突时会保留原始失败,不执行导出、清库发布和导入回灌。 + +Jenkins 参数 `CLEAR_DATABASE=true` 或手工执行 `./start.sh --clear-database` 时,语义是人工确认清库发布;此时 `spacetime publish` 追加 `-c=on-conflict`,不执行自动导出和导入回灌。 + +自动迁移依赖两个引导密钥: + +- 导出旧库:优先使用 `run/migration-bootstrap-secret.previous.txt`,也就是旧模块编译时注入的密钥。 +- 导入新库:使用当前发布包 `migration-bootstrap-secret.txt`,也就是新模块编译时注入的密钥。 + +如果不是通过 Jenkins 部署脚本覆盖发布包,而是手工替换文件,必须在覆盖前保留旧 `migration-bootstrap-secret.txt`;否则旧库迁移 procedure 可能无法授权导出。 + ### 删除表和删除字段 迁移文件来自旧模块时,可能包含新模块已经删除的表或字段。导入阶段按以下规则处理: diff --git a/jenkins/Jenkinsfile.build-and-deploy b/jenkins/Jenkinsfile.build-and-deploy index 62554c07..88af1f56 100644 --- a/jenkins/Jenkinsfile.build-and-deploy +++ b/jenkins/Jenkinsfile.build-and-deploy @@ -12,6 +12,8 @@ pipeline { string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER') string(name: 'WEB_PORT', defaultValue: '25001', description: '发布包内静态网站端口,默认 25001') booleanParam(name: 'CLEAR_DATABASE', defaultValue: false, description: '部署时是否清空 SpacetimeDB 数据后再发布 wasm') + booleanParam(name: 'MIGRATE_ON_CONFLICT', defaultValue: true, description: '普通发布遇到 SpacetimeDB schema 冲突时自动导出、清库发布并导入回灌') + string(name: 'MIGRATION_DIRECTORY', defaultValue: '', description: '自动迁移 JSON 输出目录,留空则使用部署目录内 database-migrations/') booleanParam(name: 'RUN_NPM_CI', defaultValue: false, description: '构建前是否执行 npm ci') string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Deploy', description: '部署流水线作业名') string(name: 'DEPLOY_DIRECTORY', defaultValue: '/var/lib/jenkins/deploy/Genarrative', description: '固定部署目录') @@ -97,6 +99,8 @@ pipeline { string(name: 'DEPLOY_DIRECTORY', value: params.DEPLOY_DIRECTORY), string(name: 'WEB_PORT', value: env.EFFECTIVE_WEB_PORT), booleanParam(name: 'CLEAR_DATABASE', value: params.CLEAR_DATABASE), + booleanParam(name: 'MIGRATE_ON_CONFLICT', value: params.MIGRATE_ON_CONFLICT), + string(name: 'MIGRATION_DIRECTORY', value: params.MIGRATION_DIRECTORY), booleanParam(name: 'RUN_DEPLOY_HOOKS_WITH_SUDO', value: params.RUN_DEPLOY_HOOKS_WITH_SUDO), string(name: 'EXPECTED_UPSTREAM_JOB', value: env.JOB_NAME), ] diff --git a/jenkins/Jenkinsfile.deploy b/jenkins/Jenkinsfile.deploy index c2e4e69b..f2cf727b 100644 --- a/jenkins/Jenkinsfile.deploy +++ b/jenkins/Jenkinsfile.deploy @@ -13,6 +13,8 @@ pipeline { string(name: 'DEPLOY_DIRECTORY', defaultValue: '/var/lib/jenkins/deploy/Genarrative', description: '固定部署目录') string(name: 'WEB_PORT', defaultValue: '25001', description: '静态网站监听端口,默认 25001,上游构建并部署流水线会透传同名参数') booleanParam(name: 'CLEAR_DATABASE', defaultValue: false, description: '部署时是否清空 SpacetimeDB 数据后再发布 wasm') + booleanParam(name: 'MIGRATE_ON_CONFLICT', defaultValue: true, description: '普通发布遇到 SpacetimeDB schema 冲突时自动导出、清库发布并导入回灌') + string(name: 'MIGRATION_DIRECTORY', defaultValue: '', description: '自动迁移 JSON 输出目录,留空则使用部署目录内 database-migrations/') booleanParam(name: 'RUN_DEPLOY_HOOKS_WITH_SUDO', defaultValue: true, description: 'start.sh / stop.sh 是否通过 sudo -n 执行') string(name: 'EXPECTED_UPSTREAM_JOB', defaultValue: '', description: '允许触发本作业的上游作业名') } @@ -111,6 +113,14 @@ pipeline { if [[ "${params.CLEAR_DATABASE}" == "true" ]]; then deploy_args+=(--clear-database) fi + if [[ "${params.MIGRATE_ON_CONFLICT}" == "true" ]]; then + deploy_args+=(--migrate-on-conflict) + else + deploy_args+=(--no-migrate-on-conflict) + fi + if [[ -n "${params.MIGRATION_DIRECTORY}" ]]; then + deploy_args+=(--migration-dir "${params.MIGRATION_DIRECTORY}") + fi if [[ "${params.RUN_DEPLOY_HOOKS_WITH_SUDO}" == "true" ]]; then deploy_args+=(--hook-with-sudo) fi diff --git a/scripts/deploy-rust-remote.sh b/scripts/deploy-rust-remote.sh index 954bde1d..fbaeb6c7 100644 --- a/scripts/deploy-rust-remote.sh +++ b/scripts/deploy-rust-remote.sh @@ -366,6 +366,19 @@ if [[ "${MIGRATION_BOOTSTRAP_SECRET_MODE}" != "disabled" ]]; then chmod 600 "${TARGET_DIR}/migration-bootstrap-secret.txt" fi +mkdir -p "${TARGET_DIR}/scripts" +for migration_script in \ + spacetime-migration-common.mjs \ + spacetime-export-migration-json.mjs \ + spacetime-import-migration-json.mjs \ + spacetime-authorize-migration-operator.mjs \ + spacetime-revoke-migration-operator.mjs; do + copy_required_file \ + "${SCRIPT_DIR}/${migration_script}" \ + "${TARGET_DIR}/scripts/${migration_script}" \ + "SpacetimeDB 迁移脚本 ${migration_script}" +done + cat >"${TARGET_DIR}/web-server.mjs" <<'WEB_SERVER' import http from 'node:http'; import fs from 'node:fs'; @@ -549,7 +562,8 @@ usage() { 说明: 1. 启动当前发布包内的静态网站、SpacetimeDB 与 api-server。 2. 默认发布 spacetime_module.wasm 到 GENARRATIVE_SPACETIME_DATABASE,但不清库。 - 3. 只有显式传入 --clear-database 时才会在 schema 冲突时清理旧模块数据后重发。 + 3. 默认遇到 schema 冲突时自动导出旧库、清库发布新模块并导入回灌。 + 4. 显式传入 --clear-database 时代表人工确认清库,不执行自动回灌。 EOF } @@ -580,12 +594,18 @@ SPACETIME_PORT="${GENARRATIVE_SPACETIME_PORT:-__GENARRATIVE_DEFAULT_SPACETIME_PO SPACETIME_SERVER_URL="${GENARRATIVE_SPACETIME_SERVER_URL:-http://${SPACETIME_HOST}:${SPACETIME_PORT}}" SPACETIME_DATABASE="${GENARRATIVE_SPACETIME_DATABASE:-__GENARRATIVE_DEFAULT_SPACETIME_DATABASE__}" SPACETIME_TIMEOUT_SECONDS="${GENARRATIVE_SPACETIME_TIMEOUT_SECONDS:-60}" +SPACETIME_MIGRATE_ON_CONFLICT="${GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT:-true}" +SPACETIME_MIGRATION_DIR="${GENARRATIVE_SPACETIME_MIGRATION_DIR:-}" API_HOST="${GENARRATIVE_API_HOST:-__GENARRATIVE_DEFAULT_API_HOST__}" API_PORT="${GENARRATIVE_API_PORT:-__GENARRATIVE_DEFAULT_API_PORT__}" API_LOG="${GENARRATIVE_API_LOG:-info,tower_http=info}" WEB_HOST="${GENARRATIVE_WEB_HOST:-__GENARRATIVE_DEFAULT_WEB_HOST__}" WEB_PORT="${GENARRATIVE_WEB_PORT:-__GENARRATIVE_DEFAULT_WEB_PORT__}" MIGRATION_BOOTSTRAP_SECRET_FILE="${SCRIPT_DIR}/migration-bootstrap-secret.txt" +PREVIOUS_MIGRATION_BOOTSTRAP_SECRET_FILE="${SCRIPT_DIR}/run/migration-bootstrap-secret.previous.txt" +MIGRATION_SCRIPT_DIR="${SCRIPT_DIR}/scripts" +MIGRATION_EXPORT_SCRIPT="${MIGRATION_SCRIPT_DIR}/spacetime-export-migration-json.mjs" +MIGRATION_IMPORT_SCRIPT="${MIGRATION_SCRIPT_DIR}/spacetime-import-migration-json.mjs" # 日志默认落文件,显式关闭 ANSI 颜色码,避免控制字符写入 *.log。 export NO_COLOR="${NO_COLOR:-1}" @@ -600,6 +620,154 @@ require_command() { fi } +is_truthy() { + local normalized + + normalized="$(printf "%s" "${1:-}" | tr '[:upper:]' '[:lower:]')" + case "${normalized}" in + 1|true|yes|y|on) + return 0 + ;; + *) + return 1 + ;; + esac +} + +timestamp_slug() { + date -u +%Y-%m-%dT%H-%M-%SZ +} + +sanitize_path_segment() { + printf "%s" "$1" | tr -c 'A-Za-z0-9._-' '_' +} + +is_publish_conflict_output() { + local output="$1" + local normalized + + normalized="$(printf "%s" "${output}" | tr '[:upper:]' '[:lower:]')" + [[ "${normalized}" == *"requires a manual migration"* ]] \ + || [[ "${normalized}" == *"manual migration"* ]] \ + || [[ "${normalized}" == *"schema"* && "${normalized}" == *"conflict"* ]] \ + || [[ "${normalized}" == *"clear-database"* ]] \ + || [[ "${normalized}" == *"clear database"* && "${normalized}" == *"publish"* ]] +} + +read_migration_bootstrap_secret() { + local secret_file="$1" + local label="$2" + local secret="" + + if [[ ! -f "${secret_file}" ]]; then + echo "[start] schema 冲突自动迁移需要${label}: ${secret_file}" >&2 + echo "[start] 请使用默认带迁移引导密钥的发布包,或设置 GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT=false 后人工处理。" >&2 + return 1 + fi + + secret="$(tr -d '\r\n' <"${secret_file}")" + if [[ -z "${secret}" ]]; then + echo "[start] 迁移引导密钥为空${label}: ${secret_file}" >&2 + return 1 + fi + + printf "%s" "${secret}" +} + +read_export_migration_bootstrap_secret() { + if [[ -f "${PREVIOUS_MIGRATION_BOOTSTRAP_SECRET_FILE}" ]]; then + read_migration_bootstrap_secret "${PREVIOUS_MIGRATION_BOOTSTRAP_SECRET_FILE}" "(旧模块导出)" + return + fi + + read_migration_bootstrap_secret "${MIGRATION_BOOTSTRAP_SECRET_FILE}" "(当前模块导出兜底)" +} + +read_import_migration_bootstrap_secret() { + read_migration_bootstrap_secret "${MIGRATION_BOOTSTRAP_SECRET_FILE}" "(新模块导入)" +} + +require_migration_script() { + local script_path="$1" + + if [[ ! -f "${script_path}" ]]; then + echo "[start] 发布包缺少 SpacetimeDB 迁移脚本: ${script_path}" >&2 + exit 1 + fi +} + +run_publish() { + local output_file="$1" + shift + + set +e + spacetime --root-dir="${SPACETIME_ROOT_DIR}" "$@" >"${output_file}" 2>&1 + local status=$? + set -e + cat "${output_file}" + return "${status}" +} + +run_conflict_migration_publish() { + local export_bootstrap_secret="" + local import_bootstrap_secret="" + local migration_database_slug="" + local migration_root="" + local migration_file="" + local publish_log="" + + export_bootstrap_secret="$(read_export_migration_bootstrap_secret)" + import_bootstrap_secret="$(read_import_migration_bootstrap_secret)" + require_migration_script "${MIGRATION_EXPORT_SCRIPT}" + require_migration_script "${MIGRATION_IMPORT_SCRIPT}" + + migration_database_slug="$(sanitize_path_segment "${SPACETIME_DATABASE}")" + migration_root="${SPACETIME_MIGRATION_DIR:-${SCRIPT_DIR}/database-migrations/${migration_database_slug}}" + mkdir -p "${migration_root}" + migration_file="${migration_root}/$(timestamp_slug).json" + + echo "[start] 检测到 SpacetimeDB schema 冲突,开始导出旧库迁移 JSON: ${migration_file}" + node "${MIGRATION_EXPORT_SCRIPT}" \ + --server "${SPACETIME_SERVER_URL}" \ + --server-url "${SPACETIME_SERVER_URL}" \ + --root-dir "${SPACETIME_ROOT_DIR}" \ + --database "${SPACETIME_DATABASE}" \ + --bootstrap-secret "${export_bootstrap_secret}" \ + --out "${migration_file}" \ + --note "deploy conflict export $(date -u +%Y-%m-%dT%H:%M:%SZ)" + + echo "[start] 清库发布新 SpacetimeDB wasm" + publish_log="$(mktemp)" + if ! run_publish "${publish_log}" \ + publish \ + "${SPACETIME_DATABASE}" \ + --server "${SPACETIME_SERVER_URL}" \ + --bin-path "${SCRIPT_DIR}/spacetime_module.wasm" \ + --clear-database \ + --yes; then + echo "[start] 清库发布失败,迁移 JSON 已保留: ${migration_file}" >&2 + rm -f "${publish_log}" + exit 1 + fi + rm -f "${publish_log}" + + echo "[start] 导入迁移 JSON 回灌数据" + if ! node "${MIGRATION_IMPORT_SCRIPT}" \ + --server "${SPACETIME_SERVER_URL}" \ + --server-url "${SPACETIME_SERVER_URL}" \ + --root-dir "${SPACETIME_ROOT_DIR}" \ + --database "${SPACETIME_DATABASE}" \ + --bootstrap-secret "${import_bootstrap_secret}" \ + --in "${migration_file}" \ + --replace-existing \ + --note "deploy conflict import $(date -u +%Y-%m-%dT%H:%M:%SZ)"; then + echo "[start] 导入失败,迁移 JSON 已保留: ${migration_file}" >&2 + exit 1 + fi + + echo "[start] schema 冲突自动迁移完成,迁移 JSON: ${migration_file}" +} + wait_for_spacetime() { local process_pid="${1:-}" local deadline=$((SECONDS + SPACETIME_TIMEOUT_SECONDS)) @@ -840,14 +1008,28 @@ if [[ -f "${MIGRATION_BOOTSTRAP_SECRET_FILE}" ]]; then else echo "[start] 未启用迁移引导密钥。" fi -if ! spacetime --root-dir="${SPACETIME_ROOT_DIR}" "${PUBLISH_ARGS[@]}"; then - echo "[start] SpacetimeDB 发布失败。" >&2 - echo "[start] 如果错误包含 403 Forbidden 或 is not authorized,通常是当前 CLI 身份无权更新目标数据库。" >&2 - echo "[start] 当前 start.sh 使用的 CLI root: ${SPACETIME_ROOT_DIR}" >&2 - spacetime --root-dir="${SPACETIME_ROOT_DIR}" login show >&2 || true - echo "[start] 如果目标是本地库且可以清空数据:先执行 ./stop.sh,备份或删除 ${SPACETIME_ROOT_DIR},再重新执行 ./start.sh。" >&2 - echo "[start] 如果目标是 Maincloud 或必须保留数据:请切换到创建该数据库的 SpacetimeDB 身份,或把 GENARRATIVE_SPACETIME_DATABASE 改为当前身份有权限的库。" >&2 - exit 1 +PUBLISH_LOG="$(mktemp)" +if ! run_publish "${PUBLISH_LOG}" "${PUBLISH_ARGS[@]}"; then + PUBLISH_OUTPUT="$(cat "${PUBLISH_LOG}")" + rm -f "${PUBLISH_LOG}" + if [[ "${CLEAR_DATABASE}" -eq 0 ]] \ + && is_truthy "${SPACETIME_MIGRATE_ON_CONFLICT}" \ + && is_publish_conflict_output "${PUBLISH_OUTPUT}"; then + run_conflict_migration_publish + else + if [[ "${CLEAR_DATABASE}" -eq 0 ]] && ! is_truthy "${SPACETIME_MIGRATE_ON_CONFLICT}"; then + echo "[start] 已禁用 schema 冲突自动迁移: GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT=${SPACETIME_MIGRATE_ON_CONFLICT}" >&2 + fi + echo "[start] SpacetimeDB 发布失败。" >&2 + echo "[start] 如果错误包含 403 Forbidden 或 is not authorized,通常是当前 CLI 身份无权更新目标数据库。" >&2 + echo "[start] 当前 start.sh 使用的 CLI root: ${SPACETIME_ROOT_DIR}" >&2 + spacetime --root-dir="${SPACETIME_ROOT_DIR}" login show >&2 || true + echo "[start] 如果目标是本地库且可以清空数据:先执行 ./stop.sh,备份或删除 ${SPACETIME_ROOT_DIR},再重新执行 ./start.sh --clear-database。" >&2 + echo "[start] 如果目标是 Maincloud 或必须保留数据:请切换到创建该数据库的 SpacetimeDB 身份,或把 GENARRATIVE_SPACETIME_DATABASE 改为当前身份有权限的库。" >&2 + exit 1 + fi +else + rm -f "${PUBLISH_LOG}" fi export GENARRATIVE_API_HOST="${API_HOST}" @@ -931,6 +1113,7 @@ cat >"${TARGET_DIR}/README.md" <"${TARGET_DIR}/README.md" </\`,随后清库发布新 wasm,并用 \`--replace-existing\` 导入回灌。 + ## 环境变量 - 启动时会先加载发布目录根部的 \`.env\` 与 \`.env.local\`,再回退到脚本内默认值。 @@ -958,6 +1143,8 @@ cat >"${TARGET_DIR}/README.md" </\`。 - OSS、LLM、短信、微信等业务密钥仍通过目标服务器环境变量或同目录 \`.env.local\` 管理。 - 迁移引导密钥由构建发布包时随机生成,构建日志和服务器 \`start.sh\` 发布日志都会显示同一份密钥。 EOF diff --git a/scripts/jenkins-deploy-release.sh b/scripts/jenkins-deploy-release.sh index 9fdc7032..40103225 100644 --- a/scripts/jenkins-deploy-release.sh +++ b/scripts/jenkins-deploy-release.sh @@ -5,20 +5,24 @@ set -euo pipefail usage() { cat <<'EOF' 用法: - ./scripts/jenkins-deploy-release.sh --source-dir /path/to/build/123 --deploy-dir /var/lib/jenkins/deploy/Genarrative --web-port 25001 [--clear-database] [--hook-with-sudo] + ./scripts/jenkins-deploy-release.sh --source-dir /path/to/build/123 --deploy-dir /var/lib/jenkins/deploy/Genarrative --web-port 25001 [--clear-database] [--no-migrate-on-conflict] [--migration-dir /path/to/migrations] [--hook-with-sudo] 说明: 1. 如果部署目录已有旧版本且存在 stop.sh,则先执行旧版本 stop.sh。 2. 仅删除并替换发布产物文件或目录,保留部署目录中的运行数据目录。 3. 把指定发布目录中的白名单产物复制覆盖到部署目录。 4. 如指定 --clear-database,则以清库模式执行新版本 start.sh。 - 5. 最后执行新版本 start.sh。 + 5. 默认允许新版本 start.sh 在 schema 冲突时自动导出、清库发布、导入回灌。 + 6. 最后执行新版本 start.sh。 参数: --source-dir 必填,待部署的发布目录,例如 build/123 --deploy-dir 必填,固定部署目录,例如 /var/lib/jenkins/deploy/Genarrative --web-port 必填,本次部署后静态网站监听端口 --clear-database 可选,启动新版本时追加 --clear-database + --migrate-on-conflict 可选,普通发布遇到 schema 冲突时自动迁移,默认启用 + --no-migrate-on-conflict 可选,禁用 schema 冲突自动迁移 + --migration-dir 可选,自动迁移 JSON 输出目录,默认部署目录内 database-migrations/ --hook-with-sudo 可选,仅对 start.sh/stop.sh 使用 sudo -n 执行 EOF } @@ -106,13 +110,17 @@ SOURCE_DIR="" DEPLOY_DIR="" WEB_PORT="" CLEAR_DATABASE="0" +MIGRATE_ON_CONFLICT="true" +MIGRATION_DIR="" HOOK_WITH_SUDO="0" DEPLOY_ITEMS=( ".env" ".env.local" "README.md" "api-server" + "migration-bootstrap-secret.txt" "spacetime_module.wasm" + "scripts" "start.sh" "stop.sh" "web" @@ -141,6 +149,18 @@ while [[ $# -gt 0 ]]; do CLEAR_DATABASE="1" shift ;; + --migrate-on-conflict) + MIGRATE_ON_CONFLICT="true" + shift + ;; + --no-migrate-on-conflict) + MIGRATE_ON_CONFLICT="false" + shift + ;; + --migration-dir) + MIGRATION_DIR="${2:?缺少 --migration-dir 的值}" + shift 2 + ;; --hook-with-sudo) HOOK_WITH_SUDO="1" shift @@ -211,6 +231,15 @@ else echo "[jenkins-deploy] 部署目录无可执行 stop.sh,跳过停服" fi +if [[ -f "${DEPLOY_DIR}/migration-bootstrap-secret.txt" ]]; then + mkdir -p "${DEPLOY_DIR}/run" + cp "${DEPLOY_DIR}/migration-bootstrap-secret.txt" "${DEPLOY_DIR}/run/migration-bootstrap-secret.previous.txt" + chmod 600 "${DEPLOY_DIR}/run/migration-bootstrap-secret.previous.txt" 2>/dev/null || true + echo "[jenkins-deploy] 已保存旧模块迁移引导密钥,用于 schema 冲突时导出旧库。" +else + rm -f "${DEPLOY_DIR}/run/migration-bootstrap-secret.previous.txt" 2>/dev/null || true +fi + echo "[jenkins-deploy] 清空部署目录: ${DEPLOY_DIR}" for item in "${DEPLOY_ITEMS[@]}"; do if [[ -e "${DEPLOY_DIR}/${item}" ]]; then @@ -241,6 +270,8 @@ fi normalize_release_env_files "${DEPLOY_DIR}" write_env_override "${DEPLOY_DIR}/.env.local" "GENARRATIVE_WEB_PORT" "${WEB_PORT}" +write_env_override "${DEPLOY_DIR}/.env.local" "GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT" "${MIGRATE_ON_CONFLICT}" +write_env_override "${DEPLOY_DIR}/.env.local" "GENARRATIVE_SPACETIME_MIGRATION_DIR" "${MIGRATION_DIR}" echo "[jenkins-deploy] 启动新版本: ${DEPLOY_DIR}" if [[ "${CLEAR_DATABASE}" == "1" ]]; then -- 2.43.0