Fix release version labels across deployment views
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import type { LaunchFileSummary } from './deployment-plan.js';
|
||||
|
||||
/** 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,
|
||||
@@ -10,3 +12,14 @@ export function artifactVersionLabel(
|
||||
? `Unversioned (upload ${version.version})`
|
||||
: `v${version.version}`;
|
||||
}
|
||||
|
||||
export function launchFileVersionLabel(
|
||||
file: Pick<LaunchFileSummary, 'version' | 'releaseVersion' | 'kind'>
|
||||
): string {
|
||||
return file.version === null
|
||||
? 'Generated'
|
||||
: artifactVersionLabel(
|
||||
{ version: file.version, release_version: file.releaseVersion },
|
||||
file.kind
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ export interface LaunchFileSummary {
|
||||
path: string;
|
||||
versionId: number | null;
|
||||
version: number | null;
|
||||
/** Missing on snapshots recorded before publisher release versions were retained. */
|
||||
releaseVersion?: string | null;
|
||||
sha256: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
@@ -171,6 +171,7 @@ function buildRecipe(
|
||||
path: script.target_rel_path,
|
||||
versionId: version.id,
|
||||
version: version.version,
|
||||
releaseVersion: version.release_version,
|
||||
sha256: version.sha256,
|
||||
size: version.size
|
||||
});
|
||||
@@ -187,6 +188,7 @@ function buildRecipe(
|
||||
kind: 'config',
|
||||
versionId: null,
|
||||
version: null,
|
||||
releaseVersion: null,
|
||||
sha256: sha256Buffer(content),
|
||||
size: content.length,
|
||||
...(includeContent ? { content: content.toString('base64') } : {})
|
||||
@@ -249,18 +251,41 @@ export function recipeSummaryForDeployment(
|
||||
)
|
||||
.get(id) as { recipe_summary: string | null; blueprint_name: string | null } | undefined;
|
||||
if (!row) return null;
|
||||
if (row.recipe_summary !== null)
|
||||
return { recipe: JSON.parse(row.recipe_summary), blueprintName: row.blueprint_name };
|
||||
if (row.recipe_summary !== null) {
|
||||
const saved: LaunchRecipeSummary = JSON.parse(row.recipe_summary);
|
||||
const summary = withHistoricalReleaseVersions(saved);
|
||||
if (summary !== saved)
|
||||
db()
|
||||
.prepare(
|
||||
'UPDATE server_launch_recipes SET recipe_summary = ? WHERE deployment_id = ?'
|
||||
)
|
||||
.run(JSON.stringify(summary), id);
|
||||
return { recipe: summary, blueprintName: row.blueprint_name };
|
||||
}
|
||||
// Old snapshots remain readable. Populate on first access, without making
|
||||
// controller startup scan and decode every retained plugin in its history.
|
||||
const launch = recipeForDeployment(id)!;
|
||||
const summary = summarizeRecipe(launch.recipe);
|
||||
const summary = withHistoricalReleaseVersions(summarizeRecipe(launch.recipe));
|
||||
db()
|
||||
.prepare('UPDATE server_launch_recipes SET recipe_summary = ? WHERE deployment_id = ?')
|
||||
.run(JSON.stringify(summary), id);
|
||||
return { recipe: summary, blueprintName: row.blueprint_name };
|
||||
}
|
||||
|
||||
/** Restore display metadata from the exact retained upload, preserving the captured recipe. */
|
||||
function withHistoricalReleaseVersions(summary: LaunchRecipeSummary): LaunchRecipeSummary {
|
||||
const files = summary.files.map((file) => {
|
||||
if (file.releaseVersion !== undefined || file.versionId === null) return file;
|
||||
const version = getVersionMetadata(file.versionId);
|
||||
if (!version || version.version !== file.version || version.sha256 !== file.sha256)
|
||||
return file;
|
||||
return { ...file, releaseVersion: version.release_version };
|
||||
});
|
||||
return files.some((file, index) => file !== summary.files[index])
|
||||
? { ...summary, files }
|
||||
: summary;
|
||||
}
|
||||
|
||||
export function recipeForDeployment(
|
||||
id: string
|
||||
): { recipe: LaunchRecipePlan; blueprintName: string | null } | null {
|
||||
|
||||
@@ -192,12 +192,17 @@ export function getVersion(id: number): ScriptVersionRow | undefined {
|
||||
ScriptVersionRow | undefined;
|
||||
}
|
||||
|
||||
export type ScriptVersionMetadata = Pick<ScriptVersionRow, 'id' | 'version' | 'sha256' | 'size'>;
|
||||
export type ScriptVersionMetadata = Pick<
|
||||
ScriptVersionRow,
|
||||
'id' | 'version' | 'release_version' | 'sha256' | 'size'
|
||||
>;
|
||||
|
||||
/** Planning a launch never needs to load potentially large script or plugin bytes. */
|
||||
export function getVersionMetadata(id: number): ScriptVersionMetadata | undefined {
|
||||
return db()
|
||||
.prepare('SELECT id, version, sha256, size FROM script_versions WHERE id = ?')
|
||||
.prepare(
|
||||
'SELECT id, version, release_version, sha256, size FROM script_versions WHERE id = ?'
|
||||
)
|
||||
.get(id) as ScriptVersionMetadata | undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export function applyActivationFiles(
|
||||
): void {
|
||||
const changes = db()
|
||||
.prepare(
|
||||
`SELECT f.*, ${blobSize('f.content_hash')} AS size, v.version FROM staged_file_changes f
|
||||
`SELECT f.*, ${blobSize('f.content_hash')} AS size, v.version, v.release_version FROM staged_file_changes f
|
||||
JOIN provisioning_sources s ON s.kind=f.source_kind AND s.source_id=f.source_id AND s.server=f.server
|
||||
LEFT JOIN script_versions v ON v.id=f.script_version_id
|
||||
WHERE s.operation_id=? AND f.state='pending' ORDER BY f.id`
|
||||
@@ -21,6 +21,7 @@ export function applyActivationFiles(
|
||||
content_hash: string | null;
|
||||
size: number;
|
||||
version: number | null;
|
||||
release_version: string | null;
|
||||
})[];
|
||||
for (const change of changes) {
|
||||
assertPublicFileTarget(change.server, change.kind, change.rel_path);
|
||||
@@ -36,6 +37,7 @@ export function applyActivationFiles(
|
||||
path: change.rel_path,
|
||||
versionId: change.script_version_id,
|
||||
version: change.version,
|
||||
releaseVersion: change.release_version,
|
||||
sha256: change.content_hash,
|
||||
size: change.size,
|
||||
...(includeContent
|
||||
|
||||
@@ -38,11 +38,17 @@ export function captureMoveRecipe(server: string): LaunchRecipePlan {
|
||||
? undefined
|
||||
: (db()
|
||||
.prepare(
|
||||
`SELECT v.version,v.kind,s.name
|
||||
`SELECT v.version,v.release_version,v.kind,s.name
|
||||
FROM script_versions v JOIN scripts s ON s.id=v.script_id WHERE v.id=?`
|
||||
)
|
||||
.get(file.script_version_id) as
|
||||
{ version: number; kind: string; name: string } | undefined);
|
||||
| {
|
||||
version: number;
|
||||
release_version: string | null;
|
||||
kind: string;
|
||||
name: string;
|
||||
}
|
||||
| undefined);
|
||||
const kind = version?.kind === 'baseline' ? 'config' : (version?.kind ?? before?.kind);
|
||||
if (!['script', 'plugin', 'config'].includes(kind ?? ''))
|
||||
throw new Error(`The deployed file type is unavailable for ${file.rel_path}.`);
|
||||
@@ -59,6 +65,7 @@ export function captureMoveRecipe(server: string): LaunchRecipePlan {
|
||||
path: file.rel_path,
|
||||
versionId: file.script_version_id,
|
||||
version: version?.version ?? null,
|
||||
releaseVersion: version?.release_version ?? null,
|
||||
sha256: file.sha256,
|
||||
size: file.size
|
||||
};
|
||||
|
||||
@@ -1149,7 +1149,8 @@ function sameLaunchFiles(first: LaunchRecipePlan, second: LaunchRecipePlan): boo
|
||||
expectedMap: recipe.expectedMap,
|
||||
configFile: recipe.configFile,
|
||||
configSource: recipe.configSource,
|
||||
files: recipe.files
|
||||
// Older frozen recipes lack release labels; display metadata does not change a launch.
|
||||
files: recipe.files.map(({ releaseVersion: _releaseVersion, ...file }) => file)
|
||||
});
|
||||
return JSON.stringify(selected(first)) === JSON.stringify(selected(second));
|
||||
}
|
||||
|
||||
+23
-6
@@ -1,15 +1,21 @@
|
||||
import { statusPresentation, type StatusTone } from './status.js';
|
||||
import { plural } from './plural.js';
|
||||
import { artifactVersionLabel } from '../artifact-version.js';
|
||||
|
||||
export type CampaignServerGroup = 'attention' | 'processing' | 'queued' | 'complete' | 'inactive';
|
||||
|
||||
export interface CampaignSummary {
|
||||
interface CampaignScriptSummary {
|
||||
script_name: string | null;
|
||||
script_version: number | null;
|
||||
script_release_version: string | null;
|
||||
script_kind: string | null;
|
||||
}
|
||||
|
||||
export interface CampaignSummary extends CampaignScriptSummary {
|
||||
action: string;
|
||||
beta_id: number | null;
|
||||
rolled_back_by: string | null;
|
||||
script_name: string | null;
|
||||
script_version: number | null;
|
||||
scripts: { script_name: string | null; script_version: number | null }[];
|
||||
scripts: CampaignScriptSummary[];
|
||||
servers: { restart_state: string; server_deleted_at?: string | null }[];
|
||||
}
|
||||
|
||||
@@ -23,8 +29,19 @@ export function campaignTitle(campaign: CampaignSummary): string {
|
||||
return `${action} · Beta #${campaign.beta_id}`;
|
||||
if (campaign.scripts.length > 1) return `${action} · ${campaign.scripts.length} scripts`;
|
||||
const script = campaign.scripts[0] ?? campaign;
|
||||
if (script.script_name)
|
||||
return `${action} · ${script.script_name}${script.script_version !== null ? ` v${script.script_version}` : ''}`;
|
||||
if (script.script_name) {
|
||||
const version =
|
||||
script.script_version === null
|
||||
? ''
|
||||
: ` ${artifactVersionLabel(
|
||||
{
|
||||
version: script.script_version,
|
||||
release_version: script.script_release_version
|
||||
},
|
||||
script.script_kind ?? 'script'
|
||||
)}`;
|
||||
return `${action} · ${script.script_name}${version}`;
|
||||
}
|
||||
return campaign.beta_id ? `${action} · Beta #${campaign.beta_id}` : action;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
} from '$ui/collection-summary.js';
|
||||
import { serverTitle } from '$ui/server-labels.js';
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { launchFileVersionLabel } from '$lib/artifact-version.js';
|
||||
import type { NativeHostSelectionStatus } from '$ui/native-game-files.js';
|
||||
import type { NativeDeploymentSelection } from '$lib/hosts.js';
|
||||
import type { ZombieGame } from '$lib/map-aliases.js';
|
||||
@@ -609,9 +610,7 @@
|
||||
<ul class="file-list">
|
||||
{#each entry.recipe.files as file}<li>
|
||||
<strong>{file.name}</strong><span
|
||||
>{file.version === null
|
||||
? 'Generated'
|
||||
: `v${file.version}`} · {file.kind}</span
|
||||
>{launchFileVersionLabel(file)} · {file.kind}</span
|
||||
><code>{file.path}</code>
|
||||
</li>{/each}
|
||||
</ul>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { createServerLaunchController, type ServerLaunchState } from '$ui/server-launch.js';
|
||||
import { serverTitle } from '$ui/server-labels.js';
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { launchFileVersionLabel } from '$lib/artifact-version.js';
|
||||
import type { NativeHostSelectionStatus } from '$ui/native-game-files.js';
|
||||
import type { NativeDeploymentSelection } from '$lib/hosts.js';
|
||||
import type { ZombieMapEntry, ZombieGame } from '$lib/map-aliases.js';
|
||||
@@ -378,7 +379,7 @@
|
||||
{#each preview.recipe.files as file}<li>
|
||||
<div>
|
||||
<strong>{file.name}</strong><span
|
||||
>{file.version === null ? 'Generated' : `v${file.version}`} ·
|
||||
>{launchFileVersionLabel(file)} ·
|
||||
{file.kind}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import RelativeTime from '$ui/components/RelativeTime.svelte';
|
||||
import { launchFileVersionLabel } from '$lib/artifact-version.js';
|
||||
import type { LaunchRecipeSummary } from '$lib/deployment-plan.js';
|
||||
let { recipe, historical = false }: { recipe: LaunchRecipeSummary; historical?: boolean } =
|
||||
$props();
|
||||
@@ -36,7 +37,7 @@
|
||||
{#each recipe.files as file}<li>
|
||||
<div>
|
||||
<strong>{file.name}</strong><span
|
||||
>{file.version !== null ? `v${file.version}` : 'Generated'} · {file.kind}
|
||||
>{launchFileVersionLabel(file)} · {file.kind}
|
||||
· {file.size.toLocaleString()} bytes</span
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -70,8 +70,9 @@ export async function GET({ url }: { url: URL }) {
|
||||
.prepare(
|
||||
`SELECT d.id, d.action, d.started_at, d.finished_at, d.triggered_by,
|
||||
d.script_id, d.script_version_id, d.prior_version_id, d.beta_id, d.rolled_back_by,
|
||||
s.name AS script_name, sv.version AS script_version,
|
||||
pv.version AS prior_version
|
||||
s.name AS script_name, s.kind AS script_kind,
|
||||
sv.version AS script_version, sv.release_version AS script_release_version,
|
||||
pv.version AS prior_version, pv.release_version AS prior_release_version
|
||||
FROM deployments d
|
||||
LEFT JOIN scripts s ON s.id = d.script_id
|
||||
LEFT JOIN script_versions sv ON sv.id = d.script_version_id
|
||||
@@ -92,8 +93,11 @@ export async function GET({ url }: { url: URL }) {
|
||||
beta_id: number | null;
|
||||
rolled_back_by: string | null;
|
||||
script_name: string | null;
|
||||
script_kind: string | null;
|
||||
script_version: number | null;
|
||||
script_release_version: string | null;
|
||||
prior_version: number | null;
|
||||
prior_release_version: string | null;
|
||||
}>;
|
||||
|
||||
const ids = deployments.map((deployment) => deployment.id);
|
||||
@@ -126,8 +130,9 @@ export async function GET({ url }: { url: URL }) {
|
||||
|
||||
const scriptRows = db()
|
||||
.prepare(
|
||||
`SELECT ds.deployment_id, ds.script_id, s.name AS script_name,
|
||||
sv.version AS script_version, pv.version AS prior_version
|
||||
`SELECT ds.deployment_id, ds.script_id, s.name AS script_name, ds.kind AS script_kind,
|
||||
sv.version AS script_version, sv.release_version AS script_release_version,
|
||||
pv.version AS prior_version, pv.release_version AS prior_release_version
|
||||
FROM deployment_scripts ds
|
||||
LEFT JOIN scripts s ON s.id = ds.script_id
|
||||
LEFT JOIN script_versions sv ON sv.id = ds.script_version_id
|
||||
@@ -138,8 +143,11 @@ export async function GET({ url }: { url: URL }) {
|
||||
deployment_id: string;
|
||||
script_id: number;
|
||||
script_name: string | null;
|
||||
script_kind: string | null;
|
||||
script_version: number | null;
|
||||
script_release_version: string | null;
|
||||
prior_version: number | null;
|
||||
prior_release_version: string | null;
|
||||
}>;
|
||||
const scriptsByDep = new Map<string, typeof scriptRows>();
|
||||
for (const s of scriptRows) {
|
||||
|
||||
@@ -27,9 +27,9 @@ export async function GET({ params }: { params: { id: string } }) {
|
||||
|
||||
const scripts = db()
|
||||
.prepare(
|
||||
`SELECT ds.script_id, s.name AS script_name, s.target_rel_path,
|
||||
ds.script_version_id, sv.version AS script_version,
|
||||
ds.prior_version_id, pv.version AS prior_version
|
||||
`SELECT ds.script_id, s.name AS script_name, s.target_rel_path, ds.kind AS script_kind,
|
||||
ds.script_version_id, sv.version AS script_version, sv.release_version AS script_release_version,
|
||||
ds.prior_version_id, pv.version AS prior_version, pv.release_version AS prior_release_version
|
||||
FROM deployment_scripts ds
|
||||
LEFT JOIN scripts s ON s.id = ds.script_id
|
||||
LEFT JOIN script_versions sv ON sv.id = ds.script_version_id
|
||||
|
||||
@@ -31,9 +31,9 @@ export async function GET({ params, url }: { params: { id: string }; url?: URL }
|
||||
|
||||
const scripts = db()
|
||||
.prepare(
|
||||
`SELECT ds.script_id, s.name AS script_name, s.target_rel_path,
|
||||
ds.script_version_id, sv.version AS script_version,
|
||||
ds.prior_version_id, pv.version AS prior_version
|
||||
`SELECT ds.script_id, s.name AS script_name, s.target_rel_path, ds.kind AS script_kind,
|
||||
ds.script_version_id, sv.version AS script_version, sv.release_version AS script_release_version,
|
||||
ds.prior_version_id, pv.version AS prior_version, pv.release_version AS prior_release_version
|
||||
FROM deployment_scripts ds
|
||||
LEFT JOIN scripts s ON s.id = ds.script_id
|
||||
LEFT JOIN script_versions sv ON sv.id = ds.script_version_id
|
||||
|
||||
Reference in New Issue
Block a user