mirror of
https://github.com/github/codeql-action.git
synced 2026-10-03 17:41:28 +00:00
Merge pull request #4116 from github/mario-campos/validate-cn-workflow
Validate change-notes as a PR check
This commit is contained in:
4
.github/workflows/pr-checks.yml
vendored
4
.github/workflows/pr-checks.yml
vendored
@@ -114,6 +114,10 @@ jobs:
|
|||||||
working-directory: pr-checks
|
working-directory: pr-checks
|
||||||
run: npx tsx --test
|
run: npx tsx --test
|
||||||
|
|
||||||
|
- name: Run `pr-checks/changenotes.mts` to ensure that all unreleased change notes are valid
|
||||||
|
if: ${{ !cancelled() && steps.install-deps.outcome == 'success' }}
|
||||||
|
run: npx tsx pr-checks/changenotes.mts validate
|
||||||
|
|
||||||
- name: Verify all Actions use the same Node version
|
- name: Verify all Actions use the same Node version
|
||||||
id: head-version
|
id: head-version
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export default [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ["**/*.ts", "**/*.js"],
|
files: ["**/*.ts", "**/*.js", "**/*.mts"],
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
|||||||
@@ -119,3 +119,14 @@ export function isValidChangenoteFile(filename: string): boolean {
|
|||||||
|
|
||||||
return isValid;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import assert from "node:assert/strict";
|
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 { describe, it } from "node:test";
|
||||||
|
|
||||||
import { withTmpFile } from "../../src/util";
|
import { withTmpDir, withTmpFile } from "../../src/util";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
hasValidChangenoteCategory,
|
hasValidChangenoteCategory,
|
||||||
|
isValidAllChangenoteFiles,
|
||||||
isValidChangenoteContent,
|
isValidChangenoteContent,
|
||||||
isValidChangenoteFile,
|
isValidChangenoteFile,
|
||||||
isValidChangenoteFilename,
|
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 it("rejects invalid filename", async () => {
|
||||||
await withTmpFile(
|
await withTmpFile(
|
||||||
"fix-bug.md",
|
"fix-bug.md",
|
||||||
@@ -180,3 +187,39 @@ await describe("isValidChangenoteFile", async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await describe("isValidAllChangenoteFiles", async () => {
|
||||||
|
await it("accepts list of file paths of valid change-notes", 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 the empty list", async () => {
|
||||||
|
assert.equal(isValidAllChangenoteFiles([]), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await it("accepts list of .gitkeep", async () => {
|
||||||
|
assert.equal(isValidAllChangenoteFiles([".gitkeep"]), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await it("rejects list containing a file path to an invalid change-note", 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
#!/usr/bin/env npx tsx
|
#!/usr/bin/env npx tsx
|
||||||
|
|
||||||
|
import * as fs from "node:fs";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
import { parseArgs } from "node:util";
|
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];
|
const entryPoint = process.argv[1];
|
||||||
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
|
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
|
||||||
@@ -20,13 +22,13 @@ function main(): number {
|
|||||||
allowPositionals: true,
|
allowPositionals: true,
|
||||||
strict: true,
|
strict: true,
|
||||||
});
|
});
|
||||||
const [command, ...paths] = positionals;
|
const [command] = positionals;
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case undefined:
|
case undefined:
|
||||||
case "help":
|
case "help":
|
||||||
return usage();
|
return usage();
|
||||||
case "validate":
|
case "validate":
|
||||||
return validate(paths);
|
return validate();
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown command: ${command}`);
|
console.error(`Unknown command: ${command}`);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -34,20 +36,21 @@ function main(): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function usage(): number {
|
function usage(): number {
|
||||||
console.log(`Usage: changenotes.mts validate <path> [<path> ...]`);
|
console.log(`Usage: changenotes.mts validate`);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate(paths: string[]): number {
|
function validate(): number {
|
||||||
let valid = true;
|
try {
|
||||||
if (paths.length === 0) {
|
if (isValidAllChangenoteFiles(fs.readdirSync(CHANGENOTES_DIR))) {
|
||||||
console.error("error: no paths provided (see 'help' command for usage)");
|
console.log(`All changenotes in '${CHANGENOTES_DIR}' are valid.`);
|
||||||
return 1;
|
return 0;
|
||||||
}
|
|
||||||
for (const path of paths) {
|
|
||||||
if (!isValidChangenoteFile(path)) {
|
|
||||||
valid = false;
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to read changenotes directory '${CHANGENOTES_DIR}'`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return valid ? 0 : 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ export const PACKAGE_JSON = path.join(REPO_ROOT, "package.json");
|
|||||||
/** The path of the changelog. */
|
/** The path of the changelog. */
|
||||||
export const CHANGELOG_FILE = path.join(REPO_ROOT, "CHANGELOG.md");
|
export const CHANGELOG_FILE = path.join(REPO_ROOT, "CHANGELOG.md");
|
||||||
|
|
||||||
|
/** The path to the unreleased change-notes directory. */
|
||||||
|
export const CHANGENOTES_DIR = path.join(REPO_ROOT, "unreleased-change-notes");
|
||||||
|
|
||||||
/** The path to the esbuild metadata file. */
|
/** The path to the esbuild metadata file. */
|
||||||
export const BUNDLE_METADATA_FILE = path.join(REPO_ROOT, "meta.json");
|
export const BUNDLE_METADATA_FILE = path.join(REPO_ROOT, "meta.json");
|
||||||
|
|
||||||
|
|||||||
0
unreleased-change-notes/.gitkeep
Normal file
0
unreleased-change-notes/.gitkeep
Normal file
Reference in New Issue
Block a user