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,93 +1,12 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readdir, readFile, stat } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join, posix, relative, sep } from 'node:path';
|
||||
import { betaSrc, config } from '../config.js';
|
||||
import { readFile, stat } from 'node:fs/promises';
|
||||
import { posix } from 'node:path';
|
||||
|
||||
// Ported from push_scripts.sh:414-464. These are the exact filter rules —
|
||||
// change one and the parity check against the shell script will fail.
|
||||
|
||||
const EXCLUDE_NAMES_EXACT = new Set(['.git']);
|
||||
const EXCLUDE_SUFFIXES = ['.swp', '.OLD'];
|
||||
const EXCLUDE_TRAILING = ['~'];
|
||||
|
||||
function isExcluded(name: string): boolean {
|
||||
if (EXCLUDE_NAMES_EXACT.has(name)) return true;
|
||||
if (name.startsWith('.')) return false; // dotfiles allowed except .git
|
||||
for (const s of EXCLUDE_SUFFIXES) if (name.endsWith(s)) return true;
|
||||
for (const t of EXCLUDE_TRAILING) if (name.endsWith(t)) return true;
|
||||
// *-beta.* — script line 417 excludes any file with "-beta." in its name
|
||||
if (/-beta\./.test(name)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function toPosix(p: string): string {
|
||||
return p.split(sep).join(posix.sep);
|
||||
}
|
||||
|
||||
async function walk(root: string, prune: (rel: string) => boolean): Promise<string[]> {
|
||||
const out: string[] = [];
|
||||
async function recur(dir: string): Promise<void> {
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const e of entries) {
|
||||
const abs = join(dir, e.name);
|
||||
const rel = toPosix(relative(root, abs));
|
||||
if (prune(rel)) continue;
|
||||
if (e.isDirectory()) {
|
||||
await recur(abs);
|
||||
} else if (e.isFile()) {
|
||||
if (isExcluded(e.name)) continue;
|
||||
out.push(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
await recur(root);
|
||||
return out.sort();
|
||||
}
|
||||
// Naming rules for beta artifacts, preserved from the legacy shell script.
|
||||
|
||||
/**
|
||||
* Every file under SHARED_SCRIPTS except the betas/ tree.
|
||||
* Mirrors `list_source_files` in push_scripts.sh:414.
|
||||
*/
|
||||
export async function listSourceFiles(): Promise<string[]> {
|
||||
return walk(config.sharedScripts, (rel) => rel === 'betas' || rel.startsWith('betas/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Every file directly in betas/ (one level deep, ignore subfolders — those
|
||||
* are open betas). Mirrors `staged_files` in push_scripts.sh:420.
|
||||
*/
|
||||
export async function stagedFiles(): Promise<string[]> {
|
||||
if (!existsSync(betaSrc())) return [];
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(betaSrc(), { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return entries
|
||||
.filter((e) => e.isFile() && !isExcluded(e.name) && !e.name.startsWith('.'))
|
||||
.map((e) => e.name)
|
||||
.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find every source file whose basename matches `base`.
|
||||
* Mirrors `find_targets` in push_scripts.sh:426.
|
||||
*/
|
||||
export async function findTargets(base: string): Promise<string[]> {
|
||||
const all = await listSourceFiles();
|
||||
return all.filter((rel) => posix.basename(rel) === base);
|
||||
}
|
||||
|
||||
/**
|
||||
* `foo/bar.gsc` -> `foo/bar-beta.gsc`
|
||||
* Mirrors `beta_file` in push_scripts.sh:450.
|
||||
* `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);
|
||||
@@ -100,8 +19,8 @@ export function betaFile(rel: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* `foo/bar.gsc` -> `foo/bar.OLD`
|
||||
* Mirrors `old_file` in push_scripts.sh:458.
|
||||
* `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);
|
||||
@@ -112,17 +31,6 @@ export function oldFile(rel: string): string {
|
||||
return dir === '.' ? named : `${dir}/${named}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slugify a beta name the same way the shell script does.
|
||||
* Mirrors `slugify` in push_scripts.sh:399.
|
||||
*/
|
||||
export function slugify(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
export async function sha256(path: string): Promise<string> {
|
||||
const buf = await readFile(path);
|
||||
return createHash('sha256').update(buf).digest('hex');
|
||||
@@ -136,3 +44,7 @@ export async function sha256OrNull(path: string): Promise<string | null> {
|
||||
}
|
||||
return sha256(path);
|
||||
}
|
||||
|
||||
export function sha256Buffer(buf: Buffer): string {
|
||||
return createHash('sha256').update(buf).digest('hex');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user