From 2772054c9a760976ba7de37878dfeb8d53d7a02e Mon Sep 17 00:00:00 2001 From: Lovisa Berggren Date: Mon, 24 Feb 2025 17:27:24 +0000 Subject: [PATCH 1/6] CLOUDP-301224: Generate README with IPA rules --- .github/workflows/code-health-tools.yml | 8 ++ package-lock.json | 13 +++ package.json | 2 + .../ipa/scripts/generateRulesetReadme.js | 96 +++++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 tools/spectral/ipa/scripts/generateRulesetReadme.js diff --git a/.github/workflows/code-health-tools.yml b/.github/workflows/code-health-tools.yml index 764b2b3c67..67e50dde44 100644 --- a/.github/workflows/code-health-tools.yml +++ b/.github/workflows/code-health-tools.yml @@ -87,6 +87,14 @@ jobs: - name: Run ESLint on JS files run: | npm run lint-js + - name: Check IPA docs up-to-date + run: | + npm run gen-ipa-docs + if [ -n $(git status --porcelain) ]; then + echo "IPA docs not up to date, please run 'npm run gen-ipa-docs' and commit the changes" + exit 1 + fi + exit 0 - name: Install Go uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 with: diff --git a/package-lock.json b/package-lock.json index 73b6838083..ded80a64f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "apache-arrow": "^19.0.0", "dotenv": "^16.4.7", "eslint-plugin-jest": "^28.10.0", + "markdown-table": "^3.0.4", "openapi-to-postmanv2": "4.25.0", "parquet-wasm": "^0.6.1" }, @@ -30,6 +31,9 @@ "globals": "^15.14.0", "jest": "^29.7.0", "prettier": "3.5.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@ampproject/remapping": { @@ -8876,6 +8880,15 @@ "integrity": "sha512-Trz4v0+XWlwy68LJIyw3bLbsJiC8XAbRCKF9DbEtZjyndKOGVx6n+wNB0VfoRmY2LKboQLeniap3xrb6LGSJ8A==", "license": "MIT" }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index b045a2db2e..4c9030b62e 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "format": "npx prettier . --write", "format-check": "npx prettier . --check", "lint-js": "npx eslint **/*.js", + "gen-ipa-docs": "node tools/spectral/ipa/scripts/generateRulesetReadme.js", "ipa-validation": "spectral lint ./openapi/v2.yaml --ruleset=./tools/spectral/ipa/ipa-spectral.yaml", "test": "jest" }, @@ -28,6 +29,7 @@ "apache-arrow": "^19.0.0", "dotenv": "^16.4.7", "eslint-plugin-jest": "^28.10.0", + "markdown-table": "^3.0.4", "openapi-to-postmanv2": "4.25.0", "parquet-wasm": "^0.6.1" }, diff --git a/tools/spectral/ipa/scripts/generateRulesetReadme.js b/tools/spectral/ipa/scripts/generateRulesetReadme.js new file mode 100644 index 0000000000..ac7529fb4b --- /dev/null +++ b/tools/spectral/ipa/scripts/generateRulesetReadme.js @@ -0,0 +1,96 @@ +import fs from 'node:fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import spectral from '@stoplight/spectral-core'; +import { markdownTable } from 'markdown-table'; +import { loadRuleset } from '../metrics/utils/metricCollectionUtils.js'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); +const readmeFilePath = path.join(dirname, '../rulesets', 'README.md'); + +const rulesetsSection = await getRulesetsSection(); + +const fileContent = + '\n\n' + + '# IPA Validation Rules\n\n' + + 'All Spectral rules used in the IPA validation are defined in rulesets grouped by IPA number (`IPA-XXX.yaml`). These rulesets are imported into the main IPA ruleset [ipa-spectral.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/ipa-spectral.yaml) which is used for running the validation.\n\n' + + `${rulesetsSection}` + + '\n'; + +fs.writeFile(readmeFilePath, fileContent, (error) => { + if (error) { + console.error('Error while generating the IPA rulesets README.md:', error); + process.exit(1); + } + console.log('Successfully updated the IPA rulesets README.md'); +}); + +async function getRulesetsSection() { + let content = + '## Rulesets\n\n' + 'The tables below lists all available rules, their descriptions and severity level.\n\n'; + + const rules = await getAllRules(); + const ruleNames = Object.keys(rules); + const ipaNumbers = getIpaNumbers(ruleNames); + + ipaNumbers.forEach((ipaNumber) => { + const ipaRules = filterRulesByIpaNumber(ipaNumber, rules); + const table = generateRulesetTable(ipaRules); + content += `### ${ipaNumber}\n\n` + `${table}\n\n`; + }); + + return content; +} + +function generateRulesetTable(rules) { + const table = [['Rule Name', 'Description', 'Severity']]; + const tableRows = []; + + const ruleNames = Object.keys(rules); + ruleNames.forEach((ruleName) => { + const rule = rules[ruleName]; + tableRows.push([ruleName, rule.description, rule.definition.severity]); + }); + + tableRows.sort(sortBySeverity); + tableRows.forEach((row) => table.push(row)); + return markdownTable(table); +} + +async function getAllRules() { + const rulesetFilePath = path.join(dirname, '..', 'ipa-spectral.yaml'); + const { Spectral } = spectral; + const ruleset = await loadRuleset(rulesetFilePath, new Spectral()); + return ruleset.rules; +} + +function getIpaNumbers(ruleNames) { + const ipaNumbers = []; + ruleNames.forEach((name) => { + const ipaName = name.substring(5, 12); + if (!ipaNumbers.includes(ipaName)) { + ipaNumbers.push(ipaName); + } + }); + return ipaNumbers.sort(); +} + +function filterRulesByIpaNumber(ipaNumber, rules) { + return Object.keys(rules) + .filter((key) => key.includes(ipaNumber)) + .reduce((obj, key) => { + return { + ...obj, + [key]: rules[key], + }; + }, {}); +} + +function sortBySeverity(a, b) { + if (a[2] < b[2]) { + return -1; + } else if (a[2] > b[2]) { + return 1; + } + return 0; +} From 0d7352d9ca6e7a59fd9fd0995afb61dbcae8668b Mon Sep 17 00:00:00 2001 From: Lovisa Berggren Date: Mon, 24 Feb 2025 17:30:05 +0000 Subject: [PATCH 2/6] CLOUDP-301224: Fix --- .github/workflows/code-health-tools.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-health-tools.yml b/.github/workflows/code-health-tools.yml index 67e50dde44..0456e24cd6 100644 --- a/.github/workflows/code-health-tools.yml +++ b/.github/workflows/code-health-tools.yml @@ -90,7 +90,7 @@ jobs: - name: Check IPA docs up-to-date run: | npm run gen-ipa-docs - if [ -n $(git status --porcelain) ]; then + if [[ -n $(git status --porcelain) ]]; then echo "IPA docs not up to date, please run 'npm run gen-ipa-docs' and commit the changes" exit 1 fi From 19ddd1a4a560630d4dc5429151351ba3fc583b67 Mon Sep 17 00:00:00 2001 From: Lovisa Berggren Date: Mon, 24 Feb 2025 17:31:23 +0000 Subject: [PATCH 3/6] CLOUDP-301224: Fix --- tools/spectral/ipa/scripts/generateRulesetReadme.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/spectral/ipa/scripts/generateRulesetReadme.js b/tools/spectral/ipa/scripts/generateRulesetReadme.js index ac7529fb4b..c4f5cc473f 100644 --- a/tools/spectral/ipa/scripts/generateRulesetReadme.js +++ b/tools/spectral/ipa/scripts/generateRulesetReadme.js @@ -22,7 +22,6 @@ fs.writeFile(readmeFilePath, fileContent, (error) => { console.error('Error while generating the IPA rulesets README.md:', error); process.exit(1); } - console.log('Successfully updated the IPA rulesets README.md'); }); async function getRulesetsSection() { From 69893be44dfde0673bdcd3d78493fb326d190eff Mon Sep 17 00:00:00 2001 From: Lovisa Berggren Date: Mon, 24 Feb 2025 17:37:50 +0000 Subject: [PATCH 4/6] CLOUDP-301224: Add readme --- tools/spectral/ipa/rulesets/README.md | 48 +++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tools/spectral/ipa/rulesets/README.md diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md new file mode 100644 index 0000000000..df0c55fcb0 --- /dev/null +++ b/tools/spectral/ipa/rulesets/README.md @@ -0,0 +1,48 @@ + + +# IPA Validation Rules + +All Spectral rules used in the IPA validation are defined in rulesets grouped by IPA number (`IPA-XXX.yaml`). These rulesets are imported into the main IPA ruleset [ipa-spectral.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/ipa-spectral.yaml) which is used for running the validation. + +## Rulesets + +The tables below lists all available rules, their descriptions and severity level. + +### IPA-005 + +| Rule Name | Description | Severity | +| --------------------------------------- | ------------------------------------------------------------------------ | -------- | +| xgen-IPA-005-exception-extension-format | IPA exception extensions must follow the correct format. http://go/ipa/5 | warn | + +### IPA-102 + +| Rule Name | Description | Severity | +| ---------------------------------------------------- | -------------------------------------------------------------------------------- | -------- | +| xgen-IPA-102-path-alternate-resource-name-path-param | Paths should alternate between resource names and path params. http://go/ipa/102 | warn | + +### IPA-104 + +| Rule Name | Description | Severity | +| ----------------------------- | --------------------------------------------------------------- | -------- | +| xgen-IPA-104-resource-has-GET | APIs must provide a get method for resources. http://go/ipa/104 | warn | + +### IPA-109 + +| Rule Name | Description | Severity | +| ---------------------------------------------- | ------------------------------------------------------------------------- | -------- | +| xgen-IPA-109-custom-method-must-be-GET-or-POST | The HTTP method for custom methods must be GET or POST. http://go/ipa/109 | warn | +| xgen-IPA-109-custom-method-must-use-camel-case | The custom method must use camelCase format. http://go/ipa/109 | warn | + +### IPA-113 + +| Rule Name | Description | Severity | +| --------------------------------------- | ------------------------------------------------------------------------------------------- | -------- | +| xgen-IPA-113-singleton-must-not-have-id | Singleton resources must not have a user-provided or system-generated ID. http://go/ipa/113 | warn | + +### IPA-123 + +| Rule Name | Description | Severity | +| ------------------------------------------------- | ------------------------------------------------------- | -------- | +| xgen-IPA-123-enum-values-must-be-upper-snake-case | Enum values must be UPPER_SNAKE_CASE. http://go/ipa/123 | warn | + + From 0f8b30283c801fd23edd3122037aa89bd8f49341 Mon Sep 17 00:00:00 2001 From: Lovisa Berggren Date: Mon, 24 Feb 2025 17:52:32 +0000 Subject: [PATCH 5/6] CLOUDP-301224: Add links to rule definitions --- tools/spectral/ipa/rulesets/README.md | 12 ++++++++++++ tools/spectral/ipa/scripts/generateRulesetReadme.js | 7 ++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index df0c55fcb0..1698192e74 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -10,24 +10,32 @@ The tables below lists all available rules, their descriptions and severity leve ### IPA-005 +For rule definitions, see [IPA-005.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-005.yaml). + | Rule Name | Description | Severity | | --------------------------------------- | ------------------------------------------------------------------------ | -------- | | xgen-IPA-005-exception-extension-format | IPA exception extensions must follow the correct format. http://go/ipa/5 | warn | ### IPA-102 +For rule definitions, see [IPA-102.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-102.yaml). + | Rule Name | Description | Severity | | ---------------------------------------------------- | -------------------------------------------------------------------------------- | -------- | | xgen-IPA-102-path-alternate-resource-name-path-param | Paths should alternate between resource names and path params. http://go/ipa/102 | warn | ### IPA-104 +For rule definitions, see [IPA-104.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-104.yaml). + | Rule Name | Description | Severity | | ----------------------------- | --------------------------------------------------------------- | -------- | | xgen-IPA-104-resource-has-GET | APIs must provide a get method for resources. http://go/ipa/104 | warn | ### IPA-109 +For rule definitions, see [IPA-109.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-109.yaml). + | Rule Name | Description | Severity | | ---------------------------------------------- | ------------------------------------------------------------------------- | -------- | | xgen-IPA-109-custom-method-must-be-GET-or-POST | The HTTP method for custom methods must be GET or POST. http://go/ipa/109 | warn | @@ -35,12 +43,16 @@ The tables below lists all available rules, their descriptions and severity leve ### IPA-113 +For rule definitions, see [IPA-113.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-113.yaml). + | Rule Name | Description | Severity | | --------------------------------------- | ------------------------------------------------------------------------------------------- | -------- | | xgen-IPA-113-singleton-must-not-have-id | Singleton resources must not have a user-provided or system-generated ID. http://go/ipa/113 | warn | ### IPA-123 +For rule definitions, see [IPA-123.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-123.yaml). + | Rule Name | Description | Severity | | ------------------------------------------------- | ------------------------------------------------------- | -------- | | xgen-IPA-123-enum-values-must-be-upper-snake-case | Enum values must be UPPER_SNAKE_CASE. http://go/ipa/123 | warn | diff --git a/tools/spectral/ipa/scripts/generateRulesetReadme.js b/tools/spectral/ipa/scripts/generateRulesetReadme.js index c4f5cc473f..ad45091132 100644 --- a/tools/spectral/ipa/scripts/generateRulesetReadme.js +++ b/tools/spectral/ipa/scripts/generateRulesetReadme.js @@ -35,7 +35,8 @@ async function getRulesetsSection() { ipaNumbers.forEach((ipaNumber) => { const ipaRules = filterRulesByIpaNumber(ipaNumber, rules); const table = generateRulesetTable(ipaRules); - content += `### ${ipaNumber}\n\n` + `${table}\n\n`; + content += + `### ${ipaNumber}\n\n` + `For rule definitions, see ${getIpaRulesetUrl(ipaNumber)}.\n\n` + `${table}\n\n`; }); return content; @@ -74,6 +75,10 @@ function getIpaNumbers(ruleNames) { return ipaNumbers.sort(); } +function getIpaRulesetUrl(ipaNumber) { + return `[${ipaNumber}.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/${ipaNumber}.yaml)`; +} + function filterRulesByIpaNumber(ipaNumber, rules) { return Object.keys(rules) .filter((key) => key.includes(ipaNumber)) From 0e99a094788c75640e32852e59e302623188b2b4 Mon Sep 17 00:00:00 2001 From: Lovisa Berggren Date: Tue, 25 Feb 2025 11:19:15 +0000 Subject: [PATCH 6/6] CLOUDP-301224: Refactor --- .../spectral/ipa/metrics/metricCollection.js | 5 ++- .../metrics/utils/metricCollectionUtils.js | 33 ++++--------------- .../ipa/scripts/generateRulesetReadme.js | 2 +- tools/spectral/ipa/utils.js | 21 ++++++++++++ 4 files changed, 30 insertions(+), 31 deletions(-) create mode 100644 tools/spectral/ipa/utils.js diff --git a/tools/spectral/ipa/metrics/metricCollection.js b/tools/spectral/ipa/metrics/metricCollection.js index 059e5f8740..2254c9ec6a 100644 --- a/tools/spectral/ipa/metrics/metricCollection.js +++ b/tools/spectral/ipa/metrics/metricCollection.js @@ -3,10 +3,9 @@ import { extractTeamOwnership, getSeverityPerRule, loadCollectorResults, - loadOpenAPIFile, - loadRuleset, merge, } from './utils/metricCollectionUtils.js'; +import { loadJsonFile, loadRuleset } from '../utils.js'; export async function runMetricCollectionJob( { @@ -18,7 +17,7 @@ export async function runMetricCollectionJob( ) { try { console.log(`Loading OpenAPI file: ${oasFilePath}`); - const oasContent = loadOpenAPIFile(oasFilePath); + const oasContent = loadJsonFile(oasFilePath); console.log('Extracting team ownership data...'); const ownershipData = extractTeamOwnership(oasContent); diff --git a/tools/spectral/ipa/metrics/utils/metricCollectionUtils.js b/tools/spectral/ipa/metrics/utils/metricCollectionUtils.js index 8222a40d1b..eabf88ccbf 100644 --- a/tools/spectral/ipa/metrics/utils/metricCollectionUtils.js +++ b/tools/spectral/ipa/metrics/utils/metricCollectionUtils.js @@ -1,15 +1,5 @@ -import fs from 'node:fs'; -import { bundleAndLoadRuleset } from '@stoplight/spectral-ruleset-bundler/with-loader'; import { EntryType } from '../collector.js'; - -export function loadOpenAPIFile(filePath) { - try { - const content = fs.readFileSync(filePath, 'utf8'); - return JSON.parse(content); - } catch (error) { - throw new Error(`Failed to load OpenAPI file: ${error.message}`); - } -} +import { loadJsonFile } from '../../utils.js'; export function getSeverityPerRule(ruleset) { const rules = ruleset.rules || {}; @@ -20,16 +10,6 @@ export function getSeverityPerRule(ruleset) { return map; } -export async function loadRuleset(rulesetPath, spectral) { - try { - const ruleset = await bundleAndLoadRuleset(rulesetPath, { fs, fetch }); - await spectral.setRuleset(ruleset); - return ruleset; - } catch (error) { - throw new Error(`Failed to load ruleset: ${error.message}`); - } -} - export function extractTeamOwnership(oasContent) { const ownerTeams = {}; const paths = oasContent.paths || {}; @@ -53,15 +33,14 @@ export function extractTeamOwnership(oasContent) { export function loadCollectorResults(collectorResultsFilePath) { try { - const content = fs.readFileSync(collectorResultsFilePath, 'utf8'); - const contentParsed = JSON.parse(content); + const content = loadJsonFile(collectorResultsFilePath); return { - [EntryType.VIOLATION]: contentParsed[EntryType.VIOLATION], - [EntryType.ADOPTION]: contentParsed[EntryType.ADOPTION], - [EntryType.EXCEPTION]: contentParsed[EntryType.EXCEPTION], + [EntryType.VIOLATION]: content[EntryType.VIOLATION], + [EntryType.ADOPTION]: content[EntryType.ADOPTION], + [EntryType.EXCEPTION]: content[EntryType.EXCEPTION], }; } catch (error) { - throw new Error(`Failed to load Collector Results file: ${error.message}`); + throw new Error(`Failed to parse Collector Results: ${error.message}`); } } diff --git a/tools/spectral/ipa/scripts/generateRulesetReadme.js b/tools/spectral/ipa/scripts/generateRulesetReadme.js index ad45091132..5e39fa18b6 100644 --- a/tools/spectral/ipa/scripts/generateRulesetReadme.js +++ b/tools/spectral/ipa/scripts/generateRulesetReadme.js @@ -3,7 +3,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; import spectral from '@stoplight/spectral-core'; import { markdownTable } from 'markdown-table'; -import { loadRuleset } from '../metrics/utils/metricCollectionUtils.js'; +import { loadRuleset } from '../utils.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); const readmeFilePath = path.join(dirname, '../rulesets', 'README.md'); diff --git a/tools/spectral/ipa/utils.js b/tools/spectral/ipa/utils.js new file mode 100644 index 0000000000..632f25fe19 --- /dev/null +++ b/tools/spectral/ipa/utils.js @@ -0,0 +1,21 @@ +import { bundleAndLoadRuleset } from '@stoplight/spectral-ruleset-bundler/with-loader'; +import fs from 'node:fs'; + +export async function loadRuleset(rulesetPath, spectral) { + try { + const ruleset = await bundleAndLoadRuleset(rulesetPath, { fs, fetch }); + await spectral.setRuleset(ruleset); + return ruleset; + } catch (error) { + throw new Error(`Failed to load ruleset: ${error.message}`); + } +} + +export function loadJsonFile(filePath) { + try { + const content = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(content); + } catch (error) { + throw new Error(`Failed to load file: ${error.message}`); + } +}