110 lines
4.8 KiB
TypeScript
110 lines
4.8 KiB
TypeScript
import { json } from '@sveltejs/kit';
|
|
import {
|
|
listBetas,
|
|
openBeta,
|
|
BetaError
|
|
} from '$lib/server/fleet/beta.js';
|
|
import { betaScriptMetadata } from '$lib/server/fleet/library-metadata.js';
|
|
import { db } from '$lib/server/db/index.js';
|
|
import type { BetaServerRow } from '$lib/server/fleet/beta.js';
|
|
import type { StoredPlan } from '$lib/server/fleet/beta-tests.js';
|
|
import { kick } from '$lib/server/pipeline/worker.js';
|
|
import { parseBetaPlan, type BetaPlan } from '$lib/beta-plan.js';
|
|
|
|
export async function GET({ url }: { url: URL }) {
|
|
const stateFilter = url.searchParams.get('state') as
|
|
| 'running'
|
|
| 'ready-promote'
|
|
| 'promoted'
|
|
| 'rolled-back'
|
|
| null;
|
|
const betas = listBetas(stateFilter ?? undefined);
|
|
const ids = JSON.stringify(betas.map(beta => beta.id));
|
|
const bundles = betaScriptMetadata(betas.map(beta => beta.id));
|
|
const plans = new Map((db().prepare(`SELECT p.*, COALESCE((SELECT started_at FROM beta_revisions r
|
|
WHERE r.beta_id=p.beta_id ORDER BY revision DESC LIMIT 1), b.opened_at) started_at
|
|
FROM beta_test_plans p JOIN betas b ON b.id=p.beta_id
|
|
WHERE p.beta_id IN (SELECT value FROM json_each(?))`).all(ids) as Array<StoredPlan & { started_at: string }>).map(plan => [plan.beta_id, plan]));
|
|
const members = db().prepare('SELECT * FROM beta_servers WHERE beta_id IN (SELECT value FROM json_each(?)) ORDER BY server').all(ids) as BetaServerRow[];
|
|
const rows = betas.map(b => {
|
|
const scripts = bundles.get(b.id) ?? [];
|
|
const test = plans.get(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(test!.started_at) + plan.timeoutHours * 3600000).toISOString() : null,
|
|
scripts,
|
|
servers: members.filter(member => member.beta_id === 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;
|
|
}
|
|
}
|