Ports push_scripts.sh (1497 lines of bash) to a SvelteKit + SQLite web app that manages a fleet of T5 Plutonium servers running on Komodo. Same push and beta workflow as the shell script, with a live SSE pipeline view, a !rt-token-gated auth modal that hard-pauses deployments on stale tokens, per-file audit diffs, and a Komodo-container deployment target. - Push/beta engines port the exact filter, held-by-beta, conflict, and stale-backup rules from the shell script - SQLite (better-sqlite3) replaces script_metadata.json; a one-shot importer moves legacy state over on first boot - 16 vitest tests cover push add/update/skip, beta open→rollback, open→promote, conflict guards, sweep frozen/manual branches, and IW4M login/status parsing - docker/ + komodo/stack.toml deploy as a managed container on the same host as the game servers, /root/zombies bind-mounted at the same path the shell script used
200 lines
6.6 KiB
JavaScript
200 lines
6.6 KiB
JavaScript
#!/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);
|
|
});
|