Merge remote-tracking branch 'origin/master' into codex/publish-flow
Some checks failed
CI / verify (pull_request) Has been cancelled
Some checks failed
CI / verify (pull_request) Has been cancelled
# Conflicts: # docs/technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md # docs/technical/README.md # jenkins/Jenkinsfile.database-export # jenkins/Jenkinsfile.database-import
This commit is contained in:
@@ -37,25 +37,18 @@ function loadEnvFile(path, target) {
|
||||
const mergedEnv = { ...process.env };
|
||||
loadEnvFile(resolve(repoRoot, '.env'), mergedEnv);
|
||||
loadEnvFile(resolve(repoRoot, '.env.local'), mergedEnv);
|
||||
loadEnvFile(resolve(repoRoot, '.env.secrets.local'), mergedEnv);
|
||||
|
||||
mergedEnv.GENARRATIVE_API_HOST = mergedEnv.GENARRATIVE_API_HOST || '127.0.0.1';
|
||||
mergedEnv.GENARRATIVE_API_PORT = mergedEnv.GENARRATIVE_API_PORT || '3100';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL =
|
||||
mergedEnv.GENARRATIVE_SPACETIME_MAINCLOUD_SERVER_URL ||
|
||||
mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL ||
|
||||
'https://maincloud.spacetimedb.com';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_DATABASE =
|
||||
mergedEnv.GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE ||
|
||||
mergedEnv.GENARRATIVE_SPACETIME_DATABASE ||
|
||||
'';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_TOKEN =
|
||||
mergedEnv.GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN ||
|
||||
mergedEnv.GENARRATIVE_SPACETIME_TOKEN ||
|
||||
'';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL || 'http://127.0.0.1:3101';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_DATABASE = mergedEnv.GENARRATIVE_SPACETIME_DATABASE || '';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_TOKEN = mergedEnv.GENARRATIVE_SPACETIME_TOKEN || '';
|
||||
|
||||
if (!mergedEnv.GENARRATIVE_SPACETIME_DATABASE) {
|
||||
console.error(
|
||||
'[api-server:maincloud] 缺少 GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE 或 GENARRATIVE_SPACETIME_DATABASE。',
|
||||
'[api-server] 缺少 GENARRATIVE_SPACETIME_DATABASE。',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -78,7 +71,7 @@ function stopExistingWindowsApiServer() {
|
||||
' Wait-Process -Id $process.Id -Timeout 5 -ErrorAction SilentlyContinue',
|
||||
' Write-Output $process.Id',
|
||||
' } catch {',
|
||||
' Write-Error "[api-server:maincloud] 忽略旧进程清理瞬时失败 pid=$($process.Id): $($_.Exception.Message)"',
|
||||
' Write-Error "[api-server] 忽略旧进程清理瞬时失败 pid=$($process.Id): $($_.Exception.Message)"',
|
||||
' }',
|
||||
'}',
|
||||
'exit 0',
|
||||
@@ -97,7 +90,7 @@ function stopExistingWindowsApiServer() {
|
||||
).trim();
|
||||
|
||||
if (output) {
|
||||
console.log(`[api-server:maincloud] 已停止旧 api-server 进程: ${output}`);
|
||||
console.log(`[api-server] 已停止旧 api-server 进程: ${output}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,13 +98,13 @@ try {
|
||||
stopExistingWindowsApiServer();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[api-server:maincloud] 清理旧 api-server 进程失败: ${error.message}`,
|
||||
`[api-server] 清理旧 api-server 进程失败: ${error.message}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[api-server:maincloud] SpacetimeDB ${mergedEnv.GENARRATIVE_SPACETIME_DATABASE} @ ${mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL}`,
|
||||
`[api-server] SpacetimeDB ${mergedEnv.GENARRATIVE_SPACETIME_DATABASE} @ ${mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL}`,
|
||||
);
|
||||
|
||||
const child = spawn(
|
||||
@@ -125,13 +118,13 @@ const child = spawn(
|
||||
);
|
||||
|
||||
child.on('error', (error) => {
|
||||
console.error(`[api-server:maincloud] 启动 cargo 失败: ${error.message}`);
|
||||
console.error(`[api-server] 启动 cargo 失败: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
console.error(`[api-server:maincloud] api-server 被信号终止: ${signal}`);
|
||||
console.error(`[api-server] api-server 被信号终止: ${signal}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
276
scripts/check-server-rs-ddd-boundaries.mjs
Normal file
276
scripts/check-server-rs-ddd-boundaries.mjs
Normal file
@@ -0,0 +1,276 @@
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { basename, join, relative } from 'node:path';
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const cratesDir = join(repoRoot, 'server-rs', 'crates');
|
||||
const spacetimeModuleSrcDir = join(cratesDir, 'spacetime-module', 'src');
|
||||
const spacetimeMigrationPath = join(spacetimeModuleSrcDir, 'migration.rs');
|
||||
const spacetimeTableCatalogPath = join(
|
||||
repoRoot,
|
||||
'docs',
|
||||
'technical',
|
||||
'SPACETIMEDB_TABLE_CATALOG.md',
|
||||
);
|
||||
const migrationExcludedTables = new Set([
|
||||
'database_migration_operator',
|
||||
'database_migration_import_chunk',
|
||||
]);
|
||||
const requiredModuleFiles = [
|
||||
'domain.rs',
|
||||
'commands.rs',
|
||||
'application.rs',
|
||||
'events.rs',
|
||||
'errors.rs',
|
||||
];
|
||||
const requiredLibModules = ['domain', 'commands', 'application', 'events', 'errors'];
|
||||
const forbiddenModuleWidePatterns = [
|
||||
{
|
||||
pattern: /\baxum::/u,
|
||||
message: 'module-* 不允许直接依赖 Axum',
|
||||
},
|
||||
{
|
||||
pattern: /\bspacetimedb::(?:table|reducer|procedure|ReducerContext|ProcedureContext|Table)\b/u,
|
||||
message: 'module-* 不允许声明 SpacetimeDB table/reducer/procedure 或直接操作表',
|
||||
},
|
||||
];
|
||||
const forbiddenCorePatterns = [
|
||||
{
|
||||
pattern: /\breqwest::/u,
|
||||
message: 'DDD 核心文件不允许直接依赖 reqwest',
|
||||
},
|
||||
{
|
||||
pattern: /\bplatform_oss::/u,
|
||||
message: 'DDD 核心文件不允许直接依赖 OSS adapter',
|
||||
},
|
||||
{
|
||||
pattern: /\bplatform_llm::/u,
|
||||
message: 'DDD 核心文件不允许直接依赖 LLM adapter',
|
||||
},
|
||||
{
|
||||
pattern: /\bspacetime_client::/u,
|
||||
message: 'DDD 核心文件不允许直接依赖 SpacetimeDB client adapter',
|
||||
},
|
||||
{
|
||||
pattern: /\bstd::fs\b/u,
|
||||
message: 'DDD 核心文件不允许直接访问文件系统',
|
||||
},
|
||||
{
|
||||
pattern: /\btokio::/u,
|
||||
message: 'DDD 核心文件不允许绑定异步运行时',
|
||||
},
|
||||
];
|
||||
|
||||
function normalizePath(path) {
|
||||
return path.replace(/\\/gu, '/');
|
||||
}
|
||||
|
||||
function readText(path) {
|
||||
return readFileSync(path, 'utf8');
|
||||
}
|
||||
|
||||
function listRustFiles(dir) {
|
||||
const files = [];
|
||||
|
||||
function walk(currentDir) {
|
||||
for (const name of readdirSync(currentDir)) {
|
||||
const fullPath = join(currentDir, name);
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
walk(fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name.endsWith('.rs')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
function collectSpacetimeTables() {
|
||||
if (!existsSync(spacetimeModuleSrcDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tableByAccessor = new Map();
|
||||
const tablePattern =
|
||||
/#\[spacetimedb::table\(([\s\S]*?)\)\]\s*(?:#\[[^\]]+\]\s*)*(?:pub\s+)?struct\s+([A-Za-z0-9_]+)/gu;
|
||||
|
||||
for (const rustFile of listRustFiles(spacetimeModuleSrcDir)) {
|
||||
const text = readText(rustFile);
|
||||
let match;
|
||||
while ((match = tablePattern.exec(text)) !== null) {
|
||||
const accessorMatch = /accessor\s*=\s*([A-Za-z0-9_]+)/u.exec(match[1]);
|
||||
if (!accessorMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const accessor = accessorMatch[1];
|
||||
const relativePath = normalizePath(relative(repoRoot, rustFile));
|
||||
const previous = tableByAccessor.get(accessor);
|
||||
if (previous) {
|
||||
failures.push(
|
||||
`SpacetimeDB table accessor ${accessor} 重复定义于 ${previous.path} 与 ${relativePath}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
tableByAccessor.set(accessor, {
|
||||
accessor,
|
||||
structName: match[2],
|
||||
path: relativePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [...tableByAccessor.values()].sort((left, right) =>
|
||||
left.accessor.localeCompare(right.accessor),
|
||||
);
|
||||
}
|
||||
|
||||
function collectMigrationTables() {
|
||||
if (!existsSync(spacetimeMigrationPath)) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const migrationText = readText(spacetimeMigrationPath);
|
||||
const macroMatch =
|
||||
/macro_rules!\s+migration_tables\s*\{[\s\S]*?\$macro_name!\s*\{([\s\S]*?)\n\s*\}\s*\n\s*\};\s*\n\}/u.exec(
|
||||
migrationText,
|
||||
);
|
||||
if (!macroMatch) {
|
||||
failures.push('migration.rs 无法解析 migration_tables! 白名单');
|
||||
return new Set();
|
||||
}
|
||||
|
||||
return new Set(
|
||||
[...macroMatch[1].matchAll(/\b([a-z][a-z0-9_]*)\b/gu)]
|
||||
.map((match) => match[1])
|
||||
.filter((name) => !['arg'].includes(name)),
|
||||
);
|
||||
}
|
||||
|
||||
function collectCatalogTables() {
|
||||
if (!existsSync(spacetimeTableCatalogPath)) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const catalogText = readText(spacetimeTableCatalogPath);
|
||||
return new Set(
|
||||
[...catalogText.matchAll(/^### `([^`]+)`/gmu)].map((match) => match[1]),
|
||||
);
|
||||
}
|
||||
|
||||
function checkSpacetimeTableCatalogAndMigration() {
|
||||
const tables = collectSpacetimeTables();
|
||||
const tableNames = new Set(tables.map((table) => table.accessor));
|
||||
const migrationTables = collectMigrationTables();
|
||||
const catalogTables = collectCatalogTables();
|
||||
|
||||
for (const table of tables) {
|
||||
if (!migrationExcludedTables.has(table.accessor) && !migrationTables.has(table.accessor)) {
|
||||
failures.push(
|
||||
`${table.path}: SpacetimeDB 表 ${table.accessor} 缺少 migration.rs 白名单`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!catalogTables.has(table.accessor)) {
|
||||
failures.push(
|
||||
`${table.path}: SpacetimeDB 表 ${table.accessor} 缺少 SPACETIMEDB_TABLE_CATALOG.md 目录项`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tableName of migrationTables) {
|
||||
if (!tableNames.has(tableName)) {
|
||||
failures.push(`migration.rs 白名单包含不存在的 SpacetimeDB 表 ${tableName}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tableName of catalogTables) {
|
||||
if (!tableNames.has(tableName)) {
|
||||
failures.push(`SPACETIMEDB_TABLE_CATALOG.md 包含不存在的 SpacetimeDB 表 ${tableName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectModuleCrates() {
|
||||
return readdirSync(cratesDir)
|
||||
.filter((name) => name.startsWith('module-'))
|
||||
.filter((name) => existsSync(join(cratesDir, name, 'Cargo.toml')))
|
||||
.sort();
|
||||
}
|
||||
|
||||
const failures = [];
|
||||
const moduleCrates = collectModuleCrates();
|
||||
|
||||
for (const crateName of moduleCrates) {
|
||||
const crateDir = join(cratesDir, crateName);
|
||||
const srcDir = join(crateDir, 'src');
|
||||
const libPath = join(srcDir, 'lib.rs');
|
||||
|
||||
for (const fileName of requiredModuleFiles) {
|
||||
const filePath = join(srcDir, fileName);
|
||||
if (!existsSync(filePath)) {
|
||||
failures.push(`${crateName} 缺少 DDD 落位文件 src/${fileName}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (existsSync(libPath)) {
|
||||
const libText = readText(libPath);
|
||||
for (const moduleName of requiredLibModules) {
|
||||
const moduleDeclaration = new RegExp(
|
||||
`(?:^|\\n)\\s*(?:pub(?:\\([^)]*\\))?\\s+)?mod\\s+${moduleName}\\s*;`,
|
||||
'u',
|
||||
);
|
||||
if (!moduleDeclaration.test(libText)) {
|
||||
failures.push(`${crateName} 的 lib.rs 缺少模块声明 mod ${moduleName};`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const rustFile of listRustFiles(srcDir)) {
|
||||
const relativePath = normalizePath(relative(repoRoot, rustFile));
|
||||
const fileName = basename(rustFile);
|
||||
const text = readText(rustFile);
|
||||
|
||||
if (fileName === 'mapper.rs') {
|
||||
failures.push(`${relativePath} 不能位于 module-*,mapper 只能放在 adapter crate`);
|
||||
}
|
||||
|
||||
for (const rule of forbiddenModuleWidePatterns) {
|
||||
if (rule.pattern.test(text)) {
|
||||
failures.push(`${relativePath}: ${rule.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const isDddCoreFile = requiredModuleFiles.some((name) =>
|
||||
relativePath.endsWith(`/src/${name}`),
|
||||
);
|
||||
if (!isDddCoreFile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const rule of forbiddenCorePatterns) {
|
||||
if (rule.pattern.test(text)) {
|
||||
failures.push(`${relativePath}: ${rule.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkSpacetimeTableCatalogAndMigration();
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error('server-rs DDD boundary check failed:');
|
||||
for (const failure of failures) {
|
||||
console.error(`- ${failure}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`server-rs DDD boundary check passed for ${moduleCrates.length} module crate(s).`);
|
||||
@@ -438,12 +438,6 @@ const proxyPrefixes = [
|
||||
'/admin/api',
|
||||
'/api/',
|
||||
'/api',
|
||||
'/generated-character-drafts',
|
||||
'/generated-characters',
|
||||
'/generated-animations',
|
||||
'/generated-custom-world-scenes',
|
||||
'/generated-custom-world-covers',
|
||||
'/generated-qwen-sprites',
|
||||
'/healthz',
|
||||
];
|
||||
|
||||
@@ -1134,7 +1128,7 @@ if ! run_publish "${PUBLISH_LOG}" "${PUBLISH_ARGS[@]}"; then
|
||||
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
|
||||
echo "[start] 如果必须保留数据:请切换到创建该数据库的 SpacetimeDB 身份,或把 GENARRATIVE_SPACETIME_DATABASE 改为当前身份有权限的库。" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {spawn} from 'node:child_process';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {existsSync} from 'node:fs';
|
||||
import {cp, mkdir, readdir, rm, stat} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
@@ -55,7 +55,7 @@ for (const target of selectedTargets) {
|
||||
await recreateTempDir(tempOutDir);
|
||||
|
||||
console.log(`[spacetime:generate] 生成 ${target.name} bindings 到短路径: ${tempOutDir}`);
|
||||
await run('spacetime', buildGenerateArgs(target, tempOutDir));
|
||||
await generateBindings(target, tempOutDir);
|
||||
|
||||
const fileCount = await countFiles(tempOutDir);
|
||||
if (fileCount === 0) {
|
||||
@@ -148,7 +148,79 @@ function buildGenerateArgs(target, outDir) {
|
||||
return generateArgs;
|
||||
}
|
||||
|
||||
function run(command, commandArgs) {
|
||||
async function generateBindings(target, outDir) {
|
||||
const result = await run('spacetime', buildGenerateArgs(target, outDir), {
|
||||
allowGeneratedFormatFailure: target.lang === 'rust',
|
||||
});
|
||||
|
||||
if (result.generatedFormatFailed) {
|
||||
// Windows 下 SpacetimeDB CLI 2.1.0 会把所有 Rust 文件一次性传给 formatter;
|
||||
// 这里只接管“文件已生成但 CLI 格式化失败”的尾段,并仍然只同步生成目录。
|
||||
console.warn(
|
||||
`[spacetime:generate] ${target.name} bindings 已生成,但 SpacetimeDB CLI 自带格式化失败;改用短路径分批 rustfmt。`,
|
||||
);
|
||||
await formatRustBindings(outDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function formatRustBindings(outDir) {
|
||||
const rustFiles = await collectRustFiles(outDir);
|
||||
if (rustFiles.length === 0) {
|
||||
throw new Error(`Rust bindings 未生成任何 .rs 文件,无法格式化: ${outDir}`);
|
||||
}
|
||||
|
||||
for (const chunk of chunkCommandArgs(rustFiles)) {
|
||||
await run('rustfmt', ['--edition', '2024', ...chunk]);
|
||||
}
|
||||
}
|
||||
|
||||
async function collectRustFiles(dir) {
|
||||
const files = [];
|
||||
const entries = await readdir(dir, {withFileTypes: true});
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await collectRustFiles(entryPath)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile() && entry.name.endsWith('.rs')) {
|
||||
files.push(entryPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function chunkCommandArgs(argsToChunk) {
|
||||
// Windows CreateProcess 受命令行长度限制;分批能避免 bindings 文件变多后再次失败。
|
||||
const maxCommandLineChars = process.platform === 'win32' ? 20_000 : 100_000;
|
||||
const chunks = [];
|
||||
let current = [];
|
||||
let currentLength = 0;
|
||||
|
||||
for (const arg of argsToChunk) {
|
||||
const argLength = arg.length + 3;
|
||||
if (current.length > 0 && currentLength + argLength > maxCommandLineChars) {
|
||||
chunks.push(current);
|
||||
current = [];
|
||||
currentLength = 0;
|
||||
}
|
||||
|
||||
current.push(arg);
|
||||
currentLength += argLength;
|
||||
}
|
||||
|
||||
if (current.length > 0) {
|
||||
chunks.push(current);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function run(command, commandArgs, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, commandArgs, {
|
||||
cwd: REPO_ROOT,
|
||||
@@ -178,13 +250,21 @@ function run(command, commandArgs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const generatedFormatFailed = output.includes('Could not format generated files');
|
||||
|
||||
if (generatedFormatFailed && options.allowGeneratedFormatFailure) {
|
||||
console.warn(`[spacetime:generate] ${command} generated files but formatting failed; continuing with validation.`);
|
||||
resolve({generatedFormatFailed});
|
||||
return;
|
||||
}
|
||||
|
||||
if (generatedFormatFailed) {
|
||||
reject(new Error(`${command} generated files but formatting failed.`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (code === 0) {
|
||||
if (output.includes('Could not format generated files')) {
|
||||
// 中文注释:Windows 下 Rust 绑定文件很多时,SpacetimeDB CLI 可能已生成成功但 rustfmt 启动失败。
|
||||
// 这里保留后续文件数量校验,避免把格式化警告误判成绑定生成失败。
|
||||
console.warn(`[spacetime:generate] ${command} 生成后格式化失败,继续校验并同步生成文件。`);
|
||||
}
|
||||
resolve();
|
||||
resolve({generatedFormatFailed: false});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -181,7 +181,6 @@ MIGRATION_IMPORT_TOKEN=""
|
||||
PRESERVED_MIGRATION_EXPORT_TOKEN=""
|
||||
PRESERVED_MIGRATION_IMPORT_TOKEN=""
|
||||
PRESERVED_SPACETIME_TOKEN=""
|
||||
PRESERVED_SPACETIME_MAINCLOUD_TOKEN=""
|
||||
DEPLOY_COMPLETED="0"
|
||||
RESTORE_PREVIOUS_MIGRATION_BOOTSTRAP_SECRET_ON_FAILURE="0"
|
||||
DEPLOY_ITEMS=(
|
||||
@@ -402,7 +401,6 @@ normalize_release_env_files "${SOURCE_DIR}"
|
||||
PRESERVED_MIGRATION_EXPORT_TOKEN="$(read_env_value "GENARRATIVE_SPACETIME_MIGRATION_EXPORT_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")"
|
||||
PRESERVED_MIGRATION_IMPORT_TOKEN="$(read_env_value "GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")"
|
||||
PRESERVED_SPACETIME_TOKEN="$(read_env_value "GENARRATIVE_SPACETIME_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")"
|
||||
PRESERVED_SPACETIME_MAINCLOUD_TOKEN="$(read_env_value "GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")"
|
||||
|
||||
if [[ -x "${DEPLOY_DIR}/stop.sh" ]]; then
|
||||
echo "[jenkins-deploy] 先停止旧版本: ${DEPLOY_DIR}"
|
||||
@@ -464,14 +462,8 @@ elif [[ -n "${PRESERVED_MIGRATION_IMPORT_TOKEN}" ]] \
|
||||
write_env_override "${DEPLOY_DIR}/.env.local" "GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN" "${PRESERVED_MIGRATION_IMPORT_TOKEN}"
|
||||
fi
|
||||
if [[ -n "${PRESERVED_SPACETIME_TOKEN}" ]] \
|
||||
&& [[ -z "$(read_env_value "GENARRATIVE_SPACETIME_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")" ]] \
|
||||
&& [[ -z "$(read_env_value "GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")" ]]; then
|
||||
write_env_override "${DEPLOY_DIR}/.env.local" "GENARRATIVE_SPACETIME_TOKEN" "${PRESERVED_SPACETIME_TOKEN}"
|
||||
fi
|
||||
if [[ -n "${PRESERVED_SPACETIME_MAINCLOUD_TOKEN}" ]] \
|
||||
&& [[ -z "$(read_env_value "GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")" ]] \
|
||||
&& [[ -z "$(read_env_value "GENARRATIVE_SPACETIME_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")" ]]; then
|
||||
write_env_override "${DEPLOY_DIR}/.env.local" "GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN" "${PRESERVED_SPACETIME_MAINCLOUD_TOKEN}"
|
||||
write_env_override "${DEPLOY_DIR}/.env.local" "GENARRATIVE_SPACETIME_TOKEN" "${PRESERVED_SPACETIME_TOKEN}"
|
||||
fi
|
||||
|
||||
DEPLOY_DATABASE="$(read_env_value "GENARRATIVE_SPACETIME_DATABASE" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")"
|
||||
|
||||
@@ -8,27 +8,15 @@ export function parseArgs(argv) {
|
||||
process.env.GENARRATIVE_SPACETIME_MIGRATION_CHUNK_SIZE,
|
||||
'GENARRATIVE_SPACETIME_MIGRATION_CHUNK_SIZE',
|
||||
),
|
||||
database:
|
||||
process.env.GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE ||
|
||||
process.env.GENARRATIVE_SPACETIME_DATABASE ||
|
||||
'',
|
||||
database: process.env.GENARRATIVE_SPACETIME_DATABASE || '',
|
||||
bootstrapSecret: process.env.GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET || '',
|
||||
includeTables: [],
|
||||
operatorIdentity: process.env.GENARRATIVE_SPACETIME_MIGRATION_OPERATOR_IDENTITY || '',
|
||||
passthrough: [],
|
||||
note: '',
|
||||
server:
|
||||
process.env.GENARRATIVE_SPACETIME_MAINCLOUD_SERVER ||
|
||||
process.env.GENARRATIVE_SPACETIME_SERVER ||
|
||||
'',
|
||||
serverUrl:
|
||||
process.env.GENARRATIVE_SPACETIME_MAINCLOUD_SERVER_URL ||
|
||||
process.env.GENARRATIVE_SPACETIME_SERVER_URL ||
|
||||
'',
|
||||
token:
|
||||
process.env.GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN ||
|
||||
process.env.GENARRATIVE_SPACETIME_TOKEN ||
|
||||
'',
|
||||
server: process.env.GENARRATIVE_SPACETIME_SERVER || '',
|
||||
serverUrl: process.env.GENARRATIVE_SPACETIME_SERVER_URL || '',
|
||||
token: process.env.GENARRATIVE_SPACETIME_TOKEN || '',
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
@@ -117,11 +105,7 @@ export function buildSpacetimeCallArgs(options, procedureName, input) {
|
||||
args.push(`--root-dir=${options.rootDir}`);
|
||||
}
|
||||
args.push('call');
|
||||
if (options.server) {
|
||||
args.push('-s', options.server);
|
||||
} else if (options.serverUrl) {
|
||||
args.push('-s', options.serverUrl);
|
||||
}
|
||||
args.push('-s', resolveCliServer(options));
|
||||
args.push(...options.passthrough);
|
||||
if (!options.passthrough.includes('--no-config')) {
|
||||
args.push('--no-config');
|
||||
@@ -388,7 +372,7 @@ export function resolveServerUrl(options) {
|
||||
return options.serverUrl;
|
||||
}
|
||||
|
||||
const server = (options.server || 'maincloud').trim();
|
||||
const server = (options.server || 'dev').trim();
|
||||
if (server.startsWith('http://') || server.startsWith('https://')) {
|
||||
return server;
|
||||
}
|
||||
@@ -398,13 +382,25 @@ export function resolveServerUrl(options) {
|
||||
if (server === 'local') {
|
||||
return 'http://127.0.0.1:3000';
|
||||
}
|
||||
if (!server || server === 'maincloud') {
|
||||
return 'https://maincloud.spacetimedb.com';
|
||||
if (!server) {
|
||||
return 'http://127.0.0.1:3101';
|
||||
}
|
||||
|
||||
throw new Error(`未知 SpacetimeDB server: ${server}。请改用 --server-url 显式传入地址。`);
|
||||
}
|
||||
|
||||
function resolveCliServer(options) {
|
||||
if (options.serverUrl) {
|
||||
return options.serverUrl;
|
||||
}
|
||||
|
||||
const server = (options.server || '').trim();
|
||||
if (!server || server === 'dev') {
|
||||
return 'http://127.0.0.1:3101';
|
||||
}
|
||||
return server;
|
||||
}
|
||||
|
||||
function trimPreview(text) {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.length <= 4000) {
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
SERVER_RS_DIR="${REPO_ROOT}/server-rs"
|
||||
MODULE_PATH="${SERVER_RS_DIR}/target/wasm32-unknown-unknown/release/spacetime_module.wasm"
|
||||
SPACETIME_SERVER_ALIAS="maincloud"
|
||||
CLEAR_DATABASE=0
|
||||
MIGRATE_ON_CONFLICT=1
|
||||
MIGRATION_DIR=""
|
||||
MIGRATION_BOOTSTRAP_SECRET=""
|
||||
MIGRATION_BOOTSTRAP_SECRET_MODE="auto"
|
||||
|
||||
load_env_file() {
|
||||
local env_file="$1"
|
||||
local line key value
|
||||
|
||||
if [[ ! -f "${env_file}" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
while IFS= read -r line || [[ -n "${line}" ]]; do
|
||||
line="${line%$'\r'}"
|
||||
line="${line#$'\xef\xbb\xbf'}"
|
||||
[[ -z "${line}" || "${line}" == \#* ]] && continue
|
||||
[[ "${line}" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] || continue
|
||||
key="${BASH_REMATCH[1]}"
|
||||
value="${BASH_REMATCH[2]}"
|
||||
value="${value%\"}"
|
||||
value="${value#\"}"
|
||||
value="${value%\'}"
|
||||
value="${value#\'}"
|
||||
if [[ -z "${!key+x}" ]]; then
|
||||
export "${key}=${value}"
|
||||
fi
|
||||
done <"${env_file}"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
npm run spacetime:publish:maincloud
|
||||
npm run spacetime:publish:maincloud -- --database <database>
|
||||
npm run spacetime:publish:maincloud -- --clear-database
|
||||
npm run spacetime:publish:maincloud -- --no-migrate-on-conflict
|
||||
npm run spacetime:publish:maincloud -- --no-migration-bootstrap-secret
|
||||
|
||||
说明:
|
||||
发布 server-rs/crates/spacetime-module 到 SpacetimeDB Maincloud。
|
||||
数据库名优先读取 --database,其次读取 GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE。
|
||||
默认遇到 schema 冲突时会先导出迁移 JSON,再清库发布并导入回灌。
|
||||
默认在构建 wasm 前随机生成迁移引导密钥,注入 GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET 并显示在控制台。
|
||||
EOF
|
||||
}
|
||||
|
||||
generate_migration_bootstrap_secret() {
|
||||
node -e 'const crypto = require("crypto"); process.stdout.write(crypto.randomBytes(32).toString("hex"));'
|
||||
}
|
||||
|
||||
prepare_migration_bootstrap_secret() {
|
||||
case "${MIGRATION_BOOTSTRAP_SECRET_MODE}" in
|
||||
auto)
|
||||
MIGRATION_BOOTSTRAP_SECRET="$(generate_migration_bootstrap_secret)"
|
||||
;;
|
||||
manual)
|
||||
if [[ "${#MIGRATION_BOOTSTRAP_SECRET}" -lt 16 ]]; then
|
||||
echo "[spacetime:maincloud] 迁移引导密钥至少需要 16 个字符。" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
disabled)
|
||||
unset GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET
|
||||
echo "[spacetime:maincloud] 未启用迁移引导密钥。"
|
||||
return
|
||||
;;
|
||||
*)
|
||||
echo "[spacetime:maincloud] 未知迁移引导密钥模式: ${MIGRATION_BOOTSTRAP_SECRET_MODE}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
export GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET="${MIGRATION_BOOTSTRAP_SECRET}"
|
||||
echo "[spacetime:maincloud] 迁移引导密钥: ${MIGRATION_BOOTSTRAP_SECRET}"
|
||||
}
|
||||
|
||||
timestamp_slug() {
|
||||
node -e 'process.stdout.write(new Date().toISOString().replace(/[:.]/g, "-"));'
|
||||
}
|
||||
|
||||
validate_spacetime_database_name() {
|
||||
local database="$1"
|
||||
|
||||
if [[ ! "${database}" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
|
||||
echo "[spacetime:maincloud] --database 必须匹配 SpacetimeDB 数据库名规则 ^[a-z0-9]+(-[a-z0-9]+)*$,只能使用小写字母、数字,并用单个短横线分隔: ${database}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_publish_conflict_output() {
|
||||
local output="$1"
|
||||
[[ "${output}" == *"conflict"* ]] \
|
||||
|| [[ "${output}" == *"schema"* && "${output}" == *"clear"* ]] \
|
||||
|| [[ "${output}" == *"manual migration"* ]] \
|
||||
|| [[ "${output}" == *"default value annotation"* ]] \
|
||||
|| [[ "${output}" == *"delete-data"* ]]
|
||||
}
|
||||
|
||||
run_publish() {
|
||||
local output_file="$1"
|
||||
shift
|
||||
set +e
|
||||
spacetime "$@" >"${output_file}" 2>&1
|
||||
local status=$?
|
||||
set -e
|
||||
cat "${output_file}"
|
||||
return "${status}"
|
||||
}
|
||||
|
||||
run_conflict_migration_publish() {
|
||||
local migration_root migration_file publish_log
|
||||
|
||||
if [[ "${MIGRATION_BOOTSTRAP_SECRET_MODE}" == "disabled" ]]; then
|
||||
echo "[spacetime:maincloud] schema 冲突需要迁移引导密钥;请去掉 --no-migration-bootstrap-secret 后重试。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
migration_root="${MIGRATION_DIR:-${REPO_ROOT}/tmp/spacetime-migrations/maincloud/${SPACETIME_DATABASE}}"
|
||||
mkdir -p "${migration_root}"
|
||||
migration_file="${migration_root}/$(timestamp_slug).json"
|
||||
publish_log="$(mktemp)"
|
||||
|
||||
echo "[spacetime:maincloud] 检测到 schema 冲突,开始导出旧库迁移 JSON: ${migration_file}"
|
||||
node "${REPO_ROOT}/scripts/spacetime-export-migration-json.mjs" \
|
||||
--server "${SPACETIME_SERVER_ALIAS}" \
|
||||
--server-url "${SPACETIME_SERVER_URL}" \
|
||||
--database "${SPACETIME_DATABASE}" \
|
||||
--bootstrap-secret "${MIGRATION_BOOTSTRAP_SECRET}" \
|
||||
--out "${migration_file}" \
|
||||
--note "publish conflict export $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
echo "[spacetime:maincloud] 清库发布新 SpacetimeDB wasm"
|
||||
if ! run_publish "${publish_log}" publish "${SPACETIME_DATABASE}" --server "${SPACETIME_SERVER_ALIAS}" --bin-path "${MODULE_PATH}" --clear-database --yes; then
|
||||
echo "[spacetime:maincloud] 清库发布失败,迁移 JSON 已保留: ${migration_file}" >&2
|
||||
rm -f "${publish_log}"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "${publish_log}"
|
||||
|
||||
echo "[spacetime:maincloud] 导入迁移 JSON 回灌数据"
|
||||
if ! node "${REPO_ROOT}/scripts/spacetime-import-migration-json.mjs" \
|
||||
--server "${SPACETIME_SERVER_ALIAS}" \
|
||||
--server-url "${SPACETIME_SERVER_URL}" \
|
||||
--database "${SPACETIME_DATABASE}" \
|
||||
--bootstrap-secret "${MIGRATION_BOOTSTRAP_SECRET}" \
|
||||
--in "${migration_file}" \
|
||||
--note "publish conflict import $(date -u +%Y-%m-%dT%H:%M:%SZ)"; then
|
||||
echo "[spacetime:maincloud] 导入失败,迁移 JSON 已保留: ${migration_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[spacetime:maincloud] schema 冲突迁移完成,迁移 JSON: ${migration_file}"
|
||||
}
|
||||
|
||||
load_env_file "${REPO_ROOT}/.env"
|
||||
load_env_file "${REPO_ROOT}/.env.local"
|
||||
|
||||
SPACETIME_DATABASE="${GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE:-}"
|
||||
SPACETIME_SERVER_URL="${GENARRATIVE_SPACETIME_MAINCLOUD_SERVER_URL:-https://maincloud.spacetimedb.com}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--database)
|
||||
SPACETIME_DATABASE="${2:?缺少 --database 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--server-url)
|
||||
SPACETIME_SERVER_URL="${2:?缺少 --server-url 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--clear-database)
|
||||
CLEAR_DATABASE=1
|
||||
shift
|
||||
;;
|
||||
--no-migrate-on-conflict)
|
||||
MIGRATE_ON_CONFLICT=0
|
||||
shift
|
||||
;;
|
||||
--migration-dir)
|
||||
MIGRATION_DIR="${2:?缺少 --migration-dir 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--migration-bootstrap-secret)
|
||||
MIGRATION_BOOTSTRAP_SECRET="${2:?缺少 --migration-bootstrap-secret 的值}"
|
||||
MIGRATION_BOOTSTRAP_SECRET_MODE="manual"
|
||||
shift 2
|
||||
;;
|
||||
--no-migration-bootstrap-secret)
|
||||
MIGRATION_BOOTSTRAP_SECRET=""
|
||||
MIGRATION_BOOTSTRAP_SECRET_MODE="disabled"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "[spacetime:maincloud] 未知参数: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${SPACETIME_DATABASE}" ]]; then
|
||||
echo "[spacetime:maincloud] 缺少 GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE。" >&2
|
||||
echo "[spacetime:maincloud] 请在 .env.local 中配置,或通过 --database <database> 传入。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
validate_spacetime_database_name "${SPACETIME_DATABASE}"
|
||||
|
||||
echo "[spacetime:maincloud] SpacetimeDB 发布数据库: ${SPACETIME_DATABASE}"
|
||||
echo "[spacetime:maincloud] SpacetimeDB server: ${SPACETIME_SERVER_ALIAS} (${SPACETIME_SERVER_URL})"
|
||||
|
||||
if ! command -v cargo >/dev/null 2>&1; then
|
||||
echo "[spacetime:maincloud] 缺少 cargo 命令。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "[spacetime:maincloud] 缺少 node 命令,无法生成迁移引导密钥。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v spacetime >/dev/null 2>&1; then
|
||||
echo "[spacetime:maincloud] 缺少 spacetime CLI,请先安装并登录 Maincloud。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
prepare_migration_bootstrap_secret
|
||||
|
||||
echo "[spacetime:maincloud] 构建 spacetime-module wasm"
|
||||
cargo build \
|
||||
--manifest-path "${SERVER_RS_DIR}/Cargo.toml" \
|
||||
-p spacetime-module \
|
||||
--target wasm32-unknown-unknown \
|
||||
--release
|
||||
|
||||
PUBLISH_ARGS=(
|
||||
publish
|
||||
"${SPACETIME_DATABASE}"
|
||||
--server "${SPACETIME_SERVER_ALIAS}"
|
||||
--bin-path "${MODULE_PATH}"
|
||||
--yes
|
||||
)
|
||||
|
||||
if [[ "${CLEAR_DATABASE}" -eq 1 ]]; then
|
||||
# Maincloud 清库只在 schema 冲突时触发,避免无冲突升级误删线上数据。
|
||||
PUBLISH_ARGS+=(-c=on-conflict)
|
||||
fi
|
||||
|
||||
echo "[spacetime:maincloud] 发布 SpacetimeDB wasm: ${SPACETIME_DATABASE} -> ${SPACETIME_SERVER_ALIAS}"
|
||||
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 && "${MIGRATE_ON_CONFLICT}" -eq 1 ]] && is_publish_conflict_output "${PUBLISH_OUTPUT}"; then
|
||||
run_conflict_migration_publish
|
||||
else
|
||||
echo "[spacetime:maincloud] 发布失败。" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
rm -f "${PUBLISH_LOG}"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
[spacetime:maincloud] 发布完成。api-server 可使用以下环境:
|
||||
GENARRATIVE_SPACETIME_SERVER_URL=${SPACETIME_SERVER_URL}
|
||||
GENARRATIVE_SPACETIME_DATABASE=${SPACETIME_DATABASE}
|
||||
GENARRATIVE_SPACETIME_TOKEN=
|
||||
EOF
|
||||
Reference in New Issue
Block a user