updates: allow confirmed unversioned repo updates
This commit is contained in:
parent
c611790370
commit
d1a5d3b832
@ -22,6 +22,12 @@ install.
|
||||
If the latest version is unsafe, the UI shows the safe target as the actionable
|
||||
version and lists why the absolute latest is not selected.
|
||||
|
||||
Updates to or from an unversioned install or target remain available so an admin
|
||||
can recover directly from the repository. They are treated as manual repo
|
||||
updates: the action button stays enabled, but the UI shows warnings and the
|
||||
confirmation modal states that version ordering, changelog range, and rollback
|
||||
safety cannot be verified from metadata.
|
||||
|
||||
## Metadata
|
||||
|
||||
The updater looks for these files on the source branch:
|
||||
|
||||
@ -4,6 +4,7 @@ const { spawnSync } = require("child_process");
|
||||
const { getSetting } = require("./settings");
|
||||
const { scanPluginDirectories, getPlugins } = require("./plugins");
|
||||
const {
|
||||
parseSemver,
|
||||
compareSemver,
|
||||
findSafeTarget,
|
||||
collectChangelogRange,
|
||||
@ -123,6 +124,10 @@ function manifestVersions(manifest, fallbackVersion) {
|
||||
return normalizeVersions(entries);
|
||||
}
|
||||
|
||||
function manifestRawVersion(manifest, fallbackVersion) {
|
||||
return String(manifest?.version || fallbackVersion || "").trim();
|
||||
}
|
||||
|
||||
function coreManifest(ref) {
|
||||
return readJsonGitFile(ref, "update-manifest.json") ||
|
||||
readJsonGitFile(ref, "lumi.manifest.json") ||
|
||||
@ -154,10 +159,30 @@ function snapshotAvailability(kind, id = null) {
|
||||
|
||||
function buildStatus({ kind, id, name, currentVersion, manifest, changelog, sourceBranch, channel }) {
|
||||
const versions = manifestVersions(manifest, manifest?.version);
|
||||
const rawVersion = manifestRawVersion(manifest);
|
||||
const targetVersion = rawVersion || latestEntry(versions)?.version || "";
|
||||
const currentIsVersioned = Boolean(parseSemver(currentVersion));
|
||||
const targetIsVersioned = Boolean(parseSemver(targetVersion));
|
||||
const unversionedUpdate = !currentIsVersioned || !targetIsVersioned;
|
||||
const latest = latestEntry(versions);
|
||||
const targetResult = findSafeTarget(currentVersion, versions);
|
||||
const unversionedTarget = {
|
||||
...(latest || manifest || {}),
|
||||
version: targetVersion || "unversioned",
|
||||
rollback_safe: false,
|
||||
unversioned: true
|
||||
};
|
||||
const targetResult = unversionedUpdate
|
||||
? {
|
||||
target: unversionedTarget,
|
||||
latest: latest || unversionedTarget,
|
||||
blocked: false,
|
||||
warning: "This update involves an unversioned source or target. It is available as a manual repo update, but version ordering, changelog range, and rollback safety cannot be verified."
|
||||
}
|
||||
: findSafeTarget(currentVersion, versions);
|
||||
const target = targetResult.target;
|
||||
const range = target ? collectChangelogRange(currentVersion, target.version, changelog.length ? changelog : versions) : [];
|
||||
const range = target && !unversionedUpdate
|
||||
? collectChangelogRange(currentVersion, target.version, changelog.length ? changelog : versions)
|
||||
: changelog;
|
||||
const warnings = [];
|
||||
const dangers = [];
|
||||
if (!changelog.length) warnings.push("Changelog metadata is missing.");
|
||||
@ -169,9 +194,11 @@ function buildStatus({ kind, id, name, currentVersion, manifest, changelog, sour
|
||||
id: id || kind,
|
||||
name: name || manifest?.name || id || "Lumi core",
|
||||
current_version: currentVersion,
|
||||
latest_available_version: latest?.version || currentVersion,
|
||||
latest_available_version: latest?.version || rawVersion || currentVersion,
|
||||
safe_target_version: target?.version || null,
|
||||
update_available: Boolean(target && compareSemver(target.version, currentVersion) > 0),
|
||||
update_available: Boolean(
|
||||
target && (unversionedUpdate || compareSemver(target.version, currentVersion) > 0)
|
||||
),
|
||||
blocked: Boolean(targetResult.blocked),
|
||||
blocked_reason: targetResult.reason || null,
|
||||
source_branch: sourceBranch,
|
||||
@ -184,10 +211,12 @@ function buildStatus({ kind, id, name, currentVersion, manifest, changelog, sour
|
||||
size_delta_label: target?.size || manifest?.size ? String(target.size || manifest.size) : "unknown",
|
||||
warnings,
|
||||
dangers,
|
||||
unversioned_update: unversionedUpdate,
|
||||
requires_manual_confirmation: unversionedUpdate,
|
||||
requirements: target?.requirements || manifest?.requirements || [],
|
||||
migration_notes: target?.migration_notes || manifest?.migration_notes || "",
|
||||
rollback_safe: target?.rollback_safe !== false,
|
||||
major_crossing: target ? target.version.split(".")[0] !== String(currentVersion).split(".")[0] : false,
|
||||
major_crossing: target && !unversionedUpdate ? target.version.split(".")[0] !== String(currentVersion).split(".")[0] : false,
|
||||
snapshot: snapshotAvailability(kind === "plugin" ? "plugin" : "bot", id),
|
||||
raw_target: target || null
|
||||
};
|
||||
|
||||
@ -10,6 +10,13 @@
|
||||
const badgeClass = (item) => item?.blocked ? "danger" : item?.update_available ? "warning" : "success";
|
||||
const badgeText = (item) => item?.blocked ? "Blocked" : item?.update_available ? "Update available" : "Current";
|
||||
const changelogItems = (item) => Array.isArray(item?.changelog_range) ? item.changelog_range : [];
|
||||
const applyLabel = (item) => item?.unversioned_update ? "Apply manual repo update" : "Apply safe target";
|
||||
const applyConfirmText = (item, label) => {
|
||||
const base = `${label}: create a snapshot, write a recovery marker, apply ${item?.safe_target_version || "the selected repo target"}, and verify before finishing.`;
|
||||
if (!item?.unversioned_update) return base;
|
||||
const warnings = (item.warnings || []).join(" ");
|
||||
return `${base} Warning: this update is to or from an unversioned install/target, so Lumi cannot verify version ordering, changelog range, or rollback safety. ${warnings}`;
|
||||
};
|
||||
%>
|
||||
|
||||
<section class="card">
|
||||
@ -87,6 +94,7 @@
|
||||
</div>
|
||||
<p><%= core.version_description %></p>
|
||||
<% if (core.warnings?.length) { %><div class="callout"><strong>Warnings</strong><ul><% core.warnings.forEach((item) => { %><li><%= item %></li><% }) %></ul></div><% } %>
|
||||
<% if (core.unversioned_update) { %><div class="callout danger"><strong>Manual confirmation required</strong><p>This core update is to or from an unversioned state. It remains available, but rollback safety and version ordering cannot be verified from metadata.</p></div><% } %>
|
||||
<% if (core.dangers?.length) { %><div class="callout danger"><strong>Dangers</strong><ul><% core.dangers.forEach((item) => { %><li><%= item %></li><% }) %></ul></div><% } %>
|
||||
<% if (core.requirements?.length) { %><div class="callout"><strong>Requirements</strong><ul><% core.requirements.forEach((item) => { %><li><%= item %></li><% }) %></ul></div><% } %>
|
||||
<% if (core.migration_notes) { %><div class="callout"><strong>Migration notes</strong><p><%= core.migration_notes %></p></div><% } %>
|
||||
@ -107,9 +115,9 @@
|
||||
<input type="hidden" name="source" value="<%= selectedSource %>" />
|
||||
<button class="button subtle" type="submit">Check core</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/updates/core/apply" data-update-action data-confirm-mode="modal" data-confirm-title="Apply core update" data-confirm-text="Create a snapshot, write a recovery marker, apply the safe target, verify, then restart Lumi after a 5 second notice." data-confirm-label="Apply update">
|
||||
<form method="post" action="/admin/updates/core/apply" data-update-action data-confirm-mode="modal" data-confirm-title="<%= core.unversioned_update ? "Apply unversioned core update" : "Apply core update" %>" data-confirm-text="<%= applyConfirmText(core, "Core update") %>" data-confirm-label="<%= applyLabel(core) %>">
|
||||
<input type="hidden" name="source" value="<%= selectedSource %>" />
|
||||
<button class="button" type="submit" <%= core.blocked || !core.update_available ? "disabled" : "" %>>Apply safe target</button>
|
||||
<button class="button" type="submit" <%= core.blocked || !core.update_available ? "disabled" : "" %>><%= applyLabel(core) %></button>
|
||||
</form>
|
||||
<% if (core.snapshot.available) { %>
|
||||
<form method="post" action="/admin/updates/core/revert" data-update-action data-confirm-mode="modal" data-confirm-title="Revert core snapshot" data-confirm-text="Revert only the previous core version snapshot. Major rollback is blocked unless metadata marks it rollback safe." data-confirm-label="Revert core">
|
||||
@ -167,6 +175,7 @@
|
||||
</div>
|
||||
<p><%= plugin.version_description %></p>
|
||||
<% if (plugin.warnings?.length) { %><div class="callout"><strong>Warnings</strong><ul><% plugin.warnings.forEach((item) => { %><li><%= item %></li><% }) %></ul></div><% } %>
|
||||
<% if (plugin.unversioned_update) { %><div class="callout danger"><strong>Manual confirmation required</strong><p>This plugin update is to or from an unversioned state. It remains available, but rollback safety and version ordering cannot be verified from metadata.</p></div><% } %>
|
||||
<% if (plugin.dangers?.length) { %><div class="callout danger"><strong>Dangers</strong><ul><% plugin.dangers.forEach((item) => { %><li><%= item %></li><% }) %></ul></div><% } %>
|
||||
<% if (plugin.migration_notes) { %><div class="callout"><strong>Migration notes</strong><p><%= plugin.migration_notes %></p></div><% } %>
|
||||
<h3>Changelog to target</h3>
|
||||
@ -184,9 +193,9 @@
|
||||
<input type="hidden" name="source" value="<%= selectedSource %>" />
|
||||
<button class="button subtle" type="submit">Check plugin</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/updates/plugins/<%= plugin.id %>/apply" data-update-action data-confirm-mode="modal" data-confirm-title="Apply plugin update" data-confirm-text="Create a plugin snapshot, write a recovery marker, apply the safe target, and verify this plugin." data-confirm-label="Apply plugin">
|
||||
<form method="post" action="/admin/updates/plugins/<%= plugin.id %>/apply" data-update-action data-confirm-mode="modal" data-confirm-title="<%= plugin.unversioned_update ? `Apply unversioned ${plugin.name} update` : "Apply plugin update" %>" data-confirm-text="<%= applyConfirmText(plugin, `${plugin.name} update`) %>" data-confirm-label="<%= applyLabel(plugin) %>">
|
||||
<input type="hidden" name="source" value="<%= selectedSource %>" />
|
||||
<button class="button" type="submit" <%= plugin.blocked || !plugin.update_available ? "disabled" : "" %>>Apply safe target</button>
|
||||
<button class="button" type="submit" <%= plugin.blocked || !plugin.update_available ? "disabled" : "" %>><%= applyLabel(plugin) %></button>
|
||||
</form>
|
||||
<% if (plugin.snapshot.available) { %>
|
||||
<form method="post" action="/admin/updates/plugins/<%= plugin.id %>/revert" data-update-action data-confirm-mode="modal" data-confirm-title="Revert plugin snapshot" data-confirm-text="Revert only the previous plugin version snapshot. Major rollback is blocked unless metadata marks it rollback safe." data-confirm-label="Revert plugin">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user