1
0
mirror of synced 2025-12-19 18:10:59 -05:00

Automation pipelines update markdown (#35194)

This commit is contained in:
Rachael Sewell
2023-02-28 15:09:15 -08:00
committed by GitHub
parent a606617b21
commit a3df74ff0a
4 changed files with 365 additions and 0 deletions

View File

@@ -12,5 +12,12 @@
"github.ae": "ghae",
"ghes": "ghes",
"ghec": "ghec"
},
"frontmatterDefaults": {
"topics": [
"API"
],
"autogenerated": "rest",
"allowTitleToDifferFromFilename": true
}
}

View File

@@ -3,6 +3,7 @@ import { existsSync } from 'fs'
import path from 'path'
import mkdirp from 'mkdirp'
import { updateMarkdownFiles } from './update-markdown.js'
import { allVersions } from '../../../../lib/all-versions.js'
import { createOperations, processOperations } from './get-operations.js'
import { REST_DATA_DIR, REST_SCHEMA_FILENAME } from '../../lib/index.js'
@@ -47,6 +48,7 @@ export async function syncRestData(sourceDirectory, restSchemas) {
console.log(`✅ Wrote ${targetPath}`)
})
)
await updateMarkdownFiles()
await updateRestMetaData(restSchemas)
}

View File

@@ -0,0 +1,273 @@
import path from 'path'
import { existsSync } from 'fs'
import walk from 'walk-sync'
import matter from 'gray-matter'
import { difference } from 'lodash-es'
import { readFile, writeFile, unlink } from 'fs/promises'
import { allVersions, getDocsVersion } from '../../../../lib/all-versions.js'
import { REST_DATA_DIR, REST_SCHEMA_FILENAME } from '../../lib/index.js'
const frontmatterDefaults = JSON.parse(
await readFile(path.join(REST_DATA_DIR, 'meta.json'), 'utf-8')
).frontmatterDefaults
export async function updateMarkdownFiles() {
const restVersions = await getDataFrontmatter(REST_DATA_DIR, REST_SCHEMA_FILENAME)
const restContentFrontmatter = await getMarkdownFrontmatter(restVersions)
const restContentFiles = Object.keys(restContentFrontmatter)
// Get a list of existing markdown files so we can make deletions
const restMarkdownFiles = walk('content/rest', {
includeBasePath: true,
directories: false,
}).filter((file) => !file.includes('index.md') && !file.includes('README.md'))
const existingAutogeneratedFiles = (
await Promise.all(
restMarkdownFiles.map(async (file) => {
const data = await readFile(file, 'utf-8')
const frontmatter = matter(data)
if (frontmatter.data.autogenerated === 'rest') {
return file
}
})
)
).filter(Boolean)
// If the first array contains items that the second array does not,
// it means that a Markdown page was deleted from the OpenAPI schema
const filesToRemove = difference(existingAutogeneratedFiles, restContentFiles)
// Markdown files that need to be deleted
for (const file of filesToRemove) {
await unlink(file)
await updateIndexFile(file, 'remove')
}
// Markdown files that need to be added or updated
for (const [file, newFrontmatter] of Object.entries(restContentFrontmatter)) {
if (existsSync(file)) {
// update only the versions property of the file, assuming
// the other properties have already been added and edited
const { data, content } = matter(await readFile(file, 'utf-8'))
data.versions = newFrontmatter.versions
await writeFile(file, matter.stringify(content, data))
} else {
// create a new file placeholder metadata
await writeFile(file, matter.stringify('', newFrontmatter))
updateIndexFile(file, 'add')
}
}
}
// Adds or removes children properties from index.md pages
async function updateIndexFile(file, changeType) {
const filename = path.basename(file, '.md')
const indexDirectory = path.basename(path.dirname(file))
const rootDir = file.split(indexDirectory)[0]
const indexFilePath = path.join(rootDir, indexDirectory, 'index.md')
const { data, content } = matter(await readFile(indexFilePath, 'utf-8'))
if (changeType === 'remove') {
const index = data.children.indexOf(`/${filename}`)
data.children.splice(index, 1)
}
if (changeType === 'add') {
data.children.push(`/${filename}`)
}
await writeFile(indexFilePath, matter.stringify(content, data))
}
/* Takes a list of versions in the format:
[
'free-pro-team@latest',
'enterprise-cloud@latest',
'enterprise-server@3.3',
'enterprise-server@3.4',
'enterprise-server@3.5',
'enterprise-server@3.6',
'enterprise-server@3.7',
'github-ae@latest'
]
and returns the frontmatter equivalent JSON:
{
fpt: '*',
ghae: '*',
ghec: '*',
ghes: '*'
}
*/
export async function convertVersionsToFrontmatter(versions) {
const frontmatterVersions = {}
const numberedReleases = {}
// Currently, only GHES is numbered. Number releases have to be
// handled differently because they use semantic versioning.
versions.forEach((version) => {
const docsVersion = allVersions[version]
if (!docsVersion.hasNumberedReleases) {
frontmatterVersions[docsVersion.shortName] = '*'
} else {
// Each version that has numbered releases in allVersions
// has a string for the number (currentRelease) and an array
// of all of the available releases (e.g. ['3.3', '3.4', '3.5'])
// This creates an array of the applicable releases in the same
// order as the available releases array. This is used to track when
// a release is no longer supported.
const i = docsVersion.releases.sort().indexOf(docsVersion.currentRelease)
if (!numberedReleases[docsVersion.shortName]) {
const availableReleases = Array(docsVersion.releases.length).fill(undefined)
availableReleases[i] = docsVersion.currentRelease
numberedReleases[docsVersion.shortName] = {
availableReleases,
}
} else {
numberedReleases[docsVersion.shortName].availableReleases[i] = docsVersion.currentRelease
}
}
})
// Create semantic versions for numbered releases
Object.keys(numberedReleases).forEach((key) => {
const availableReleases = numberedReleases[key].availableReleases
const versionContinuity = checkVersionContinuity(availableReleases)
if (availableReleases.every(Boolean)) {
frontmatterVersions[key] = '*'
} else if (!versionContinuity) {
// If there happens to be version gaps, just enumerate each version
// using syntax like =3.x || =3.x
const semVer = availableReleases
.filter(Boolean)
.map((release) => `=${release}`)
.join(' || ')
frontmatterVersions[key] = semVer
} else {
const semVer = []
if (!availableReleases[availableReleases.length - 1]) {
const endVersion = availableReleases.filter(Boolean).pop()
semVer.push(`<=${endVersion}`)
}
if (!availableReleases[0]) {
const startVersion = availableReleases.filter(Boolean).shift()
semVer.push(`>=${startVersion}`)
}
frontmatterVersions[key] = semVer.join(' ')
}
})
const sortedFrontmatterVersions = Object.keys(frontmatterVersions)
.sort()
.reduce((acc, key) => {
acc[key] = frontmatterVersions[key]
return acc
}, {})
return sortedFrontmatterVersions
}
// This is uncommon, but we potentially could have the case where an
// article was versioned for say 3.2, not for 3.3, and then again
// versioned for 3.4. This will result in a custom semantic version range
function checkVersionContinuity(versions) {
const availableVersions = [...versions]
// values at the beginning or end of the array are not gaps but normal
// starts and ends of version ranges
while (!availableVersions[0]) {
availableVersions.shift()
}
while (!availableVersions[availableVersions.length - 1]) {
availableVersions.pop()
}
return availableVersions.every(Boolean)
}
// Reads data files from the directory provided and returns a
// JSON object that lists the versions for each category/subcategory
// The data files are split up by version, so all files must be
// read to get a complete list of versions.
async function getDataFrontmatter(dataDirectory, schemaFilename) {
const fileList = walk(dataDirectory, { includeBasePath: true }).filter(
(file) => path.basename(file) === schemaFilename
)
const restVersions = {}
for (const file of fileList) {
const data = JSON.parse(await readFile(file, 'utf-8'))
const docsVersionName = getDocsVersion(path.basename(path.dirname(file)))
Object.keys(data).forEach((category) => {
// Used to automatically update Markdown files
const subcategories = Object.keys(data[category])
subcategories.forEach((subcategory) => {
if (!restVersions[category]) {
restVersions[category] = {}
}
if (!restVersions[category][subcategory]) {
restVersions[category][subcategory] = {
versions: [docsVersionName],
}
} else if (!restVersions[category][subcategory].versions.includes(docsVersionName)) {
restVersions[category][subcategory].versions.push(docsVersionName)
}
})
})
}
return restVersions
}
/*
Take an object that includes the version frontmatter
that should be applied to the Markdown page that corresponds
to the category and subcategory. The format looks like this:
{
"actions": {
"artifacts": {
"versions": {
"free-pro-team@latest",
"github-ae@latest",
"enterprise-cloud@latest",
"enterprise-server@3.4",
"enterprise-server@3.5",
"enterprise-server@3.6",
"enterprise-server@3.7",
"enterprise-server@3.8"
}
}
}
}
*/
async function getMarkdownFrontmatter(versions) {
const markdownUpdates = {}
for (const category of Object.keys(versions)) {
const subcategories = Object.keys(versions[category])
// When there is only a single subcategory, the Markdown file
// will be at the root of the content/rest directory. The
// filen path will be content/rest/<category>.md
if (subcategories.length === 1) {
// this will be a file in the root of the rest directory
const filepath = path.join('content/rest', `${category}.md`)
markdownUpdates[filepath] = {
title: category,
shortTitle: category,
intro: '',
versions: await convertVersionsToFrontmatter(versions[category][subcategories[0]].versions),
...frontmatterDefaults,
}
continue
}
// The file path will be content/rest/<category>/<subcategory>.md
for (const subcategory of subcategories) {
const filepath = path.join('content/rest', category, `${subcategory}.md`)
markdownUpdates[filepath] = {
title: subcategory,
shortTitle: subcategory,
intro: '',
versions: await convertVersionsToFrontmatter(versions[category][subcategory].versions),
...frontmatterDefaults,
}
}
}
return markdownUpdates
}

View File

@@ -0,0 +1,83 @@
import { describe, expect } from '@jest/globals'
import { supported } from '../../lib/enterprise-server-releases.js'
import { allVersionKeys, allVersions } from '../../lib/all-versions.js'
import { convertVersionsToFrontmatter } from '../../src/rest/scripts/utils/update-markdown.js'
describe('frontmatter versions are generated correctly from REST data', () => {
test('non-continuous enterprise server versions', async () => {
const fromVersions = allVersionKeys.filter(
(version) =>
version !== `enterprise-server@${supported[0]}` &&
version !== `enterprise-server@${supported[2]}`
)
const expectedEneterpriseServerVersions = fromVersions
.map(
(version) =>
version.includes('enterprise-server@') && version.replace('enterprise-server@', '')
)
.filter(Boolean)
const expectedEnterpriseServerVersions = expectedEneterpriseServerVersions
.sort()
.map((version) => `=${version}`)
.join(' || ')
const expectedVersions = {
fpt: '*',
ghae: '*',
ghec: '*',
ghes: expectedEnterpriseServerVersions,
}
const toFrontmatter = await convertVersionsToFrontmatter(fromVersions)
expect(toFrontmatter).toEqual(expectedVersions)
})
test('less than the latest enterprise server version', async () => {
const fromVersions = Object.values(allVersions)
.filter(
(version) =>
!(version.currentRelease === version.latestRelease && version.hasNumberedReleases)
)
.map((version) => version.version)
const nextLatestRelease = [...supported].sort()[supported.length - 2]
const expectedVersions = {
fpt: '*',
ghae: '*',
ghec: '*',
ghes: `<=${nextLatestRelease}`,
}
const toFrontmatter = await convertVersionsToFrontmatter(fromVersions)
expect(toFrontmatter).toEqual(expectedVersions)
})
test('greater than the oldest enterprise server version', async () => {
const oldestRelease = [...supported].sort()[0]
const fromVersions = Object.values(allVersions)
.filter((version) => !(version.version === `enterprise-server@${oldestRelease}`))
.map((version) => version.version)
const secondOldestRelease = [...supported].sort()[1]
const expectedVersions = {
fpt: '*',
ghae: '*',
ghec: '*',
ghes: `>=${secondOldestRelease}`,
}
const toFrontmatter = await convertVersionsToFrontmatter(fromVersions)
expect(toFrontmatter).toEqual(expectedVersions)
})
test('no non-numbered release versions', async () => {
const fromVersions = Object.values(allVersions)
.filter((version) => version.hasNumberedReleases)
.map((version) => version.version)
const expectedVersions = {
ghes: `*`,
}
const toFrontmatter = await convertVersionsToFrontmatter(fromVersions)
expect(toFrontmatter).toEqual(expectedVersions)
})
})