Skip to content

Commit 828ad86

Browse files
authored
Add GHD067 lint rule: require valid category on REST endpoint files (#61381)
1 parent 7b9cc24 commit 828ad86

16 files changed

Lines changed: 287 additions & 0 deletions

File tree

content/rest/agents/secrets.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ versions: # DO NOT MANUALLY EDIT. CHANGES WILL BE OVERWRITTEN BY A 🤖
77
ghec: '*'
88
autogenerated: rest
99
allowTitleToDifferFromFilename: true
10+
category:
11+
- Automate CI/CD workflows
1012
---
1113

1214
<!-- Content after this section is automatically generated -->

content/rest/agents/variables.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ versions: # DO NOT MANUALLY EDIT. CHANGES WILL BE OVERWRITTEN BY A 🤖
77
ghec: '*'
88
autogenerated: rest
99
allowTitleToDifferFromFilename: true
10+
category:
11+
- Automate CI/CD workflows
1012
---
1113

1214
<!-- Content after this section is automatically generated -->

content/rest/code-quality/code-quality.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ versions: # DO NOT MANUALLY EDIT. CHANGES WILL BE OVERWRITTEN BY A 🤖
66
fpt: '*'
77
autogenerated: rest
88
allowTitleToDifferFromFilename: true
9+
category:
10+
- Secure code and manage vulnerabilities
911
---
1012

1113
<!-- Content after this section is automatically generated -->

content/rest/copilot/copilot-cloud-agent-management.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ versions: # DO NOT MANUALLY EDIT. CHANGES WILL BE OVERWRITTEN BY A 🤖
77
ghec: '*'
88
autogenerated: rest
99
allowTitleToDifferFromFilename: true
10+
category:
11+
- Use Copilot and AI services
1012
---
1113

1214
<!-- Content after this section is automatically generated -->

content/rest/copilot/copilot-coding-agent-management.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ versions: # DO NOT MANUALLY EDIT. CHANGES WILL BE OVERWRITTEN BY A 🤖
99
ghec: '*'
1010
autogenerated: rest
1111
allowTitleToDifferFromFilename: true
12+
category:
13+
- Use Copilot and AI services
1214
---
1315

1416
<!-- Content after this section is automatically generated -->

content/rest/enterprise-admin/credential-authorizations.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ versions: # DO NOT MANUALLY EDIT. CHANGES WILL BE OVERWRITTEN BY A 🤖
66
ghec: '*'
77
autogenerated: rest
88
allowTitleToDifferFromFilename: true
9+
category:
10+
- Administer enterprises and billing
911
---
1012

1113
<!-- Content after this section is automatically generated -->

data/reusables/contributing/content-linter-rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
| GHD064 | rai-app-card-structure | RAI application/platform card articles must follow the required template structure | error | feature, rai |
6969
| GHD065 | frontmatter-content-type | Content files in content-type directories must have a contentType frontmatter property that matches the parent directory. | error | frontmatter, content-type |
7070
| GHD066 | frontmatter-docs-team-metrics | Articles whose path contains a path-enforced docsTeamMetrics value must include that value in their docsTeamMetrics frontmatter property. | error | frontmatter, docs-team-metrics |
71+
| GHD067 | frontmatter-rest-api-category | Autogenerated REST API endpoint files must have a valid `category` frontmatter property | error | frontmatter, rest, category |
7172
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | deprecated liquid syntax: octicon-<icon-name> | The octicon liquid syntax used is deprecated. Use this format instead `octicon "<octicon-name>" aria-label="<Octicon aria label>"` | error | |
7273
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | deprecated liquid syntax: site.data | Catch occurrences of deprecated liquid data syntax. | error | |
7374
| [search-replace](https://github.com/OnkarRuikar/markdownlint-rule-search-replace) | developer-domain | Catch occurrences of developer.github.com domain. | error | |
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
import matter from '@gr2m/gray-matter'
4+
import { addError } from 'markdownlint-rule-helpers'
5+
6+
import { getFrontmatter } from '../helpers/utils'
7+
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
8+
9+
// Lazily computed list of valid category values from content/rest/index.md.
10+
let validCategories: string[] | null = null
11+
12+
/**
13+
* Read the `includedCategories` frontmatter from content/rest/index.md.
14+
*/
15+
function getValidCategories(): string[] {
16+
if (validCategories) return validCategories
17+
18+
const restIndexPath = path.resolve(process.env.ROOT || '.', 'content/rest/index.md')
19+
const content = fs.readFileSync(restIndexPath, 'utf-8')
20+
const { data } = matter(content)
21+
validCategories = (data.includedCategories as string[]) || []
22+
return validCategories
23+
}
24+
25+
/**
26+
* Reset the cached valid categories. Exported so tests can call it
27+
* between test cases if the fixture changes.
28+
*/
29+
export function resetCache(): void {
30+
validCategories = null
31+
}
32+
33+
export const frontmatterRestApiCategory = {
34+
names: ['GHD067', 'frontmatter-rest-api-category'],
35+
description:
36+
'Autogenerated REST API endpoint files must have a valid `category` frontmatter property',
37+
tags: ['frontmatter', 'rest', 'category'],
38+
function: (params: RuleParams, onError: RuleErrorCallback) => {
39+
const filePath = params.name
40+
const rootDir = process.env.ROOT || '.'
41+
const restDir = path.resolve(rootDir, 'content/rest')
42+
43+
// Resolve the file path against CWD (markdownlint provides paths
44+
// relative to CWD when using `files`, or string keys when using `strings`).
45+
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(filePath)
46+
const relativePath = path.relative(restDir, resolved)
47+
48+
// Skip files that aren't under content/rest/
49+
if (relativePath.startsWith('..')) return
50+
51+
// Skip index files — they are category-level pages, not endpoint pages
52+
if (path.basename(resolved) === 'index.md') return
53+
54+
const fm = getFrontmatter(params.lines)
55+
if (!fm) return
56+
57+
// Only apply to autogenerated REST files
58+
if (fm.autogenerated !== 'rest') return
59+
60+
// Find error line: prefer the category line, fall back to opening ---
61+
const categoryLineIndex = params.lines.findIndex((line) =>
62+
line.trimStart().startsWith('category'),
63+
)
64+
const fmOpenLine = params.lines.indexOf('---')
65+
const errorLine =
66+
categoryLineIndex !== -1 ? categoryLineIndex + 1 : fmOpenLine !== -1 ? fmOpenLine + 1 : 1
67+
68+
// Check: category must exist
69+
if (!fm.category) {
70+
addError(
71+
onError,
72+
errorLine,
73+
'Missing `category` frontmatter. Autogenerated REST endpoint files must have a `category` property with a value from `content/rest/index.md` `includedCategories`.',
74+
undefined,
75+
undefined,
76+
undefined,
77+
)
78+
return
79+
}
80+
81+
// Check: category must be an array
82+
if (!Array.isArray(fm.category)) {
83+
addError(
84+
onError,
85+
errorLine,
86+
'`category` frontmatter must be an array.',
87+
undefined,
88+
undefined,
89+
undefined,
90+
)
91+
return
92+
}
93+
94+
// Check: category must have at least one value
95+
if (fm.category.length === 0) {
96+
addError(
97+
onError,
98+
errorLine,
99+
'`category` frontmatter must have at least one value.',
100+
undefined,
101+
undefined,
102+
undefined,
103+
)
104+
return
105+
}
106+
107+
// Check: each value must be in the valid list
108+
const valid = getValidCategories()
109+
for (const value of fm.category) {
110+
if (!valid.includes(value as string)) {
111+
addError(
112+
onError,
113+
errorLine,
114+
`Invalid category "${value}". Must be one of: ${valid.join(', ')}`,
115+
undefined,
116+
undefined,
117+
undefined,
118+
)
119+
}
120+
}
121+
},
122+
}

src/content-linter/lib/linting-rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { frontmatterCurlyQuotes } from './frontmatter-curly-quotes'
5757
import { raiAppCardStructure } from '@/content-linter/lib/linting-rules/rai-app-card-structure'
5858
import { frontmatterContentType } from '@/content-linter/lib/linting-rules/frontmatter-content-type'
5959
import { frontmatterDocsTeamMetrics } from '@/content-linter/lib/linting-rules/frontmatter-docs-team-metrics'
60+
import { frontmatterRestApiCategory } from '@/content-linter/lib/linting-rules/frontmatter-rest-api-category'
6061

6162
const noDefaultAltText = markdownlintGitHub.find((elem: { names: string[] }) =>
6263
elem.names.includes('no-default-alt-text'),
@@ -122,6 +123,7 @@ export const gitHubDocsMarkdownlint = {
122123
raiAppCardStructure, // GHD064
123124
frontmatterContentType, // GHD065
124125
frontmatterDocsTeamMetrics, // GHD066
126+
frontmatterRestApiCategory, // GHD067
125127

126128
// Search-replace rules
127129
searchReplace, // Open-source plugin

src/content-linter/style/github-docs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,12 @@ export const githubDocsFrontmatterConfig = {
307307
'partial-markdown-files': false,
308308
'yml-files': false,
309309
},
310+
'frontmatter-rest-api-category': {
311+
// GHD067
312+
severity: 'error',
313+
'partial-markdown-files': false,
314+
'yml-files': false,
315+
},
310316
}
311317

312318
// Configures rules from the `github/markdownlint-github` repo

0 commit comments

Comments
 (0)