Add GitHub plugin release imports and update discovery

This commit is contained in:
2026-09-18 01:29:38 -04:00
parent a718efc9e7
commit 3c49437bf9
16 changed files with 847 additions and 2 deletions
@@ -0,0 +1,20 @@
import { json } from '@sveltejs/kit';
import { importPluginRelease } from '$lib/server/fleet/plugin-releases.js';
import { GithubReleaseError } from '$lib/server/integrations/github-releases.js';
import { ScriptError } from '$lib/server/fleet/scripts.js';
export async function POST({ request }: { request: Request }) {
let input;
try { input = await request.json(); } catch { return json({ error: 'Invalid JSON body.' }, { status: 400 }); }
if (!input || typeof input !== 'object' || Array.isArray(input)) return json({ error: 'Import details are required.' }, { status: 400 });
if ((input.scopeType != null && input.scopeType !== 'game' && input.scopeType !== 'map') ||
(input.scopeValue != null && typeof input.scopeValue !== 'string')) {
return json({ error: 'Invalid plugin scope.' }, { status: 400 });
}
try { return json({ ok: true, ...await importPluginRelease(input) }); }
catch (error) {
if (error instanceof GithubReleaseError) return json({ error: error.message }, { status: error.status });
if (error instanceof ScriptError) return json({ error: error.message }, { status: 409 });
throw error;
}
}
@@ -0,0 +1,10 @@
import { json } from '@sveltejs/kit';
import { getGithubRelease, GithubReleaseError } from '$lib/server/integrations/github-releases.js';
export async function GET({ url }: { url: URL }) {
try { return json({ release: await getGithubRelease(url.searchParams.get('url') ?? '') }); }
catch (error) {
if (error instanceof GithubReleaseError) return json({ error: error.message }, { status: error.status });
throw error;
}
}
+25
View File
@@ -0,0 +1,25 @@
import { json } from '@sveltejs/kit';
import { listScripts } from '$lib/server/fleet/scripts.js';
import { checkPluginUpdate } from '$lib/server/fleet/plugin-releases.js';
import { GithubReleaseError } from '$lib/server/integrations/github-releases.js';
import type { PluginUpdate } from '$lib/plugin-github.js';
export async function GET({ url }: { url: URL }) {
const raw = url.searchParams.get('pluginId');
if (raw !== null) {
const id = Number(raw);
if (!Number.isSafeInteger(id) || id < 1) return json({ error: 'Invalid plugin id.' }, { status: 400 });
try { return json({ updates: [await checkPluginUpdate(id)] }); }
catch (error) {
if (error instanceof GithubReleaseError) return json({ error: error.message }, { status: error.status });
throw error;
}
}
const plugins = listScripts('plugin');
const updates: PluginUpdate[] = [];
// Bound outbound concurrency for large fleets; repository requests share a five-minute cache.
for (let i = 0; i < plugins.length; i += 4) {
updates.push(...await Promise.all(plugins.slice(i, i + 4).map(p => checkPluginUpdate(p.id))));
}
return json({ updates });
}
+5
View File
@@ -8,6 +8,8 @@ import {
type ScopeType
} from '$lib/server/fleet/scripts.js';
import { findRunningBetaForScript } from '$lib/server/fleet/beta.js';
import { db } from '$lib/server/db/index.js';
import type { PluginSource } from '$lib/plugin-github.js';
function parseId(idRaw: string): number | null {
const n = Number.parseInt(idRaw, 10);
@@ -27,6 +29,9 @@ export async function GET({ params }: { params: { id: string } }) {
sha256: v.sha256,
size: v.size,
notes: v.notes,
source: script.kind === 'plugin'
? db().prepare('SELECT * FROM plugin_imports WHERE version_id = ?').get(v.id) as PluginSource | undefined ?? null
: null,
state: v.state,
uploaded_at: v.uploaded_at
}));
+21 -1
View File
@@ -82,7 +82,7 @@
let poll: ReturnType<typeof setInterval> | null = null;
onMount(() => {
load();
void load().then(seedImportedPlugin).catch(error => { flash = (error as Error).message; });
poll = setInterval(refreshBetas, 5000);
});
onDestroy(() => {
@@ -102,6 +102,26 @@
return eligibleScripts.filter((s) => !takenIds.has(s.id));
}
async function seedImportedPlugin(): Promise<void> {
const query = new URLSearchParams(window.location.search);
if (!query.has('pluginId')) return;
const pluginId = Number(query.get('pluginId'));
const versionId = Number(query.get('versionId'));
if (!eligibleScripts.some(s => s.id === pluginId && s.kind === 'plugin')) {
flash = 'This plugin is already promoted or has a running beta. Open its detail page to review its status.';
return;
}
picks = [{ key: nextKey++, scriptId: pluginId, versionId: null }];
await onPickScript(0);
if (!versionsForRow(picks[0]).some(v => v.id === versionId)) {
picks[0].versionId = null;
flash = 'The imported version is no longer available for testing. Choose a version to continue.';
return;
}
picks[0].versionId = versionId;
picks = [...picks];
}
async function onPickScript(rowIdx: number): Promise<void> {
flash = null;
const pick = picks[rowIdx];
+6
View File
@@ -1,5 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import PluginGithubImport from '$ui/components/PluginGithubImport.svelte';
import PluginUpdates from '$ui/components/PluginUpdates.svelte';
import { artifactVersionLabel } from '$lib/artifact-version.js';
import Card from '$ui/components/primitives/Card.svelte';
import Badge from '$ui/components/primitives/Badge.svelte';
@@ -31,6 +33,7 @@
interface ScopeOptions { games: string[]; mapsByGame: Record<string, string[]>; }
let plugins = $state<ArtifactRow[]>([]);
let importCount = $state(0);
let loading = $state(true);
let flash = $state<string | null>(null);
let uploading = $state(false);
@@ -149,6 +152,9 @@
<div class="page">
<SectionHeader title="Plugins" subtitle="Native game plugins deployed alongside scripts." />
<PluginGithubImport onimport={async () => { importCount++; await load(); }} />
{#key importCount}<PluginUpdates />{/key}
<Card padding="lg">
<h2 class="panel-title">Upload a plugin</h2>
{#if successBanner}