Add deploy pipeline SpacetimeDB auto migration
Some checks failed
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-04-29 18:52:09 +08:00
parent 37a03dc994
commit 4a0faf5f51
8 changed files with 304 additions and 30 deletions

View File

@@ -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" <<EOF
- \`api-server\`x86_64-unknown-linux-gnu release 可执行文件
- \`spacetime_module.wasm\`wasm32-unknown-unknown release 模块
- \`migration-bootstrap-secret.txt\`:本发布包 wasm 编译时注入的迁移引导密钥;服务器 \`start.sh\` 发布时会显示,迁移授权完成后可删除
- \`scripts/spacetime-*.mjs\`:部署时 schema 冲突自动导出、导入回灌使用的 SpacetimeDB 迁移脚本
- \`web-server.mjs\`:静态网站与 API 反代入口
- \`start.sh\` / \`stop.sh\`:目标服务器启动与停止脚本
@@ -946,6 +1129,8 @@ cat >"${TARGET_DIR}/README.md" <<EOF
./start.sh --clear-database
\`\`\`
默认启动会先尝试无清库发布;如果 SpacetimeDB 返回 schema 冲突,\`start.sh\` 会把旧库导出到 \`database-migrations/<database>/\`,随后清库发布新 wasm并用 \`--replace-existing\` 导入回灌。
## 环境变量
- 启动时会先加载发布目录根部的 \`.env\` 与 \`.env.local\`,再回退到脚本内默认值。
@@ -958,6 +1143,8 @@ cat >"${TARGET_DIR}/README.md" <<EOF
- \`GENARRATIVE_SPACETIME_SERVER_URL\` / \`GENARRATIVE_SPACETIME_DATABASE\`
- \`GENARRATIVE_SPACETIME_ROOT_DIR\`:默认使用发布目录下的 \`.spacetimedb/\`,同时承载本地 SpacetimeDB 运行数据与 CLI 身份。
- \`GENARRATIVE_SPACETIME_TIMEOUT_SECONDS\`:等待 SpacetimeDB 就绪的秒数,默认 \`60\`。
- \`GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT\`:默认 \`true\`,普通发布遇到 schema 冲突时自动导出、清库发布、导入回灌;设为 \`false\` 时保留原始发布失败。
- \`GENARRATIVE_SPACETIME_MIGRATION_DIR\`:自动迁移 JSON 输出目录,默认 \`database-migrations/<database>/\`。
- OSS、LLM、短信、微信等业务密钥仍通过目标服务器环境变量或同目录 \`.env.local\` 管理。
- 迁移引导密钥由构建发布包时随机生成,构建日志和服务器 \`start.sh\` 发布日志都会显示同一份密钥。
EOF