feat(cli): add release name handling for topic releases

This commit is contained in:
Prad Nukala committed 2026-09-25 14:05:52 -04:00
1 parent 6f9e4ecb3b
commit 8e8522a773
1 file changed
+30 -11
+30 -11
View File
@@ -111,15 +111,16 @@ finished.sort((a, b) => a.finishedAt.localeCompare(b.finishedAt) || a.topic.numb
// ── existing releases ────────────────────────────────────────────
/** Topic -> its release. The body is kept in sync with render() on every run. */
const released = new Map<number, { id: number; tag: string; body: string }>();
/** Topic -> its release. Name and body are kept in sync with the render on every run. */
const released = new Map<number, { id: number; tag: string; name: string; body: string }>();
for await (const raw of gh.list(`/repos/${gh.repo}/releases`)) {
if (!raw || typeof raw !== "object") continue;
if (!("id" in raw) || typeof raw.id !== "number") continue;
if (!("tag_name" in raw) || typeof raw.tag_name !== "string") continue;
if (!("body" in raw) || typeof raw.body !== "string") continue;
const name = "name" in raw && typeof raw.name === "string" ? raw.name : "";
const m = raw.body.match(/<!-- release-topics: #(\d+) -->/);
if (m) released.set(Number(m[1]), { id: raw.id, tag: raw.tag_name, body: raw.body });
if (m) released.set(Number(m[1]), { id: raw.id, tag: raw.tag_name, name, body: raw.body });
}
// ── timever ──────────────────────────────────────────────────────
@@ -158,6 +159,17 @@ const ET_DAY = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
});
/**
* The release name: the ET day the topic finished (`September 4, 2026`). The
* docs changelog titles each entry with it, so the timeline reads by day; the
* topic itself leads the body's summary sentence.
*/
const ET_NAME = new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
month: "long",
day: "numeric",
year: "numeric",
});
/** Topic body sections worth carrying into the changelog (skipped while still `TODO`). */
const NOTES = ["Trigger", "Invariant", "Trap"] as const;
@@ -202,10 +214,11 @@ function render(f: Finished, tag: string, sha: string, files: Map<number, string
// Blume's changelog derives each page's meta description from the opening
// prose, stripping markdown and line breaks without adding a space — so lead
// with one self-contained sentence and keep the fields on one `·` line.
// with one self-contained sentence and keep the fields on one `·` line. The
// release name is only the date, so the sentence names the topic.
const when = ET_WHEN.format(new Date(f.finishedAt));
const summary =
`Finished ${when} ET: all ${f.core.length} core problems closed, ` +
`**${topic.title}** finished ${when} ET: all ${f.core.length} core problems closed, ` +
`${onDay.length} ${onDay.length === 1 ? "problem" : "problems"} closed that day.`;
const fields: string[] = [];
const scheduled = bodyField(topic.body, "Scheduled");
@@ -243,8 +256,13 @@ for (const f of finished) {
const { topic } = f;
const finishedEt = etDate(new Date(f.finishedAt));
const existing = released.get(topic.number);
// An existing release keeps its tag and commit; only its body is reconciled.
// An existing release keeps its tag and commit; only its name and body are
// reconciled. Its tag may postdate this clone (released by another run), so
// fetch it when missing — read-only, so a dry run does it too.
const tag = existing?.tag ?? tags.get(topic.number)!;
if (existing && Bun.spawnSync(["git", "rev-parse", "-q", "--verify", `refs/tags/${tag}`], { cwd: ROOT }).exitCode !== 0) {
git("fetch", "-q", "origin", `refs/tags/${tag}:refs/tags/${tag}`);
}
const sha = existing
? git("rev-parse", `${tag}^{commit}`)
: git("rev-list", "-1", "--first-parent", `--before=${f.finishedAt}`, "HEAD");
@@ -263,17 +281,18 @@ for (const f of finished) {
files.set(Number(lc), [...(files.get(Number(lc)) ?? []), path]);
}
const body = render(f, tag, sha, files);
const name = ET_NAME.format(new Date(f.finishedAt));
if (existing) {
if (existing.body === body) {
if (existing.body === body && existing.name === name) {
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "released"]);
} else if (DRY) {
previews.push(`## ${tag} — ${topic.title}\n\n${body}`);
previews.push(`## ${name} (${tag})\n\n${body}`);
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "would update"]);
} else {
await gh.api(`/repos/${gh.repo}/releases/${existing.id}`, {
method: "PATCH",
body: JSON.stringify({ body }),
body: JSON.stringify({ name, body }),
});
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "updated"]);
updated++;
@@ -282,7 +301,7 @@ for (const f of finished) {
}
if (DRY) {
previews.push(`## ${tag} — ${topic.title}\n\n${body}`);
previews.push(`## ${name} (${tag})\n\n${body}`);
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "would release"]);
continue;
}
@@ -303,7 +322,7 @@ for (const f of finished) {
method: "POST",
body: JSON.stringify({
tag_name: tag,
name: topic.title,
name,
body,
make_latest: topic.number === latest ? "true" : "false",
}),