Skip to content

Commit bbc5778

Browse files
authored
add new options to rest schema script (github#21571)
1 parent 031995b commit bbc5778

File tree

1 file changed

+141
-23
lines changed

1 file changed

+141
-23
lines changed

script/rest/update-files.js

Lines changed: 141 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,72 @@
66
//
77
// [end-readme]
88

9-
import fs from 'fs'
9+
import { stat, readFile, writeFile, readdir } from 'fs/promises'
1010
import path from 'path'
1111
import program from 'commander'
1212
import { execSync } from 'child_process'
1313
import mkdirp from 'mkdirp'
1414
import rimraf from 'rimraf'
1515
import getOperations from './utils/get-operations.js'
16+
import yaml from 'js-yaml'
1617

1718
const tempDocsDir = path.join(process.cwd(), 'openapiTmp')
1819
const githubRepoDir = path.join(process.cwd(), '../github')
1920
const dereferencedPath = path.join(process.cwd(), 'lib/rest/static/dereferenced')
20-
const schemas = fs.readdirSync(dereferencedPath)
2121
const decoratedPath = path.join(process.cwd(), 'lib/rest/static/decorated')
22+
const openApiReleasesDir = `${githubRepoDir}/app/api/description/config/releases`
2223

2324
program
2425
.description('Generate dereferenced OpenAPI and decorated schema files.')
2526
.option(
2627
'--decorate-only',
2728
'⚠️ Only used by a 🤖 to generate decorated schema files from existing dereferenced schema files.'
2829
)
30+
.option(
31+
'-v --versions <VERSIONS...>',
32+
'A list of undeprecated, published versions to build, separated by a space. Example "ghes-3.1" or "api.github.com github.ae"'
33+
)
34+
.option('-d --include-deprecated', 'Includes schemas that are marked as `deprecated: true`')
35+
.option('-u --include-unpublished', 'Includes schemas that are marked as `published: false`')
2936
.parse(process.argv)
3037

31-
const decorateOnly = program.opts().decorateOnly
38+
const { decorateOnly, versions, includeUnpublished, includeDeprecated } = program.opts()
39+
40+
// Check that the github/github repo exists. If the files are only being
41+
// decorated, the github/github repo isn't needed.
42+
if (!decorateOnly) {
43+
try {
44+
await stat(githubRepoDir)
45+
} catch (error) {
46+
console.log(
47+
`🛑 The ${githubRepoDir} does not exist. Make sure you have a local, bootstrapped checkout of github/github at the same level as your github/docs-internal repo before running this script.`
48+
)
49+
process.exit(1)
50+
}
51+
}
52+
53+
// When the input parameter type is decorate-only, use the local
54+
// `github/docs-internal` repo to generate a list of schema files.
55+
// Otherwise, use the `github/github` list of config files
56+
const referenceSchemaDirectory = decorateOnly ? dereferencedPath : openApiReleasesDir
57+
// A full list of unpublished, deprecated, and active schemas
58+
const allSchemas = await getOpenApiSchemas(referenceSchemaDirectory)
59+
60+
await validateInputParameters(allSchemas)
61+
62+
// Format the command supplied to the bundle script in `github/github`
63+
const commandParameters = await getCommandParameters()
64+
// Get the list of schemas for this bundle, depending on options
65+
const schemas = await getSchemas(allSchemas)
3266

3367
main()
3468

3569
async function main() {
3670
// Generate the dereferenced OpenAPI schema files
3771
if (!decorateOnly) {
38-
if (!fs.existsSync(githubRepoDir)) {
39-
console.log(
40-
`🛑 The ${githubRepoDir} does not exist. Make sure you have a local, bootstrapped checkout of github/github at the same level as your github/docs-internal repo before running this script.`
41-
)
42-
process.exit(1)
43-
}
44-
4572
await getDereferencedFiles()
4673
}
47-
74+
// Decorate the dereferenced files in a format ingestible by docs.github.com
4875
await decorate()
4976

5077
console.log(
@@ -64,16 +91,17 @@ async function getDereferencedFiles() {
6491
execSync('git pull', { cwd: githubRepoDir })
6592
}
6693

67-
// create a tmp directory to store schema files generated from github/github
94+
// Create a tmp directory to store schema files generated from github/github
6895
rimraf.sync(tempDocsDir)
6996
await mkdirp(tempDocsDir)
7097

7198
console.log(
7299
`\n🏃‍♀️🏃🏃‍♀️Running \`bin/openapi bundle\` in branch '${githubBranch}' of your github/github checkout to generate the dereferenced OpenAPI schema files.\n`
73100
)
74101
try {
102+
console.log(`bundle -o ${tempDocsDir} ${commandParameters}`)
75103
execSync(
76-
`${path.join(githubRepoDir, 'bin/openapi')} bundle -o ${tempDocsDir} --include_unpublished`,
104+
`${path.join(githubRepoDir, 'bin/openapi')} bundle -o ${tempDocsDir} ${commandParameters}`,
77105
{ stdio: 'inherit' }
78106
)
79107
} catch (error) {
@@ -92,21 +120,22 @@ async function getDereferencedFiles() {
92120
// property in the dereferenced schema is replaced with the branch
93121
// name of the `github/github` checkout. A CI test
94122
// checks the version and fails if it's not a semantic version.
95-
schemas.forEach((filename) => {
96-
const schema = JSON.parse(fs.readFileSync(path.join(dereferencedPath, filename)))
123+
for (const filename of schemas) {
124+
const schema = JSON.parse(await readFile(path.join(dereferencedPath, filename)))
125+
97126
schema.info.version = `${githubBranch} !!DEVELOPMENT MODE - DO NOT MERGE!!`
98-
fs.writeFileSync(path.join(dereferencedPath, filename), JSON.stringify(schema, null, 2))
99-
})
127+
await writeFile(path.join(dereferencedPath, filename), JSON.stringify(schema, null, 2))
128+
}
100129
}
101130

102131
async function decorate() {
103132
console.log('\n🎄 Decorating the OpenAPI schema files in lib/rest/static/dereferenced.\n')
104-
105-
const dereferencedSchemas = schemas.reduce((acc, filename) => {
106-
const schema = JSON.parse(fs.readFileSync(path.join(dereferencedPath, filename)))
133+
const dereferencedSchemas = {}
134+
for (const filename of schemas) {
135+
const schema = JSON.parse(await readFile(path.join(dereferencedPath, filename)))
107136
const key = filename.replace('.deref.json', '')
108-
return { ...acc, [key]: schema }
109-
}, {})
137+
dereferencedSchemas[key] = schema
138+
}
110139

111140
for (const [schemaName, schema] of Object.entries(dereferencedSchemas)) {
112141
try {
@@ -118,7 +147,7 @@ async function decorate() {
118147

119148
const filename = path.join(decoratedPath, `${schemaName}.json`).replace('.deref', '')
120149
// write processed operations to disk
121-
fs.writeFileSync(filename, JSON.stringify(operations, null, 2))
150+
await writeFile(filename, JSON.stringify(operations, null, 2))
122151

123152
console.log('Wrote', path.relative(process.cwd(), filename))
124153
} catch (error) {
@@ -130,3 +159,92 @@ async function decorate() {
130159
}
131160
}
132161
}
162+
163+
async function validateInputParameters(schemas) {
164+
// The `--versions` and `--decorate-only` options cannot be used
165+
// with the `--include-deprecated` or `--include-unpublished` options
166+
const numberOfOptions = Object.keys(program.opts()).length
167+
168+
if (numberOfOptions > 1 && (decorateOnly || versions)) {
169+
console.log(
170+
`🛑 You cannot use the versions and decorate-only options with any other options.\nThe decorate-only switch will decorate all dereferenced schemas files in the docs-internal repo.\nThis script doesn't support generating individual deprecated or unpublished schemas.\nPlease reach out to #docs-engineering if this is a use case that you need.`
171+
)
172+
process.exit(1)
173+
}
174+
175+
// Validate individual versions provided
176+
if (versions) {
177+
versions.forEach((version) => {
178+
if (
179+
schemas.deprecated.includes(`${version}.deref.json`) ||
180+
schemas.unpublished.includes(`${version}.deref.json`)
181+
) {
182+
console.log(
183+
`🛑 This script doesn't support generating individual deprecated or unpublished schemas. Please reach out to #docs-engineering if this is a use case that you need.`
184+
)
185+
process.exit(1)
186+
} else if (!schemas.currentReleases.includes(`${version}.deref.json`)) {
187+
console.log(`🛑 The version (${version}) you specified is not valid.`)
188+
process.exit(1)
189+
}
190+
})
191+
}
192+
}
193+
194+
async function getOpenApiSchemas(directory) {
195+
const openAPIConfigs = await readdir(directory)
196+
const unpublished = []
197+
const deprecated = []
198+
const currentReleases = []
199+
200+
for (const file of openAPIConfigs) {
201+
const newFileName = `${path.basename(file, 'yaml')}deref.json`
202+
const content = await readFile(path.join(directory, file), 'utf8')
203+
const yamlContent = yaml.load(content)
204+
if (!yamlContent.published) {
205+
unpublished.push(newFileName)
206+
}
207+
if (yamlContent.deprecated) {
208+
deprecated.push(newFileName)
209+
}
210+
if (!yamlContent.deprecated && yamlContent.published) {
211+
currentReleases.push(newFileName)
212+
}
213+
}
214+
215+
return { currentReleases, unpublished, deprecated }
216+
}
217+
218+
async function getCommandParameters() {
219+
let includeParams = []
220+
221+
if (versions) {
222+
includeParams = versions
223+
}
224+
if (includeUnpublished) {
225+
includeParams.push('--include_unpublished')
226+
}
227+
if (includeDeprecated) {
228+
includeParams.push('--include_deprecated')
229+
}
230+
231+
return includeParams.join(' ')
232+
}
233+
234+
async function getSchemas(allSchemas) {
235+
if (decorateOnly) {
236+
const files = await readdir(dereferencedPath)
237+
return files
238+
} else if (versions) {
239+
return versions.map((elem) => `${elem}.deref.json`)
240+
} else {
241+
const schemas = allSchemas.currentReleases
242+
if (includeUnpublished) {
243+
schemas.push(...allSchemas.unpublished)
244+
}
245+
if (includeDeprecated) {
246+
schemas.push(...allSchemas.deprecated)
247+
}
248+
return schemas
249+
}
250+
}

0 commit comments

Comments
 (0)