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:
2026-09-13 17:57:43 -04:00
co-authored by Claude Opus 4.7
parent 30b0d1d4da
commit 8c15cae80d
8 changed files with 154 additions and 163 deletions
+25
View File
@@ -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;