Add publisher release versions to plugins
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
/** Keep the upload number visible: two builds may share a publisher's release tag. */
|
||||
export function artifactVersionLabel(version: { version: number; release_version?: string | null } | null | undefined): string {
|
||||
if (!version) return '—';
|
||||
return version.release_version
|
||||
? `${version.release_version} (upload ${version.version})`
|
||||
: `v${version.version}`;
|
||||
}
|
||||
@@ -65,6 +65,7 @@ const SCRIPTS_COLUMNS: Array<{ name: string; ddl: string }> = [
|
||||
];
|
||||
|
||||
const SCRIPT_VERSIONS_COLUMNS: Array<{ name: string; ddl: string }> = [
|
||||
{ name: 'release_version', ddl: 'ALTER TABLE script_versions ADD COLUMN release_version TEXT' },
|
||||
// state was added post-launch to make untested/tested/test-failed visible
|
||||
// in the UI. Existing rows default to 'untested' — this is safe because
|
||||
// any real promoted version's script has current_version_id set (which is
|
||||
|
||||
@@ -72,6 +72,7 @@ CREATE TABLE IF NOT EXISTS script_versions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
script_id INTEGER NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
|
||||
version INTEGER NOT NULL,
|
||||
release_version TEXT, -- publisher's version/tag, independent of upload order
|
||||
sha256 TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
content BLOB NOT NULL,
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface ScriptVersionRow {
|
||||
id: number;
|
||||
script_id: number;
|
||||
version: number;
|
||||
release_version: string | null;
|
||||
sha256: string;
|
||||
size: number;
|
||||
content: Buffer;
|
||||
@@ -66,6 +67,7 @@ export class ScriptError extends Error {
|
||||
| 'baseline-requires-scope'
|
||||
| 'baseline-exists'
|
||||
| 'empty-overlay'
|
||||
| 'invalid-release-version'
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
@@ -75,6 +77,14 @@ function sha256(buf: Buffer): string {
|
||||
return createHash('sha256').update(buf).digest('hex');
|
||||
}
|
||||
|
||||
function normalizeReleaseVersion(value: string | null | undefined): string | null {
|
||||
if (value == null) return null;
|
||||
if (typeof value !== 'string' || value.length > 80 || /[\u0000-\u001f\u007f]/.test(value)) {
|
||||
throw new ScriptError('release version must be a single line of at most 80 characters', 'invalid-release-version');
|
||||
}
|
||||
return value.trim() || null;
|
||||
}
|
||||
|
||||
export function getScript(id: number): ScriptRow | undefined {
|
||||
return db().prepare('SELECT * FROM scripts WHERE id = ?').get(id) as ScriptRow | undefined;
|
||||
}
|
||||
@@ -157,6 +167,8 @@ export interface CreateScriptInput {
|
||||
scopeValue?: string | null;
|
||||
content: Buffer;
|
||||
notes?: string;
|
||||
/** Publisher's release tag, e.g. v3.1.0. Upload numbering stays independent. */
|
||||
releaseVersion?: string;
|
||||
/** Defaults to 'script' for back-compat with existing callers. */
|
||||
kind?: ArtifactKind;
|
||||
}
|
||||
@@ -206,6 +218,7 @@ export function createScript(input: CreateScriptInput): { script: ScriptRow; ver
|
||||
if (getScriptByName(input.name)) throw new ScriptError(`script "${input.name}" already exists`, 'name-taken');
|
||||
|
||||
const sha = sha256(input.content);
|
||||
const releaseVersion = normalizeReleaseVersion(input.releaseVersion);
|
||||
const size = input.content.byteLength;
|
||||
|
||||
const insertScript = db().prepare(
|
||||
@@ -213,8 +226,8 @@ export function createScript(input: CreateScriptInput): { script: ScriptRow; ver
|
||||
VALUES (?, ?, ?, ?, ?)`
|
||||
);
|
||||
const insertVersion = db().prepare(
|
||||
`INSERT INTO script_versions (script_id, version, sha256, size, content, notes, kind)
|
||||
VALUES (?, 1, ?, ?, ?, ?, ?)`
|
||||
`INSERT INTO script_versions (script_id, version, sha256, size, content, notes, kind, release_version)
|
||||
VALUES (?, 1, ?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
|
||||
let scriptId!: number;
|
||||
@@ -222,7 +235,7 @@ export function createScript(input: CreateScriptInput): { script: ScriptRow; ver
|
||||
db().transaction(() => {
|
||||
const s = insertScript.run(input.name, targetRelPath, input.scopeType ?? null, input.scopeValue ?? null, kind);
|
||||
scriptId = Number(s.lastInsertRowid);
|
||||
const v = insertVersion.run(scriptId, sha, size, input.content, input.notes ?? null, kind);
|
||||
const v = insertVersion.run(scriptId, sha, size, input.content, input.notes ?? null, kind, releaseVersion);
|
||||
versionId = Number(v.lastInsertRowid);
|
||||
})();
|
||||
|
||||
@@ -232,7 +245,7 @@ export function createScript(input: CreateScriptInput): { script: ScriptRow; ver
|
||||
};
|
||||
}
|
||||
|
||||
export function uploadNewVersion(scriptId: number, content: Buffer, notes?: string): ScriptVersionRow {
|
||||
export function uploadNewVersion(scriptId: number, content: Buffer, notes?: string, releaseVersion?: string): ScriptVersionRow {
|
||||
const script = getScript(scriptId);
|
||||
if (!script) throw new ScriptError(`no such script (id=${scriptId})`, 'no-such-script');
|
||||
|
||||
@@ -249,13 +262,14 @@ export function uploadNewVersion(scriptId: number, content: Buffer, notes?: stri
|
||||
const latest = getLatestVersion(scriptId);
|
||||
const nextVersion = (latest?.version ?? 0) + 1;
|
||||
const sha = sha256(content);
|
||||
const release = normalizeReleaseVersion(releaseVersion);
|
||||
|
||||
const res = db()
|
||||
.prepare(
|
||||
`INSERT INTO script_versions (script_id, version, sha256, size, content, notes, kind)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
`INSERT INTO script_versions (script_id, version, sha256, size, content, notes, kind, release_version)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(scriptId, nextVersion, sha, content.byteLength, content, notes ?? null, script.kind);
|
||||
.run(scriptId, nextVersion, sha, content.byteLength, content, notes ?? null, script.kind, release);
|
||||
return getVersion(Number(res.lastInsertRowid))!;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { artifactVersionLabel } from '$lib/artifact-version.js';
|
||||
import { goto } from '$app/navigation';
|
||||
import Card from './primitives/Card.svelte';
|
||||
import Badge from './primitives/Badge.svelte';
|
||||
@@ -28,6 +29,7 @@
|
||||
}
|
||||
interface Version {
|
||||
id: number; version: number; sha256: string; size: number;
|
||||
release_version: string | null;
|
||||
notes: string | null; state: 'untested' | 'tested' | 'test-failed';
|
||||
uploaded_at: string;
|
||||
}
|
||||
@@ -49,6 +51,7 @@
|
||||
|
||||
let versionFile = $state<File | null>(null);
|
||||
let versionNotes = $state('');
|
||||
let releaseVersion = $state('');
|
||||
let uploadingVersion = $state(false);
|
||||
|
||||
let scopeDialog = $state<HTMLDialogElement | null>(null);
|
||||
@@ -143,6 +146,7 @@
|
||||
}
|
||||
function openUploadModal(): void {
|
||||
if (runningBetaId !== null) return;
|
||||
releaseVersion = '';
|
||||
versionFile = null;
|
||||
versionNotes = '';
|
||||
flash = null;
|
||||
@@ -169,6 +173,7 @@
|
||||
uploadingVersion = true; flash = null;
|
||||
const form = new FormData();
|
||||
form.append('content', versionFile);
|
||||
if (family === 'plugin') form.append('releaseVersion', releaseVersion.trim());
|
||||
if (versionNotes.trim()) form.append('notes', versionNotes.trim());
|
||||
try {
|
||||
const res = await fetch(`/api/scripts/${id}/versions`, { method: 'POST', body: form });
|
||||
@@ -302,7 +307,7 @@
|
||||
<span class="k">Current version</span>
|
||||
<span class="v mono">
|
||||
{#if currentVersion}
|
||||
v{currentVersion.version} · {currentVersion.sha256.slice(0, 8)}
|
||||
{artifactVersionLabel(currentVersion)} · {currentVersion.sha256.slice(0, 8)}
|
||||
<Badge variant="online" size="sm">Promoted</Badge>
|
||||
{:else}
|
||||
<span class="dim">Never promoted — open a <a href="/betas">beta</a></span>
|
||||
@@ -313,7 +318,7 @@
|
||||
<div class="kv">
|
||||
<span class="k">Latest version</span>
|
||||
<span class="v mono">
|
||||
v{latestVersion.version} · {latestVersion.sha256.slice(0, 8)}
|
||||
{artifactVersionLabel(latestVersion)} · {latestVersion.sha256.slice(0, 8)}
|
||||
<Badge variant={stateVariant(latestVersion.state)} size="sm">{latestVersion.state}</Badge>
|
||||
</span>
|
||||
</div>
|
||||
@@ -332,7 +337,7 @@
|
||||
<div class="version-list">
|
||||
{#each versions as v (v.id)}
|
||||
<div class="version-row" class:current={a.current_version_id === v.id}>
|
||||
<span class="v-num">v{v.version}</span>
|
||||
<span class="v-num">{artifactVersionLabel(v)}</span>
|
||||
<span class="v-sha">{v.sha256.slice(0, 12)}</span>
|
||||
<span class="v-size">{v.size} B</span>
|
||||
<span class="v-notes">{v.notes ?? ''}</span>
|
||||
@@ -391,8 +396,13 @@
|
||||
<button type="button" class="close" onclick={cancelUploadModal} aria-label="Close">×</button>
|
||||
</header>
|
||||
<Field label="File">
|
||||
<Input type="file" onchange={pickVersionFile} disabled={uploadingVersion} />
|
||||
<Input type="file" accept={family === 'plugin' ? '.dll' : undefined} onchange={pickVersionFile} disabled={uploadingVersion} />
|
||||
</Field>
|
||||
{#if family === 'plugin'}
|
||||
<Field label="Release version" hint="Optional. Use the publisher’s version or release tag, such as v3.1.0.">
|
||||
<Input bind:value={releaseVersion} placeholder="v3.1.0" disabled={uploadingVersion} />
|
||||
</Field>
|
||||
{/if}
|
||||
<Field label="Notes" hint="Optional. What changed since the previous version.">
|
||||
<Input bind:value={versionNotes} placeholder="what changed" disabled={uploadingVersion} />
|
||||
</Field>
|
||||
@@ -473,7 +483,7 @@
|
||||
.version-list { display: flex; flex-direction: column; gap: var(--space-1); }
|
||||
.version-row {
|
||||
display: grid;
|
||||
grid-template-columns: 60px 130px 80px 1fr 220px auto;
|
||||
grid-template-columns: minmax(100px, 1fr) 130px 80px 1fr 220px auto;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
@@ -487,7 +497,7 @@
|
||||
border-color: rgba(124, 212, 255, 0.35);
|
||||
background: rgba(124, 212, 255, 0.04);
|
||||
}
|
||||
.v-num { color: var(--text-primary); font-weight: var(--weight-semibold); }
|
||||
.v-num { color: var(--text-primary); font-weight: var(--weight-semibold); overflow-wrap: anywhere; }
|
||||
.v-sha, .v-size, .v-when { color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
|
||||
.v-notes { color: var(--text-secondary); font-family: var(--font-body); font-size: var(--text-sm); }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user