diff --git a/pr-checks/changelog/validate.mts b/pr-checks/changelog/validate.mts index 3c83276f3..2e28a4ab1 100644 --- a/pr-checks/changelog/validate.mts +++ b/pr-checks/changelog/validate.mts @@ -119,3 +119,14 @@ export function isValidChangenoteFile(filename: string): boolean { return isValid; } + +/** + * Validates the change-note files of the given list of file paths, ignoring ".gitkeep". + * @param filepaths A list of filepaths to validate + * @returns True if all the paths are valid, false otherwise. + */ +export function isValidAllChangenoteFiles(filepaths: string[]): boolean { + return filepaths + .filter((f) => f !== ".gitkeep") + .reduce((r, filePath) => r && isValidChangenoteFile(filePath), true); +} diff --git a/pr-checks/changelog/validate.test.mts b/pr-checks/changelog/validate.test.mts index 917e7403f..f6fb43680 100644 --- a/pr-checks/changelog/validate.test.mts +++ b/pr-checks/changelog/validate.test.mts @@ -1,10 +1,13 @@ import assert from "node:assert/strict"; +import * as fs from "node:fs"; +import * as path from "node:path"; import { describe, it } from "node:test"; -import { withTmpFile } from "../../src/util"; +import { withTmpDir, withTmpFile } from "../../src/util"; import { hasValidChangenoteCategory, + isValidAllChangenoteFiles, isValidChangenoteContent, isValidChangenoteFile, isValidChangenoteFilename, @@ -150,6 +153,10 @@ await describe("isValidChangenoteFile", async () => { ); }); + await it("rejects a non-existent path", async () => { + assert.equal(isValidChangenoteFile("non-existent-file.md"), false); + }); + await it("rejects invalid filename", async () => { await withTmpFile( "fix-bug.md", @@ -180,3 +187,39 @@ await describe("isValidChangenoteFile", async () => { ); }); }); + +await describe("isValidAllChangenoteFiles", async () => { + await it("accepts a directory of valid change-note files", async () => { + await withTmpDir(async (tmpDir) => { + const fileName1 = path.join(tmpDir, "2026-01-01-fix-bug.md"); + const fileName2 = path.join(tmpDir, "2026-01-02-add-feature.md"); + fs.writeFileSync(fileName1, "---\ncategory: fix\n---\n- Fixed a bug\n"); + fs.writeFileSync( + fileName2, + "---\ncategory: feature\n---\n- Added a feature\n", + ); + assert.equal(isValidAllChangenoteFiles([fileName1, fileName2]), true); + }); + }); + + await it("accepts an empty list", async () => { + assert.equal(isValidAllChangenoteFiles([]), true); + }); + + await it("accepts a list of .gitkeep only", async () => { + assert.equal(isValidAllChangenoteFiles([".gitkeep"]), true); + }); + + await it("rejects directory with an invalid change-note file", async () => { + await withTmpDir(async (tmpDir) => { + const fileName1 = path.join(tmpDir, "2026-01-01-fix-bug.md"); + const fileName2 = path.join(tmpDir, "2026-01-02-wrong-category.md"); + fs.writeFileSync(fileName1, "---\ncategory: fix\n---\n- Fixed a bug\n"); + fs.writeFileSync( + fileName2, + "---\ncategory: foobar\n---\n- Added a feature\n", + ); + assert.equal(isValidAllChangenoteFiles([fileName1, fileName2]), false); + }); + }); +}); diff --git a/pr-checks/changenotes.mts b/pr-checks/changenotes.mts index 200a1de99..37e00338f 100755 --- a/pr-checks/changenotes.mts +++ b/pr-checks/changenotes.mts @@ -1,9 +1,11 @@ #!/usr/bin/env npx tsx +import * as fs from "node:fs"; import { pathToFileURL } from "node:url"; import { parseArgs } from "node:util"; -import { isValidChangenoteFile } from "./changelog/validate.mjs"; +import { isValidAllChangenoteFiles } from "./changelog/validate.mjs"; +import { CHANGENOTES_DIR } from "./config"; const entryPoint = process.argv[1]; if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) { @@ -20,13 +22,13 @@ function main(): number { allowPositionals: true, strict: true, }); - const [command, ...paths] = positionals; + const [command] = positionals; switch (command) { case undefined: case "help": return usage(); case "validate": - return validate(paths); + return validate(); default: console.error(`Unknown command: ${command}`); return 1; @@ -34,20 +36,18 @@ function main(): number { } function usage(): number { - console.log(`Usage: changenotes.mts validate [ ...]`); + console.log(`Usage: changenotes.mts validate`); return 0; } -function validate(paths: string[]): number { - let valid = true; - if (paths.length === 0) { - console.error("error: no paths provided (see 'help' command for usage)"); +function validate(): number { + try { + return isValidAllChangenoteFiles(fs.readdirSync(CHANGENOTES_DIR)) ? 0 : 1; + } catch (error) { + console.error( + `${CHANGENOTES_DIR}: failed to read file or directory`, + error, + ); return 1; } - for (const path of paths) { - if (!isValidChangenoteFile(path)) { - valid = false; - } - } - return valid ? 0 : 1; }