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
-30
View File
@@ -1,35 +1,5 @@
import { createHash } from 'node:crypto';
import { readFile, stat } from 'node:fs/promises';
import { posix } from 'node:path';
// Naming rules for beta artifacts, preserved from the legacy shell script.
/**
* `foo/bar.gsc` → `foo/bar-beta.gsc`. Used to name the beta copy that lives
* next to (or in place of) the target file on each server during a beta.
*/
export function betaFile(rel: string): string {
const dir = posix.dirname(rel);
const base = posix.basename(rel);
const dot = base.lastIndexOf('.');
const stem = dot === -1 ? base : base.slice(0, dot);
const ext = dot === -1 ? '' : base.slice(dot);
const named = `${stem}-beta${ext}`;
return dir === '.' ? named : `${dir}/${named}`;
}
/**
* `foo/bar.gsc` → `foo/bar.OLD`. Used to back up the previous target when a
* beta lands on top of it, so rollback can restore.
*/
export function oldFile(rel: string): string {
const dir = posix.dirname(rel);
const base = posix.basename(rel);
const dot = base.lastIndexOf('.');
const stem = dot === -1 ? base : base.slice(0, dot);
const named = `${stem}.OLD`;
return dir === '.' ? named : `${dir}/${named}`;
}
export async function sha256(path: string): Promise<string> {
const buf = await readFile(path);