From edad6414fa7a7e1938ffe44068c63470c0db603b Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 23 Oct 2026 17:34:17 +0000 Subject: [PATCH] Parse changelog fragments without the release script's dependencies The Semantic Diff workflow runs commitRecords.js without node_modules, and changelog.js now imports magicNumbers.js, which imports semver, so every pull request's semantic diff failed. Fragment parsing moves to changelogFragments.js, which imports nothing, and a test keeps the renderer's imports to Node's modules and its own files. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AzPS5F8RDSGZ6dhfK6NdZp Signed-off-by: Christoph Knittel --- scripts/changelog.js | 59 +++++------------------------------ scripts/changelogFragments.js | 58 ++++++++++++++++++++++++++++++++++ scripts/commitRecords.js | 2 +- scripts/commitRecords.test.js | 22 +++++++++++++ 4 files changed, 88 insertions(+), 53 deletions(-) create mode 100644 scripts/changelogFragments.js diff --git a/scripts/changelog.js b/scripts/changelog.js index 30e1ca017d..7edb1d423a 100644 --- a/scripts/changelog.js +++ b/scripts/changelog.js @@ -2,6 +2,12 @@ import { execFileSync } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { + categories, + parseFragment, + placeholderLink, + trailingPrLinks, +} from "./changelogFragments.js"; import { checkMagicNumbers, configPath, @@ -9,61 +15,10 @@ import { showAt, } from "./magicNumbers.js"; -export const categories = { - breaking: "#### :boom: Breaking Change", - feature: "#### :rocket: New Feature", - fix: "#### :bug: Bug fix", - docs: "#### :memo: Documentation", - polish: "#### :nail_care: Polish", - internal: "#### :house: Internal", -}; +export { categories, parseFragment, placeholderLink }; -const prLink = - /https:\/\/github\.com\/rescript-lang\/rescript(?:-compiler)?\/pull\/([1-9]\d*)/; -export const placeholderLink = - "https://github.com/rescript-lang/rescript/pull/XXXX"; -const trailingPrLinks = new RegExp( - `${prLink.source}(?:\\s+${prLink.source})*$`, -); const repositoryUrl = "https://github.com/rescript-lang/rescript"; -export function parseFragment(name, content) { - const match = /^([a-z0-9][a-z0-9-]*)\.([a-z]+)\.md$/.exec(name); - if (!match || !Object.hasOwn(categories, match[2])) { - throw new Error( - `${name}: expected ..md; allowed categories: ${Object.keys(categories).join(", ")}`, - ); - } - const text = content.trim(); - if (text.includes(placeholderLink)) { - throw new Error( - `${name}: replace the placeholder ${placeholderLink} with the PR link`, - ); - } - const bullets = text.split(/\n(?=- )/); - const invalid = bullets.some( - bullet => - !/^- \S/.test(bullet) || - bullet - .split("\n") - .slice(1) - .some(line => line && !/^ {2}\S/.test(line)), - ); - if (!text || invalid) { - throw new Error( - `${name}: expected Markdown bullets (indent continuation lines by two spaces)`, - ); - } - const links = [...text.matchAll(new RegExp(prLink, "g"))].map(link => - Number(link[1]), - ); - // A bullet without a link gets the link of the pull request that added the - // fragment when the release is prepared. - const unlinked = bullets.some(bullet => !trailingPrLinks.test(bullet)); - const pr = links.length ? Math.max(...links) : null; - return { name, category: match[2], text, pr, unlinked }; -} - // The pull request that added a fragment, from the subject of the squash // commit that added it ("... (#1234)"). Rebase merges and merge commits carry // no such subject, which is why pull requests are squash-merged. diff --git a/scripts/changelogFragments.js b/scripts/changelogFragments.js new file mode 100644 index 0000000000..d46ebdd494 --- /dev/null +++ b/scripts/changelogFragments.js @@ -0,0 +1,58 @@ +// Changelog fragments (changes/..md): their +// categories and their parsing. This file imports nothing, so that scripts +// run without node_modules can use it, such as the Semantic Diff workflow's +// scripts/commitRecords.js. + +export const categories = { + breaking: "#### :boom: Breaking Change", + feature: "#### :rocket: New Feature", + fix: "#### :bug: Bug fix", + docs: "#### :memo: Documentation", + polish: "#### :nail_care: Polish", + internal: "#### :house: Internal", +}; + +const prLink = + /https:\/\/github\.com\/rescript-lang\/rescript(?:-compiler)?\/pull\/([1-9]\d*)/; +export const placeholderLink = + "https://github.com/rescript-lang/rescript/pull/XXXX"; +export const trailingPrLinks = new RegExp( + `${prLink.source}(?:\\s+${prLink.source})*$`, +); + +export function parseFragment(name, content) { + const match = /^([a-z0-9][a-z0-9-]*)\.([a-z]+)\.md$/.exec(name); + if (!match || !Object.hasOwn(categories, match[2])) { + throw new Error( + `${name}: expected ..md; allowed categories: ${Object.keys(categories).join(", ")}`, + ); + } + const text = content.trim(); + if (text.includes(placeholderLink)) { + throw new Error( + `${name}: replace the placeholder ${placeholderLink} with the PR link`, + ); + } + const bullets = text.split(/\n(?=- )/); + const invalid = bullets.some( + bullet => + !/^- \S/.test(bullet) || + bullet + .split("\n") + .slice(1) + .some(line => line && !/^ {2}\S/.test(line)), + ); + if (!text || invalid) { + throw new Error( + `${name}: expected Markdown bullets (indent continuation lines by two spaces)`, + ); + } + const links = [...text.matchAll(new RegExp(prLink, "g"))].map(link => + Number(link[1]), + ); + // A bullet without a link gets the link of the pull request that added the + // fragment when the release is prepared. + const unlinked = bullets.some(bullet => !trailingPrLinks.test(bullet)); + const pr = links.length ? Math.max(...links) : null; + return { name, category: match[2], text, pr, unlinked }; +} diff --git a/scripts/commitRecords.js b/scripts/commitRecords.js index 6ac055c354..d0d3390967 100644 --- a/scripts/commitRecords.js +++ b/scripts/commitRecords.js @@ -20,7 +20,7 @@ import { execFileSync } from "node:child_process"; import { readFileSync, writeFileSync } from "node:fs"; -import { categories, parseFragment } from "./changelog.js"; +import { categories, parseFragment } from "./changelogFragments.js"; import { commentMarkdown } from "./recordMarkdown.js"; import { CSS, esc, pageBody, pageTitle, parseRecord } from "./recordRender.js"; diff --git a/scripts/commitRecords.test.js b/scripts/commitRecords.test.js index 72160b78fa..69399910db 100644 --- a/scripts/commitRecords.test.js +++ b/scripts/commitRecords.test.js @@ -114,6 +114,28 @@ Review: Does a PPX still see the extension? Yes: the v0 bridge maps it back, ${SIGNED} `; +test("the renderer imports no packages: the workflow runs it without node_modules", async () => { + const seen = new Set(); + /** @param {string} file */ + const visit = async file => { + if (seen.has(file)) return; + seen.add(file); + const source = await fs.readFile(file, "utf8"); + for (const [, spec] of source.matchAll( + /^(?:import|export)\b[^"'`;]*?\bfrom\s+["']([^"']+)["']/gm, + )) { + assert.ok( + spec.startsWith("./") || spec.startsWith("node:"), + `${path.basename(file)} imports ${spec}`, + ); + if (spec.startsWith("./")) + await visit(path.resolve(path.dirname(file), spec)); + } + }; + await visit(script); + assert.ok(seen.size > 3, [...seen].join(", ")); +}); + test("a minimal record draws its identity and subject", async () => { const html = await render( async (git, dir) => {