Add configurable beta test plans and staged rollout
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import BetaPlanBuilder from "$ui/components/BetaPlanBuilder.svelte";
|
||||
import { defaultPlan, parseBetaPlan } from "$lib/beta-plan.js";
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import Card from '$ui/components/primitives/Card.svelte';
|
||||
@@ -12,6 +14,7 @@
|
||||
interface BetaListRow {
|
||||
id: number;
|
||||
state: 'running' | 'ready-promote' | 'promoted' | 'rolled-back';
|
||||
testOutcome: string | null;
|
||||
wait_for_health: number;
|
||||
wait_for_match: number;
|
||||
opened_at: string;
|
||||
@@ -45,6 +48,7 @@
|
||||
let nextKey = 0;
|
||||
let picks = $state<Pick[]>([{ key: nextKey++, scriptId: null, versionId: null }]);
|
||||
let pickedServers = $state<Set<string>>(new Set());
|
||||
let testPlan = $state(defaultPlan());
|
||||
let waitForHealth = $state(true);
|
||||
let waitForMatch = $state(false);
|
||||
let openingBeta = $state(false);
|
||||
@@ -175,6 +179,7 @@
|
||||
body: JSON.stringify({
|
||||
scripts: picks.map((p) => ({ scriptId: p.scriptId!, scriptVersionId: p.versionId! })),
|
||||
servers: [...pickedServers],
|
||||
testPlan: parseBetaPlan(testPlan),
|
||||
waitForHealth,
|
||||
waitForMatch
|
||||
})
|
||||
@@ -185,7 +190,7 @@
|
||||
return;
|
||||
}
|
||||
if (body.result) await goto(`/betas/${body.result.betaId}`);
|
||||
} finally {
|
||||
} catch (error) { flash = (error as Error).message; } finally {
|
||||
openingBeta = false;
|
||||
}
|
||||
}
|
||||
@@ -298,18 +303,7 @@
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<Field label="Options">
|
||||
<div class="opts">
|
||||
<label class="chk">
|
||||
<input type="checkbox" bind:checked={waitForHealth} />
|
||||
<span>Wait for server start healthy</span>
|
||||
</label>
|
||||
<label class="chk">
|
||||
<input type="checkbox" bind:checked={waitForMatch} />
|
||||
<span>Wait for server match</span>
|
||||
</label>
|
||||
</div>
|
||||
</Field>
|
||||
<BetaPlanBuilder bind:plan={testPlan} />
|
||||
|
||||
<div class="form-actions">
|
||||
<Button
|
||||
@@ -341,7 +335,7 @@
|
||||
<a class="beta-row" href="/betas/{b.id}">
|
||||
<span class="beta-id">#{b.id}</span>
|
||||
<span class="beta-script">{summarizeScripts(b)}</span>
|
||||
<Badge variant={betaStateVariant(b.state)} size="sm" uppercase>{b.state}</Badge>
|
||||
<Badge variant={betaStateVariant(b.state)} size="sm" uppercase>{b.testOutcome && b.state === 'running' ? b.testOutcome : b.state}</Badge>
|
||||
<span class="beta-servers">{b.servers.length} server{b.servers.length === 1 ? '' : 's'}</span>
|
||||
</a>
|
||||
{/each}
|
||||
@@ -455,7 +449,6 @@
|
||||
}
|
||||
.chk input { accent-color: var(--accent-primary); cursor: pointer; }
|
||||
|
||||
.opts { display: flex; flex-wrap: wrap; gap: var(--space-6); }
|
||||
|
||||
.form-actions { display: flex; align-items: center; gap: var(--space-4); }
|
||||
.flash { color: var(--state-danger); font-family: var(--font-mono); font-size: var(--text-xs); }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import BetaTestProgress from "$ui/components/BetaTestProgress.svelte";
|
||||
import type { BetaTestView } from "$lib/beta-plan.js";
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import Card from '$ui/components/primitives/Card.svelte';
|
||||
@@ -30,6 +32,7 @@
|
||||
version: { id: number; version: number; sha256: string } | null;
|
||||
}
|
||||
|
||||
let test = $state<BetaTestView | null>(null);
|
||||
let beta = $state<BetaRow | null>(null);
|
||||
let scripts = $state<BetaScript[]>([]);
|
||||
let servers = $state<BetaServerRow[]>([]);
|
||||
@@ -50,7 +53,7 @@
|
||||
if (!res.ok) throw new Error(res.status === 404 ? 'Beta not found.' : 'Could not refresh beta.');
|
||||
const body = await res.json();
|
||||
if (current !== request || requestedId !== id) return;
|
||||
beta = body.beta; scripts = body.scripts; servers = body.servers; events = body.events;
|
||||
test = body.test; beta = body.beta; scripts = body.scripts; servers = body.servers; events = body.events;
|
||||
loadError = '';
|
||||
} catch (err) {
|
||||
if (current === request && requestedId === id) loadError = (err as Error).message;
|
||||
@@ -79,7 +82,8 @@
|
||||
const prompt = isReject
|
||||
? `Reject beta #${id}? This restores original files on every beta server and marks the version as test-failed.`
|
||||
: `Rollback beta #${id}? This restores original files on every beta server.`;
|
||||
if (!confirm(prompt)) return;
|
||||
const restart = test ? test.plan.rollbackRestart === 'when-empty' ? ' Restarts will roll out when servers are empty.' : ' Restart servers manually to load the restored files.' : '';
|
||||
if (!confirm(prompt + restart)) return;
|
||||
acting = 'rollback'; flash = null;
|
||||
try {
|
||||
const res = await fetch(`/api/betas/${id}/rollback`, { method: 'POST' });
|
||||
@@ -143,7 +147,7 @@
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<OperationHistory steps={betaFlow(b, servers)} {events} active={b.state === 'running' || b.state === 'ready-promote'} />
|
||||
<OperationHistory steps={betaFlow({ ...b, testPlan: !!test }, servers)} {events} active={b.state === 'running' || b.state === 'ready-promote'} />
|
||||
|
||||
<!-- Artifacts -->
|
||||
<Card padding="lg">
|
||||
@@ -168,6 +172,9 @@
|
||||
{/if}
|
||||
</Card>
|
||||
|
||||
{#if test}
|
||||
<Card padding="lg"><BetaTestProgress {test} betaId={id} closed={b.state === "promoted" || b.state === "rolled-back"} onUpdate={load} /></Card>
|
||||
{:else}
|
||||
<!-- Options -->
|
||||
<Card padding="lg">
|
||||
<div class="panel-head"><h2>Options</h2></div>
|
||||
@@ -183,6 +190,8 @@
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/if}
|
||||
|
||||
<!-- Server progress -->
|
||||
<Card padding="lg">
|
||||
<div class="panel-head">
|
||||
@@ -206,7 +215,7 @@
|
||||
{#if bs.error}
|
||||
<span class="err">{bs.error}</span>
|
||||
{/if}
|
||||
{#if bs.stage === 'failed'}
|
||||
{#if bs.stage === 'failed' && !test}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -231,7 +240,7 @@
|
||||
onclick={promote}
|
||||
disabled={b.state !== 'ready-promote' || acting !== null}
|
||||
loading={acting === 'promote'}
|
||||
title={b.state !== 'ready-promote' ? 'All servers must reach ready first' : ''}
|
||||
title={b.state !== 'ready-promote' ? 'The test plan must pass before promotion' : ''}
|
||||
>
|
||||
Promote
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user