diff --git a/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md b/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md index f09953e0..cb98efda 100644 --- a/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md +++ b/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md @@ -509,8 +509,10 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module 构建: - 先按 `SOURCE_BRANCH` / `COMMIT_HASH` 解析并 checkout 目标源码,默认构建 `origin/master` 最新 commit。 -- 使用 `spacetime build` 构建 `spacetime_module.wasm`。 -- 归档 wasm、发布脚本和 `release-manifest.json`。 +- 构建 `spacetime_module.wasm` 前默认生成 32 字节随机 hex 迁移引导密钥,注入 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET`,并把同一份密钥写入 `build//migration-bootstrap-secret.txt`。构建日志只输出密钥来源和长度,不打印明文。 +- `Genarrative-Stdb-Module-Build` 提供 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 参数:留空时自动生成新密钥;填写 Jenkins Secret Text 凭据 ID 时,构建环境注入 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET` 并复用该值。仅在明确传 `--no-migration-bootstrap-secret` 时才构建不带引导密钥的 wasm。 +- 使用 Rust wasm target 构建 `spacetime_module.wasm`。 +- 归档 wasm、`migration-bootstrap-secret.txt` 和 `release-manifest.json`。`migration-bootstrap-secret.txt` 属于敏感产物,只用于创建首个迁移操作员或录入数据库导入/导出流水线的 `BOOTSTRAP_SECRET_CREDENTIAL_ID` 指向的 Jenkins Secret Text;授权完成后不要把明文留在公开归档或聊天记录中。 发布: @@ -520,6 +522,7 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module - 在生产实例本机执行 `spacetime --root-dir=/stdb publish --server http://127.0.0.1:3101 --bin-path spacetime_module.wasm --yes --no-config`。 - 发布动作默认以 `spacetimedb` 服务用户执行,避免 root 默认 CLI 身份对自托管数据库验签失败,也避免 root 写入 `/stdb/config` 造成后续服务用户启动权限错误。 - `Stdb publish` 固定追加 `--no-config`,只依赖显式传入的 `--root-dir`、`--server`、`--bin-path` 与数据库名,避免 agent 工作区、本机用户目录或仓库内 `spacetime` 配置干扰发布目标。 +- 首次迁移操作员授权时,使用本次 Stdb module 构建归档的 `migration-bootstrap-secret.txt` 创建 Jenkins Secret Text,然后在 `Genarrative-Database-Export` / `Genarrative-Database-Import` 的 `BOOTSTRAP_SECRET_CREDENTIAL_ID` 中填写该凭据 ID。后续已有迁移操作员时优先改用 `TOKEN_CREDENTIAL_ID`。 - 成功后执行必要 smoke test。 - 成功后解除维护模式。 - 失败时保留维护模式并发邮件。 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 cc8ab42f..6288eb0b 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 @@ -71,6 +71,7 @@ node scripts/spacetime-revoke-migration-operator.mjs \ - `npm run dev:rust`:在本地 `spacetime publish --module-path` 前生成密钥,控制台输出 `[dev:rust] 迁移引导密钥: ...`。 - `npm run deploy:rust:remote`:在构建发布包 wasm 前生成密钥,控制台输出 `[deploy:rust] 迁移引导密钥: ...`,并把同一份密钥写入发布包根目录的 `migration-bootstrap-secret.txt`。服务器执行 `./start.sh` 发布 wasm 时也会再次显示该文件里的密钥。 +- `npm run build:production-release -- --component spacetime-module`:在生产 Stdb module 构建前默认生成或复用 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET`,注入 `spacetime_module.wasm`,并写入 `build//migration-bootstrap-secret.txt`。生产构建日志只显示密钥来源和长度,不打印明文;该文件应保存为 Jenkins Secret Text,供 `Genarrative-Database-Export` / `Genarrative-Database-Import` 的 `BOOTSTRAP_SECRET_CREDENTIAL_ID` 使用。 如果迁移完成后不希望 wasm 继续携带引导密钥,重新发布时传 `--no-migration-bootstrap-secret`。远端发布包若使用 `--skip-spacetime-build`,必须同时传 `--no-migration-bootstrap-secret`,否则脚本会拒绝生成一个无法注入旧 wasm 的新密钥。 diff --git a/jenkins/Jenkinsfile.production-full-build-and-deploy b/jenkins/Jenkinsfile.production-full-build-and-deploy index 2e12870a..c7a061eb 100644 --- a/jenkins/Jenkinsfile.production-full-build-and-deploy +++ b/jenkins/Jenkinsfile.production-full-build-and-deploy @@ -21,6 +21,7 @@ pipeline { string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER') booleanParam(name: 'RUN_NPM_CI', defaultValue: true, description: 'Web 构建前是否执行 npm ci') string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 genarrative-notification-emails 合并发送') + string(name: 'MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID', defaultValue: '', description: '可选,透传给 Stdb module 构建的迁移 bootstrap secret 凭据 ID;留空则由 Stdb 构建自动生成') string(name: 'WEB_BUILD_JOB_NAME', defaultValue: 'Genarrative-Web-Build', description: 'Web 构建流水线作业名') string(name: 'API_BUILD_JOB_NAME', defaultValue: 'Genarrative-Api-Build', description: 'API 构建流水线作业名') string(name: 'STDB_BUILD_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Build', description: 'Stdb 构建流水线作业名') @@ -120,6 +121,7 @@ pipeline { string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT), string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION), string(name: 'NOTIFICATION_EMAILS', value: params.NOTIFICATION_EMAILS ?: ''), + string(name: 'MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID', value: params.MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID ?: ''), string(name: 'DATABASE', value: params.DATABASE), ] env.STDB_BUILD_NUMBER = stdbRun.number.toString() diff --git a/jenkins/Jenkinsfile.production-stdb-module-build b/jenkins/Jenkinsfile.production-stdb-module-build index da5ffbb1..3715a615 100644 --- a/jenkins/Jenkinsfile.production-stdb-module-build +++ b/jenkins/Jenkinsfile.production-stdb-module-build @@ -24,6 +24,7 @@ pipeline { string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit') string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER') string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 genarrative-notification-emails 合并发送') + string(name: 'MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID', defaultValue: '', description: '可选,复用既有迁移 bootstrap secret 的 Jenkins Secret Text 凭据 ID;留空则本次构建自动生成') booleanParam(name: 'PUBLISH_AFTER_BUILD', defaultValue: false, description: '构建成功后是否触发 Stdb module 发布') string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Publish', description: 'Stdb module 发布流水线作业名') choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: 'PUBLISH_AFTER_BUILD=true 时的逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent') @@ -67,30 +68,32 @@ pipeline { stage('Build Stdb Module') { steps { - powershell ''' - $ErrorActionPreference = 'Stop' - $workspaceTmp = if ($env:WORKSPACE_TMP) { $env:WORKSPACE_TMP } else { "$env:WORKSPACE@tmp" } - $env:CARGO_HOME = "$workspaceTmp/cargo-home" - $env:CARGO_TARGET_DIR = "$workspaceTmp/cargo-target/prod-release" - $env:SCCACHE_DIR = "$env:USERPROFILE/.cache/sccache-stdb-module" - $env:PATH = "$env:CARGO_HOME/bin;$env:PATH" - $gitBash = @( - $env:GENARRATIVE_BASH, - 'C:/Program Files/Git/bin/bash.exe', - 'C:/Program Files/Git/usr/bin/bash.exe', - 'C:/msys64/usr/bin/bash.exe', - 'bash' - ) | Where-Object { $_ -and (($_ -eq 'bash') -or (Test-Path $_)) } | Select-Object -First 1 - if (-not $gitBash) { - throw '[stdb-build] Windows 构建节点缺少 Git Bash,无法执行仓库现有生产构建脚本。请先安装 Git for Windows,并确保 bash 在 PATH 中。' - } - $env:GENARRATIVE_BASH = $gitBash - if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { - throw '[stdb-build] 缺少 cargo。请先在 Windows 构建节点安装 Rust 工具链,并确保 cargo 在 PATH 中。' - } - # sccache 只是可选缓存;PATH 中存在但不可执行时必须回退到 rustc。 + script { + def buildStep = { + powershell ''' + $ErrorActionPreference = 'Stop' + $workspaceTmp = if ($env:WORKSPACE_TMP) { $env:WORKSPACE_TMP } else { "$env:WORKSPACE@tmp" } + $env:CARGO_HOME = "$workspaceTmp/cargo-home" + $env:CARGO_TARGET_DIR = "$workspaceTmp/cargo-target/prod-release" + $env:SCCACHE_DIR = "$env:USERPROFILE/.cache/sccache-stdb-module" + $env:PATH = "$env:CARGO_HOME/bin;$env:PATH" + $gitBash = @( + $env:GENARRATIVE_BASH, + 'C:/Program Files/Git/bin/bash.exe', + 'C:/Program Files/Git/usr/bin/bash.exe', + 'C:/msys64/usr/bin/bash.exe', + 'bash' + ) | Where-Object { $_ -and (($_ -eq 'bash') -or (Test-Path $_)) } | Select-Object -First 1 + if (-not $gitBash) { + throw '[stdb-build] Windows 构建节点缺少 Git Bash,无法执行仓库现有生产构建脚本。请先安装 Git for Windows,并确保 bash 在 PATH 中。' + } + $env:GENARRATIVE_BASH = $gitBash + if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { + throw '[stdb-build] 缺少 cargo。请先在 Windows 构建节点安装 Rust 工具链,并确保 cargo 在 PATH 中。' + } + # sccache 只是可选缓存;PATH 中存在但不可执行时必须回退到 rustc。 $sccacheCommand = Get-Command sccache -ErrorAction SilentlyContinue - $sccacheUsable = $false + $sccacheUsable = $false if ($sccacheCommand) { try { & $sccacheCommand.Source --version | Out-Host @@ -101,16 +104,28 @@ pipeline { } if (-not $sccacheUsable) { Write-Host '[stdb-build] 未找到可用 sccache,改用 rustc 直接构建。' - Remove-Item Env:RUSTC_WRAPPER -ErrorAction SilentlyContinue + Remove-Item Env:RUSTC_WRAPPER -ErrorAction SilentlyContinue + } + npm run build:production-release -- --component spacetime-module --name "$env:EFFECTIVE_BUILD_VERSION" + ''' } - npm run build:production-release -- --component spacetime-module --name "$env:EFFECTIVE_BUILD_VERSION" - ''' + if (params.MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID?.trim()) { + withCredentials([ + string(credentialsId: params.MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID.trim(), variable: 'GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET') + ]) { + buildStep() + } + } else { + buildStep() + } + } } } stage('Archive') { steps { archiveArtifacts artifacts: "build/${env.EFFECTIVE_BUILD_VERSION}/spacetime_module.wasm,build/${env.EFFECTIVE_BUILD_VERSION}/spacetime_module.wasm.sha256,build/${env.EFFECTIVE_BUILD_VERSION}/release-manifest.json", fingerprint: true + archiveArtifacts artifacts: "build/${env.EFFECTIVE_BUILD_VERSION}/migration-bootstrap-secret.txt", fingerprint: false } } diff --git a/scripts/build-production-release.sh b/scripts/build-production-release.sh index 03cd25cb..0967523e 100644 --- a/scripts/build-production-release.sh +++ b/scripts/build-production-release.sh @@ -18,6 +18,8 @@ usage() { --skip-web-build 跳过主站与后台构建,仅复制已有 dist 产物 --skip-api-build 跳过 api-server 构建,仅复制已有 release 二进制 --skip-spacetime-build 跳过 spacetime-module 构建,仅复制已有 wasm + --no-migration-bootstrap-secret + 构建不带迁移引导密钥的 spacetime-module wasm EOF } @@ -73,6 +75,61 @@ write_sha256_file() { ) >"${checksum_path}" } +generate_migration_bootstrap_secret() { + node -e 'const crypto = require("crypto"); process.stdout.write(crypto.randomBytes(32).toString("hex"));' +} + +prepare_migration_bootstrap_secret() { + local secret_source="generated" + + if [[ "${BUILD_SPACETIME}" -ne 1 || "${SKIP_SPACETIME_BUILD}" -eq 1 ]]; then + return + fi + + if [[ "${MIGRATION_BOOTSTRAP_SECRET_MODE}" == "disabled" ]]; then + unset GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET + echo "[production-release] 未启用迁移引导密钥。" + return + fi + + if [[ -n "${GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET:-}" ]]; then + MIGRATION_BOOTSTRAP_SECRET="${GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET}" + secret_source="environment" + else + MIGRATION_BOOTSTRAP_SECRET="$(generate_migration_bootstrap_secret)" + export GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET="${MIGRATION_BOOTSTRAP_SECRET}" + fi + + if [[ "${#MIGRATION_BOOTSTRAP_SECRET}" -lt 16 ]]; then + echo "[production-release] 迁移引导密钥至少需要 16 个字符。" >&2 + exit 1 + fi + + echo "[production-release] 已准备迁移引导密钥: source=${secret_source}, length=${#MIGRATION_BOOTSTRAP_SECRET}" +} + +write_migration_bootstrap_secret_file() { + local target_path="${TARGET_DIR}/migration-bootstrap-secret.txt" + + if [[ "${BUILD_SPACETIME}" -ne 1 || "${SKIP_SPACETIME_BUILD}" -eq 1 ]]; then + return + fi + + if [[ "${MIGRATION_BOOTSTRAP_SECRET_MODE}" == "disabled" ]]; then + return + fi + + if [[ -z "${MIGRATION_BOOTSTRAP_SECRET}" ]]; then + echo "[production-release] 迁移引导密钥为空,无法写入发布产物。" >&2 + exit 1 + fi + + printf "%s\n" "${MIGRATION_BOOTSTRAP_SECRET}" >"${target_path}" + chmod 600 "${target_path}" 2>/dev/null || true + MIGRATION_BOOTSTRAP_SECRET_ARTIFACT=1 + echo "[production-release] 已写入迁移引导密钥文件: ${target_path}" +} + write_release_manifest() { RELEASE_MANIFEST_PATH="${TARGET_DIR}/release-manifest.json" \ RELEASE_VERSION="${BUILD_NAME}" \ @@ -83,6 +140,7 @@ write_release_manifest() { RELEASE_INCLUDE_WEB="${BUILD_WEB}" \ RELEASE_INCLUDE_API="${BUILD_API}" \ RELEASE_INCLUDE_SPACETIME="${BUILD_SPACETIME}" \ + RELEASE_INCLUDE_MIGRATION_BOOTSTRAP_SECRET="${MIGRATION_BOOTSTRAP_SECRET_ARTIFACT}" \ node <<'NODE' const fs = require('fs'); @@ -108,6 +166,13 @@ if (process.env.RELEASE_INCLUDE_SPACETIME === '1') { checksum_path: 'spacetime_module.wasm.sha256', }); } +if (process.env.RELEASE_INCLUDE_MIGRATION_BOOTSTRAP_SECRET === '1') { + artifacts.push({ + component: 'spacetime-module', + path: 'migration-bootstrap-secret.txt', + sensitive: true, + }); +} const manifest = { version: process.env.RELEASE_VERSION, @@ -131,6 +196,9 @@ COMPONENT="all" SKIP_WEB_BUILD=0 SKIP_API_BUILD=0 SKIP_SPACETIME_BUILD=0 +MIGRATION_BOOTSTRAP_SECRET="" +MIGRATION_BOOTSTRAP_SECRET_ARTIFACT=0 +MIGRATION_BOOTSTRAP_SECRET_MODE="auto" BUILD_COMPLETED=0 while [[ $# -gt 0 ]]; do @@ -159,6 +227,11 @@ while [[ $# -gt 0 ]]; do SKIP_SPACETIME_BUILD=1 shift ;; + --no-migration-bootstrap-secret) + MIGRATION_BOOTSTRAP_SECRET="" + MIGRATION_BOOTSTRAP_SECRET_MODE="disabled" + shift + ;; *) echo "[production-release] 未知参数: $1" >&2 usage >&2 @@ -262,6 +335,7 @@ mkdir -p "${TARGET_DIR}" echo "[production-release] 发布包目录: ${TARGET_DIR}" echo "[production-release] 构建组件: ${COMPONENT}" +prepare_migration_bootstrap_secret if [[ "${BUILD_WEB}" -eq 1 ]]; then mkdir -p "${WEB_DIR}" @@ -364,6 +438,7 @@ fi if [[ "${BUILD_SPACETIME}" -eq 1 ]]; then copy_required_file "${WASM_SOURCE}" "${TARGET_DIR}/spacetime_module.wasm" "spacetime-module wasm" write_sha256_file "${TARGET_DIR}/spacetime_module.wasm" + write_migration_bootstrap_secret_file fi mkdir -p "${TARGET_DIR}/scripts" "${TARGET_DIR}/deploy" @@ -402,6 +477,7 @@ cat >"${TARGET_DIR}/README.md" <