feat(release): publish verified source tags
Some checks failed
Dependency Audit / dependency-audit (push) Failing after 14s
Security Audit / security-audit (push) Failing after 12s

This commit is contained in:
2026-07-22 01:17:07 +02:00
parent eca75cbc93
commit 30f9ed152a
12 changed files with 1735 additions and 40 deletions

View File

@@ -692,15 +692,34 @@
<section>
<div class="section-head">
<h2>3. Release Tag Plan</h2>
<small id="planStatus"></small>
<h2>3. Source Release Tag</h2>
<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>
<div class="form-grid">
<div>
<label for="tagMessage">Tag message</label>
<input id="tagMessage" type="text" placeholder="default Release &lt;repo&gt; &lt;tag&gt;" />
</div>
<div>
<label for="tagConfirmText">Confirm</label>
<input id="tagConfirmText" type="text" placeholder="TAG or PUBLISH" />
</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>
</div>
<div class="actions" id="tagOutput"></div>
</div>
<div class="actions" id="actions"></div>
</section>
<section>
<div class="section-head">
<h2>4. Signed Release</h2>
<h2>4. Signed Website Catalog</h2>
<small id="workflowStatus"></small>
</div>
<div class="details">
@@ -721,8 +740,8 @@
<div class="button-row">
<button id="generateCandidate">Generate</button>
<button class="secondary" id="previewPublish">Preview</button>
<button class="secondary" id="applyPublish">Apply + Tag</button>
<button class="danger" id="pushPublish">Push</button>
<button class="secondary" id="applyPublish">Apply + Website Tag</button>
<button class="danger" id="pushPublish">Push Website Release</button>
</div>
<div class="actions" id="workflowOutput"></div>
</div>
@@ -801,6 +820,13 @@
catalogStatus: document.getElementById("catalogStatus"),
actions: document.getElementById("actions"),
planStatus: document.getElementById("planStatus"),
tagStatus: document.getElementById("tagStatus"),
tagMessage: document.getElementById("tagMessage"),
tagConfirmText: document.getElementById("tagConfirmText"),
tagOutput: document.getElementById("tagOutput"),
previewReleaseTags: document.getElementById("previewReleaseTags"),
createReleaseTags: document.getElementById("createReleaseTags"),
publishReleaseTags: document.getElementById("publishReleaseTags"),
signingKey: document.getElementById("signingKey"),
candidateDir: document.getElementById("candidateDir"),
confirmText: document.getElementById("confirmText"),
@@ -858,6 +884,9 @@
elements.previewPrepare.addEventListener("click", () => prepareSelectedRepos({ apply: false }));
elements.commitPrepare.addEventListener("click", () => prepareSelectedRepos({ apply: true }));
elements.buildSelectedPlan.addEventListener("click", buildSelectedPlan);
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 }));
elements.selectUnpushedUnits.addEventListener("click", selectUnpushedUnits);
elements.selectChangedUnits.addEventListener("click", selectChangedUnits);
elements.selectUnreleasedUnits.addEventListener("click", selectUnreleasedUnits);
@@ -974,18 +1003,18 @@
: "No dirty worktrees in the current dashboard."
),
workflowStage(
"3. Generate release tag",
"3. Publish source release tags",
tagStatus,
releaseBlockers ? "block" : selected ? "ok" : "warn",
selected
? `${selected} selected; cockpit builds the tag plan, release tagging still runs through the guarded release script.`
: "Select repositories, set target versions, then build the release tag plan."
? `${selected} selected; use Preview Tag + Publish, then Create Tags or Publish Tags after reviewing the source-release preflight.`
: "Select repositories, set target versions, then preview the source release tags."
),
workflowStage(
"4. Sign and publish release",
"4. Sign and publish website catalog",
signedStatus,
hasCandidate ? "ok" : "warn",
hasCandidate ? `Candidate: ${elements.candidateDir.value.trim()}` : "Generate a signed candidate, preview it, then apply/tag/push the website publication."
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."
),
workflowStage(
"5. Maintain install catalog",
@@ -1595,6 +1624,65 @@
}).join("");
}
async function releaseSelectedTags(options) {
const repos = selectedRepoNames();
if (!repos.length) {
elements.tagStatus.textContent = "idle";
elements.tagOutput.innerHTML = `<div class="action"><h3>${pill("idle", "warn")} No repositories selected</h3><p>Select one or more repositories first.</p></div>`;
return;
}
elements.tagStatus.textContent = options.apply ? (options.push ? "Publishing..." : "Tagging...") : "Previewing...";
setBusy([elements.previewReleaseTags, elements.createReleaseTags, elements.publishReleaseTags], true);
try {
const result = await postJson("/api/repositories/tag", {
repos,
repo_versions: selectedRepoVersions(),
remote: elements.pushRemote.value.trim() || "origin",
message: elements.tagMessage.value.trim() || null,
apply: options.apply === true,
push: options.push === true,
confirm: elements.tagConfirmText.value.trim(),
});
elements.tagStatus.textContent = result.status;
renderTagResult(result);
await load();
} catch (error) {
elements.tagStatus.textContent = "error";
elements.tagOutput.innerHTML = `<div class="action"><h3>${pill("error", "block")} Source release failed</h3><p>${escapeHtml(error.message)}</p></div>`;
} finally {
setBusy([elements.previewReleaseTags, elements.createReleaseTags, elements.publishReleaseTags], false);
}
}
function renderTagResult(result) {
const rows = Array.isArray(result.repositories) ? result.repositories : [];
if (!rows.length) {
elements.tagOutput.innerHTML = `<div class="muted">No source release actions returned.</div>`;
return;
}
elements.tagOutput.innerHTML = rows.map((row) => {
const kind = row.status === "blocked" || row.status === "failed" ? "block" : row.status === "published" || row.status === "tagged" || row.status === "planned" ? "ok" : "warn";
const commands = [row.create_command, row.publish_command]
.filter(Boolean)
.map((command) => `<pre>${escapeHtml(command)}</pre>`)
.join("");
const output = [row.stdout, row.stderr].filter(Boolean).map((value) => `<pre>${escapeHtml(value)}</pre>`).join("");
const meta = [
row.tag,
row.branch ? `branch ${row.branch}` : "",
row.head ? `HEAD ${row.head}` : "",
row.remote ? `remote ${row.remote}` : "",
].filter(Boolean).join(" | ");
return `<div class="action">
<h3>${pill(row.status, kind)} ${escapeHtml(row.repo)}</h3>
<p>${escapeHtml(row.detail || "")}</p>
${meta ? `<p class="action-meta">${escapeHtml(meta)}</p>` : ""}
${commands}
${output}
</div>`;
}).join("");
}
async function syncSelectedRepos(options) {
const repos = selectedRepoNames();
if (!repos.length) {
@@ -1738,7 +1826,18 @@
const steps = Array.isArray(result.steps)
? result.steps.map((step) => `<div class="action"><h3>${pill(step.status, step.status === "blocked" ? "block" : step.status === "planned" ? "warn" : "ok")} ${escapeHtml(step.title)}</h3><p>${escapeHtml(step.detail)}</p>${step.command ? `<pre>${escapeHtml(step.command)}</pre>` : ""}</div>`).join("")
: "";
elements.workflowOutput.innerHTML = `<div class="action"><h3>${escapeHtml(title)}</h3>${lines.join("")}${changes}</div>${steps}`;
const notes = Array.isArray(result.notes)
? result.notes.map((note) => `<p>${escapeHtml(note)}</p>`).join("")
: "";
const resultKind = result.status === "blocked" || result.status === "failed" || result.status === "partial"
? "block"
: result.status === "ready" || result.status === "published" || result.status === "applied"
? "ok"
: "warn";
const noteDetails = notes
? `<div class="action"><details open><summary>Validation and provenance details</summary>${notes}</details></div>`
: "";
elements.workflowOutput.innerHTML = `<div class="action"><h3>${pill(result.status || "result", resultKind)} ${escapeHtml(title)}</h3>${lines.join("")}${changes}</div>${noteDetails}${steps}`;
}
function setBusy(buttons, busy) {