Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 7 additions & 52 deletions scripts/changelog.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,23 @@ 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,
releaseMagicNumbers,
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 <description>.<category>.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.
Expand Down
58 changes: 58 additions & 0 deletions scripts/changelogFragments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Changelog fragments (changes/<description>.<category>.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 <description>.<category>.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 };
}
2 changes: 1 addition & 1 deletion scripts/commitRecords.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
22 changes: 22 additions & 0 deletions scripts/commitRecords.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading