Require release versions for script uploads
CI / checks (push) Successful in 5m26s

This commit is contained in:
2026-10-12 18:46:57 -04:00
parent ffda04f276
commit 2f52dcb1ef
50 changed files with 871 additions and 113 deletions
@@ -39,7 +39,14 @@ export async function POST({ params, request }: { params: { id: string }; reques
if (isHttpError(error)) throw error;
return json(
{ ok: false, error: (error as Error).message },
{ status: error instanceof BetaError || error instanceof ScriptError ? 409 : 400 }
{
status:
error instanceof ScriptError && error.kind === 'invalid-release-version'
? 400
: error instanceof BetaError || error instanceof ScriptError
? 409
: 400
}
);
}
}
+4 -1
View File
@@ -89,7 +89,10 @@ export async function POST({ request, url }: { request: Request; url: URL }) {
return json({ ok: true, script, versionId: version.id });
} catch (err) {
if (err instanceof ScriptError) {
return json({ ok: false, error: err.message, kind: err.kind }, { status: 409 });
return json(
{ ok: false, error: err.message, kind: err.kind },
{ status: err.kind === 'invalid-release-version' ? 400 : 409 }
);
}
throw err;
}
+1 -1
View File
@@ -50,7 +50,7 @@ export async function GET({ params }: { params: { id: string } }) {
script.kind === 'script' || script.kind === 'plugin'
? db()
.prepare(
`SELECT i.server, v.version, v.id AS versionId, i.updated_at AS recordedAt FROM server_scripts i JOIN script_versions v ON v.id = i.script_version_id WHERE v.script_id = ? AND i.rel_path = ? ORDER BY i.server`
`SELECT i.server, v.version, v.release_version, v.id AS versionId, i.updated_at AS recordedAt FROM server_scripts i JOIN script_versions v ON v.id = i.script_version_id WHERE v.script_id = ? AND i.rel_path = ? ORDER BY i.server`
)
.all(id, script.target_rel_path)
: [],
+13 -12
View File
@@ -1,8 +1,11 @@
import { artifactFormBody } from '$lib/server/request-body.js';
import { json } from '@sveltejs/kit';
import { uploadNewVersion, getScript, ScriptError } from '$lib/server/fleet/scripts.js';
import { createHash } from 'node:crypto';
import { db } from '$lib/server/db/index.js';
import {
uploadNewVersion,
getScript,
getLatestVersion,
ScriptError
} from '$lib/server/fleet/scripts.js';
import { findRunningBetaForScript } from '$lib/server/fleet/beta.js';
function parseId(idRaw: string): number | null {
@@ -11,7 +14,7 @@ function parseId(idRaw: string): number | null {
}
// Multipart POST — creates a new version from an uploaded file. Fields:
// content (file), notes and releaseVersion (optional). Response includes a "betaRequired" hint
// content (file), notes (optional) and releaseVersion (required for scripts). Response includes a "betaRequired" hint
// when the script has a scope so the UI can nudge the operator to open a beta.
export async function POST({ params, request }: { params: { id: string }; request: Request }) {
const scriptId = parseId(params.id);
@@ -45,21 +48,16 @@ export async function POST({ params, request }: { params: { id: string }; reques
{ status: 409 }
);
}
const sha = createHash('sha256').update(content).digest('hex');
const existing = db()
.prepare(
'SELECT id, version, sha256, size FROM script_versions WHERE script_id = ? AND sha256 = ? ORDER BY version DESC LIMIT 1'
)
.get(scriptId, sha);
if (existing) return json({ ok: true, version: existing, reused: true });
}
try {
const releaseVersion = form.has('releaseVersion')
? String(form.get('releaseVersion'))
: undefined;
const previous = getLatestVersion(scriptId);
const version = uploadNewVersion(scriptId, content, notes, releaseVersion);
return json({
ok: true,
reused: script.kind === 'script' && version.version <= (previous?.version ?? 0),
version: {
id: version.id,
version: version.version,
@@ -73,7 +71,10 @@ export async function POST({ params, request }: { params: { id: string }; reques
});
} catch (err) {
if (err instanceof ScriptError) {
return json({ ok: false, error: err.message, kind: err.kind }, { status: 409 });
return json(
{ ok: false, error: err.message, kind: err.kind },
{ status: err.kind === 'invalid-release-version' ? 400 : 409 }
);
}
throw err;
}
+1 -1
View File
@@ -15,7 +15,7 @@ export async function POST({ request }: { request: Request }) {
.digest('hex');
const duplicates = db()
.prepare(
`SELECT v.script_id AS scriptId, v.id AS versionId, v.version
`SELECT v.script_id AS scriptId, v.id AS versionId, v.version, v.release_version
FROM script_versions v JOIN scripts s ON s.id = v.script_id
WHERE s.kind = 'script' AND v.sha256 = ? ORDER BY v.version DESC`
)
+5 -1
View File
@@ -261,7 +261,11 @@
>
<td class="files"
>{#if beta.scripts.length === 1}{beta.scripts[0].name}
<span>{artifactVersionLabel(beta.scripts[0].version)}</span
<span
>{artifactVersionLabel(
beta.scripts[0].version,
beta.scripts[0].kind
)}</span
>{:else}{beta.scripts.length} files{/if}</td
>
<td
+7 -3
View File
@@ -620,7 +620,7 @@
$page.url
)}>{file.name}</a
>
· {artifactVersionLabel(file.version)}</summary
· {artifactVersionLabel(file.version, file.kind)}</summary
>
<p><code>{file.target_rel_path}</code></p>
{#if file.version}<p class="muted">
@@ -661,7 +661,10 @@
<a
href={`/api/betas/${id}/revisions/${item.revision}/files/${file.script_id}`}
download={file.path.split('/').pop()}
>{file.name} · {artifactVersionLabel(file)}</a
>{file.name} · {artifactVersionLabel(
file,
file.kind
)}</a
><code>{file.path}</code><span
>{file.sha256.slice(0, 12)}</span
>
@@ -744,7 +747,8 @@
<div class="file-list">
{#each preview.files as file}<details open>
<summary
>{file.name} · {artifactVersionLabel(file)} · {file.servers.length} servers</summary
>{file.name} · {artifactVersionLabel(file, file.kind)} · {file.servers
.length} servers</summary
>
<p><code>{file.path}</code> · {file.scope} · SHA {file.sha256.slice(0, 12)}</p>
<ul>
+10 -5
View File
@@ -712,10 +712,14 @@
? 'Loading versions…'
: 'Choose a version'}</option
>{#each versionsFor(pick) as version}<option value={version.id}
>{artifactVersionLabel(version)} · {version.sha256.slice(
0,
8
)} · {version.state.replaceAll('-', ' ')}</option
>{artifactVersionLabel(
version,
scripts.find((file) => file.id === pick.scriptId)
?.kind
)} · {version.sha256.slice(0, 8)} · {version.state.replaceAll(
'-',
' '
)}</option
>{/each}</Select
></Field
>
@@ -926,7 +930,8 @@
?.name}</strong
>
· {artifactVersionLabel(
versionsFor(pick).find((v) => v.id === pick.versionId)
versionsFor(pick).find((v) => v.id === pick.versionId),
scripts.find((file) => file.id === pick.scriptId)?.kind
)} · {versionsFor(pick)
.find((v) => v.id === pick.versionId)
?.sha256.slice(
+6 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { artifactVersionLabel } from '$lib/artifact-version.js';
import Select from '$ui/components/primitives/Select.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import Field from '$ui/components/primitives/Field.svelte';
@@ -336,6 +337,7 @@
<td>{scopeLabel(s)}</td>
<td
><LibraryVersions
kind="script"
current={s.currentVersion}
latest={s.latestVersion}
betaId={s.runningBetaId}
@@ -389,7 +391,10 @@
>
{#if reviewSelection}<ul class="selection-review">
{#each selectedScripts as script}<li>
{script.name} · v{script.latestVersion?.version}<Button
{script.name} · {artifactVersionLabel(
script.latestVersion,
'script'
)}<Button
size="sm"
variant="ghost"
onclick={() => toggle(script.id)}
+10 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { artifactVersionLabel } from '$lib/artifact-version.js';
import RelativeTime from '$ui/components/RelativeTime.svelte';
import { tick } from 'svelte';
import ActivityLog from '$ui/components/ActivityLog.svelte';
@@ -564,8 +565,15 @@
)}>{f.relPath}</a
>{:else}{f.relPath}{/if}</td
><td>{f.kind ?? 'file'}</td><td
>{f.releaseVersion ??
(f.version ? `v${f.version}` : '—')}</td
>{artifactVersionLabel(
f.version
? {
version: f.version,
release_version: f.releaseVersion
}
: null,
f.kind ?? undefined
)}</td
><td
><details>
<summary