Execute receipt-bound durable release runs
This commit is contained in:
@@ -608,7 +608,7 @@
|
||||
</div>
|
||||
<div class="units-toolbar">
|
||||
<button id="buildSelectedPlan">Build Plan</button>
|
||||
<button id="generateSelectedCandidate">Generate Candidate</button>
|
||||
<button id="generateSelectedCandidate" disabled data-release-disabled="true" title="Use the receipt-bound generator in Durable Run State.">Generate Candidate</button>
|
||||
<button class="secondary" id="selectUnpushedUnits" title="Select repositories with committed branch changes to push.">Select Unpushed</button>
|
||||
<button class="secondary" id="selectChangedUnits" title="Select repositories with dirty/untracked paths, missing HEAD, or unpushed commits.">Select Dirty/Changed</button>
|
||||
<button class="secondary" id="selectUnreleasedUnits">Select Unreleased</button>
|
||||
@@ -642,7 +642,7 @@
|
||||
<small id="runStatus">no run selected</small>
|
||||
</div>
|
||||
<div class="details">
|
||||
<p class="hint"><strong>State tracking only.</strong> A run freezes the selected inputs and plan, guides recovery, and records bounded state events. It does not execute or confirm tags, signing, publication, or catalog changes.</p>
|
||||
<p class="hint"><strong>Durable execution.</strong> A run freezes the selected inputs and plan, claims each supported executor before its effect, and keeps explicit confirmation and reconciliation boundaries. Unsupported dependency-ordering or version-metadata steps stay visible and disabled.</p>
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label for="releaseRun">Saved run</label>
|
||||
@@ -678,7 +678,7 @@
|
||||
<small id="pushStatus"></small>
|
||||
</div>
|
||||
<div class="details">
|
||||
<p class="hint">Sync selected fetches remote refs and tags only. It does not merge, rebase, commit, tag, or push.</p>
|
||||
<p class="hint">Repository sync and push are preview-only until they have a receipt-bound durable maintenance executor.</p>
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label for="pushRemote">Remote</label>
|
||||
@@ -691,9 +691,9 @@
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="secondary" id="previewRepoSync">Preview Sync</button>
|
||||
<button id="syncRepos">Sync Selected</button>
|
||||
<button id="syncRepos" disabled data-release-disabled="true" title="Durable maintenance executor required">Sync Selected</button>
|
||||
<button class="secondary" id="previewRepoPush">Preview Push</button>
|
||||
<button class="danger" id="pushRepos">Push Selected</button>
|
||||
<button class="danger" id="pushRepos" disabled data-release-disabled="true" title="Durable maintenance executor required">Push Selected</button>
|
||||
</div>
|
||||
<div class="actions" id="pushOutput"></div>
|
||||
</div>
|
||||
@@ -705,7 +705,7 @@
|
||||
<small id="prepareStatus"></small>
|
||||
</div>
|
||||
<div class="details">
|
||||
<p class="hint">Prepare commits selected dirty worktrees only. It does not tag or push.</p>
|
||||
<p class="hint">Preview preparation only. Commit mutation is disabled outside a content-bound durable executor; commit and review dirty or version-changing work outside this console, then build a new run.</p>
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label for="prepareMessage">Commit message</label>
|
||||
@@ -713,12 +713,12 @@
|
||||
</div>
|
||||
<div>
|
||||
<label for="prepareConfirmText">Confirm</label>
|
||||
<input id="prepareConfirmText" type="text" placeholder="COMMIT" />
|
||||
<input id="prepareConfirmText" type="text" placeholder="durable commit executor pending" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="secondary" id="previewPrepare">Preview Prepare</button>
|
||||
<button class="danger" id="commitPrepare">Commit Selected</button>
|
||||
<button class="danger" id="commitPrepare" data-release-disabled="true" disabled title="No content-bound durable commit executor is available yet.">Commit Selected</button>
|
||||
</div>
|
||||
<div class="actions" id="prepareOutput"></div>
|
||||
</div>
|
||||
@@ -730,7 +730,7 @@
|
||||
<small><span id="tagStatus">idle</span> · plan <span id="planStatus">idle</span></small>
|
||||
</div>
|
||||
<div class="details">
|
||||
<p class="hint">Creates an annotated source tag at the selected clean HEAD. Publish pushes the branch and tag atomically; existing release tags are never moved.</p>
|
||||
<p class="hint">Preview is available here. Tag creation and publication use the matching durable run steps, bound to the frozen HEAD, branch, clean worktree, and registered origin.</p>
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<label for="tagMessage">Tag message</label>
|
||||
@@ -738,13 +738,13 @@
|
||||
</div>
|
||||
<div>
|
||||
<label for="tagConfirmText">Confirm</label>
|
||||
<input id="tagConfirmText" type="text" placeholder="TAG or PUBLISH" />
|
||||
<input id="tagConfirmText" type="text" placeholder="use durable run controls" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="secondary" id="previewReleaseTags">Preview Tag + Publish</button>
|
||||
<button id="createReleaseTags">Create Tags</button>
|
||||
<button class="danger" id="publishReleaseTags">Publish Tags</button>
|
||||
<button id="createReleaseTags" data-release-disabled="true" disabled title="Use the receipt-bound tag step in Durable Run State.">Create Tags</button>
|
||||
<button class="danger" id="publishReleaseTags" data-release-disabled="true" disabled title="Use the receipt-bound push step in Durable Run State.">Publish Tags</button>
|
||||
</div>
|
||||
<div class="actions" id="tagOutput"></div>
|
||||
</div>
|
||||
@@ -764,18 +764,18 @@
|
||||
</div>
|
||||
<div>
|
||||
<label for="candidateDir">Candidate dir</label>
|
||||
<input id="candidateDir" type="text" placeholder="runtime/release-candidates/..." />
|
||||
<input id="candidateDir" type="text" placeholder="private state/release-candidates/candidate-..." />
|
||||
</div>
|
||||
<div>
|
||||
<label for="confirmText">Confirm</label>
|
||||
<input id="confirmText" type="text" placeholder="APPLY or PUSH" />
|
||||
<input id="confirmText" type="text" placeholder="use durable run controls" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button id="generateCandidate">Generate</button>
|
||||
<button id="generateCandidate" disabled data-release-disabled="true" title="Use the receipt-bound generator in Durable Run State.">Generate</button>
|
||||
<button class="secondary" id="previewPublish">Preview</button>
|
||||
<button class="secondary" id="applyPublish">Apply + Website Tag</button>
|
||||
<button class="danger" id="pushPublish">Push Website Release</button>
|
||||
<button class="secondary" id="applyPublish" data-release-disabled="true" disabled title="Use the receipt-bound publication step in Durable Run State.">Apply + Website Tag</button>
|
||||
<button class="danger" id="pushPublish" data-release-disabled="true" disabled title="Use the receipt-bound publication step in Durable Run State.">Push Website Release</button>
|
||||
</div>
|
||||
<div class="actions" id="workflowOutput"></div>
|
||||
</div>
|
||||
@@ -1047,23 +1047,23 @@
|
||||
renderCatalog(data.catalog, data.target_version);
|
||||
}
|
||||
|
||||
function renderReleaseRunList(runs, options = {}) {
|
||||
function renderReleaseRunList(runs, renderOptions = {}) {
|
||||
releaseState.runStoreAvailable = true;
|
||||
const incoming = Array.isArray(runs) ? runs : [];
|
||||
releaseState.runs = options.append
|
||||
releaseState.runs = renderOptions.append
|
||||
? [...new Map([...releaseState.runs, ...incoming].map((run) => [run.run_id, run])).values()]
|
||||
: incoming;
|
||||
releaseState.runNextCursor = options.nextCursor || null;
|
||||
releaseState.runNextCursor = renderOptions.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 runOptions = releaseState.runs.map((run) => {
|
||||
const repositories = run.repo_versions ? Object.keys(run.repo_versions).length : 0;
|
||||
const label = `${run.run_id} · ${run.status || "unknown"} · ${repositories} repos`;
|
||||
return `<option value="${escapeAttr(run.run_id)}" ${run.run_id === selectedId ? "selected" : ""}>${escapeHtml(label)}</option>`;
|
||||
}).join("");
|
||||
elements.releaseRun.innerHTML = `<option value="">No saved run selected</option>${options}`;
|
||||
elements.releaseRun.innerHTML = `<option value="">No saved run selected</option>${runOptions}`;
|
||||
if (selectedId && releaseState.runs.some((run) => run.run_id === selectedId)) {
|
||||
elements.releaseRun.value = selectedId;
|
||||
}
|
||||
@@ -1252,8 +1252,15 @@
|
||||
const stepHtml = steps.map((step) => {
|
||||
const kind = step.state === "succeeded" ? "ok" : ["failed", "interrupted"].includes(step.state) ? "block" : step.available ? "ok" : "warn";
|
||||
const retryReason = step.retry_available ? "Prepare this known failed or read-only interrupted step for an explicit retry." : (step.disabled_reason || "Retry is unavailable for this state.");
|
||||
const executor = step.executor || {};
|
||||
const confirmation = executor.confirmation || "";
|
||||
const executeReason = executor.available ? (step.disabled_reason || "Execute this frozen plan step.") : "No bounded executor is available for this frozen step.";
|
||||
const executorHtml = executor.available ? `<div class="form-grid" data-run-execute-step="${escapeAttr(step.id)}" data-run-execute-confirmation="${escapeAttr(confirmation)}" data-run-execute-available="${step.available ? "true" : "false"}">
|
||||
${confirmation ? `<div><label>Confirmation</label><input type="text" data-run-execute-confirm placeholder="Type ${escapeAttr(confirmation)}" autocomplete="off" /></div>` : ""}
|
||||
<div class="button-row">${executor.kind === "catalog_publish" ? `<button class="secondary" data-run-preview-submit ${step.available ? "" : "disabled"}>Preview receipt-bound candidate</button>` : ""}<button data-run-execute-submit ${step.available && !confirmation ? "" : "disabled"} title="${escapeAttr(executeReason)}">Execute Step</button></div>
|
||||
</div>` : `<p class="action-meta">No durable executor: ${escapeHtml(step.status || "unsupported")}</p>`;
|
||||
const reconciliationHtml = step.reconciliation_required ? `<div class="form-grid" data-run-reconcile-step="${escapeAttr(step.id)}">
|
||||
<div><label>Observed external effect</label><select data-run-reconcile-outcome><option value="">Choose verified outcome</option><option value="effect_absent">Effect is absent — retry is safe</option><option value="effect_succeeded">Effect succeeded — advance the run</option><option value="unresolved">Still unresolved — keep blocked</option></select></div>
|
||||
<div><label>Observed external effect</label><select data-run-reconcile-outcome><option value="">Choose verified outcome</option><option value="effect_absent">Effect is absent — retry is safe</option><option value="effect_succeeded">Effect succeeded — advance the run after server verification</option><option value="unresolved">Still unresolved — keep blocked</option></select></div>
|
||||
<div><label>Confirmation</label><input type="text" data-run-reconcile-confirm placeholder="Type RECONCILE" autocomplete="off" /></div>
|
||||
<div class="button-row"><button class="secondary" data-run-reconcile-submit disabled title="Verify local and remote state independently, choose the observed outcome, and type RECONCILE.">Record Reconciliation</button></div>
|
||||
</div>` : "";
|
||||
@@ -1261,6 +1268,8 @@
|
||||
<div class="stage-title"><h3>${escapeHtml(`${step.order}. ${step.title || step.id}`)}</h3>${pill(step.state, kind)}</div>
|
||||
<p>${escapeHtml(step.detail || "")}</p>
|
||||
${step.result_code ? `<p class="action-meta">Result code: ${escapeHtml(step.result_code)}</p>` : ""}
|
||||
${releaseReceiptHtml(step.result_receipt)}
|
||||
${executorHtml}
|
||||
<div class="button-row"><button class="secondary" data-run-retry-step="${escapeAttr(step.id)}" ${step.retry_available ? "" : "disabled"} title="${escapeAttr(retryReason)}">Prepare Retry</button></div>
|
||||
${reconciliationHtml}
|
||||
</div>`;
|
||||
@@ -1268,7 +1277,7 @@
|
||||
const eventHtml = events.length ? `<div class="action"><h3>Recent bounded state events</h3>${events.map((event) => `<p class="action-meta">${escapeHtml(`${event.at} · ${event.type}${event.step_id ? ` · ${event.step_id}` : ""}${event.result_code ? ` · ${event.result_code}` : ""}`)}</p>`).join("")}</div>` : "";
|
||||
elements.runStatus.textContent = state.status || "unknown";
|
||||
elements.resumeReleaseRun.disabled = state.status !== "running";
|
||||
elements.runOutput.innerHTML = `<div class="action"><h3>${pill(state.status || "unknown", state.status === "blocked" ? "block" : state.status === "completed" ? "ok" : "warn")} ${escapeHtml(run.run_id)}</h3><p>Frozen plan ${escapeHtml((run.immutable?.digest || "").slice(0, 16))}… · updated ${escapeHtml(run.updated_at || "-")}</p><p class="action-meta">Executor controls remain separate and keep their existing confirmations.</p></div>${recommendationHtml}${stepHtml}${eventHtml}`;
|
||||
elements.runOutput.innerHTML = `<div class="action"><h3>${pill(state.status || "unknown", state.status === "blocked" ? "block" : state.status === "completed" ? "ok" : "warn")} ${escapeHtml(run.run_id)}</h3><p>Frozen plan ${escapeHtml((run.immutable?.digest || "").slice(0, 16))}… · updated ${escapeHtml(run.updated_at || "-")}</p><p class="action-meta">Every supported mutation is claimed durably before the existing narrow executor is called.</p></div>${recommendationHtml}${stepHtml}${eventHtml}`;
|
||||
for (const button of elements.runOutput.querySelectorAll("[data-run-retry-step]")) {
|
||||
button.addEventListener("click", () => retryReleaseRunStep(button.dataset.runRetryStep));
|
||||
}
|
||||
@@ -1283,6 +1292,78 @@
|
||||
confirmation.addEventListener("input", refresh);
|
||||
submit.addEventListener("click", () => reconcileReleaseRunStep(controls.dataset.runReconcileStep, outcome.value, confirmation.value.trim(), submit));
|
||||
}
|
||||
for (const controls of elements.runOutput.querySelectorAll("[data-run-execute-step]")) {
|
||||
const required = controls.dataset.runExecuteConfirmation || "";
|
||||
const available = controls.dataset.runExecuteAvailable === "true";
|
||||
const input = controls.querySelector("[data-run-execute-confirm]");
|
||||
const submit = controls.querySelector("[data-run-execute-submit]");
|
||||
const preview = controls.querySelector("[data-run-preview-submit]");
|
||||
if (input) input.addEventListener("input", () => {
|
||||
submit.disabled = !available || input.value.trim() !== required;
|
||||
});
|
||||
submit.addEventListener("click", () => executeReleaseRunStep(controls.dataset.runExecuteStep, required, submit));
|
||||
if (preview) preview.addEventListener("click", () => previewReleaseRunStep(controls.dataset.runExecuteStep, preview));
|
||||
}
|
||||
}
|
||||
|
||||
async function executeReleaseRunStep(stepId, confirmation, button) {
|
||||
const run = releaseState.currentRun;
|
||||
if (!run || !stepId) return;
|
||||
const commandKey = `execute:${run.run_id}:${stepId}:${confirmation || "NONE"}`;
|
||||
const commandBody = {
|
||||
confirm: confirmation,
|
||||
remote: "origin",
|
||||
signing_keys: elements.signingKey.value.trim() ? [elements.signingKey.value.trim()] : [],
|
||||
};
|
||||
const commandRequestId = inFlightRunCommandId(commandKey, "execute");
|
||||
setBusy([button], true);
|
||||
try {
|
||||
const executed = await postJson(`/api/release-runs/${encodeURIComponent(run.run_id)}/steps/${encodeURIComponent(stepId)}/execute`, {
|
||||
request_id: commandRequestId,
|
||||
...commandBody,
|
||||
});
|
||||
clearInFlightRunCommand(commandKey, commandRequestId);
|
||||
releaseState.currentRun = executed;
|
||||
renderReleaseRun(executed);
|
||||
const result = executed.execution_result || {};
|
||||
elements.runOutput.insertAdjacentHTML("afterbegin", `<div class="action"><h3>${pill(result.status || "recorded", result.status === "failed" ? "block" : "ok")} Executor result</h3><p>${escapeHtml(result.detail || result.result_code || "The bounded result was recorded.")}</p></div>`);
|
||||
} catch (error) {
|
||||
if (deterministicRunCommandError(error)) clearInFlightRunCommand(commandKey, commandRequestId);
|
||||
elements.runOutput.insertAdjacentHTML("afterbegin", `<div class="action"><h3>${pill("reconcile", "block")} Executor did not establish a safe result</h3><p>${escapeHtml(error.message)}</p></div>`);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function previewReleaseRunStep(stepId, button) {
|
||||
const run = releaseState.currentRun;
|
||||
if (!run || !stepId) return;
|
||||
setBusy([button], true);
|
||||
try {
|
||||
const result = await postJson(`/api/release-runs/${encodeURIComponent(run.run_id)}/steps/${encodeURIComponent(stepId)}/preview`, {
|
||||
remote: "origin",
|
||||
});
|
||||
elements.runOutput.insertAdjacentHTML("afterbegin", `<div class="action"><h3>${pill(result.status || "preview", result.status === "blocked" ? "block" : "ok")} Receipt-bound publication preview</h3><p>${escapeHtml((result.notes || []).join("; ") || "Preview completed without changing run state.")}</p></div>`);
|
||||
} catch (error) {
|
||||
elements.runOutput.insertAdjacentHTML("afterbegin", `<div class="action"><h3>${pill("blocked", "block")} Preview failed</h3><p>${escapeHtml(error.message)}</p></div>`);
|
||||
} finally {
|
||||
setBusy([button], false);
|
||||
}
|
||||
}
|
||||
|
||||
function releaseReceiptHtml(receipt) {
|
||||
if (!receipt || typeof receipt !== "object") return "";
|
||||
if (receipt.kind === "catalog_candidate") {
|
||||
return `<p class="action-meta">Candidate ${escapeHtml(receipt.candidate_id || "-")} · ${escapeHtml((receipt.catalog_sha256 || "").slice(0, 16))}…</p>`;
|
||||
}
|
||||
if (receipt.kind === "catalog_publication") {
|
||||
return `<p class="action-meta">Published ${escapeHtml(receipt.candidate_id || "-")} · commit ${escapeHtml((receipt.publication_commit_sha || "").slice(0, 12))} · tag ${escapeHtml(receipt.tag_name || "-")} (${escapeHtml((receipt.publication_tag_object_sha || "").slice(0, 12))}) · ${escapeHtml(receipt.remote || "origin")}/${escapeHtml(receipt.branch || "-")}</p>`;
|
||||
}
|
||||
if (receipt.kind === "repository_state") {
|
||||
const tag = receipt.tag_object ? ` · tag ${(receipt.tag_object || "").slice(0, 12)}` : "";
|
||||
return `<p class="action-meta">Bound ${escapeHtml(receipt.repo || "-")} · HEAD ${escapeHtml((receipt.head || "").slice(0, 12))}${escapeHtml(tag)} · ${receipt.worktree_clean ? "clean" : "dirty"}</p>`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function resumeReleaseRun() {
|
||||
@@ -1346,7 +1427,17 @@
|
||||
function readInFlightRunCommands() {
|
||||
try {
|
||||
const commands = JSON.parse(sessionStorage.getItem(pendingRunCommandsKey) || "{}");
|
||||
return commands && typeof commands === "object" && !Array.isArray(commands) ? commands : {};
|
||||
if (!commands || typeof commands !== "object" || Array.isArray(commands)) return {};
|
||||
const normalized = {};
|
||||
for (const [commandKey, savedCommand] of Object.entries(commands)) {
|
||||
const requestIdToReplay = typeof savedCommand === "string" ? savedCommand : savedCommand?.request_id;
|
||||
if (typeof requestIdToReplay === "string") normalized[commandKey] = requestIdToReplay;
|
||||
}
|
||||
if (JSON.stringify(normalized) !== JSON.stringify(commands)) {
|
||||
if (Object.keys(normalized).length) sessionStorage.setItem(pendingRunCommandsKey, JSON.stringify(normalized));
|
||||
else sessionStorage.removeItem(pendingRunCommandsKey);
|
||||
}
|
||||
return normalized;
|
||||
} catch (_error) {
|
||||
sessionStorage.removeItem(pendingRunCommandsKey);
|
||||
return {};
|
||||
@@ -1387,6 +1478,19 @@
|
||||
body: { request_id: requestIdToReplay, outcome, confirm: "RECONCILE" },
|
||||
};
|
||||
}
|
||||
if (commandKey.startsWith("execute:")) {
|
||||
const remainder = commandKey.slice("execute:".length);
|
||||
const runSeparator = remainder.indexOf(":");
|
||||
const confirmationSeparator = remainder.lastIndexOf(":");
|
||||
const runId = remainder.slice(0, runSeparator);
|
||||
const stepId = remainder.slice(runSeparator + 1, confirmationSeparator);
|
||||
const confirmation = remainder.slice(confirmationSeparator + 1);
|
||||
if (runSeparator < 1 || confirmationSeparator <= runSeparator + 1 || !stepId) return null;
|
||||
return {
|
||||
path: `/api/release-runs/${encodeURIComponent(runId)}/steps/${encodeURIComponent(stepId)}/execute`,
|
||||
body: { request_id: requestIdToReplay, confirm: confirmation === "NONE" ? "" : confirmation },
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1398,7 +1502,12 @@
|
||||
let recoveredRun = null;
|
||||
let pendingError = null;
|
||||
let rejectedError = null;
|
||||
for (const [commandKey, requestIdToReplay] of entries) {
|
||||
for (const [commandKey, savedCommand] of entries) {
|
||||
const requestIdToReplay = typeof savedCommand === "string" ? savedCommand : savedCommand?.request_id;
|
||||
if (typeof requestIdToReplay !== "string") {
|
||||
delete commands[commandKey];
|
||||
continue;
|
||||
}
|
||||
const command = pendingRunCommandRequest(commandKey, requestIdToReplay);
|
||||
if (!command) {
|
||||
clearInFlightRunCommand(commandKey, requestIdToReplay);
|
||||
@@ -1438,6 +1547,7 @@
|
||||
function inFlightRunCommandId(commandKey, prefix) {
|
||||
const commands = readInFlightRunCommands();
|
||||
if (typeof commands[commandKey] === "string") return commands[commandKey];
|
||||
if (typeof commands[commandKey]?.request_id === "string") return commands[commandKey].request_id;
|
||||
const identifier = requestId(prefix);
|
||||
commands[commandKey] = identifier;
|
||||
sessionStorage.setItem(pendingRunCommandsKey, JSON.stringify(commands));
|
||||
@@ -1446,7 +1556,8 @@
|
||||
|
||||
function clearInFlightRunCommand(commandKey, requestIdToClear) {
|
||||
const commands = readInFlightRunCommands();
|
||||
if (commands[commandKey] !== requestIdToClear) return;
|
||||
const storedRequestId = typeof commands[commandKey] === "string" ? commands[commandKey] : commands[commandKey]?.request_id;
|
||||
if (storedRequestId !== requestIdToClear) return;
|
||||
delete commands[commandKey];
|
||||
if (Object.keys(commands).length) sessionStorage.setItem(pendingRunCommandsKey, JSON.stringify(commands));
|
||||
else sessionStorage.removeItem(pendingRunCommandsKey);
|
||||
@@ -1493,14 +1604,14 @@
|
||||
tagStatus,
|
||||
releaseBlockers ? "block" : selected ? "ok" : "warn",
|
||||
selected
|
||||
? `${selected} selected; use Preview Tag + Publish, then Create Tags or Publish Tags after reviewing the source-release preflight.`
|
||||
? `${selected} selected; preview here, then create a durable run and execute its enabled tag/push steps.`
|
||||
: "Select repositories, set target versions, then preview the source release tags."
|
||||
),
|
||||
workflowStage(
|
||||
"4. Sign and publish website catalog",
|
||||
signedStatus,
|
||||
hasCandidate ? "ok" : "warn",
|
||||
hasCandidate ? `Candidate: ${elements.candidateDir.value.trim()}` : "After every selected source tag is remotely published, generate a signed candidate, preview it, then apply/tag/push the website publication."
|
||||
hasCandidate ? `Preview candidate: ${elements.candidateDir.value.trim()}` : "After every selected source tag is remotely published, use a durable run to generate and publish its receipt-bound candidate."
|
||||
),
|
||||
workflowStage(
|
||||
"5. Maintain install catalog",
|
||||
@@ -2348,7 +2459,7 @@
|
||||
|
||||
function setBusy(buttons, busy) {
|
||||
for (const button of buttons) {
|
||||
button.disabled = busy;
|
||||
button.disabled = busy || button.dataset.releaseDisabled === "true";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user