Rework as scripts-centric, upload-only, gated beta lifecycle
Scripts become first-class: named, versioned, uploaded via the UI, optionally scoped to a game or map. Any change that would land on scope-matched servers (marking-required or new version of a scoped script) must first pass a beta. The beta runs a per-server state machine — deploy -beta.gsc → wait empty → Komodo restart → wait healthy → optionally wait for a real match end (games_sp.log 'ExitLevel: executed') — before it's eligible for promotion. Rollback is always available and restores from .OLD. The shared/scripts mirror and its betas/ staging folder are gone. Every server now has a tracked script inventory (server_scripts table), populated only by explicit upload flows. New: /scripts, /scripts/[id], /betas/[id] UI; /api/scripts + /api/betas + /api/servers/[name]/scripts endpoints; scripts.ts, inventory.ts, beta.ts (rewritten), beta-runner.ts, match-log.ts, komodoInspectHealthy(). Removed: push.ts, /api/push, BetaPanel, legacy importer. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,199 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* One-shot importer: reads script_metadata.json (the format push_scripts.sh
|
||||
* writes) and populates the electric-cherry SQLite DB with the equivalent
|
||||
* rows. Idempotent — safe to re-run, will INSERT OR IGNORE on collisions.
|
||||
*
|
||||
* Trigger: docker/entrypoint.sh calls this when LEGACY_METADATA is set and
|
||||
* DB_PATH is empty.
|
||||
*/
|
||||
|
||||
import { readFile, rename, stat } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { env, exit } from 'node:process';
|
||||
import { db } from '../src/lib/server/db/index.js';
|
||||
|
||||
interface LegacyServer {
|
||||
komodo_type?: string;
|
||||
komodo_name?: string;
|
||||
komodo_server?: string;
|
||||
iw4m_match?: string;
|
||||
iw4m_value?: string;
|
||||
}
|
||||
interface LegacyBetaScript {
|
||||
staged_as: string;
|
||||
target: string;
|
||||
beta_file: string;
|
||||
backup: string;
|
||||
sha256: string;
|
||||
revision?: number;
|
||||
deployed?: Array<{ server: string; had_original: boolean }>;
|
||||
}
|
||||
interface LegacyBeta {
|
||||
name: string;
|
||||
folder: string;
|
||||
opened: string;
|
||||
updated?: string;
|
||||
revision?: number;
|
||||
servers?: string[];
|
||||
scripts?: LegacyBetaScript[];
|
||||
}
|
||||
interface LegacyDeployServer {
|
||||
server: string;
|
||||
files: number;
|
||||
restart: string;
|
||||
players: number | null;
|
||||
checked: string | null;
|
||||
restarted: string | null;
|
||||
attempts: number;
|
||||
error: string | null;
|
||||
}
|
||||
interface LegacyDeployment {
|
||||
id: string;
|
||||
action: string;
|
||||
started: string;
|
||||
servers?: LegacyDeployServer[];
|
||||
}
|
||||
interface LegacyRoot {
|
||||
servers?: Record<string, LegacyServer>;
|
||||
betas?: Record<string, LegacyBeta>;
|
||||
deployments?: LegacyDeployment[];
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const src = env.LEGACY_METADATA;
|
||||
if (!src) {
|
||||
console.error('LEGACY_METADATA env var not set');
|
||||
exit(1);
|
||||
}
|
||||
if (!existsSync(src)) {
|
||||
console.log(`no legacy file at ${src} — nothing to import`);
|
||||
return;
|
||||
}
|
||||
const stats = await stat(src);
|
||||
if (stats.size === 0) {
|
||||
console.log(`legacy file at ${src} is empty — nothing to import`);
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = await readFile(src, 'utf8');
|
||||
const data = JSON.parse(raw) as LegacyRoot;
|
||||
const handle = db();
|
||||
|
||||
let servers = 0;
|
||||
let betas = 0;
|
||||
let betaScripts = 0;
|
||||
let betaDeploys = 0;
|
||||
let deployments = 0;
|
||||
let deploymentServers = 0;
|
||||
|
||||
const tx = handle.transaction(() => {
|
||||
// servers
|
||||
const insServer = handle.prepare(
|
||||
`INSERT OR IGNORE INTO servers
|
||||
(name, komodo_type, komodo_name, komodo_server, iw4m_match, iw4m_value)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
for (const [name, s] of Object.entries(data.servers ?? {})) {
|
||||
const r = insServer.run(
|
||||
name,
|
||||
s.komodo_type ?? 'deployment',
|
||||
s.komodo_name ?? name.replace(/_/g, '-'),
|
||||
s.komodo_server ?? null,
|
||||
s.iw4m_match ?? null,
|
||||
s.iw4m_value ?? null
|
||||
);
|
||||
if (r.changes > 0) servers++;
|
||||
}
|
||||
|
||||
// betas
|
||||
const insBeta = handle.prepare(
|
||||
`INSERT OR IGNORE INTO betas (slug, name, folder, opened_at, updated_at, revision)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
const insScript = handle.prepare(
|
||||
`INSERT INTO beta_scripts (beta_slug, staged_as, target, beta_file, backup, sha256, revision)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
const insDeploy = handle.prepare(
|
||||
`INSERT OR IGNORE INTO beta_deploys (beta_script_id, server, had_original)
|
||||
VALUES (?, ?, ?)`
|
||||
);
|
||||
|
||||
for (const [slug, b] of Object.entries(data.betas ?? {})) {
|
||||
const br = insBeta.run(slug, b.name, b.folder, b.opened, b.updated ?? b.opened, b.revision ?? 1);
|
||||
if (br.changes > 0) betas++;
|
||||
for (const s of b.scripts ?? []) {
|
||||
const sr = insScript.run(
|
||||
slug,
|
||||
s.staged_as,
|
||||
s.target,
|
||||
s.beta_file,
|
||||
s.backup,
|
||||
s.sha256,
|
||||
s.revision ?? 1
|
||||
);
|
||||
betaScripts++;
|
||||
const scriptId = Number(sr.lastInsertRowid);
|
||||
for (const d of s.deployed ?? []) {
|
||||
// Skip deploys whose server isn't tracked — matches the
|
||||
// FK constraint. Log so we don't silently lose rows.
|
||||
const exists = handle.prepare('SELECT 1 FROM servers WHERE name = ?').get(d.server);
|
||||
if (!exists) {
|
||||
console.warn(`skipping beta_deploy for unknown server ${d.server}`);
|
||||
continue;
|
||||
}
|
||||
const dr = insDeploy.run(scriptId, d.server, d.had_original ? 1 : 0);
|
||||
if (dr.changes > 0) betaDeploys++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deployments
|
||||
const insDep = handle.prepare(
|
||||
`INSERT OR IGNORE INTO deployments (id, action, started_at, triggered_by)
|
||||
VALUES (?, ?, ?, 'legacy')`
|
||||
);
|
||||
const insDepSrv = handle.prepare(
|
||||
`INSERT OR IGNORE INTO deployment_servers
|
||||
(deployment_id, server, files_changed, restart_state, players, checked_at, restarted_at, attempts, error)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
for (const d of data.deployments ?? []) {
|
||||
const r = insDep.run(d.id, d.action, d.started);
|
||||
if (r.changes > 0) deployments++;
|
||||
for (const s of d.servers ?? []) {
|
||||
const exists = handle.prepare('SELECT 1 FROM servers WHERE name = ?').get(s.server);
|
||||
if (!exists) continue;
|
||||
const sr = insDepSrv.run(
|
||||
d.id,
|
||||
s.server,
|
||||
s.files ?? 0,
|
||||
s.restart ?? 'manual',
|
||||
s.players ?? null,
|
||||
s.checked ?? null,
|
||||
s.restarted ?? null,
|
||||
s.attempts ?? 0,
|
||||
s.error ?? null
|
||||
);
|
||||
if (sr.changes > 0) deploymentServers++;
|
||||
}
|
||||
}
|
||||
});
|
||||
tx();
|
||||
|
||||
console.log(
|
||||
`imported: ${servers} servers, ${betas} betas (${betaScripts} scripts, ${betaDeploys} deploys), ` +
|
||||
`${deployments} deployments (${deploymentServers} server rows)`
|
||||
);
|
||||
|
||||
// Move the source out of the way so we don't re-import on future boots.
|
||||
const moved = `${src}.migrated`;
|
||||
await rename(src, moved);
|
||||
console.log(`renamed ${src} → ${moved}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('legacy import failed:', err);
|
||||
exit(2);
|
||||
});
|
||||
+11
-34
@@ -3,59 +3,36 @@
|
||||
* Seed a test-fixtures/zombies/ tree so the app has something to point at
|
||||
* during local dev on Windows/macOS (where /root/zombies doesn't exist).
|
||||
*
|
||||
* Since the upload-only rework, there's no shared/scripts source dir — the
|
||||
* seed just creates empty server folders. Upload scripts via the /scripts UI
|
||||
* to populate them.
|
||||
*
|
||||
* Usage:
|
||||
* npm run seed:fixture
|
||||
* SHARED_SCRIPTS=./test-fixtures/zombies/shared/scripts \
|
||||
* SERVERS_ROOT=./test-fixtures/zombies/t5 \
|
||||
* BETA_ARCHIVE_DIR=./test-fixtures/zombies/beta-archive \
|
||||
* npm run dev
|
||||
*/
|
||||
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const root = process.env.FIXTURE_ROOT ?? './test-fixtures/zombies';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const shared = join(root, 'shared/scripts');
|
||||
const staging = join(shared, 'betas');
|
||||
const t5 = join(root, 't5');
|
||||
await mkdir(shared, { recursive: true });
|
||||
await mkdir(staging, { recursive: true });
|
||||
const betaArchive = join(root, 'beta-archive');
|
||||
await mkdir(betaArchive, { recursive: true });
|
||||
|
||||
// A handful of representative GSC files
|
||||
await writeFile(
|
||||
join(shared, 'hud.gsc'),
|
||||
'#include maps\\_utility;\n\ninit() { println("hud v1"); }\n'
|
||||
);
|
||||
await writeFile(
|
||||
join(shared, 'zom.gsc'),
|
||||
'#include maps\\_zombiemode;\n\ninit() { println("zom v1"); }\n'
|
||||
);
|
||||
await writeFile(
|
||||
join(shared, 'perks.gsc'),
|
||||
'#include maps\\_zombiemode_perks;\n\ninit() { println("perks v1"); }\n'
|
||||
);
|
||||
|
||||
// Three servers, one already up-to-date, one empty, one with old copies
|
||||
const rel = 'runtime/plutonium/storage/t5/raw/scripts/sp';
|
||||
for (const name of ['t5_kino_1', 't5_ascension_1', 't5_cotd_1']) {
|
||||
const dest = join(t5, name, rel);
|
||||
await mkdir(dest, { recursive: true });
|
||||
await mkdir(join(t5, name, rel), { recursive: true });
|
||||
await mkdir(join(t5, name, 'logs'), { recursive: true });
|
||||
}
|
||||
// t5_kino_1 is already up-to-date
|
||||
await writeFile(
|
||||
join(t5, 't5_kino_1', rel, 'hud.gsc'),
|
||||
'#include maps\\_utility;\n\ninit() { println("hud v1"); }\n'
|
||||
);
|
||||
// t5_cotd_1 has an older version of zom
|
||||
await writeFile(
|
||||
join(t5, 't5_cotd_1', rel, 'zom.gsc'),
|
||||
'#include maps\\_zombiemode;\n\ninit() { println("zom v0"); }\n'
|
||||
);
|
||||
|
||||
console.log(`seeded ${root}`);
|
||||
console.log(` set SHARED_SCRIPTS=${shared}`);
|
||||
console.log(` set SERVERS_ROOT=${t5}`);
|
||||
console.log(` set BETA_ARCHIVE_DIR=${betaArchive}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
|
||||
Reference in New Issue
Block a user