Replace -beta sidecar and .OLD backup with DB snapshots
Beta deploys used to rename foo.gsc -> foo.OLD and write beta bytes to a foo-beta.gsc sidecar, forcing the mod loader to know the naming convention. Now the beta content lives at the actual target filename (same as configs already did) and the pre-beta bytes are captured in a new beta_file_snapshots table for rollback. - schema.sql: new beta_file_snapshots(beta_id, server, rel_path, prior_content BLOB, prior_sha256, prior_size). Handles scripts, plugins, and the composed dedicated_sp.cfg uniformly. - db/index.ts: assertSnapshotSchemeSafe boot guard refuses to start if a legacy running/ready-promote beta is present on first upgrade. A settings sentinel (snapshots_v1) makes it one-shot. - beta.ts openBeta: read prior bytes, insert snapshot, overwrite target directly. - beta.ts rollbackBeta: restore from snapshot if had_original, else delete the fresh file. Drop snapshot rows after restore. - beta.ts promoteBeta: no more rm -beta / rm .OLD calls, pull prior content from the snapshot table for the deployment audit, drop snapshots after promote. - files.ts: delete betaFile() and oldFile(). - drift.ts: docstring updated; no special-casing needed since beta bytes and inventory now agree on the target rel_path. - servers/[name]/+page.svelte: drop dead -beta/.OLD row filters. - tests: plugin write asserts the target filename; unused betaFile/oldFile tests removed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -149,6 +149,30 @@ function dropLegacyBetaTables(handle: Database.Database): void {
|
||||
}
|
||||
}
|
||||
|
||||
// One-shot guard for the -beta/.OLD → DB-snapshot cutover. Legacy running/
|
||||
// ready-promote betas from the old code have -beta.<ext> and .OLD files on
|
||||
// disk with no corresponding beta_file_snapshots rows — restoring them under
|
||||
// the new rollback path would delete-instead-of-restore. If a legacy active
|
||||
// beta is detected on the first boot after upgrade, refuse to start so the
|
||||
// operator can rollback/promote under the old code. A settings sentinel marks
|
||||
// the cutover complete so subsequent boots with (legitimately new-scheme)
|
||||
// active betas aren't blocked.
|
||||
function assertSnapshotSchemeSafe(handle: Database.Database): void {
|
||||
const done = handle.prepare("SELECT value FROM settings WHERE key = 'snapshots_v1'").get() as
|
||||
| { value: string }
|
||||
| undefined;
|
||||
if (done) return;
|
||||
const active = handle
|
||||
.prepare("SELECT id FROM betas WHERE state IN ('running','ready-promote') LIMIT 1")
|
||||
.get() as { id: number } | undefined;
|
||||
if (active) {
|
||||
throw new Error(
|
||||
`close active beta ${active.id} before upgrading — snapshot scheme is incompatible with legacy -beta/.OLD state`
|
||||
);
|
||||
}
|
||||
handle.prepare("INSERT INTO settings (key, value) VALUES ('snapshots_v1', '1')").run();
|
||||
}
|
||||
|
||||
export function db(): Database.Database {
|
||||
if (_db) return _db;
|
||||
|
||||
@@ -179,6 +203,7 @@ export function db(): Database.Database {
|
||||
);
|
||||
handle.exec(`CREATE INDEX IF NOT EXISTS idx_scripts_kind ON scripts(kind)`);
|
||||
backfillMultiScriptJoins(handle);
|
||||
assertSnapshotSchemeSafe(handle);
|
||||
} catch (err) {
|
||||
handle.close();
|
||||
throw err;
|
||||
|
||||
@@ -123,8 +123,9 @@ CREATE INDEX IF NOT EXISTS idx_beta_scripts_beta ON beta_scripts(beta_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_beta_scripts_script ON beta_scripts(script_id);
|
||||
|
||||
-- Per (beta, server, script) prior-file state. A server row in beta_servers
|
||||
-- covers restart progress; this table covers what each script's target
|
||||
-- looked like on that server before the beta wrote its -beta.gsc.
|
||||
-- covers restart progress; this table covers whether each script's target
|
||||
-- existed on that server before the beta overwrote it — snapshot content
|
||||
-- lives in beta_file_snapshots.
|
||||
CREATE TABLE IF NOT EXISTS beta_server_scripts (
|
||||
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
||||
server TEXT NOT NULL REFERENCES servers(name),
|
||||
@@ -135,6 +136,21 @@ CREATE TABLE IF NOT EXISTS beta_server_scripts (
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_beta_server_scripts_beta ON beta_server_scripts(beta_id);
|
||||
|
||||
-- Pre-beta bytes of every target file the beta overwrote on each server.
|
||||
-- Absence of a row for (beta, server, rel_path) means the file did not exist
|
||||
-- before the beta wrote it — rollback deletes the file rather than restoring.
|
||||
-- Handles scripts, plugins, and the composed dedicated_sp.cfg uniformly.
|
||||
CREATE TABLE IF NOT EXISTS beta_file_snapshots (
|
||||
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
||||
server TEXT NOT NULL REFERENCES servers(name) ON DELETE CASCADE,
|
||||
rel_path TEXT NOT NULL,
|
||||
prior_content BLOB NOT NULL,
|
||||
prior_sha256 TEXT NOT NULL,
|
||||
prior_size INTEGER NOT NULL,
|
||||
PRIMARY KEY (beta_id, server, rel_path)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_beta_file_snapshots_beta ON beta_file_snapshots(beta_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS deployments (
|
||||
id TEXT PRIMARY KEY,
|
||||
action TEXT NOT NULL, -- 'drift' | 'promote' | 'rollback'
|
||||
|
||||
Reference in New Issue
Block a user