🐛 Check README order within licensing subgroups

This commit is contained in:
David Gasquez
2026-09-30 16:25:14 +02:00
parent 55461cbaee
commit 5a3f486c2d
4 changed files with 118 additions and 6 deletions
+9 -3
View File
@@ -8,7 +8,7 @@ if (!basePath || !currentPath) {
process.exit(2);
}
const SORTED_SECTION = "Community Extensions";
const SORTED_SECTION = "Extensions / Community Extensions";
const IGNORED_SECTIONS = new Set(["Contents"]);
function headingName(markdown) {
@@ -18,12 +18,18 @@ function headingName(markdown) {
function parseSections(path) {
const sections = new Map();
const headings = [];
let section = null;
for (const line of readFileSync(path, "utf8").split("\n")) {
const heading = line.match(/^#{2,3}\s+(.+?)\s*$/);
const heading = line.match(/^(#{2,6})\s+(.+?)\s*$/);
if (heading) {
section = headingName(heading[1]);
const level = heading[1].length;
while (headings.length > 0 && headings.at(-1).level >= level) {
headings.pop();
}
headings.push({ level, name: headingName(heading[2]) });
section = headings.map(({ name }) => name).join(" / ");
if (!sections.has(section)) {
sections.set(section, []);
}
+103
View File
@@ -0,0 +1,103 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
const entry = (name) => `- [${name}](https://example.com/${name}) - Description.`;
const communityHeading = "## Extensions\n### [Community Extensions](https://duckdb.org/community_extensions/)";
const subgroupBase = [
"## Tools Powered by DuckDB",
"### Business Intelligence & Dashboards",
"#### Free & Open Source",
entry("Pondview"),
"#### Free (Proprietary / Source-Available)",
entry("Proprietary"),
"### Fun & Demos",
"#### Free & Open Source",
entry("Demo"),
].join("\n");
const cases = [
{
name: "append to a flat section",
base: `## Resources\n${entry("Existing")}`,
current: `## Resources\n${entry("Existing")}\n${entry("New")}`,
valid: true,
},
{
name: "reject insertion before an existing flat entry",
base: `## Resources\n${entry("Existing")}`,
current: `## Resources\n${entry("New")}\n${entry("Existing")}`,
valid: false,
},
{
name: "append to a licensing subgroup before another subgroup",
base: subgroupBase,
current: subgroupBase.replace(entry("Pondview"), `${entry("Pondview")}\n${entry("DUX")}`),
valid: true,
},
{
name: "reject insertion within a subgroup despite matching entries in another category",
base: subgroupBase,
current: subgroupBase.replace(entry("Pondview"), `${entry("Demo")}\n${entry("Pondview")}`),
valid: false,
},
{
name: "move an entry to the bottom of its destination subgroup",
base: `${subgroupBase}\n${entry("DUX")}`,
current: subgroupBase.replace(entry("Pondview"), `${entry("Pondview")}\n${entry("DUX")}`),
valid: true,
},
{
name: "reset subgroup context at a top-level section",
base: `${subgroupBase}\n## Resources\n${entry("Existing")}`,
current: `${subgroupBase}\n${entry("NewDemo")}\n## Resources\n${entry("Existing")}\n${entry("NewResource")}`,
valid: true,
},
{
name: "allow updating an existing description in place",
base: `## Resources\n${entry("Existing")}\n${entry("Last")}`,
current: `## Resources\n${entry("Existing").replace("Description.", "Updated description.")}\n${entry("Last")}`,
valid: true,
},
{
name: "keep community extensions case-insensitively sorted",
base: `${communityHeading}\n${entry("`beta`")}`,
current: `${communityHeading}\n${entry("`Alpha`")}\n${entry("`beta`")}`,
valid: true,
},
{
name: "reject unsorted community extensions",
base: `${communityHeading}\n${entry("`beta`")}`,
current: `${communityHeading}\n${entry("`beta`")}\n${entry("`Alpha`")}`,
valid: false,
},
{
name: "ignore contents ordering",
base: `## Contents\n${entry("Existing")}`,
current: `## Contents\n${entry("New")}\n${entry("Existing")}`,
valid: true,
},
];
for (const { name, base, current, valid } of cases) {
test(name, () => {
const directory = mkdtempSync(join(tmpdir(), "readme-order-"));
try {
const basePath = join(directory, "base.md");
const currentPath = join(directory, "current.md");
writeFileSync(basePath, base);
writeFileSync(currentPath, current);
const result = spawnSync(process.execPath, [
new URL("./check-readme-order.mjs", import.meta.url).pathname,
basePath,
currentPath,
], { encoding: "utf8" });
assert.equal(result.status, valid ? 0 : 1, result.stderr || result.stdout);
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
}
+5 -2
View File
@@ -6,13 +6,13 @@ on:
paths:
- README.md
- .github/lychee.toml
- .github/scripts/check-readme-order.mjs
- .github/scripts/check-readme-order*.mjs
- .github/workflows/linkchecker.yml
pull_request:
paths:
- README.md
- .github/lychee.toml
- .github/scripts/check-readme-order.mjs
- .github/scripts/check-readme-order*.mjs
- .github/workflows/linkchecker.yml
workflow_dispatch:
@@ -30,6 +30,9 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Test entry order checker
run: node --test .github/scripts/check-readme-order.test.mjs
- name: Check entry order
if: github.event_name == 'pull_request'
env:
+1 -1
View File
@@ -14,7 +14,7 @@ By participating in this project, you agree to abide by its [Contributor Code of
### Entry order
Most sections preserve insertion order. **Append new entries to the bottom of the relevant list**, even when another entry seems similar. Do not alphabetize or otherwise reposition existing entries.
Most sections preserve insertion order. **Append new entries to the bottom of the relevant list**, even when another entry seems similar. In **Tools Powered by DuckDB**, append to the bottom of the relevant licensing subgroup within the most specific category, not the bottom of the whole category. Do not alphabetize or otherwise reposition existing entries.
The **Community Extensions** section is the exception: insert extensions in case-insensitive lexicographic order by extension name. Community extension names use backticks, for example:
``- [`extension_name`](https://example.com) - A concise description.``