Merge pull request 'Add deploy pipeline SpacetimeDB auto migration' (#4) from codex/jenkins into master
Some checks failed
CI / verify (push) Has been cancelled
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user