feat(release): bound durable run retention

This commit is contained in:
2026-07-22 20:16:19 +02:00
parent ddee5c00dc
commit 4edbc11fe5
5 changed files with 409 additions and 57 deletions

View File

@@ -650,6 +650,9 @@
<option value="">No saved run selected</option>
</select>
</div>
<div class="button-row">
<button class="secondary" id="loadOlderReleaseRuns" disabled>Load older</button>
</div>
</div>
<div class="button-row">
<button id="createReleaseRun">Create Run from Selection</button>
@@ -884,6 +887,7 @@
commitPrepare: document.getElementById("commitPrepare"),
buildSelectedPlan: document.getElementById("buildSelectedPlan"),
releaseRun: document.getElementById("releaseRun"),
loadOlderReleaseRuns: document.getElementById("loadOlderReleaseRuns"),
runStatus: document.getElementById("runStatus"),
runOutput: document.getElementById("runOutput"),
createReleaseRun: document.getElementById("createReleaseRun"),
@@ -907,6 +911,7 @@
manualRepo: "",
currentRun: null,
runs: [],
runNextCursor: null,
runStoreAvailable: true,
};
const pendingReleaseRunKey = "govoplanReleaseConsolePendingRunCreate";
@@ -928,6 +933,7 @@
elements.createReleaseRun.addEventListener("click", createReleaseRun);
elements.resumeReleaseRun.addEventListener("click", resumeReleaseRun);
elements.releaseRun.addEventListener("change", () => loadReleaseRun(elements.releaseRun.value));
elements.loadOlderReleaseRuns.addEventListener("click", loadOlderReleaseRuns);
elements.previewReleaseTags.addEventListener("click", () => releaseSelectedTags({ apply: false, push: true }));
elements.createReleaseTags.addEventListener("click", () => releaseSelectedTags({ apply: true, push: false }));
elements.publishReleaseTags.addEventListener("click", () => releaseSelectedTags({ apply: true, push: true }));
@@ -943,9 +949,11 @@
});
function api(path) {
const query = new URLSearchParams();
const separator = path.indexOf("?");
const basePath = separator === -1 ? path : path.slice(0, separator);
const query = new URLSearchParams(separator === -1 ? "" : path.slice(separator + 1));
if (elements.channel.value.trim()) query.set("channel", elements.channel.value.trim());
if (path === "/api/selective-plan") {
if (basePath === "/api/selective-plan") {
const selected = selectedRepoVersions();
const repoNames = Object.keys(selected);
if (repoNames.length) {
@@ -957,9 +965,9 @@
else query.set("public_catalog", "false");
if (elements.online.checked) query.set("remote_tags", "true");
if (elements.migrations.checked) query.set("include_migrations", "true");
if (elements.website.checked && path === "/api/dashboard") query.set("include_website", "true");
if (elements.website.checked && basePath === "/api/dashboard") query.set("include_website", "true");
const suffix = query.toString() ? `?${query}` : "";
return fetch(`${path}${suffix}`, {
return fetch(`${basePath}${suffix}`, {
headers: token ? { "X-Release-Console-Token": token } : {},
}).then((response) => {
if (!response.ok) {
@@ -1005,7 +1013,7 @@
if (runsResult.error) {
renderReleaseRunStoreUnavailable(runsResult.error);
} else {
renderReleaseRunList(runsResult.data?.runs || []);
renderReleaseRunList(runsResult.data?.runs || [], { nextCursor: runsResult.data?.next_cursor || null });
const pendingCreateResult = await recoverPendingReleaseRun();
const pendingCommandResult = await recoverPendingRunCommands();
const recoveryAttempted = pendingCreateResult !== null || pendingCommandResult !== null;
@@ -1039,11 +1047,16 @@
renderCatalog(data.catalog, data.target_version);
}
function renderReleaseRunList(runs) {
function renderReleaseRunList(runs, options = {}) {
releaseState.runStoreAvailable = true;
releaseState.runs = Array.isArray(runs) ? runs : [];
const incoming = Array.isArray(runs) ? runs : [];
releaseState.runs = options.append
? [...new Map([...releaseState.runs, ...incoming].map((run) => [run.run_id, run])).values()]
: incoming;
releaseState.runNextCursor = options.nextCursor || null;
elements.releaseRun.disabled = false;
elements.createReleaseRun.disabled = false;
elements.loadOlderReleaseRuns.disabled = !releaseState.runNextCursor;
const selectedId = releaseState.currentRun?.run_id || "";
const options = releaseState.runs.map((run) => {
const repositories = run.repo_versions ? Object.keys(run.repo_versions).length : 0;
@@ -1060,9 +1073,11 @@
releaseState.runStoreAvailable = false;
releaseState.runs = [];
releaseState.currentRun = null;
releaseState.runNextCursor = null;
elements.releaseRun.innerHTML = `<option value="">Run storage unavailable</option>`;
elements.releaseRun.disabled = true;
elements.createReleaseRun.disabled = true;
elements.loadOlderReleaseRuns.disabled = true;
elements.resumeReleaseRun.disabled = true;
elements.runStatus.textContent = "unavailable";
elements.runOutput.innerHTML = `<div class="action"><h3>${pill("unavailable", "block")} Durable run storage cannot be read</h3><p>${escapeHtml(error.message)}</p><p class="action-meta">Dashboard and release previews remain available. Correct the private state-directory problem, then refresh.</p></div>`;
@@ -1157,13 +1172,31 @@
async function refreshReleaseRunListAfterCreate(run) {
try {
const runs = await api("/api/release-runs");
renderReleaseRunList(runs.runs || []);
renderReleaseRunList(runs.runs || [], { nextCursor: runs.next_cursor || null });
elements.releaseRun.value = run.run_id;
} catch (error) {
elements.runOutput.insertAdjacentHTML("afterbegin", `<div class="action"><h3>${pill("saved", "warn")} Run saved; list refresh unavailable</h3><p>${escapeHtml(error.message)}</p><p class="action-meta">The persisted run remains selected and can be reloaded after refresh.</p></div>`);
}
}
async function loadOlderReleaseRuns() {
const cursor = releaseState.runNextCursor;
if (!cursor) return;
setBusy([elements.loadOlderReleaseRuns], true);
try {
const page = await api(`/api/release-runs?cursor=${encodeURIComponent(cursor)}`);
renderReleaseRunList(page.runs || [], {
append: true,
nextCursor: page.next_cursor || null,
});
} catch (error) {
elements.runOutput.insertAdjacentHTML("afterbegin", `<div class="action"><h3>${pill("unavailable", "warn")} Older runs could not be loaded</h3><p>${escapeHtml(error.message)}</p></div>`);
} finally {
setBusy([elements.loadOlderReleaseRuns], false);
elements.loadOlderReleaseRuns.disabled = !releaseState.runNextCursor;
}
}
async function loadReleaseRun(runId, options = {}) {
if (!runId) {
releaseState.currentRun = null;