diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index ac61475d6..b1e131425 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -72,6 +72,33 @@ jobs: sarif_file: eslint.sarif category: eslint + changetool-tests: + name: changetool unit tests + permissions: + contents: read + runs-on: ubuntu-slim + timeout-minutes: 10 + + concurrency: + cancel-in-progress: ${{ github.event_name == 'pull_request' || false }} + group: pr-checks-changetool-tests-${{ github.ref }}-${{ github.event_name }} + + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run changetool unit tests + run: npm --workspace changetool test + # These checks do not need to be run as part of the same matrix that we use for the `unit-tests` # job. other-checks: diff --git a/eslint.config.mjs b/eslint.config.mjs index 34fe49a9d..f82f07132 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -209,4 +209,18 @@ export default [ ], }, }, + { + files: ["scripts/changetool/**/*.ts"], + + languageOptions: { + parserOptions: { + project: "./scripts/changetool/tsconfig.json", + }, + }, + + rules: { + "no-console": "off", + "import/extensions": "off", + }, + }, ]; diff --git a/package-lock.json b/package-lock.json index 6e59f20e9..91bb6d7f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "4.37.10", "license": "MIT", "workspaces": [ - "pr-checks" + "pr-checks", + "scripts/changetool" ], "dependencies": { "@actions/artifact": "^5.0.3", @@ -2545,6 +2546,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha1-ItHMnVQtNZPK6nZPl0MGqzYobuc=", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2578,6 +2588,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha1-fM9y7dLxqn3TQ34YDGQ3NYWATdY=", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha1-BSqmekjszEMJ1/AZG35BQ0uQu3g=", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", @@ -2637,6 +2662,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha1-rKqw+RnOaczmKcLU7S60rcG2wgw=", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.67.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", @@ -2991,6 +3022,346 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha1-zcfOgdYPHgkDSWDd+x+4gNendrY=", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha1-pV/fz6WN9Y0n2yI3zealweNacjU=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha1-ONHJFygAqR1we+xk0qNwoBZjTbQ=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha1-8f+IEAMLNdK1vg22otxlBGDqlPo=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha1-PYawPzU8WxupUWLrbONVM7/ClL0=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha1-rZS0HhruKk3MaimMe2fEM0X94y4=", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha1-2TNNltbaxv+F2pyGVYiUjek56R8=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha1-KWWu5PyHM2ATnYk9qv5jl6KROK0=", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha1-Goh6MRvtOoM/gL/Uqe03wnGTbPA=", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha1-i2PJsvRFs5PrTkPsIdoiXa3jV30=", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha1-tuijXCibPql6kqQdRhqu7Q07NuE=", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha1-Lvlmk75IYfbReWVCflsAnLvtGj4=", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha1-cyacsLq6UK6gygYERaa4jlg/HOI=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha1-OjZJ+X+vohC05uN5jBXgZgXIqQE=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha1-R+xZSRpAxHDSgH3E0rglUo/Zeas=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha1-eWvo2gvZidij+5bygB44qDZbS68=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha1-03/ipynrlCwHbEVO5/GBX699Vg8=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha1-q6jTRkw1ZacER4m6upaRa9SrLIg=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha1-ud5QoXGWOD9iYgtfnQovNK07YNc=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha1-zzt7DWzlY12spMjgHBic3N5H7Dw=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/@typespec/ts-http-runtime": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz", @@ -4123,6 +4494,20 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/changetool": { + "resolved": "scripts/changetool", + "link": true + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha1-LQnC5yzZUjB2zLIRV9/2atQ/zCI=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -4508,6 +4893,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha1-PkBgN2CHTC5YZ2kbWZ1zp9oltT8=", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "dev": true, @@ -4551,6 +4949,15 @@ "version": "2.3.1", "license": "ISC" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha1-JkQhTxmX057Q7g7OcjNUkKesZ74=", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -4561,6 +4968,19 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha1-TbfCyk3G4Og0wwvnDJS7yXbccBg=", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/diff": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", @@ -7166,6 +7586,18 @@ "node": ">= 0.8.0" } }, + "node_modules/lite-matter": { + "version": "0.1.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lite-matter/-/lite-matter-0.1.2.tgz", + "integrity": "sha1-NlO1r/xDs8OnKQcf/kw4/a4O1Ws=", + "license": "MIT", + "dependencies": { + "yaml": "^2.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/mgks" + } + }, "node_modules/load-json-file": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-7.0.1.tgz", @@ -7289,6 +7721,43 @@ "node": ">=8" } }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha1-yVgiuRqrdfGKTL6LL1G4c+0s8Mc=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha1-elEhR1VWoE5+3etnsmSq550xKBQ=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/memoize": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.2.0.tgz", @@ -7315,6 +7784,448 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha1-kTlaPhiEoZjmIRbjPJxWjjmTb9s=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha1-xpFjDkhQIaaM8o28Kyyifr9njNQ=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha1-j++OD3CB8EdPvdkt61DJkKAmRjk=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha1-UmfvqX8eUlTvx/ILRZo4yyEFi6E=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha1-NtAhLpYrKzEh+FJfx6PHwCnzNPw=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha1-I35KpdWKlYY/AQMtnumwkPHebpQ=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha1-BrJrKYPE0nv8xlezPiUTTUhosLE=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha1-L5h4MaQNTFEKwmHomFLE6XA8zaY=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha1-R/vNk0caP8yrhs/wOEf8NVLbEFE=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha1-05n6+cRcoUyLS+mLHqSBvO2Htik=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha1-Kg9JCrCL/1zC/V7sbdDKBPibMKk=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha1-/PFbZgl5OI5vEYzba/fXnXPSb+U=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha1-bLmVguXScehO/KjmGoB5lNcWHrI=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha1-DVHRwJVVHPqsNoMmljz1XxX1QLg=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha1-5AQDCWSBmGtBwQZif5j3LU0QuCU=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha1-ww13sugyrPZSb4vxqke8nJQ4wW0=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha1-4aLWLN0jcjCirhGDkCexk4HjHos=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha1-q4l4m4GKWHUrc9a1UjhiG3+qj9c=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha1-2K3lug8xl6HPaimZ+7/mNXoaGe4=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha1-5dpJTo6ysHGg0I+zT2zv7GwKGbg=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha1-8AIl9fWg68MlT5bDa2YFxLOTkI4=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -9381,6 +10292,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha1-RJxuIaiA4IVb9aq63rOnQDFKusI=", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universal-user-agent": { "version": "6.0.0", "license": "ISC" @@ -9810,6 +10734,71 @@ "@types/node": "^20.19.43", "tsx": "^4.23.12" } + }, + "scripts/changetool": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "lite-matter": "^0.1.2", + "mdast-util-from-markdown": "^2.0.3" + }, + "devDependencies": { + "@types/node": "^26.2.0", + "tsx": "^4.23.12", + "typescript": "^7.0.2" + } + }, + "scripts/changetool/node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/node/-/node-26.2.0.tgz", + "integrity": "sha1-Wkh1qGL9qP3Ffej6pXm7gey6FoU=", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "scripts/changetool/node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha1-nsdz15VKjBgsF8xbvVdaoovFFYI=", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "scripts/changetool/node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha1-ROn8nzJEZIzeo15Pm7LWgelBCAk=", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index da24ad389..ce523a32d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ }, "license": "MIT", "workspaces": [ - "pr-checks" + "pr-checks", + "scripts/changetool" ], "dependencies": { "@actions/artifact": "^5.0.3", diff --git a/scripts/changetool/cli/validate.test.ts b/scripts/changetool/cli/validate.test.ts new file mode 100644 index 000000000..ac54972fd --- /dev/null +++ b/scripts/changetool/cli/validate.test.ts @@ -0,0 +1,200 @@ +import assert from "node:assert/strict"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { describe, it } from "node:test"; + +import { + isValidChangenoteContent, + isValidChangenoteFile, + isValidChangenoteFilename, + hasValidChangenoteCategory, + VALID_CHANGE_NOTE_CATEGORIES, +} from "./validate.ts"; + +async function withTmpFile( + baseFileName: string, + contents: string, + body: (filePath: string) => Promise, +): Promise { + const tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), "changetool-validate-test-"), + ); + try { + const filePath = path.join(tmpDir, baseFileName); + fs.writeFileSync(filePath, contents); + return await body(filePath); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +} + +await describe("isValidChangenoteContent", async () => { + await it("recognizes an unordered Markdown list", async () => { + const inputs = [ + "- One changenote entry", + "- First item\n- Second item", + "\n\n\n\n- Fixed a bug\n- Added a feature", + ]; + + for (const input of inputs) { + assert.equal(isValidChangenoteContent(input), true); + } + }); + + await it("does not recognize non-Markdown text", async () => { + const inputs = [ + "This is not a list.", + '["this", "is", "JSON"]', + "---", + "***", + "___", + "paragraph", + ]; + + for (const input of inputs) { + assert.equal(isValidChangenoteContent(input), false); + } + }); + + await it("does not recognize ordered Markdown lists", async () => { + const inputs = [ + "1. First item\n2. Second item", + "\n\n\n1. First item\n1. Second item", + ]; + + for (const input of inputs) { + assert.equal(isValidChangenoteContent(input), false); + } + }); + + await it("requires all list items to use a hyphen bullet", async () => { + const inputs = [ + "* Fixed a bug\n* Added feature", + "+ Fixed a bug\n+ Added feature", + "- Fixed a bug\n* Added feature", + "- Fixed a bug\n+ Added feature", + "- Fixed a bug\n * Added feature\n + Updated docs", + "\n\n\n* Fixed a bug", + "\n\n\n+ Fixed a bug", + "---\n* Fixed a bug\n* Added feature", + ] as const; + + for (const input of inputs) { + assert.equal(isValidChangenoteContent(input), false); + } + }); + + await it("does not contain other Markdown elements", async () => { + const inputs = [ + "- Fixed a bug\n\nParagraph of text", + "- Fixed a bug\n\n* Added a feature", + "# Header\n- Fixed a bug", + "- Fixed a bug\n## Subheader", + ]; + + for (const input of inputs) { + assert.equal(isValidChangenoteContent(input), false); + } + }); +}); + +await describe("isValidChangenoteFilename", async () => { + await it("accepts valid filenames", async () => { + const inputs = [ + "2023-01-01-fix-bug.md", + "2023-12-31-add-feature.md", + "2023-06-15-update-docs.md", + ]; + + for (const input of inputs) { + assert.equal(isValidChangenoteFilename(input), true); + } + }); + + await it("rejects invalid filenames", async () => { + const inputs = [ + "missing-date-from-filename.md", + "2021-01-01.md", + "2026-12-19-wrong-file-name-extension.txt", + ]; + + for (const input of inputs) { + assert.equal(isValidChangenoteFilename(input), false); + } + }); +}); + +await describe("hasValidChangenoteCategory", async () => { + await it("accepts valid categories", async () => { + for (const category of Object.keys(VALID_CHANGE_NOTE_CATEGORIES)) { + const frontmatter = { category }; + assert.equal(hasValidChangenoteCategory(frontmatter), true); + } + }); + + await it("rejects invalid categories", async () => { + const inputs = [ + "", + "invalid-category", + "bug-fix", + "new-feature", + "security-patch", + "miscellaneous", + "documentation", + ]; + + for (const category of inputs) { + const frontmatter = { category }; + assert.equal(hasValidChangenoteCategory(frontmatter), false); + } + }); + + await it("reject missing category", async () => { + assert.equal(hasValidChangenoteCategory({}), false); + assert.equal(hasValidChangenoteCategory({ category: null }), false); + assert.equal(hasValidChangenoteCategory({ category: undefined }), false); + }); +}); + +await describe("isValidChangenoteFile", async () => { + await it("accepts a valid change-note file", async () => { + await withTmpFile( + "2026-01-01-fix-bug.md", + "---\ncategory: fix\n---\n- Fixed a bug\n", + async (filePath) => { + assert.equal(isValidChangenoteFile(filePath), true); + }, + ); + }); + + await it("rejects invalid filename", async () => { + await withTmpFile( + "fix-bug.md", + "---\ncategory: fix\n---\n- Fixed a bug\n", + async (filePath) => { + assert.equal(isValidChangenoteFile(filePath), false); + }, + ); + }); + + await it("rejects missing frontmatter", async () => { + await withTmpFile( + "2026-01-01-fix-bug.md", + "- Fixed a bug\n", + async (filePath) => { + assert.equal(isValidChangenoteFile(filePath), false); + }, + ); + }); + + await it("rejects invalid Markdown", async () => { + await withTmpFile( + "2026-01-01-fix-bug.md", + "---\ncategory: fix\n---\n* Fixed a bug\n", + async (filePath) => { + assert.equal(isValidChangenoteFile(filePath), false); + }, + ); + }); +}); diff --git a/scripts/changetool/cli/validate.ts b/scripts/changetool/cli/validate.ts new file mode 100644 index 000000000..3c83276f3 --- /dev/null +++ b/scripts/changetool/cli/validate.ts @@ -0,0 +1,121 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; + +import { matter } from "lite-matter"; +import type { List, ListItem } from "mdast"; +import { fromMarkdown } from "mdast-util-from-markdown"; + +// Regex for filename: YYYY-MM-DD-id.md +const VALID_CHANGE_NOTE_FILENAME_PATTERN = + /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])-([a-z0-9]+(?:-[a-z0-9]+)*)\.md$/; + +export const VALID_CHANGE_NOTE_CATEGORIES = { + breaking: "Breaking Changes", + feature: "New Features", + improvement: "Improvements", + securityFix: "Security Fixes", + fix: "Bug Fixes", + unship: "Removed Features", + deprecation: "Deprecations", + knownIssue: "Known Issues", + misc: "Miscellaneous", +}; + +/** + * Validates that the given Markdown string meets the criteria for a change-note, which is: + * - A single unordered list + * - Each list item must start with a hyphen (-) + * - No other Markdown elements are allowed + * @param content The Markdown string to validate + * @returns True if the string is a valid change-note, false otherwise + */ +export function isValidChangenoteContent(content: string): boolean { + const ast = fromMarkdown(content); + const lines = content.split("\n"); + + function listHasHyphenBullets(node: List | ListItem): boolean { + if (node.type === "list") { + return node.children.every(listHasHyphenBullets); + } + + const line = lines[node.position!.start.line - 1].trim(); + return ( + line.startsWith("-") && + node.children.every( + (child) => child.type !== "list" || listHasHyphenBullets(child), + ) + ); + } + + return ( + ast.children.length === 1 && + ast.children[0].type === "list" && + ast.children[0].ordered === false && + listHasHyphenBullets(ast.children[0]) + ); +} + +/** + * Validates that the given filename meets the criteria for a change-note filename. + * @param filename The name of the change-note file to validate. + * @returns True if the filename is valid, false otherwise. + */ +export function isValidChangenoteFilename(filename: string): boolean { + return filename.match(VALID_CHANGE_NOTE_FILENAME_PATTERN) !== null; +} + +/** + * Validates that the given frontmatter has a valid change-note category. + * @param frontmatter The frontmatter object to validate. + * @returns True if the frontmatter has a valid category, false otherwise. + */ +export function hasValidChangenoteCategory( + frontmatter: Record, +): boolean { + const category = frontmatter["category"]; + return ( + typeof category === "string" && + Object.hasOwn(VALID_CHANGE_NOTE_CATEGORIES, category) + ); +} + +/** + * Validates that the given change-note file meets all of the criteria for a change-note. + * @param filename The name of the change-note file to validate. + * @returns True if the file is a valid change-note, false otherwise. + */ +export function isValidChangenoteFile(filename: string): boolean { + let isValid: boolean = true; + + let fileData: string | undefined; + try { + fileData = fs.readFileSync(filename, "utf8"); + } catch (error) { + console.error(`${filename}: failed to read file`, error); + return false; + } + + const { data: frontmatter, content } = matter(fileData); + + if (!isValidChangenoteFilename(path.basename(filename))) { + isValid = false; + console.error( + `${filename}: invalid filename; must match pattern YYYY-MM-DD-id.md`, + ); + } + if (!hasValidChangenoteCategory(frontmatter)) { + isValid = false; + const categories = Object.keys(VALID_CHANGE_NOTE_CATEGORIES).join(", "); + console.error( + `${filename}: invalid category; must be one of: ${categories}`, + ); + } + if (!isValidChangenoteContent(content)) { + isValid = false; + console.error( + `${filename}: invalid Markdown; content must be a single unordered list with hyphen bullets and no other Markdown elements`, + ); + } + + return isValid; +} diff --git a/scripts/changetool/index.ts b/scripts/changetool/index.ts new file mode 100644 index 000000000..249448b3f --- /dev/null +++ b/scripts/changetool/index.ts @@ -0,0 +1,51 @@ +import { pathToFileURL } from "node:url"; +import { parseArgs } from "node:util"; + +import { isValidChangenoteFile } from "./cli/validate.ts"; + +const entryPoint = process.argv[1]; +if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) { + try { + process.exit(main()); + } catch (error) { + console.error(error); + process.exit(1); + } +} + +function main(): number { + const { positionals } = parseArgs({ + allowPositionals: true, + strict: true, + }); + const [command, ...paths] = positionals; + switch (command) { + case undefined: + case "help": + return usage(); + case "validate": + return validate(paths); + default: + console.error(`Unknown command: ${command}`); + return 1; + } +} + +function usage(): number { + console.log("Usage: changetool 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)"); + return 1; + } + for (const path of paths) { + if (!isValidChangenoteFile(path)) { + valid = false; + } + } + return valid ? 0 : 1; +} diff --git a/scripts/changetool/package.json b/scripts/changetool/package.json new file mode 100644 index 000000000..7eb8ceb9b --- /dev/null +++ b/scripts/changetool/package.json @@ -0,0 +1,21 @@ +{ + "name": "changetool", + "version": "1.0.0", + "private": true, + "description": "Validates change-notes and merges them into CHANGELOG.md", + "license": "MIT", + "type": "module", + "scripts": { + "start": "tsx index.ts", + "test": "node --test --experimental-strip-types cli/*.test.ts" + }, + "devDependencies": { + "@types/node": "^26.2.0", + "tsx": "^4.23.12", + "typescript": "^7.0.2" + }, + "dependencies": { + "lite-matter": "^0.1.2", + "mdast-util-from-markdown": "^2.0.3" + } +} diff --git a/scripts/changetool/tsconfig.json b/scripts/changetool/tsconfig.json new file mode 100644 index 000000000..ee76bd186 --- /dev/null +++ b/scripts/changetool/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "preserve", + "allowImportingTsExtensions": true, + "rootDir": ".", + "sourceMap": false + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/tsconfig.json b/tsconfig.json index 66545447c..d2b39b5ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,5 +37,5 @@ "@octokit/core/dist-types/types": ["./node_modules/@octokit/core/dist-types/types.d.ts"] }, }, - "exclude": ["node_modules", "pr-checks"] + "exclude": ["node_modules", "pr-checks", "scripts/changetool"] }