multi script beta
This commit is contained in:
@@ -7,19 +7,11 @@ import { emit } from '../pipeline/bus.js';
|
||||
import { recordScript, removeScript } from './inventory.js';
|
||||
import { getVersion } from './scripts.js';
|
||||
|
||||
// Roll a promoted script back to whatever version was current before the
|
||||
// promote landed. Uses the deployment_files snapshots already captured at
|
||||
// promote time — no on-disk archaeology needed.
|
||||
//
|
||||
// Semantics:
|
||||
// - Reverses only the file writes recorded in deployment_files. Servers that
|
||||
// failed at promote time (no file row) are left alone.
|
||||
// - Restores scripts.current_version_id to the promote row's prior_version_id.
|
||||
// - Marks the original promote as rolled_back_by = <new deployment id>.
|
||||
// - Records a fresh action='rollback' deployment with pending server rows so
|
||||
// the sweep drains-and-restarts every affected server. Beta servers that
|
||||
// were still running the promoted bytes get restarted the same as any
|
||||
// other; the beta itself is not touched (it stays in state='promoted').
|
||||
// Roll a promoted deployment back to whatever versions were current before
|
||||
// the promote landed. Reads deployment_scripts as the source of truth for
|
||||
// which scripts were involved (and their prior versions); falls back to the
|
||||
// deprecated deployments.script_id/version_id/prior_version_id columns for
|
||||
// pre-multi-script legacy rows.
|
||||
|
||||
export class RollbackError extends Error {
|
||||
constructor(message: string, public readonly kind: 'no-such-deployment' | 'not-promote' | 'already-rolled-back') {
|
||||
@@ -36,6 +28,13 @@ interface DeploymentRow {
|
||||
rolled_back_by: string | null;
|
||||
}
|
||||
|
||||
interface DepScriptRow {
|
||||
deployment_id: string;
|
||||
script_id: number;
|
||||
script_version_id: number;
|
||||
prior_version_id: number | null;
|
||||
}
|
||||
|
||||
interface FileRow {
|
||||
id: number;
|
||||
server: string;
|
||||
@@ -47,6 +46,11 @@ interface FileRow {
|
||||
content_after: Buffer | null;
|
||||
}
|
||||
|
||||
interface ScriptTargetRow {
|
||||
id: number;
|
||||
target_rel_path: string;
|
||||
}
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
@@ -57,10 +61,9 @@ function rollbackDeploymentId(promoteId: string): string {
|
||||
}
|
||||
|
||||
export interface RollbackDeploymentResult {
|
||||
deploymentId: string; // the new rollback deployment
|
||||
reverses: string; // the promote deployment id
|
||||
scriptId: number | null;
|
||||
priorVersionId: number | null;
|
||||
deploymentId: string; // the new rollback deployment
|
||||
reverses: string; // the promote deployment id
|
||||
scriptIds: number[]; // scripts whose current_version_id got reverted
|
||||
affectedServers: string[];
|
||||
}
|
||||
|
||||
@@ -70,8 +73,27 @@ export async function rollbackPromoteDeployment(promoteId: string): Promise<Roll
|
||||
FROM deployments WHERE id = ?`)
|
||||
.get(promoteId) as DeploymentRow | undefined;
|
||||
if (!dep) throw new RollbackError(`no such deployment ${promoteId}`, 'no-such-deployment');
|
||||
if (dep.action !== 'promote') throw new RollbackError(`deployment ${promoteId} is action='${dep.action}', not 'promote'`, 'not-promote');
|
||||
if (dep.rolled_back_by) throw new RollbackError(`deployment ${promoteId} already rolled back by ${dep.rolled_back_by}`, 'already-rolled-back');
|
||||
if (dep.action !== 'promote') {
|
||||
throw new RollbackError(`deployment ${promoteId} is action='${dep.action}', not 'promote'`, 'not-promote');
|
||||
}
|
||||
if (dep.rolled_back_by) {
|
||||
throw new RollbackError(`deployment ${promoteId} already rolled back by ${dep.rolled_back_by}`, 'already-rolled-back');
|
||||
}
|
||||
|
||||
// Source of truth for which scripts (and prior versions) this promote
|
||||
// touched. Prefer deployment_scripts; fall back to the deprecated
|
||||
// singular columns for legacy single-script deployments.
|
||||
let scripts = db()
|
||||
.prepare(`SELECT * FROM deployment_scripts WHERE deployment_id = ? ORDER BY script_id`)
|
||||
.all(promoteId) as DepScriptRow[];
|
||||
if (scripts.length === 0 && dep.script_id !== null && dep.script_version_id !== null) {
|
||||
scripts = [{
|
||||
deployment_id: promoteId,
|
||||
script_id: dep.script_id,
|
||||
script_version_id: dep.script_version_id,
|
||||
prior_version_id: dep.prior_version_id
|
||||
}];
|
||||
}
|
||||
|
||||
const fileRows = db()
|
||||
.prepare(
|
||||
@@ -80,10 +102,26 @@ export async function rollbackPromoteDeployment(promoteId: string): Promise<Roll
|
||||
)
|
||||
.all(promoteId) as FileRow[];
|
||||
|
||||
// Map rel_path → scriptId (and prior_version_id) so we can restore the
|
||||
// inventory row's script_version_id correctly during file restore.
|
||||
const scriptByTarget = new Map<string, DepScriptRow>();
|
||||
if (scripts.length > 0) {
|
||||
const placeholders = scripts.map(() => '?').join(',');
|
||||
const targetRows = db()
|
||||
.prepare(`SELECT id, target_rel_path FROM scripts WHERE id IN (${placeholders})`)
|
||||
.all(...scripts.map((s) => s.script_id)) as ScriptTargetRow[];
|
||||
const targetById = new Map<number, string>();
|
||||
for (const t of targetRows) targetById.set(t.id, t.target_rel_path);
|
||||
for (const s of scripts) {
|
||||
const t = targetById.get(s.script_id);
|
||||
if (t) scriptByTarget.set(t, s);
|
||||
}
|
||||
}
|
||||
|
||||
const newDepId = rollbackDeploymentId(promoteId);
|
||||
const startedAt = nowIso();
|
||||
|
||||
// Group by server so each server gets exactly one deployment_servers row.
|
||||
// Group files by server so each server gets exactly one deployment_servers row.
|
||||
const perServer = new Map<string, FileRow[]>();
|
||||
for (const f of fileRows) {
|
||||
const arr = perServer.get(f.server);
|
||||
@@ -100,14 +138,15 @@ export async function rollbackPromoteDeployment(promoteId: string): Promise<Roll
|
||||
for (const f of files) {
|
||||
const abs = join(dest, f.rel_path);
|
||||
await mkdir(join(dest, dirname(f.rel_path)), { recursive: true });
|
||||
const owningScript = scriptByTarget.get(f.rel_path);
|
||||
if (f.verb === 'added') {
|
||||
// Promote created this file — undo by removing it.
|
||||
if (existsSync(abs)) await rm(abs, { force: true });
|
||||
removeScript(server, f.rel_path);
|
||||
} else if (f.verb === 'updated' && f.content_before) {
|
||||
await writeFile(abs, f.content_before);
|
||||
// Restore inventory to the prior version if we can identify it.
|
||||
const priorVer = dep.prior_version_id ? getVersion(dep.prior_version_id) : null;
|
||||
const priorVer = owningScript?.prior_version_id
|
||||
? getVersion(owningScript.prior_version_id)
|
||||
: null;
|
||||
recordScript(
|
||||
server,
|
||||
f.rel_path,
|
||||
@@ -117,7 +156,6 @@ export async function rollbackPromoteDeployment(promoteId: string): Promise<Roll
|
||||
`rollback:${newDepId}`
|
||||
);
|
||||
} else if (f.verb === 'removed' && f.content_before) {
|
||||
// Should not occur on a promote row, but handle symmetrically.
|
||||
await writeFile(abs, f.content_before);
|
||||
recordScript(server, f.rel_path, null, f.sha256_before!, f.content_before.byteLength, `rollback:${newDepId}`);
|
||||
}
|
||||
@@ -132,15 +170,17 @@ export async function rollbackPromoteDeployment(promoteId: string): Promise<Roll
|
||||
const insertDep = db().prepare(
|
||||
`INSERT INTO deployments
|
||||
(id, action, started_at, triggered_by, script_id, script_version_id, prior_version_id, beta_id)
|
||||
VALUES (?, 'rollback', ?, 'ui', ?, ?, ?, NULL)`
|
||||
VALUES (?, 'rollback', ?, 'ui', NULL, NULL, NULL, NULL)`
|
||||
);
|
||||
const insertDepScript = db().prepare(
|
||||
`INSERT INTO deployment_scripts (deployment_id, script_id, script_version_id, prior_version_id)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
);
|
||||
const insertDepServer = db().prepare(
|
||||
`INSERT INTO deployment_servers
|
||||
(deployment_id, server, files_changed, restart_state, players, checked_at, restarted_at, attempts, error)
|
||||
VALUES (?, ?, ?, ?, NULL, NULL, NULL, 0, ?)`
|
||||
);
|
||||
// Mirror the promote's deployment_files into the rollback row so it's
|
||||
// browsable as a diff (before/after swapped).
|
||||
const insertDepFile = db().prepare(
|
||||
`INSERT INTO deployment_files
|
||||
(deployment_id, server, verb, rel_path, sha256_before, sha256_after, content_before, content_after)
|
||||
@@ -148,13 +188,13 @@ export async function rollbackPromoteDeployment(promoteId: string): Promise<Roll
|
||||
);
|
||||
|
||||
db().transaction(() => {
|
||||
insertDep.run(
|
||||
newDepId,
|
||||
startedAt,
|
||||
dep.script_id,
|
||||
dep.prior_version_id, // this rollback lands the *prior* version, so that's now current
|
||||
dep.script_version_id, // what was in place immediately before this rollback
|
||||
);
|
||||
insertDep.run(newDepId, startedAt);
|
||||
// Mirror the promote's script list — swapped: the "current" version
|
||||
// after rollback is the promote's prior; the "prior" of the rollback
|
||||
// is what was current up until this call (i.e. the promote's version).
|
||||
for (const s of scripts) {
|
||||
insertDepScript.run(newDepId, s.script_id, s.prior_version_id ?? s.script_version_id, s.script_version_id);
|
||||
}
|
||||
for (const [server, files] of perServer) {
|
||||
if (perServerFailures.has(server)) {
|
||||
insertDepServer.run(newDepId, server, 0, 'failed', perServerFailures.get(server)!);
|
||||
@@ -179,9 +219,9 @@ export async function rollbackPromoteDeployment(promoteId: string): Promise<Roll
|
||||
}
|
||||
}
|
||||
db().prepare(`UPDATE deployments SET rolled_back_by = ? WHERE id = ?`).run(newDepId, promoteId);
|
||||
// Restore the script's current version pointer.
|
||||
if (dep.script_id !== null) {
|
||||
db().prepare(`UPDATE scripts SET current_version_id = ? WHERE id = ?`).run(dep.prior_version_id, dep.script_id);
|
||||
// Restore each affected script's current_version_id.
|
||||
for (const s of scripts) {
|
||||
db().prepare(`UPDATE scripts SET current_version_id = ? WHERE id = ?`).run(s.prior_version_id, s.script_id);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -190,8 +230,7 @@ export async function rollbackPromoteDeployment(promoteId: string): Promise<Roll
|
||||
return {
|
||||
deploymentId: newDepId,
|
||||
reverses: promoteId,
|
||||
scriptId: dep.script_id,
|
||||
priorVersionId: dep.prior_version_id,
|
||||
scriptIds: scripts.map((s) => s.script_id),
|
||||
affectedServers
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user