Files
electric-cherry/src/routes/api/betas/+server.ts
T

115 lines
4.7 KiB
TypeScript

import { json } from '@sveltejs/kit';
import {
listBetas,
listBetaServers,
listBetaScripts,
openBeta,
BetaError
} from '$lib/server/fleet/beta.js';
import { getScript, getVersion } from '$lib/server/fleet/scripts.js';
import { kick } from '$lib/server/pipeline/worker.js';
import { parseBetaPlan, type BetaPlan } from '$lib/beta-plan.js';
import { storedPlan } from '$lib/server/fleet/beta-tests.js';
import { currentBetaRevision } from '$lib/server/fleet/beta-revisions.js';
export async function GET({ url }: { url: URL }) {
const stateFilter = url.searchParams.get('state') as
| 'running'
| 'ready-promote'
| 'promoted'
| 'rolled-back'
| null;
const rows = listBetas(stateFilter ?? undefined).map((b) => {
const scripts = listBetaScripts(b.id).map((bs) => {
const script = getScript(bs.script_id);
const version = getVersion(bs.script_version_id);
return {
id: bs.script_id,
name: script?.name ?? '(deleted)',
target_rel_path: script?.target_rel_path ?? '',
kind: script?.kind ?? bs.kind ?? 'script',
version: version
? { id: version.id, version: version.version, release_version: version.release_version, sha256: version.sha256 }
: null
};
});
const test = storedPlan(b.id);
const plan = test ? parseBetaPlan(JSON.parse(test.plan)) : null;
return {
...b,
testOutcome: test?.outcome ?? null,
testReason: test?.reason ?? null,
deadlineAt: plan ? new Date(Date.parse(currentBetaRevision(b.id).started_at) + plan.timeoutHours * 3600000).toISOString() : null,
scripts,
servers: listBetaServers(b.id)
};
});
return json({ betas: rows });
}
// POST body accepts either:
// { artifacts: [{ id, versionId }], ... } — polymorphic (script/config/plugin/baseline)
// or the legacy form:
// { scripts: [{ scriptId, scriptVersionId }], ... }
export async function POST({ request }: { request: Request }) {
const body = (await request.json().catch(() => ({}))) as {
artifacts?: Array<{ id?: number; versionId?: number }>;
scripts?: Array<{ scriptId?: number; scriptVersionId?: number }>;
servers?: string[];
waitForHealth?: boolean;
waitForMatch?: boolean;
notes?: string;
testPlan?: BetaPlan;
};
if (!body || typeof body !== 'object' || Array.isArray(body)) return json({ ok: false, error: 'Expected a JSON object' }, { status: 400 });
// Normalize into the openBeta shape. Both fields feed the same list.
const rawEntries: Array<{ scriptId?: number; scriptVersionId?: number }> = [];
if (Array.isArray(body.artifacts)) {
for (const a of body.artifacts) rawEntries.push({ scriptId: a.id, scriptVersionId: a.versionId });
}
if (Array.isArray(body.scripts)) {
for (const s of body.scripts) rawEntries.push({ scriptId: s.scriptId, scriptVersionId: s.scriptVersionId });
}
if (rawEntries.length === 0 || !Array.isArray(body.servers)) {
return json(
{ ok: false, error: 'artifacts[]/scripts[] (non-empty) and servers[] are required' },
{ status: 400 }
);
}
if (body.servers.length === 0) {
return json({ ok: false, error: 'at least one server is required' }, { status: 400 });
}
const normalized: Array<{ scriptId: number; scriptVersionId: number }> = [];
for (const s of rawEntries) {
if (typeof s.scriptId !== 'number' || typeof s.scriptVersionId !== 'number') {
return json(
{ ok: false, error: 'each entry needs scriptId/id and scriptVersionId/versionId' },
{ status: 400 }
);
}
normalized.push({ scriptId: s.scriptId, scriptVersionId: s.scriptVersionId });
}
try {
let testPlan: BetaPlan | undefined;
try { if (body.testPlan !== undefined) testPlan = parseBetaPlan(body.testPlan); }
catch (error) { return json({ ok: false, error: (error as Error).message }, { status: 400 }); }
const result = await openBeta({
testPlan,
scripts: normalized,
servers: body.servers,
waitForHealth: body.waitForHealth ?? true,
waitForMatch: body.waitForMatch ?? false,
notes: body.notes
});
void kick();
return json({ ok: true, result });
} catch (err) {
if (err instanceof BetaError) {
return json({ ok: false, error: err.message, kind: err.kind, details: err.details }, { status: 409 });
}
throw err;
}
}