feat: refine ISAC design and prepare production deployment
Validate and deploy production / validate (push) Successful in 18s
Validate and deploy staging / validate (push) Successful in 18s
Validate and deploy production / deploy (push) Successful in 1m1s
Validate and deploy staging / deploy (push) Successful in 28s

This commit is contained in:
JB
2026-07-26 12:25:47 -04:00
parent 75faef50b8
commit 2e868d86ff
153 changed files with 64660 additions and 264 deletions
+115
View File
@@ -0,0 +1,115 @@
import { connect } from '@tursodatabase/database';
import { spawnSync } from 'node:child_process';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import process from 'node:process';
const bundleArgument = process.argv.slice(2).find((argument) => !argument.startsWith('--'));
const targetIndex = process.argv.indexOf('--target');
const target = targetIndex >= 0 ? process.argv[targetIndex + 1] : '';
const allowed = target === 'staging'
? process.env.ALLOW_STAGING_BOOTSTRAP === 'true'
: target === 'production'
? process.env.ALLOW_PRODUCTION_BOOTSTRAP === 'true'
: false;
if (!bundleArgument || !process.argv.includes('--confirm-empty-target-replace') || !allowed) {
console.error('Environment bootstrap requires the matching ALLOW_*_BOOTSTRAP=true guard and:');
console.error('node scripts/environment-bootstrap-import.mjs <bundle.json> --target <staging|production> --confirm-empty-target-replace');
process.exit(2);
}
const bundlePath = resolve(bundleArgument);
const bundle = JSON.parse(await readFile(bundlePath, 'utf8'));
if (bundle?.format !== 'shd-build-environment-bootstrap' || bundle?.version !== 1 || bundle?.target !== target
|| bundle.database?.format !== 'shd-build-database-backup' || bundle.database?.version !== 1
|| !Array.isArray(bundle.database.tables) || !Array.isArray(bundle.files?.avatars)
|| !Array.isArray(bundle.files?.gameIcons)) {
throw new Error('The selected file is not a supported SHD.build bootstrap bundle for the selected target.');
}
for (const file of bundle.files.avatars) {
if (typeof file.name !== 'string' || !/^[0-9a-f-]{36}\.webp$/i.test(file.name) || typeof file.contentBase64 !== 'string') {
throw new Error('The bootstrap bundle contains an invalid avatar file.');
}
}
for (const file of bundle.files.gameIcons) {
if (typeof file.name !== 'string' || !/^[a-z_]+-[0-9a-f-]{36}\.webp$/i.test(file.name) || typeof file.contentBase64 !== 'string') {
throw new Error('The bootstrap bundle contains an invalid game-data icon file.');
}
}
const databasePath = resolve(process.env.TDR_DATABASE_PATH || `./data/shd-build-${target}.db`);
const safetyBackup = spawnSync(process.execPath, ['scripts/db-backup.mjs'], {
cwd: process.cwd(),
stdio: 'inherit',
env: process.env
});
if (safetyBackup.error) throw safetyBackup.error;
if (safetyBackup.status !== 0) throw new Error(`${target} safety backup failed; the target was not changed.`);
const database = await connect(databasePath);
for (const tableName of ['users', 'builds', 'audit_log', 'data_change_proposals']) {
const row = await (await database.prepare(`SELECT COUNT(*) AS count FROM "${tableName}"`)).get();
if (Number(row?.count ?? 0) > 0) {
throw new Error(`${target} bootstrap refused: target table "${tableName}" is not empty.`);
}
}
const targetTables = await (await database.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
ORDER BY rowid
`)).all();
const targetNames = new Set(targetTables.map((row) => String(row.name)));
const sourceNames = new Set(bundle.database.tables.map((table) => String(table.name)));
for (const row of targetTables) {
if (!sourceNames.has(String(row.name))) {
throw new Error(`Bootstrap snapshot is missing target-schema table "${String(row.name)}".`);
}
}
const sourceOnlyTables = bundle.database.tables
.map((table) => String(table.name))
.filter((name) => !targetNames.has(name));
if (sourceOnlyTables.length) {
console.log(`Skipping source-only legacy tables: ${sourceOnlyTables.join(', ')}`);
}
const quoteIdentifier = (identifier) => `"${String(identifier).replaceAll('"', '""')}"`;
let restoredRows = 0;
await database.exec('PRAGMA foreign_keys=OFF');
await database.exec('BEGIN');
try {
for (const row of [...targetTables].reverse()) {
await database.exec(`DELETE FROM ${quoteIdentifier(row.name)}`);
}
for (const table of bundle.database.tables.filter((candidate) => targetNames.has(candidate.name))) {
for (const row of table.rows) {
const columns = Object.keys(row);
if (!columns.length) continue;
const insert = await database.prepare(
`INSERT INTO ${quoteIdentifier(table.name)} (${columns.map(quoteIdentifier).join(', ')})
VALUES (${columns.map(() => '?').join(', ')})`
);
await insert.run(...columns.map((column) => row[column]));
restoredRows++;
}
}
await database.exec('COMMIT');
} catch (error) {
await database.exec('ROLLBACK');
throw error;
} finally {
await database.exec('PRAGMA foreign_keys=ON');
}
const avatarDirectory = resolve(process.env.TDR_AVATAR_STORAGE_PATH || './data/avatars');
const gameIconDirectory = resolve(process.env.TDR_GAME_ICON_STORAGE_PATH || './data/game-icons');
await mkdir(avatarDirectory, { recursive: true });
await mkdir(gameIconDirectory, { recursive: true });
for (const file of bundle.files.avatars) {
await writeFile(join(avatarDirectory, file.name), Buffer.from(file.contentBase64, 'base64'), { mode: 0o600 });
}
for (const file of bundle.files.gameIcons) {
await writeFile(join(gameIconDirectory, file.name), Buffer.from(file.contentBase64, 'base64'), { mode: 0o600 });
}
console.log(`Bootstrapped ${target} with ${restoredRows} database rows, ${bundle.files.avatars.length} avatars, and ${bundle.files.gameIcons.length} game-data icons.`);
console.log(`Source bundle: ${bundlePath}`);