feat(release): publish verified source tags
This commit is contained in:
@@ -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 <repo> <tag>" />
|
||||
</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) {
|
||||
|
||||
Reference in New Issue
Block a user