@@ -2,7 +2,7 @@ const path = require('path')
2
2
const slash = require ( 'slash' )
3
3
const fs = require ( 'fs' )
4
4
const walk = require ( 'walk-sync' )
5
- const { zip } = require ( 'lodash' )
5
+ const { zip, groupBy } = require ( 'lodash' )
6
6
const yaml = require ( 'js-yaml' )
7
7
const revalidator = require ( 'revalidator' )
8
8
const generateMarkdownAST = require ( 'mdast-util-from-markdown' )
@@ -12,12 +12,14 @@ const languages = require('../../lib/languages')
12
12
const { tags } = require ( '../../lib/liquid-tags/extended-markdown' )
13
13
const ghesReleaseNotesSchema = require ( '../../lib/release-notes-schema' )
14
14
const renderContent = require ( '../../lib/render-content' )
15
+ const { execSync } = require ( 'child_process' )
15
16
16
17
const rootDir = path . join ( __dirname , '../..' )
17
18
const contentDir = path . join ( rootDir , 'content' )
18
19
const reusablesDir = path . join ( rootDir , 'data/reusables' )
19
20
const variablesDir = path . join ( rootDir , 'data/variables' )
20
21
const glossariesDir = path . join ( rootDir , 'data/glossaries' )
22
+ const ghesReleaseNotesDir = path . join ( rootDir , 'data/release-notes' )
21
23
22
24
const languageCodes = Object . keys ( languages )
23
25
@@ -149,13 +151,26 @@ const oldVariableErrorText = 'Found article uses old {{ site.data... }} syntax.
149
151
const oldOcticonErrorText = 'Found octicon variables with the old {{ octicon-name }} syntax. Use {% octicon "name" %} instead!'
150
152
const oldExtendedMarkdownErrorText = 'Found extended markdown tags with the old {{#note}} syntax. Use {% note %}/{% endnote %} instead!'
151
153
152
- describe ( 'lint-files' , ( ) => {
153
- const mdWalkOptions = {
154
- globs : [ '**/*.md' ] ,
155
- ignore : [ '**/README.md' ] ,
156
- directories : false ,
157
- includeBasePath : true
158
- }
154
+ const mdWalkOptions = {
155
+ globs : [ '**/*.md' ] ,
156
+ ignore : [ '**/README.md' ] ,
157
+ directories : false ,
158
+ includeBasePath : true
159
+ }
160
+
161
+ // Also test the "data/variables/" YAML files
162
+
163
+ const yamlWalkOptions = {
164
+ globs : [ '**/*.yml' ] ,
165
+ directories : false ,
166
+ includeBasePath : true
167
+ }
168
+
169
+ // different lint rules apply to different content types
170
+ let mdToLint , ymlToLint , releaseNotesToLint
171
+
172
+ if ( ! process . env . TEST_TRANSLATION ) {
173
+ // compile lists of all the files we want to lint
159
174
160
175
const contentMarkdownAbsPaths = walk ( contentDir , mdWalkOptions ) . sort ( )
161
176
const contentMarkdownRelPaths = contentMarkdownAbsPaths . map ( p => slash ( path . relative ( rootDir , p ) ) )
@@ -165,16 +180,81 @@ describe('lint-files', () => {
165
180
const reusableMarkdownRelPaths = reusableMarkdownAbsPaths . map ( p => slash ( path . relative ( rootDir , p ) ) )
166
181
const reusableMarkdownTuples = zip ( reusableMarkdownRelPaths , reusableMarkdownAbsPaths )
167
182
168
- describe . each ( [ ...contentMarkdownTuples , ...reusableMarkdownTuples ] ) (
169
- 'in "%s"' ,
183
+ mdToLint = [ ...contentMarkdownTuples , ...reusableMarkdownTuples ]
184
+
185
+ // data/variables
186
+ const variableYamlAbsPaths = walk ( variablesDir , yamlWalkOptions ) . sort ( )
187
+ const variableYamlRelPaths = variableYamlAbsPaths . map ( p => slash ( path . relative ( rootDir , p ) ) )
188
+ const variableYamlTuples = zip ( variableYamlRelPaths , variableYamlAbsPaths )
189
+
190
+ // data/glossaries
191
+ const glossariesYamlAbsPaths = walk ( glossariesDir , yamlWalkOptions ) . sort ( )
192
+ const glossariesYamlRelPaths = glossariesYamlAbsPaths . map ( p => slash ( path . relative ( rootDir , p ) ) )
193
+ const glossariesYamlTuples = zip ( glossariesYamlRelPaths , glossariesYamlAbsPaths )
194
+
195
+ ymlToLint = [ ...variableYamlTuples , ...glossariesYamlTuples ]
196
+
197
+ // GHES release notes
198
+ const ghesReleaseNotesYamlAbsPaths = walk ( ghesReleaseNotesDir , yamlWalkOptions ) . sort ( )
199
+ const ghesReleaseNotesYamlRelPaths = ghesReleaseNotesYamlAbsPaths . map ( p => path . relative ( rootDir , p ) )
200
+ releaseNotesToLint = zip ( ghesReleaseNotesYamlRelPaths , ghesReleaseNotesYamlAbsPaths )
201
+ } else {
202
+ console . log ( 'testing translations.' )
203
+
204
+ // get all translated markdown or yaml files by comparing files changed to main branch
205
+ const changedFilesRelPaths = execSync ( 'git diff --name-only origin/main | egrep "^translations/.*/.+.(yml|md)$"' ) . toString ( ) . split ( '\n' )
206
+ console . log ( `Found ${ changedFilesRelPaths . length } translated files.` )
207
+
208
+ const { mdRelPaths, ymlRelPaths, releaseNotesRelPaths } = groupBy ( changedFilesRelPaths , ( path ) => {
209
+ // separate the changed files to different groups
210
+ if ( path . endsWith ( 'README.md' ) ) {
211
+ return 'throwAway'
212
+ } else if ( path . endsWith ( '.md' ) ) {
213
+ return 'mdRelPaths'
214
+ } else if ( path . match ( / \/ d a t a \/ ( v a r i a b l e s | g l o s s a r i e s ) \/ / i) ) {
215
+ return 'ymlRelPaths'
216
+ } else if ( path . match ( / \/ d a t a \/ r e l e a s e - n o t e s \/ / i) ) {
217
+ return 'releaseNotesRelPaths'
218
+ } else {
219
+ // we aren't linting the rest
220
+ return 'throwAway'
221
+ }
222
+ } )
223
+
224
+ const [ mdTuples , ymlTuples , releaseNotesTuples ] = [ mdRelPaths , ymlRelPaths , releaseNotesRelPaths ] . map ( relPaths => {
225
+ const absPaths = relPaths . map ( p => path . join ( rootDir , p ) )
226
+ return zip ( relPaths , absPaths )
227
+ } )
228
+
229
+ mdToLint = mdTuples
230
+ ymlToLint = ymlTuples
231
+ releaseNotesToLint = releaseNotesTuples
232
+ }
233
+
234
+ function formatLinkError ( message , links ) {
235
+ return `${ message } \n - ${ links . join ( '\n - ' ) } `
236
+ }
237
+
238
+ // Returns `content` if its a string, or `content.description` if it can.
239
+ // Used for getting the nested `description` key in glossary files.
240
+ function getContent ( content ) {
241
+ if ( typeof content === 'string' ) return content
242
+ if ( typeof content . description === 'string' ) return content . description
243
+ return null
244
+ }
245
+
246
+ describe ( 'lint markdown content' , ( ) => {
247
+ describe . each ( mdToLint ) (
248
+ '%s' ,
170
249
( markdownRelPath , markdownAbsPath ) => {
171
- let content , ast , links , isHidden , isEarlyAccess , isSitePolicy
250
+ let content , ast , links , isHidden , isEarlyAccess , isSitePolicy , frontmatterErrors
172
251
173
252
beforeAll ( async ( ) => {
174
253
const fileContents = await fs . promises . readFile ( markdownAbsPath , 'utf8' )
175
- const { data, content : bodyContent } = frontmatter ( fileContents )
254
+ const { data, content : bodyContent , errors } = frontmatter ( fileContents )
176
255
177
256
content = bodyContent
257
+ frontmatterErrors = errors
178
258
ast = generateMarkdownAST ( content )
179
259
isHidden = data . hidden === true
180
260
isEarlyAccess = markdownRelPath . split ( '/' ) . includes ( 'early-access' )
@@ -307,34 +387,20 @@ describe('lint-files', () => {
307
387
. resolves
308
388
. toBeTruthy ( )
309
389
} )
390
+
391
+ if ( ! markdownRelPath . includes ( 'data/reusables' ) ) {
392
+ test ( 'contains valid frontmatter' , ( ) => {
393
+ const errorMessage = frontmatterErrors . map ( error => `- [${ error . property } ]: ${ error . actual } , ${ error . message } ` ) . join ( '\n' )
394
+ expect ( frontmatterErrors . length , errorMessage ) . toBe ( 0 )
395
+ } )
396
+ }
310
397
}
311
398
)
399
+ } )
312
400
313
- // Also test the "data/variables/" YAML files
314
- const yamlWalkOptions = {
315
- globs : [ '**/*.yml' ] ,
316
- directories : false ,
317
- includeBasePath : true
318
- }
319
-
320
- const variableYamlAbsPaths = walk ( variablesDir , yamlWalkOptions ) . sort ( )
321
- const variableYamlRelPaths = variableYamlAbsPaths . map ( p => slash ( path . relative ( rootDir , p ) ) )
322
- const variableYamlTuples = zip ( variableYamlRelPaths , variableYamlAbsPaths )
323
-
324
- const glossariesYamlAbsPaths = walk ( glossariesDir , yamlWalkOptions ) . sort ( )
325
- const glossariesYamlRelPaths = glossariesYamlAbsPaths . map ( p => slash ( path . relative ( rootDir , p ) ) )
326
- const glossariesYamlTuples = zip ( glossariesYamlRelPaths , glossariesYamlAbsPaths )
327
-
328
- // Returns `content` if its a string, or `content.description` if it can.
329
- // Used for getting the nested `description` key in glossary files.
330
- function getContent ( content ) {
331
- if ( typeof content === 'string' ) return content
332
- if ( typeof content . description === 'string' ) return content . description
333
- return null
334
- }
335
-
336
- describe . each ( [ ...variableYamlTuples , ...glossariesYamlTuples ] ) (
337
- 'in "%s"' ,
401
+ describe ( 'lint yaml content' , ( ) => {
402
+ describe . each ( ymlToLint ) (
403
+ '%s' ,
338
404
( yamlRelPath , yamlAbsPath ) => {
339
405
let dictionary , isEarlyAccess
340
406
@@ -518,16 +584,12 @@ describe('lint-files', () => {
518
584
} )
519
585
}
520
586
)
587
+ } )
521
588
522
- // GHES release notes
523
- const ghesReleaseNotesDir = path . join ( __dirname , '../../data/release-notes' )
524
- const ghesReleaseNotesYamlAbsPaths = walk ( ghesReleaseNotesDir , yamlWalkOptions ) . sort ( )
525
- const ghesReleaseNotesYamlRelPaths = ghesReleaseNotesYamlAbsPaths . map ( p => path . relative ( rootDir , p ) )
526
- const ghesReleaseNotesYamlTuples = zip ( ghesReleaseNotesYamlRelPaths , ghesReleaseNotesYamlAbsPaths )
527
-
528
- if ( ghesReleaseNotesYamlTuples . length > 0 ) {
529
- describe . each ( ghesReleaseNotesYamlTuples ) (
530
- 'in "%s"' ,
589
+ describe ( 'lint release notes' , ( ) => {
590
+ if ( releaseNotesToLint . length > 0 ) {
591
+ describe . each ( releaseNotesToLint ) (
592
+ '%s' ,
531
593
( yamlRelPath , yamlAbsPath ) => {
532
594
let dictionary
533
595
@@ -538,14 +600,10 @@ describe('lint-files', () => {
538
600
539
601
it ( 'matches the schema' , ( ) => {
540
602
const { errors } = revalidator . validate ( dictionary , ghesReleaseNotesSchema )
541
- const errorMessage = errors . map ( error => `- [${ error . property } ]: ${ error . attribute } , ${ error . message } ` ) . join ( '\n' )
603
+ const errorMessage = errors . map ( error => `- [${ error . property } ]: ${ error . actual } , ${ error . message } ` ) . join ( '\n' )
542
604
expect ( errors . length , errorMessage ) . toBe ( 0 )
543
605
} )
544
606
}
545
607
)
546
608
}
547
609
} )
548
-
549
- function formatLinkError ( message , links ) {
550
- return `${ message } \n - ${ links . join ( '\n - ' ) } `
551
- }
0 commit comments