Skip to content

Commit dc3a073

Browse files
authored
markdownlint early access tests (#42812)
1 parent 0292a6a commit dc3a073

20 files changed

+211
-30
lines changed

content/actions/examples/using-concurrency-expressions-and-a-test-matrix.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ layout: inline
1212
topics:
1313
- Workflows
1414
---
15-
15+
<!-- markdownlint-disable early-access-references -->
1616
{% data reusables.actions.enterprise-github-hosted-runners %}
1717

1818
## Example overview
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
export function testOptions(rule, module, strings) {
1+
export function testOptions(rule, module, { strings, files }) {
22
const config = {
33
default: false,
44
[rule]: true,
55
}
66

77
const options = {
8-
strings,
98
customRules: [module],
109
config,
1110
}
11+
if (strings) options.strings = strings
12+
if (files) options.files = files
1213
return options
1314
}

src/content-linter/lib/helpers/utils.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,5 @@ export function filterTokensByOrder(tokens, tokenOrder) {
9494
}
9595
return matches
9696
}
97+
98+
export const docsDomains = ['docs.github.com', 'help.github.com', 'developer.github.com']

src/content-linter/lib/init-test.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import markdownlint from 'markdownlint'
22

33
import { testOptions } from './default-markdownlint-options.js'
44

5-
export async function runRule(module, strings) {
6-
const options = testOptions(module.names[0], module, strings)
5+
export async function runRule(module, { strings, files } = {}) {
6+
if ((!strings && !files) || (strings && files))
7+
throw new Error('Must provide either Markdown strings or files to run a rule')
8+
9+
const options = testOptions(module.names[0], module, { strings, files })
710
return await markdownlint.promises.markdownlint(options)
811
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { addError } from 'markdownlint-rule-helpers'
2+
import yaml from 'js-yaml'
3+
4+
import frontmatter from '../../../../lib/read-frontmatter.js'
5+
import { getRange } from '../helpers/utils.js'
6+
7+
export const earlyAccessReferences = {
8+
names: ['GH035', 'early-access-references'],
9+
description:
10+
'Files that are not early access should not reference early-access or early-access files.',
11+
tags: ['early-access'],
12+
severity: 'error',
13+
information: new URL('https://github.com/github/docs/blob/main/src/content-linter/README.md'),
14+
function: function GH035(params, onError) {
15+
const filepath = params.name
16+
// Early access content is allowed to use early access references
17+
// There are several existing allowed references to `early access`
18+
// as a GitHub feature. This rule focuses on references to early
19+
// access pages.
20+
const isEarlyAccess = filepath.includes('early-access')
21+
if (isEarlyAccess) return
22+
23+
const earlyAccessRegex = /early-access/i
24+
const earlyAccessArticlesRegex = /-early-access-/
25+
26+
for (let i = 0; i < params.lines.length; i++) {
27+
const line = params.lines[i]
28+
const matches = line.match(earlyAccessRegex)
29+
if (matches && !earlyAccessArticlesRegex.test(line)) {
30+
addError(
31+
onError,
32+
i + 1,
33+
'An early access reference appears to be used in a non-early access doc. Remove early access references or disable this rule.',
34+
line,
35+
getRange(line, matches[0]),
36+
null, // No fix possible
37+
)
38+
}
39+
}
40+
41+
// The only Frontmatter property that is allowed to contain
42+
// early-access is the redirect_from property.
43+
// To get the list of frontmatter lines that are applicable
44+
// we can convert the frontmatter lines array to an object
45+
// remove the property then convert it back to strings.
46+
const frontmatterString = params.frontMatterLines.join('\n')
47+
const fm = frontmatter(frontmatterString).data
48+
delete fm.redirect_from
49+
// The landing page must link to early-access content so this
50+
// case is allowed.
51+
const isLandingPage = filepath === 'content/index.md'
52+
if (isLandingPage) delete fm.children
53+
const fmStrings = yaml.dump(fm).split('\n')
54+
55+
for (let i = 0; i < fmStrings.length; i++) {
56+
const fmLine = fmStrings[i]
57+
if (earlyAccessRegex.test(fmLine) && !earlyAccessArticlesRegex.test(fmLine)) {
58+
addError(
59+
onError,
60+
1,
61+
'Frontmatter: An early access reference appears to be used in a non-early access doc. Remove early access references or disable this rule.',
62+
fmLine,
63+
null,
64+
null, // No fix possible
65+
)
66+
}
67+
}
68+
},
69+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { internalLinksSlash } from './internal-links-slash.js'
99
import { imageAltTextExcludeStartWords } from './image-alt-text-exclude-start-words.js'
1010
import { listFirstWordCapitalization } from './list-first-word-capitalization.js'
1111
import { internalLinkPunctuation } from './internal-link-punctuation.js'
12+
import { earlyAccessReferences } from './early-access-references.js'
1213

1314
export const gitHubDocsMarkdownlint = {
1415
rules: [
@@ -22,5 +23,6 @@ export const gitHubDocsMarkdownlint = {
2223
imageAltTextExcludeStartWords,
2324
listFirstWordCapitalization,
2425
internalLinkPunctuation,
26+
earlyAccessReferences,
2527
],
2628
}

src/content-linter/scripts/markdownlint.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,13 @@ function formatResult(object) {
244244
delete acc.range
245245
return acc
246246
}
247+
if (key === 'lineNumber') {
248+
if (object.errorDetail.startsWith('Frontmatter:')) {
249+
delete acc.lineNumber
250+
acc.frontmatterError = true
251+
return acc
252+
}
253+
}
247254
acc[key] = value
248255
return acc
249256
}, formattedResult)

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ export const githubDocsConfig = {
4949
severity: 'error',
5050
'partial-markdown-files': true,
5151
},
52+
'early-access-references': {
53+
severity: 'error',
54+
'partial-markdown-files': true,
55+
},
5256
'search-replace': {
5357
severity: 'error',
5458
'severity-local-env': 'warning',
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
title: early-access secret
3+
descriptions: Early Access secret
4+
versions:
5+
fpt: '*'
6+
ghes: '*'
7+
ghae: '*'
8+
ghec: '*'
9+
---
10+
11+
Links that are not allowed:
12+
13+
- [link text](/early-access/github/blah)
14+
15+
[link text](https://docs.github.com/early-access/github/blah)
16+
[link-definition-ref][]
17+
18+
[link-definition-ref]: http://help.github.com/early-access/github/blah
19+
20+
Links that are allowed:
21+
[Node.js](https://example.org/early-access/)
22+
23+
Images that are not allowed:
24+
- ![image text](/assets/images/early-access/github/blah.gif)
25+
![image text] (https://docs.github.com/assets/images/early-access/github/blah.gif)
26+
[image-definition-ref]: http://help.github.com/assets/images/early-access/github/blah.gif
27+
![link text](/assets/images/early-access/github/blah.gif)
28+
29+
Images that are allowed:
30+
![Node.js](https://example.org/assets/images/early-access/blah.gif)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
title: early-access secret
3+
descriptions: Early-Access secret
4+
redirect_from:
5+
- /early-access/secret
6+
versions:
7+
fpt: '*'
8+
ghes: '*'
9+
ghae: '*'
10+
ghec: '*'
11+
---
12+
13+
Early-access
14+
path/to-about-early-access-at-github.md
15+
Early access is ok
16+
17+
- [link text](/early-access/github/blah)
18+
19+
[link text](https://docs.github.com/early-access/github/blah)
20+
[link-definition-ref][]
21+
22+
[Node.js](https://example.org/early-access/)<!-- markdownlint-disable-line early-access-references -->
23+
24+
- ![image text](/assets/images/early-access/github/blah.gif)
25+
![image text](https://docs.github.com/assets/images/early-access/github/blah.gif)
26+
27+
![image text](/assets/images/early-access/github/blah.gif)
28+
![image-definition-ref][]
29+
30+
![Node.js](https://example.org/assets/images/early-access/blah.gif)<!-- markdownlint-disable-line early-access-references -->
31+
32+
[link-definition-ref]: http://help.github.com/early-access/github/blah
33+
[image-definition-ref]: http://help.github.com/assets/images/early-access/github/blah.gif

src/content-linter/tests/unit/code-fence-line-length.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe(codeFenceLineLength.names.join(' - '), () => {
1414
'bbb',
1515
'```',
1616
].join('\n')
17-
const result = await runRule(codeFenceLineLength, { markdown })
17+
const result = await runRule(codeFenceLineLength, { strings: { markdown } })
1818
const errors = result.markdown
1919
expect(errors.length).toBe(1)
2020
expect(errors[0].lineNumber).toBe(3)
@@ -29,7 +29,7 @@ describe(codeFenceLineLength.names.join(' - '), () => {
2929
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
3030
'```',
3131
].join('\n')
32-
const result = await runRule(codeFenceLineLength, { markdown })
32+
const result = await runRule(codeFenceLineLength, { strings: { markdown } })
3333
const errors = result.markdown
3434
expect(errors.length).toBe(0)
3535
})
@@ -41,7 +41,7 @@ describe(codeFenceLineLength.names.join(' - '), () => {
4141
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbb',
4242
'```',
4343
].join('\n')
44-
const result = await runRule(codeFenceLineLength, { markdown })
44+
const result = await runRule(codeFenceLineLength, { strings: { markdown } })
4545
const errors = result.markdown
4646
expect(errors.length).toBe(2)
4747
expect(errors[0].lineNumber).toBe(2)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { expect, jest } from '@jest/globals'
2+
3+
import { runRule } from '../../lib/init-test.js'
4+
import { earlyAccessReferences } from '../../lib/linting-rules/early-access-references.js'
5+
6+
const FIXTURE_FILEPATH_NON_EA = 'src/content-linter/tests/fixutres/not-secret.md'
7+
const FIXTURE_FILEPATH_EA = 'src/content-linter/tests/fixutres/early-access/secret.md'
8+
jest.setTimeout(20 * 1000)
9+
10+
describe(earlyAccessReferences.names.join(' - '), () => {
11+
test('non-early access file with early access references fails', async () => {
12+
const result = await runRule(earlyAccessReferences, { files: [FIXTURE_FILEPATH_NON_EA] })
13+
const errors = result[FIXTURE_FILEPATH_NON_EA]
14+
expect(errors.length).toBe(10)
15+
// Frontmatter errors won't have an accurate line number
16+
// and will have "Frontmatter: " prepended to the errorDetail message
17+
const markdownErrors = errors.filter((error) => !error.errorDetail.startsWith('Frontmatter:'))
18+
const lineNumbers = markdownErrors.map((error) => error.lineNumber)
19+
expect(lineNumbers.includes(13)).toBe(true)
20+
expect(lineNumbers.includes(14)).toBe(false)
21+
expect(markdownErrors[0].errorRange).toEqual([1, 12])
22+
expect(markdownErrors[1].errorRange).toEqual([16, 12])
23+
})
24+
test('early access file with early access references passes', async () => {
25+
const result = await runRule(earlyAccessReferences, { files: [FIXTURE_FILEPATH_EA] })
26+
const errors = result[FIXTURE_FILEPATH_EA]
27+
expect(errors.length).toBe(0)
28+
})
29+
})

src/content-linter/tests/unit/image-alt-text-end-punctuation.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe(imageAltTextEndPunctuation.names.join(' - '), () => {
1414
'',
1515
'!["image"](./image.png)',
1616
].join('\n')
17-
const result = await runRule(imageAltTextEndPunctuation, { markdown })
17+
const result = await runRule(imageAltTextEndPunctuation, { strings: { markdown } })
1818
const errors = result.markdown
1919
expect(errors.length).toBe(2)
2020
expect(errors.map((error) => error.lineNumber)).toEqual([3, 5])
@@ -48,7 +48,7 @@ describe(imageAltTextEndPunctuation.names.join(' - '), () => {
4848
'!["image"?](./image.png)',
4949
'!["image."](./image.png)',
5050
].join('\n')
51-
const result = await runRule(imageAltTextEndPunctuation, { markdown })
51+
const result = await runRule(imageAltTextEndPunctuation, { strings: { markdown } })
5252
const errors = result.markdown
5353
expect(errors.length).toBe(0)
5454
})
@@ -59,7 +59,7 @@ describe(imageAltTextEndPunctuation.names.join(' - '), () => {
5959
// Completely empty
6060
'![](/images/this-is-ok.png)',
6161
].join('\n')
62-
const result = await runRule(imageAltTextEndPunctuation, { markdown })
62+
const result = await runRule(imageAltTextEndPunctuation, { strings: { markdown } })
6363
const errors = result.markdown
6464
// This rule is not concerned with empty alt text
6565
// That will be caught by the incorrect-alt-text-length rule

src/content-linter/tests/unit/image-alt-text-exclude-start-words.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe(imageAltTextExcludeStartWords.names.join(' - '), () => {
1313
'![Graphic with alt text](/images/graphic-with-alt-text.png)',
1414
'![graphic with alt text](/images/graphic-with-alt-text.png)',
1515
].join('\n')
16-
const result = await runRule(imageAltTextExcludeStartWords, { markdown })
16+
const result = await runRule(imageAltTextExcludeStartWords, { strings: { markdown } })
1717
const errors = result.markdown
1818
expect(errors.length).toBe(4)
1919
expect(errors[0].lineNumber).toBe(1)
@@ -28,7 +28,7 @@ describe(imageAltTextExcludeStartWords.names.join(' - '), () => {
2828
'![This is ok image](/images/this-is-ok.png)',
2929
'![This is ok grapic](/images/this-is-ok.png)',
3030
].join('\n')
31-
const result = await runRule(imageAltTextExcludeStartWords, { markdown })
31+
const result = await runRule(imageAltTextExcludeStartWords, { strings: { markdown } })
3232
const errors = result.markdown
3333
expect(errors.length).toBe(0)
3434
})
@@ -39,7 +39,7 @@ describe(imageAltTextExcludeStartWords.names.join(' - '), () => {
3939
// Completely empty
4040
'![](/images/this-is-ok.png)',
4141
].join('\n')
42-
const result = await runRule(imageAltTextExcludeStartWords, { markdown })
42+
const result = await runRule(imageAltTextExcludeStartWords, { strings: { markdown } })
4343
const errors = result.markdown
4444
// This rule is not concerned with empty alt text
4545
// That will be caught by the incorrect-alt-text-empty rule

src/content-linter/tests/unit/image-alt-text-length.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe(incorrectAltTextLength.names.join(' - '), () => {
77
`![${'x'.repeat(39)}](./image.png)`,
88
`![${'x'.repeat(151)}](./image.png)`,
99
].join('\n')
10-
const result = await runRule(incorrectAltTextLength, { markdown })
10+
const result = await runRule(incorrectAltTextLength, { strings: { markdown } })
1111
const errors = result.markdown
1212
expect(errors.length).toBe(2)
1313
expect(errors[0].lineNumber).toBe(1)
@@ -20,7 +20,7 @@ describe(incorrectAltTextLength.names.join(' - '), () => {
2020
`![${'x'.repeat(40)}](./image.png)`,
2121
`![${'x'.repeat(150)}](./image.png)`,
2222
].join('\n')
23-
const result = await runRule(incorrectAltTextLength, { markdown })
23+
const result = await runRule(incorrectAltTextLength, { strings: { markdown } })
2424
const errors = result.markdown
2525
expect(errors.length).toBe(0)
2626
})
@@ -31,7 +31,7 @@ describe(incorrectAltTextLength.names.join(' - '), () => {
3131
// Completely empty
3232
'![](/images/this-is-ok.png)',
3333
].join('\n')
34-
const result = await runRule(incorrectAltTextLength, { markdown })
34+
const result = await runRule(incorrectAltTextLength, { strings: { markdown } })
3535
const errors = result.markdown
3636
expect(errors.length).toBe(1)
3737
expect(errors[0].lineNumber).toBe(3)

src/content-linter/tests/unit/image-file-kebab.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe(imageFileKebab.names.join(' - '), () => {
1515
'![Image.](imageFile-Location.png)',
1616
'![Image.](image-file-Location.jpg)',
1717
].join('\n')
18-
const result = await runRule(imageFileKebab, { markdown })
18+
const result = await runRule(imageFileKebab, { strings: { markdown } })
1919
const errors = result.markdown
2020
expect(errors.length).toBe(4)
2121
expect(errors.map((error) => error.lineNumber)).toEqual([3, 4, 5, 6])
@@ -24,7 +24,7 @@ describe(imageFileKebab.names.join(' - '), () => {
2424
})
2525
test('image file using lowercase kebab case passes', async () => {
2626
const markdown = ['![Image.](image-file.jpg)'].join('\n')
27-
const result = await runRule(imageFileKebab, { markdown })
27+
const result = await runRule(imageFileKebab, { strings: { markdown } })
2828
const errors = result.markdown
2929
expect(errors.length).toBe(0)
3030
})

0 commit comments

Comments
 (0)