Skip to content

Commit f8fcee0

Browse files
CopilotheiskrEbonsignori
authored
Convert all JavaScript files to TypeScript in src/automated-pipelines directory (#55861)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: heiskr <[email protected]> Co-authored-by: Ebonsignori <[email protected]> Co-authored-by: Evan Bonsignori <[email protected]>
1 parent 40551f3 commit f8fcee0

File tree

6 files changed

+179
-57
lines changed

6 files changed

+179
-57
lines changed

src/automated-pipelines/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ Automated pages allow for manually created content to be prepended to the automa
2121
## How does it work
2222

2323
We currently have two patterns that we used to create automated pipelines:
24+
2425
- REST, Webhooks, GitHub Apps, and GraphQL pipelines consume external structured data and transform that data into a JSON file that is used to create content for a page on docs.github.com. Typically, data files are a 1:1 mapping to a specific page on docs.github.com.
2526
- The CodeQL CLI pipeline takes an unstructured ReStructuredText file and transforms it directly into a Markdown file with frontmatter, that uses the same authoring format as the rest of the docs.
2627

2728
## Creating a new pipeline
2829

29-
Each pipeline should be evaluated individually to determine the best architecture for simplicity, maintainability, and requirements.
30+
Each pipeline should be evaluated individually to determine the best architecture for simplicity, maintainability, and requirements.
3031
For example:
32+
3133
- Is the content being displayed basic Markdown content? For example, does the content avoid using complex tables and interactive elements? If so, then writing the Markdown content directly and avoiding the need to create a structured data file that requires a React component may be the best approach. This was the case for the CodeQL CLI pipeline. One caveat to think about before writing Markdown directly is whether the content will need liquid versioning. The current pipeline that writes Markdown directly does not need to use liquid versioning. Liquid versioning which would increase the complexity quite a bit. All of the Markdown content in each article that is generated from the CodeQL CLI pipeline applies to all versions listed in the `versions` frontmatter property, simplifying the Markdown generation process.
3234
- Is the page interactive like the REST and Webhooks pages? If so, then the data will likely need to be structured data. In that case, a new React component may be needed to display the data.
3335

src/automated-pipelines/lib/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
"rest",
77
"webhooks"
88
]
9-
}
9+
}

src/automated-pipelines/lib/update-markdown.js renamed to src/automated-pipelines/lib/update-markdown.ts

Lines changed: 121 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,57 @@ import { difference, isEqual } from 'lodash-es'
99

1010
import { allVersions } from '#src/versions/lib/all-versions.js'
1111
import getApplicableVersions from '#src/versions/lib/get-applicable-versions.js'
12+
import type { MarkdownFrontmatter } from '@/types'
13+
14+
// Type definitions - extending existing type to add missing fields and make most fields optional
15+
type FrontmatterData = Partial<MarkdownFrontmatter> & {
16+
autogenerated?: string
17+
[key: string]: any
18+
}
19+
20+
type SourceContentItem = {
21+
data: FrontmatterData
22+
content: string
23+
}
24+
25+
type SourceContent = {
26+
[key: string]: SourceContentItem
27+
}
28+
29+
type IndexOrder = {
30+
[key: string]: {
31+
startsWith?: string[]
32+
}
33+
}
34+
35+
type UpdateContentDirectoryOptions = {
36+
targetDirectory: string
37+
sourceContent: SourceContent
38+
frontmatter: FrontmatterData
39+
indexOrder?: IndexOrder
40+
}
41+
42+
type UpdateDirectoryOptions = {
43+
rootDirectoryOnly?: boolean
44+
shortTitle?: boolean
45+
indexOrder?: IndexOrder
46+
}
47+
48+
type ChildUpdates = {
49+
itemsToAdd: string[]
50+
itemsToRemove: string[]
51+
}
52+
53+
type DirectoryInfo = {
54+
directoryContents: string[]
55+
directoryFiles: string[]
56+
childDirectories: string[]
57+
}
58+
59+
type ChildrenComparison = {
60+
childrenOnDisk: string[]
61+
indexChildren: string[]
62+
}
1263

1364
const ROOT_INDEX_FILE = 'content/index.md'
1465
export const MARKDOWN_COMMENT = '\n<!-- Content after this section is automatically generated -->\n'
@@ -20,15 +71,19 @@ export async function updateContentDirectory({
2071
sourceContent,
2172
frontmatter,
2273
indexOrder,
23-
}) {
74+
}: UpdateContentDirectoryOptions): Promise<void> {
2475
const sourceFiles = Object.keys(sourceContent)
2576
await createDirectory(targetDirectory)
2677
await removeMarkdownFiles(targetDirectory, sourceFiles, frontmatter.autogenerated)
2778
await updateMarkdownFiles(targetDirectory, sourceContent, frontmatter, indexOrder)
2879
}
2980

3081
// Remove markdown files that are no longer in the source data
31-
async function removeMarkdownFiles(targetDirectory, sourceFiles, autogeneratedType) {
82+
async function removeMarkdownFiles(
83+
targetDirectory: string,
84+
sourceFiles: string[],
85+
autogeneratedType: string | undefined,
86+
): Promise<void> {
3287
// Copy the autogenerated Markdown files to the target directory
3388
const autogeneratedFiles = await getAutogeneratedFiles(targetDirectory, autogeneratedType)
3489
// If the first array contains items that the second array does not,
@@ -42,29 +97,37 @@ async function removeMarkdownFiles(targetDirectory, sourceFiles, autogeneratedTy
4297

4398
// Gets a list of all files under targetDirectory that have the
4499
// `autogenerated` frontmatter set to `autogeneratedType`.
45-
async function getAutogeneratedFiles(targetDirectory, autogeneratedType) {
100+
async function getAutogeneratedFiles(
101+
targetDirectory: string,
102+
autogeneratedType: string | undefined,
103+
): Promise<string[]> {
46104
const files = walk(targetDirectory, {
47105
includeBasePath: true,
48-
childDirectories: false,
106+
directories: false,
49107
globs: ['**/*.md'],
50108
ignore: ['**/README.md', '**/index.md'],
51109
})
52110
return (
53111
await Promise.all(
54-
files.map(async (file) => {
112+
files.map(async (file: string) => {
55113
const { data } = matter(await readFile(file, 'utf-8'))
56114
if (data.autogenerated === autogeneratedType) {
57115
return file
58116
}
59117
}),
60118
)
61-
).filter(Boolean)
119+
).filter(Boolean) as string[]
62120
}
63121

64122
// The `sourceContent` object contains the new content and target file
65123
// path for the Markdown files. Ex:
66124
// { <targetFile>: { data: <frontmatter>, content: <markdownContent> } }
67-
async function updateMarkdownFiles(targetDirectory, sourceContent, frontmatter, indexOrder = {}) {
125+
async function updateMarkdownFiles(
126+
targetDirectory: string,
127+
sourceContent: SourceContent,
128+
frontmatter: FrontmatterData,
129+
indexOrder: IndexOrder = {},
130+
): Promise<void> {
68131
for (const [file, newContent] of Object.entries(sourceContent)) {
69132
await updateMarkdownFile(file, newContent.data, newContent.content)
70133
}
@@ -82,11 +145,11 @@ async function updateMarkdownFiles(targetDirectory, sourceContent, frontmatter,
82145
// edit the modifiable content of the file. If the Markdown file doesn't
83146
// exists, we create a new Markdown file.
84147
async function updateMarkdownFile(
85-
file,
86-
sourceData,
87-
sourceContent,
88-
commentDelimiter = MARKDOWN_COMMENT,
89-
) {
148+
file: string,
149+
sourceData: FrontmatterData,
150+
sourceContent: string,
151+
commentDelimiter: string = MARKDOWN_COMMENT,
152+
): Promise<void> {
90153
if (existsSync(file)) {
91154
// update only the versions property of the file, assuming
92155
// the other properties have already been added and edited
@@ -132,10 +195,10 @@ async function updateMarkdownFile(
132195
// ensure that the Markdown files have been updated and any files
133196
// that need to be deleted have been removed.
134197
async function updateDirectory(
135-
directory,
136-
frontmatter,
137-
{ rootDirectoryOnly = false, shortTitle = false, indexOrder = {} } = {},
138-
) {
198+
directory: string,
199+
frontmatter: FrontmatterData,
200+
{ rootDirectoryOnly = false, shortTitle = false, indexOrder = {} }: UpdateDirectoryOptions = {},
201+
): Promise<void> {
139202
const initialDirectoryListing = await getDirectoryInfo(directory)
140203
// If there are no children on disk, remove the directory
141204
if (initialDirectoryListing.directoryContents.length === 0 && !rootDirectoryOnly) {
@@ -162,7 +225,7 @@ async function updateDirectory(
162225
const { childrenOnDisk, indexChildren } = getChildrenToCompare(
163226
indexFile,
164227
directoryContents,
165-
data.children,
228+
data.children || [],
166229
)
167230

168231
const itemsToAdd = difference(childrenOnDisk, indexChildren)
@@ -199,12 +262,16 @@ async function updateDirectory(
199262
// Children properties include a leading slash except when the
200263
// index.md file is the root index.md file. We also want to
201264
// remove the file extension from the files on disk.
202-
function getChildrenToCompare(indexFile, directoryContents, fmChildren) {
265+
function getChildrenToCompare(
266+
indexFile: string,
267+
directoryContents: string[],
268+
fmChildren: string[] | undefined,
269+
): ChildrenComparison {
203270
if (!fmChildren) {
204271
throw new Error(`No children property found in ${indexFile}`)
205272
}
206273

207-
const isEarlyAccess = (item) => isRootIndexFile(indexFile) && item === 'early-access'
274+
const isEarlyAccess = (item: string) => isRootIndexFile(indexFile) && item === 'early-access'
208275

209276
// Get the list of children from the directory contents
210277
const childrenOnDisk = directoryContents
@@ -233,18 +300,24 @@ function getChildrenToCompare(indexFile, directoryContents, fmChildren) {
233300
//
234301
// 3. If the index file is not autogenerated, we leave the ordering
235302
// as is and append new children to the end.
236-
function updateIndexChildren(data, childUpdates, indexFile, indexOrder, rootIndex = false) {
303+
function updateIndexChildren(
304+
data: FrontmatterData,
305+
childUpdates: ChildUpdates,
306+
indexFile: string,
307+
indexOrder: IndexOrder,
308+
rootIndex: boolean = false,
309+
): FrontmatterData {
237310
const { itemsToAdd, itemsToRemove } = childUpdates
238311
const childPrefix = rootIndex ? '' : '/'
239312

240313
// Get a new list of children with added and removed items
241-
const children = [...data.children]
314+
const children = [...(data.children || [])]
242315
// remove the '/' prefix used in index.md children
243316
.map((item) => item.replace(childPrefix, ''))
244317
.filter((item) => !itemsToRemove.includes(item))
245318
children.push(...itemsToAdd)
246319

247-
const orderedIndexChildren = []
320+
const orderedIndexChildren: string[] = []
248321

249322
// Only used for tests. During testing, the content directory is
250323
// in a temp directory so the paths are not relative to
@@ -280,7 +353,11 @@ function updateIndexChildren(data, childUpdates, indexFile, indexOrder, rootInde
280353

281354
// Gets the contents of the index.md file from disk if it exits or
282355
// creates a new index.md file with the default frontmatter.
283-
async function getIndexFileContents(indexFile, frontmatter, shortTitle = false) {
356+
async function getIndexFileContents(
357+
indexFile: string,
358+
frontmatter: FrontmatterData,
359+
shortTitle: boolean = false,
360+
): Promise<{ data: FrontmatterData; content: string }> {
284361
const directory = path.dirname(indexFile)
285362
const indexFileContent = {
286363
data: {
@@ -300,8 +377,11 @@ async function getIndexFileContents(indexFile, frontmatter, shortTitle = false)
300377
// Builds the index.md versions frontmatter by consolidating
301378
// the versions from each Markdown file in the directory + the
302379
// index.md files in any subdirectories of directory.
303-
async function getIndexFileVersions(directory, files) {
304-
const versions = new Set()
380+
async function getIndexFileVersions(
381+
directory: string,
382+
files: string[],
383+
): Promise<{ [key: string]: string }> {
384+
const versions = new Set<string>()
305385
await Promise.all(
306386
files.map(async (file) => {
307387
const filepath = path.join(directory, file)
@@ -319,7 +399,7 @@ async function getIndexFileVersions(directory, files) {
319399
throw new Error(`Frontmatter in ${filepath} does not contain versions.`)
320400
}
321401
const fmVersions = getApplicableVersions(data.versions)
322-
fmVersions.forEach((version) => versions.add(version))
402+
fmVersions.forEach((version: string) => versions.add(version))
323403
}),
324404
)
325405
const versionArray = [...versions]
@@ -343,9 +423,11 @@ and returns the frontmatter equivalent JSON:
343423
ghes: '*'
344424
}
345425
*/
346-
export async function convertVersionsToFrontmatter(versions) {
347-
const frontmatterVersions = {}
348-
const numberedReleases = {}
426+
export async function convertVersionsToFrontmatter(
427+
versions: string[],
428+
): Promise<{ [key: string]: string }> {
429+
const frontmatterVersions: { [key: string]: string } = {}
430+
const numberedReleases: { [key: string]: { availableReleases: (string | undefined)[] } } = {}
349431

350432
// Currently, only GHES is numbered. Number releases have to be
351433
// handled differently because they use semantic versioning.
@@ -362,7 +444,9 @@ export async function convertVersionsToFrontmatter(versions) {
362444
// a release is no longer supported.
363445
const i = docsVersion.releases.indexOf(docsVersion.currentRelease)
364446
if (!numberedReleases[docsVersion.shortName]) {
365-
const availableReleases = Array(docsVersion.releases.length).fill(undefined)
447+
const availableReleases: (string | undefined)[] = Array(docsVersion.releases.length).fill(
448+
undefined,
449+
)
366450
availableReleases[i] = docsVersion.currentRelease
367451
numberedReleases[docsVersion.shortName] = {
368452
availableReleases,
@@ -388,7 +472,7 @@ export async function convertVersionsToFrontmatter(versions) {
388472
.join(' || ')
389473
frontmatterVersions[key] = semVer
390474
} else {
391-
const semVer = []
475+
const semVer: string[] = []
392476
if (!availableReleases[availableReleases.length - 1]) {
393477
const startVersion = availableReleases.filter(Boolean).pop()
394478
semVer.push(`>=${startVersion}`)
@@ -402,7 +486,7 @@ export async function convertVersionsToFrontmatter(versions) {
402486
})
403487
const sortedFrontmatterVersions = Object.keys(frontmatterVersions)
404488
.sort()
405-
.reduce((acc, key) => {
489+
.reduce((acc: { [key: string]: string }, key) => {
406490
acc[key] = frontmatterVersions[key]
407491
return acc
408492
}, {})
@@ -412,7 +496,7 @@ export async function convertVersionsToFrontmatter(versions) {
412496
// This is uncommon, but we potentially could have the case where an
413497
// article was versioned for say 3.2, not for 3.3, and then again
414498
// versioned for 3.4. This will result in a custom semantic version range
415-
function checkVersionContinuity(versions) {
499+
function checkVersionContinuity(versions: (string | undefined)[]): boolean {
416500
const availableVersions = [...versions]
417501

418502
// values at the beginning or end of the array are not gaps but normal
@@ -427,18 +511,18 @@ function checkVersionContinuity(versions) {
427511
}
428512

429513
// Returns true if the indexFile is the root index.md file
430-
function isRootIndexFile(indexFile) {
514+
function isRootIndexFile(indexFile: string): boolean {
431515
return indexFile === ROOT_INDEX_FILE
432516
}
433517

434518
// Creates a new directory if it doesn't exist
435-
async function createDirectory(targetDirectory) {
519+
async function createDirectory(targetDirectory: string): Promise<void> {
436520
if (!existsSync(targetDirectory)) {
437521
await mkdirp(targetDirectory)
438522
}
439523
}
440524

441-
async function getDirectoryInfo(directory) {
525+
async function getDirectoryInfo(directory: string): Promise<DirectoryInfo> {
442526
if (!existsSync(directory)) {
443527
throw new Error(`Directory ${directory} did not exist when attempting to get directory info.`)
444528
}
@@ -454,7 +538,7 @@ async function getDirectoryInfo(directory) {
454538
return { directoryContents, directoryFiles, childDirectories }
455539
}
456540

457-
function appendVersionComment(stringifiedContent) {
541+
function appendVersionComment(stringifiedContent: string): string {
458542
return stringifiedContent.replace(
459543
'\nversions:\n',
460544
`\nversions: # DO NOT MANUALLY EDIT. CHANGES WILL BE OVERWRITTEN BY A 🤖\n`,

0 commit comments

Comments
 (0)