Merge pull request 'Add deploy pipeline SpacetimeDB auto migration' (#4) from codex/jenkins into master
Some checks failed
CI / verify (push) Has been cancelled

Reviewed-on: http://82.157.175.59:3000/GenarrativeAI/Genarrative/pulls/4
This commit was merged in pull request #4.
This commit is contained in:
2026-04-29 23:47:54 +08:00
8 changed files with 303 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';
@@ -558,7 +571,8 @@ usage() {
说明:
1. 启动当前发布包内的静态网站、SpacetimeDB 与 api-server。
2. 默认发布 spacetime_module.wasm 到 GENARRATIVE_SPACETIME_DATABASE但不清库。
3. 只有显式传入 --clear-database 时才会在 schema 冲突时清理旧模块数据后重发
3. 默认遇到 schema 冲突时自动导出旧库、清库发布新模块并导入回灌
4. 显式传入 --clear-database 时代表人工确认清库,不执行自动回灌。
EOF
}
@@ -592,12 +606,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}"
@@ -612,6 +632,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))
@@ -852,14 +1020,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}"
@@ -943,6 +1125,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\`:目标服务器启动与停止脚本
@@ -958,6 +1141,8 @@ cat >"${TARGET_DIR}/README.md" <<'EOF'
./start.sh --clear-database
\`\`\`
默认启动会先尝试无清库发布;如果 SpacetimeDB 返回 schema 冲突,\`start.sh\` 会把旧库导出到 \`database-migrations/<database>/\`,随后清库发布新 wasm并用 \`--replace-existing\` 导入回灌。
## 环境变量
- 启动时会先加载发布目录根部的 \`.env\` 与 \`.env.local\`,再回退到脚本内默认值。
@@ -970,6 +1155,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

View File

@@ -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 <path> 必填,待部署的发布目录,例如 build/123
--deploy-dir <path> 必填,固定部署目录,例如 /var/lib/jenkins/deploy/Genarrative
--web-port <port> 必填,本次部署后静态网站监听端口
--clear-database 可选,启动新版本时追加 --clear-database
--migrate-on-conflict 可选,普通发布遇到 schema 冲突时自动迁移,默认启用
--no-migrate-on-conflict 可选,禁用 schema 冲突自动迁移
--migration-dir <path> 可选,自动迁移 JSON 输出目录,默认部署目录内 database-migrations/<database>
--hook-with-sudo 可选,仅对 start.sh/stop.sh 使用 sudo -n 执行
EOF
}
@@ -106,6 +110,8 @@ SOURCE_DIR=""
DEPLOY_DIR=""
WEB_PORT=""
CLEAR_DATABASE="0"
MIGRATE_ON_CONFLICT="true"
MIGRATION_DIR=""
HOOK_WITH_SUDO="0"
DEPLOY_ITEMS=(
".env"
@@ -114,6 +120,7 @@ DEPLOY_ITEMS=(
"api-server"
"migration-bootstrap-secret.txt"
"spacetime_module.wasm"
"scripts"
"start.sh"
"stop.sh"
"web"
@@ -142,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
@@ -212,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
@@ -242,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