Skip to content

Commit 9961729

Browse files
authored
Merge pull request #4116 from github/mario-campos/validate-cn-workflow
Validate change-notes as a PR check
2 parents 7dbd038 + e26a5c4 commit 9961729

7 files changed

Lines changed: 80 additions & 16 deletions

File tree

.github/workflows/pr-checks.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ jobs:
114114
working-directory: pr-checks
115115
run: npx tsx --test
116116

117+
- name: Run `pr-checks/changenotes.mts` to ensure that all unreleased change notes are valid
118+
if: ${{ !cancelled() && steps.install-deps.outcome == 'success' }}
119+
run: npx tsx pr-checks/changenotes.mts validate
120+
117121
- name: Verify all Actions use the same Node version
118122
id: head-version
119123
run: |

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export default [
158158
},
159159
},
160160
{
161-
files: ["**/*.ts", "**/*.js"],
161+
files: ["**/*.ts", "**/*.js", "**/*.mts"],
162162

163163
rules: {
164164
"@typescript-eslint/no-explicit-any": "off",

pr-checks/changelog/validate.mts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,14 @@ export function isValidChangenoteFile(filename: string): boolean {
119119

120120
return isValid;
121121
}
122+
123+
/**
124+
* Validates the change-note files of the given list of file paths, ignoring ".gitkeep".
125+
* @param filepaths A list of filepaths to validate
126+
* @returns True if all the paths are valid, false otherwise.
127+
*/
128+
export function isValidAllChangenoteFiles(filepaths: string[]): boolean {
129+
return filepaths
130+
.filter((f) => f !== ".gitkeep")
131+
.reduce((r, filePath) => r && isValidChangenoteFile(filePath), true);
132+
}

pr-checks/changelog/validate.test.mts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import assert from "node:assert/strict";
2+
import * as fs from "node:fs";
3+
import * as path from "node:path";
24
import { describe, it } from "node:test";
35

4-
import { withTmpFile } from "../../src/util";
6+
import { withTmpDir, withTmpFile } from "../../src/util";
57

68
import {
79
hasValidChangenoteCategory,
10+
isValidAllChangenoteFiles,
811
isValidChangenoteContent,
912
isValidChangenoteFile,
1013
isValidChangenoteFilename,
@@ -150,6 +153,10 @@ await describe("isValidChangenoteFile", async () => {
150153
);
151154
});
152155

156+
await it("rejects a non-existent path", async () => {
157+
assert.equal(isValidChangenoteFile("non-existent-file.md"), false);
158+
});
159+
153160
await it("rejects invalid filename", async () => {
154161
await withTmpFile(
155162
"fix-bug.md",
@@ -180,3 +187,39 @@ await describe("isValidChangenoteFile", async () => {
180187
);
181188
});
182189
});
190+
191+
await describe("isValidAllChangenoteFiles", async () => {
192+
await it("accepts list of file paths of valid change-notes", async () => {
193+
await withTmpDir(async (tmpDir) => {
194+
const fileName1 = path.join(tmpDir, "2026-01-01-fix-bug.md");
195+
const fileName2 = path.join(tmpDir, "2026-01-02-add-feature.md");
196+
fs.writeFileSync(fileName1, "---\ncategory: fix\n---\n- Fixed a bug\n");
197+
fs.writeFileSync(
198+
fileName2,
199+
"---\ncategory: feature\n---\n- Added a feature\n",
200+
);
201+
assert.equal(isValidAllChangenoteFiles([fileName1, fileName2]), true);
202+
});
203+
});
204+
205+
await it("accepts the empty list", async () => {
206+
assert.equal(isValidAllChangenoteFiles([]), true);
207+
});
208+
209+
await it("accepts list of .gitkeep", async () => {
210+
assert.equal(isValidAllChangenoteFiles([".gitkeep"]), true);
211+
});
212+
213+
await it("rejects list containing a file path to an invalid change-note", async () => {
214+
await withTmpDir(async (tmpDir) => {
215+
const fileName1 = path.join(tmpDir, "2026-01-01-fix-bug.md");
216+
const fileName2 = path.join(tmpDir, "2026-01-02-wrong-category.md");
217+
fs.writeFileSync(fileName1, "---\ncategory: fix\n---\n- Fixed a bug\n");
218+
fs.writeFileSync(
219+
fileName2,
220+
"---\ncategory: foobar\n---\n- Added a feature\n",
221+
);
222+
assert.equal(isValidAllChangenoteFiles([fileName1, fileName2]), false);
223+
});
224+
});
225+
});

pr-checks/changenotes.mts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#!/usr/bin/env npx tsx
22

3+
import * as fs from "node:fs";
34
import { pathToFileURL } from "node:url";
45
import { parseArgs } from "node:util";
56

6-
import { isValidChangenoteFile } from "./changelog/validate.mjs";
7+
import { isValidAllChangenoteFiles } from "./changelog/validate.mjs";
8+
import { CHANGENOTES_DIR } from "./config";
79

810
const entryPoint = process.argv[1];
911
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
@@ -20,34 +22,35 @@ function main(): number {
2022
allowPositionals: true,
2123
strict: true,
2224
});
23-
const [command, ...paths] = positionals;
25+
const [command] = positionals;
2426
switch (command) {
2527
case undefined:
2628
case "help":
2729
return usage();
2830
case "validate":
29-
return validate(paths);
31+
return validate();
3032
default:
3133
console.error(`Unknown command: ${command}`);
3234
return 1;
3335
}
3436
}
3537

3638
function usage(): number {
37-
console.log(`Usage: changenotes.mts validate <path> [<path> ...]`);
39+
console.log(`Usage: changenotes.mts validate`);
3840
return 0;
3941
}
4042

41-
function validate(paths: string[]): number {
42-
let valid = true;
43-
if (paths.length === 0) {
44-
console.error("error: no paths provided (see 'help' command for usage)");
45-
return 1;
46-
}
47-
for (const path of paths) {
48-
if (!isValidChangenoteFile(path)) {
49-
valid = false;
43+
function validate(): number {
44+
try {
45+
if (isValidAllChangenoteFiles(fs.readdirSync(CHANGENOTES_DIR))) {
46+
console.log(`All changenotes in '${CHANGENOTES_DIR}' are valid.`);
47+
return 0;
5048
}
49+
} catch (error) {
50+
console.error(
51+
`Failed to read changenotes directory '${CHANGENOTES_DIR}'`,
52+
error,
53+
);
5154
}
52-
return valid ? 0 : 1;
55+
return 1;
5356
}

pr-checks/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export const PACKAGE_JSON = path.join(REPO_ROOT, "package.json");
1818
/** The path of the changelog. */
1919
export const CHANGELOG_FILE = path.join(REPO_ROOT, "CHANGELOG.md");
2020

21+
/** The path to the unreleased change-notes directory. */
22+
export const CHANGENOTES_DIR = path.join(REPO_ROOT, "unreleased-change-notes");
23+
2124
/** The path to the esbuild metadata file. */
2225
export const BUNDLE_METADATA_FILE = path.join(REPO_ROOT, "meta.json");
2326

unreleased-change-notes/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)