Allow revising active betas with fresh tests and history

This commit is contained in:
2026-10-05 21:02:58 -04:00
parent 9d4ff992a4
commit f8d0ba3936
21 changed files with 713 additions and 41 deletions
+3 -1
View File
@@ -3,6 +3,7 @@ import { db } from '$lib/server/db/index.js';
import { getBeta, listBetaServers, listBetaScripts } from '$lib/server/fleet/beta.js';
import { getScript, getVersion } from '$lib/server/fleet/scripts.js';
import { testView } from '$lib/server/fleet/beta-tests.js';
import { betaRevisionHistory, currentBetaRevision } from '$lib/server/fleet/beta-revisions.js';
function parseId(raw: string): number | null {
const n = Number.parseInt(raw, 10);
@@ -37,5 +38,6 @@ export async function GET({ params }: { params: { id: string } }) {
failed: (db().prepare("SELECT COUNT(*) n FROM deployment_servers WHERE deployment_id = ? AND server_deleted_at IS NULL AND restart_state = 'failed'").get(campaign.id) as { n: number }).n } : null;
const rollback = db().prepare("SELECT id FROM deployments WHERE beta_id = ? AND action = 'rollback' ORDER BY started_at DESC LIMIT 1")
.get(id) as { id: string } | undefined;
return json({ beta, scripts, servers, events, test: testView(id), promotion, rollback: rollback ?? null });
return json({ beta, scripts, servers, events, test: testView(id), promotion, rollback: rollback ?? null,
revision: currentBetaRevision(id), revisions: betaRevisionHistory(id) });
}
@@ -0,0 +1,28 @@
import { json } from '@sveltejs/kit';
import { BetaError, reviseBeta, type BetaRevisionUpload } from '$lib/server/fleet/beta.js';
import { ScriptError } from '$lib/server/fleet/scripts.js';
import { kick } from '$lib/server/pipeline/worker.js';
export async function POST({ params, request }: { params: { id: string }; request: Request }) {
const id = Number(params.id);
if (!Number.isSafeInteger(id) || id < 1) return json({ ok: false, error: 'Invalid beta ID' }, { status: 400 });
try {
const form = await request.formData();
const expectedRevision = Number(form.get('expectedRevision'));
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 1) throw new Error('A reviewed revision is required.');
const uploads: BetaRevisionUpload[] = [];
for (const [key, value] of form) {
if (!key.startsWith('file:')) continue;
const scriptId = Number(key.slice(5));
if (!Number.isSafeInteger(scriptId) || scriptId < 1 || !(value instanceof File)) throw new Error('Invalid replacement file.');
uploads.push({ scriptId, content: Buffer.from(await value.arrayBuffer()),
releaseVersion: form.has(`release:${scriptId}`) ? String(form.get(`release:${scriptId}`)) : undefined });
}
const result = await reviseBeta(id, expectedRevision, uploads, String(form.get('notes') ?? ''));
void kick();
return json({ ok: true, result });
} catch (error) {
return json({ ok: false, error: (error as Error).message },
{ status: error instanceof BetaError || error instanceof ScriptError ? 409 : 400 });
}
}
@@ -0,0 +1,16 @@
import { db } from '$lib/server/db/index.js';
/** Current files can be reviewed before the first revision is archived. */
export async function GET({ params }: { params: { id: string; revision: string; scriptId: string } }) {
const values = [params.id, params.revision, params.scriptId].map(Number);
if (values.some(value => !Number.isSafeInteger(value) || value < 1)) return new Response('Invalid file', { status: 400 });
const [id, revision, scriptId] = values;
const file = db().prepare('SELECT content FROM beta_revision_files WHERE beta_id = ? AND revision = ? AND script_id = ?')
.get(id, revision, scriptId) as { content: Buffer } | undefined
?? (revision === 1 ? db().prepare(`SELECT v.content FROM beta_scripts bs JOIN script_versions v ON v.id = bs.script_version_id
WHERE bs.beta_id = ? AND bs.script_id = ? AND NOT EXISTS (SELECT 1 FROM beta_revisions WHERE beta_id = ?)`)
.get(id, scriptId, id) as { content: Buffer } | undefined : undefined);
if (!file) return new Response('File not found', { status: 404 });
return new Response(new Uint8Array(file.content), { headers: { 'Content-Type': 'application/octet-stream',
'Content-Disposition': 'attachment', 'Cache-Control': 'no-store' } });
}
+2 -3
View File
@@ -50,9 +50,8 @@ export async function POST({ params, request }: { params: { id: string }; reques
}
setOutcome(id, 'active', null);
} else if (body.action === 'retry') {
// Fresh attempts keep their predecessors as audit history. Preserve the original deadline:
// operators open another beta if the overall testing window has expired.
if (stored.outcome === 'inconclusive') throw new Error('The testing window ended. Roll back and open a new beta with a longer deadline');
// Fresh attempts keep history and the current revision's deadline.
if (stored.outcome === 'inconclusive') throw new Error('The testing window ended. Update the files to start a new revision, or roll back and open a new beta with a longer deadline');
if (stagedFileChanges('beta', String(id)).some(change => change.state === 'applying')) {
throw new Error('A file write was interrupted. Inspect the server and roll back before opening a fresh beta');
}