Files
siftlode/scripts/gen-changelog.mjs
T
peter a1224e9a8c test(gates): the repo-root scripts are linted and format-checked
scripts/check-node-modules.mjs and gen-changelog.mjs run in check and release but sat outside
every lane: the eslint and prettier lanes run from frontend/, and an ESLint config cannot reach
past its own directory. A contract test now holds them to the js-configs bar (recommended rules
under Node, the frontend's Prettier style); an unused variable and the old formatting are each
red. 0 lint hits; both files reformatted. Registered in docs/gates.md.
2026-10-04 03:00:49 +02:00

80 lines
3.0 KiB
JavaScript

// Generates CHANGELOG.md from frontend/src/content/release-notes/en.json — the same entries the app
// shows under Release Notes, so a self-hoster deciding whether to upgrade reads the same text an
// in-app user does, without having to install the app first (DX-14).
//
// Why derive rather than hand-write: two lists of "what changed in 0.80.0" WILL drift, and the
// in-app one is the one that gets written during a release. This makes the file a build product of it.
//
// node scripts/gen-changelog.mjs # write CHANGELOG.md
// node scripts/gen-changelog.mjs --check # exit 1 if CHANGELOG.md is stale (used by `siftlode release`)
import { readFile, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import path from "node:path";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const SOURCE = path.join(root, "frontend", "src", "content", "release-notes", "en.json");
const TARGET = path.join(root, "CHANGELOG.md");
async function loadEntries() {
const entries = JSON.parse(await readFile(SOURCE, "utf8"));
if (!Array.isArray(entries) || entries.length === 0) {
throw new Error(`${SOURCE} is not a non-empty array of release notes.`);
}
return entries;
}
function section(title, items) {
if (!items || items.length === 0) return "";
return `### ${title}\n\n${items.map((line) => `- ${line}`).join("\n")}\n\n`;
}
function render(entries) {
const head = [
"# Changelog",
"",
"Every released version of Siftlode, newest first.",
"",
"**Generated from `frontend/src/content/release-notes/en.json` by `scripts/gen-changelog.mjs` — do not edit by hand;**",
"edit the release notes and re-run the generator.",
"",
"Upgrading, pinning a version and rolling back: [docs/self-hosting.md](docs/self-hosting.md#upgrading).",
"",
"",
].join("\n");
const body = entries
.map((e) => {
const sha = e.sha ? ` (\`${e.sha}\`)` : "";
let out = `## ${e.version} — ${e.date}${sha}\n\n`;
out += section("New", e.new);
out += section("Fixed", e.fixed);
// A release that carried only internal work (none has since the owner's E28 rule).
if (e.new.length === 0 && e.fixed.length === 0) out += "Internal changes only.\n\n";
return out;
})
.join("");
return `${head}${body}`.replace(/\n{3,}$/, "\n");
}
const entries = await loadEntries();
const rendered = render(entries);
if (process.argv.includes("--check")) {
const current = await readFile(TARGET, "utf8").catch(() => null);
if (current !== rendered) {
console.error(
`CHANGELOG.md is out of date with the release notes (newest entry: ${entries[0].version}).\n` +
"Run: node scripts/gen-changelog.mjs — then commit the result.",
);
process.exit(1);
}
console.log(
`CHANGELOG.md is current (${entries.length} releases, newest ${entries[0].version}).`,
);
} else {
await writeFile(TARGET, rendered, "utf8");
console.log(`Wrote CHANGELOG.md — ${entries.length} releases, newest ${entries[0].version}.`);
}