Reorganize REST code (#34323)
This commit is contained in:
28775
src/github-apps/data/enabled-for-apps.json
Normal file
28775
src/github-apps/data/enabled-for-apps.json
Normal file
File diff suppressed because it is too large
Load Diff
25
src/rest/README.md
Normal file
25
src/rest/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# REST
|
||||
|
||||
## About this directory
|
||||
|
||||
* `src/rest/lib/index.js` is human-editable.
|
||||
* `src/rest/data/*.json` are generated by [scripts](../../src/rest/scripts/README.md).
|
||||
|
||||
## Editable files
|
||||
|
||||
* `src/rest/lib/index.js` consumes the static decorated schema files and exports `categories`, `operations`, and `operationsEnabledForGitHubApps` used by the REST middleware contextualizer.
|
||||
|
||||
## Static files
|
||||
|
||||
Generated by `src/rest/scripts/update-files.js`:
|
||||
|
||||
* `src/rest/data` - files generated from the dereferenced OpenAPI schema with the Markdown descriptions rendered in HTML
|
||||
* `src/rest/data/dereferenced` - **NOTE** These are only generated if you pass the --keep-dereferenced-files option and are not checked into the repository. This option is for debug only.
|
||||
|
||||
## Rendering docs
|
||||
|
||||
When the server starts, `middleware/contextualizers/rest.js` accesses the data exported from the static decorated JSON files, fetches the data for the current version and requested path, and adds it to the `context` object. The added property is:
|
||||
|
||||
* `req.context.currentRestOperations` - all operations with a category matching the current path
|
||||
|
||||
Markdown files in `content/rest/reference` use Liquid to loop over these context properties. The Liquid calls HTML files in the `includes` directory to do most of the rendering. Writers can add content to the Markdown files alongside the Liquid.
|
||||
521101
src/rest/data/api.github.com.2022-11-28.json
Normal file
521101
src/rest/data/api.github.com.2022-11-28.json
Normal file
File diff suppressed because one or more lines are too long
540364
src/rest/data/ghec.2022-11-28.json
Normal file
540364
src/rest/data/ghec.2022-11-28.json
Normal file
File diff suppressed because one or more lines are too long
444703
src/rest/data/ghes-3.4.json
Normal file
444703
src/rest/data/ghes-3.4.json
Normal file
File diff suppressed because one or more lines are too long
452379
src/rest/data/ghes-3.5.json
Normal file
452379
src/rest/data/ghes-3.5.json
Normal file
File diff suppressed because one or more lines are too long
466128
src/rest/data/ghes-3.6.json
Normal file
466128
src/rest/data/ghes-3.6.json
Normal file
File diff suppressed because one or more lines are too long
470516
src/rest/data/ghes-3.7.json
Normal file
470516
src/rest/data/ghes-3.7.json
Normal file
File diff suppressed because one or more lines are too long
493604
src/rest/data/ghes-3.8.json
Normal file
493604
src/rest/data/ghes-3.8.json
Normal file
File diff suppressed because one or more lines are too long
401485
src/rest/data/github.ae.json
Normal file
401485
src/rest/data/github.ae.json
Normal file
File diff suppressed because one or more lines are too long
148
src/rest/lib/index.js
Normal file
148
src/rest/lib/index.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { readCompressedJsonFileFallback } from '../../../lib/read-json-file.js'
|
||||
import { getAutomatedPageMiniTocItems } from '../../../lib/get-mini-toc-items.js'
|
||||
import { allVersions } from '../../../lib/all-versions.js'
|
||||
import languages from '../../../lib/languages.js'
|
||||
|
||||
const schemasPath = 'src/rest/data'
|
||||
const ENABLED_APPS_FILENAME = 'src/github-apps/data/enabled-for-apps.json'
|
||||
const contentPath = 'content/rest'
|
||||
|
||||
/*
|
||||
Loads the schemas from the static/decorated folder into a single
|
||||
object organized by version. Not all products are calendar date
|
||||
versioned.
|
||||
Example:
|
||||
{
|
||||
free-pro-team@latest: {
|
||||
2022-08-09: {
|
||||
category: {
|
||||
subcategory: [operations],
|
||||
}
|
||||
},
|
||||
2022-11-14: {
|
||||
category: {
|
||||
subcategory: [operations],
|
||||
}
|
||||
}
|
||||
}
|
||||
enterprise-server@3.2: {
|
||||
'not_api_versioned': {
|
||||
category: {
|
||||
subcategory: [operations],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const NOT_API_VERSIONED = 'not_api_versioned'
|
||||
const restOperationData = new Map()
|
||||
const restOperations = new Map()
|
||||
|
||||
Object.keys(languages).forEach((language) => {
|
||||
restOperationData.set(language, new Map())
|
||||
Object.keys(allVersions).forEach((version) => {
|
||||
// setting to undefined will allow us to perform checks
|
||||
// more easily later on
|
||||
restOperationData.get(language).set(version, new Map())
|
||||
if (allVersions[version].apiVersions && allVersions[version].apiVersions.length > 0) {
|
||||
allVersions[version].apiVersions.forEach((date) => {
|
||||
restOperationData.get(language).get(version).set(date, new Map())
|
||||
})
|
||||
} else {
|
||||
// Products that are not been calendar date versioned
|
||||
restOperationData.get(language).get(version).set(NOT_API_VERSIONED, new Map())
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export const categoriesWithoutSubcategories = fs
|
||||
.readdirSync(contentPath)
|
||||
.filter((file) => {
|
||||
return file.endsWith('.md') && !file.includes('index.md') && !file.includes('README.md')
|
||||
})
|
||||
.map((filteredFile) => filteredFile.replace('.md', ''))
|
||||
|
||||
// version: plan + release e.g. For ghes-3.5, ghes is the plan and 3.5 is the release
|
||||
// apiVersion (not all versions have apiVersions): REST API Calendar Dates
|
||||
// openApiVersion (below, every version has an openApiVersion mapping): There's a mapping between our Docs versions
|
||||
// and the OpenApi Version bc it's not the same
|
||||
export default async function getRest(version, apiVersion, category, subCategory) {
|
||||
const openApiVersion = getOpenApiVersion(version)
|
||||
const filename = apiVersion ? `${openApiVersion}.${apiVersion}.json` : `${openApiVersion}.json`
|
||||
const apiDate = apiVersion || NOT_API_VERSIONED
|
||||
|
||||
if (!restOperations.has(openApiVersion)) {
|
||||
restOperations.set(openApiVersion, new Map())
|
||||
restOperations.get(openApiVersion).set(apiDate, new Map())
|
||||
// The `readCompressedJsonFileFallback()` function
|
||||
// will check for both a .br and .json extension.
|
||||
restOperations
|
||||
.get(openApiVersion)
|
||||
.set(apiDate, readCompressedJsonFileFallback(path.join(schemasPath, filename)))
|
||||
} else if (!restOperations.get(openApiVersion).has(apiDate)) {
|
||||
restOperations.get(openApiVersion).set(apiDate, new Map())
|
||||
// The `readCompressedJsonFileFallback()` function
|
||||
// will check for both a .br and .json extension.
|
||||
restOperations
|
||||
.get(openApiVersion)
|
||||
.set(apiDate, readCompressedJsonFileFallback(path.join(schemasPath, filename)))
|
||||
}
|
||||
|
||||
if (subCategory) {
|
||||
return restOperations.get(openApiVersion).get(apiDate)[category][subCategory]
|
||||
} else if (category) {
|
||||
return restOperations.get(openApiVersion).get(apiDate)[category]
|
||||
} else {
|
||||
return restOperations.get(openApiVersion).get(apiDate)
|
||||
}
|
||||
}
|
||||
|
||||
function getOpenApiVersion(version) {
|
||||
if (!(version in allVersions)) {
|
||||
throw new Error(`Unrecognized version '${version}'. Not found in ${Object.keys(allVersions)}`)
|
||||
}
|
||||
return allVersions[version].openApiVersionName
|
||||
}
|
||||
|
||||
// Generates the miniToc for a rest reference page.
|
||||
export async function getRestMiniTocItems(
|
||||
category,
|
||||
subCategory,
|
||||
apiVersion,
|
||||
restOperations,
|
||||
language,
|
||||
version,
|
||||
context
|
||||
) {
|
||||
const apiDate = apiVersion || NOT_API_VERSIONED
|
||||
|
||||
if (!restOperationData.get(language).get(version).get(apiDate).has(category)) {
|
||||
restOperationData.get(language).get(version).get(apiDate).set(category, new Map())
|
||||
}
|
||||
|
||||
if (!restOperationData.get(language).get(version).get(apiDate).get(category).get(subCategory)) {
|
||||
const languageTree = restOperationData.get(language)
|
||||
const titles = restOperations.map((operation) => operation.title)
|
||||
const restOperationsMiniTocItems = await getAutomatedPageMiniTocItems(titles, context, 3)
|
||||
languageTree.get(version).get(apiDate).get(category).set(subCategory, {
|
||||
restOperationsMiniTocItems,
|
||||
})
|
||||
restOperationData.set(restOperationData, languageTree)
|
||||
}
|
||||
return restOperationData.get(language).get(version).get(apiDate).get(category).get(subCategory)
|
||||
}
|
||||
|
||||
const enabledForApps = {}
|
||||
export async function getEnabledForApps(docsVersion, apiVersion) {
|
||||
if (Object.keys(enabledForApps).length === 0) {
|
||||
// The `readCompressedJsonFileFallback()` function
|
||||
// will check for both a .br and .json extension.
|
||||
Object.assign(enabledForApps, readCompressedJsonFileFallback(ENABLED_APPS_FILENAME))
|
||||
}
|
||||
const openApiVersion = getOpenApiVersion(docsVersion) + (apiVersion ? `.${apiVersion}` : '')
|
||||
|
||||
return enabledForApps[openApiVersion]
|
||||
}
|
||||
21
src/rest/scripts/README.md
Normal file
21
src/rest/scripts/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# REST scripts
|
||||
|
||||
Writers run the [update-files.js](./update-files.js) script to get the latest dereferenced OpenAPI schema files.
|
||||
```
|
||||
src/rest/scripts/update-files.js
|
||||
```
|
||||
These scripts update the dereferenced OpenAPI files to create [the decorated files](../../src/rest/data) used to
|
||||
render REST docs. See the [`src/rest/README`](../../src/rest/README.md)
|
||||
for more info.
|
||||
|
||||
## Production `--decorate-only` option
|
||||
|
||||
When changes to the OpenAPI are merged to the default branch of the `github/github` repository, a pull request is automatically opened with the updated dereferenced OpenAPI files. When pull requests are authored by `github-openapi-bot`, a CI test runs the `src/rest/scripts/update-files.js` script with the `--decorate-only` option. The `--decorate-only` option only decorates the dereferenced OpenAPI files, using the existing dereferenced OpenAPI schema files, and checks those changes in to the existing branch. The `--decorate-only` option is only used by a 🤖 and is only used on production dereferenced OpenAPI schema files.
|
||||
|
||||
The `.github/workflows/openapi-schema-check.yml` CI test checks that the dereferenced and decorated schema files match. If the files don't match, potential causes could be:
|
||||
- something went wrong when the schema changes (created by `github-openapi-bot`) were merged into another branch
|
||||
- the workflow that generates the decorated files didn't run or failed
|
||||
|
||||
⚠️ Only do this if you know exactly what the `--decorate-only` option does. ⚠️
|
||||
|
||||
If you know that the dereferenced schema files are correct, you can run the `src/rest/scripts/update-files.js --decorate-only` command on the branch locally to update the decorated files in your branch.
|
||||
57
src/rest/scripts/openapi-check.js
Executable file
57
src/rest/scripts/openapi-check.js
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// [start-readme]
|
||||
//
|
||||
// Run this script to check if OpenAPI files can be decorated successfully.
|
||||
//
|
||||
// [end-readme]
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import glob from 'glob'
|
||||
import { program } from 'commander'
|
||||
import getOperations from './utils/get-operations.js'
|
||||
|
||||
program
|
||||
.description('Generate dereferenced OpenAPI and decorated schema files.')
|
||||
.requiredOption(
|
||||
'-f, --files [files...]',
|
||||
'A list of OpenAPI description files to check. Can parse literal glob patterns.'
|
||||
)
|
||||
.parse(process.argv)
|
||||
|
||||
const filenames = program.opts().files
|
||||
|
||||
const filesToCheck = filenames.flatMap((filename) => glob.sync(filename))
|
||||
|
||||
if (filesToCheck.length) {
|
||||
check(filesToCheck)
|
||||
} else {
|
||||
console.log('No files to verify.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
async function check(files) {
|
||||
console.log('Verifying OpenAPI files are valid with decorator')
|
||||
const documents = files.map((filename) => [
|
||||
filename,
|
||||
JSON.parse(fs.readFileSync(path.join(process.cwd(), filename))),
|
||||
])
|
||||
|
||||
for (const [filename, schema] of documents) {
|
||||
try {
|
||||
// munge OpenAPI definitions object in an array of operations objects
|
||||
const operations = await getOperations(schema)
|
||||
// process each operation, asynchronously rendering markdown and stuff
|
||||
await Promise.all(operations.map((operation) => operation.process()))
|
||||
|
||||
console.log(`Successfully could decorate OpenAPI operations for document ${filename}`)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.log(
|
||||
`🐛 Whoops! It looks like the decorator script wasn't able to parse the dereferenced schema in file ${filename}. A recent change may not yet be supported by the decorator. Please reach out in the #docs-engineering slack channel for help.`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
168
src/rest/scripts/test-open-api-schema.js
Executable file
168
src/rest/scripts/test-open-api-schema.js
Executable file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// [start-readme]
|
||||
//
|
||||
// Run this script to check if OpenAPI operations match versions in content/rest operations
|
||||
//
|
||||
// [end-readme]
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import _ from 'lodash'
|
||||
|
||||
import frontmatter from '../../../lib/read-frontmatter.js'
|
||||
import getApplicableVersions from '../../../lib/get-applicable-versions.js'
|
||||
import { allVersions, getDocsVersion } from '../../../lib/all-versions.js'
|
||||
|
||||
const contentFiles = []
|
||||
|
||||
export async function getDiffOpenAPIContentRest() {
|
||||
const contentPath = path.join(process.cwd(), 'content/rest')
|
||||
|
||||
// Recursively go through the content/rest directory and add all categories/subcategories to contentFiles
|
||||
throughDirectory(contentPath)
|
||||
|
||||
// Creating the categories/subcategories based on the current content directory
|
||||
const checkContentDir = await createCheckContentDirectory(contentFiles)
|
||||
|
||||
// Create categories/subcategories from OpenAPI Schemas
|
||||
const openAPISchemaCheck = await createOpenAPISchemasCheck()
|
||||
|
||||
// Get Differences between categories/subcategories from dereferenced schemas and the content/rest directory frontmatter versions
|
||||
const differences = getDifferences(openAPISchemaCheck, checkContentDir)
|
||||
const errorMessages = {}
|
||||
|
||||
if (Object.keys(differences).length > 0) {
|
||||
for (const schemaName in differences) {
|
||||
errorMessages[schemaName] = {}
|
||||
|
||||
differences[schemaName].forEach((category) => {
|
||||
if (!errorMessages[schemaName]) errorMessages[schemaName] = category
|
||||
|
||||
errorMessages[schemaName][category] = {
|
||||
contentDir: checkContentDir[schemaName][category],
|
||||
openAPI: openAPISchemaCheck[schemaName][category],
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return errorMessages
|
||||
}
|
||||
|
||||
async function createOpenAPISchemasCheck() {
|
||||
const schemasPath = path.join(process.cwd(), 'src/rest/data')
|
||||
const openAPICheck = createCheckObj()
|
||||
const schemas = fs.readdirSync(schemasPath)
|
||||
|
||||
schemas.forEach((file) => {
|
||||
const fileData = fs.readFileSync(path.join(schemasPath, file))
|
||||
const fileSchema = JSON.parse(fileData.toString())
|
||||
const categories = Object.keys(fileSchema).sort()
|
||||
const version = getDocsVersion(file.split(/.json/)[0])
|
||||
|
||||
categories.forEach((category) => {
|
||||
const subcategories = Object.keys(fileSchema[category])
|
||||
if (isApiVersioned(version)) {
|
||||
getOnlyApiVersions(version).forEach(
|
||||
(apiVersion) => (openAPICheck[apiVersion][category] = subcategories.sort())
|
||||
)
|
||||
} else {
|
||||
openAPICheck[version][category] = subcategories.sort()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return openAPICheck
|
||||
}
|
||||
|
||||
async function createCheckContentDirectory(contentFiles) {
|
||||
const checkContent = createCheckObj()
|
||||
|
||||
for (const filename of contentFiles) {
|
||||
const { data } = frontmatter(await fs.promises.readFile(filename, 'utf8'))
|
||||
const applicableVersions = getApplicableVersions(data.versions, filename)
|
||||
const splitPath = filename.split('/')
|
||||
const subCategory = splitPath[splitPath.length - 1].replace('.md', '')
|
||||
const category =
|
||||
splitPath[splitPath.length - 2] === 'rest' ? subCategory : splitPath[splitPath.length - 2]
|
||||
// All versions with appended calendar date versions if it exists
|
||||
const allCompleteVersions = applicableVersions.flatMap((version) => {
|
||||
return isApiVersioned(version)
|
||||
? allVersions[version].apiVersions.map(
|
||||
(apiVersion) => `${allVersions[version].version}.${apiVersion}`
|
||||
)
|
||||
: version
|
||||
})
|
||||
|
||||
allCompleteVersions.forEach((version) => {
|
||||
!checkContent[version][category]
|
||||
? (checkContent[version][category] = [subCategory])
|
||||
: checkContent[version][category].push(subCategory)
|
||||
checkContent[version][category].sort()
|
||||
})
|
||||
}
|
||||
|
||||
return checkContent
|
||||
}
|
||||
|
||||
function isApiVersioned(version) {
|
||||
return allVersions[version] && allVersions[version].apiVersions.length > 0
|
||||
}
|
||||
|
||||
function getOnlyApiVersions(version) {
|
||||
return allVersions[version].apiVersions.map(
|
||||
(apiVersion) => `${allVersions[version].version}.${apiVersion}`
|
||||
)
|
||||
}
|
||||
|
||||
function createCheckObj() {
|
||||
const versions = {}
|
||||
Object.keys(allVersions).forEach((version) => {
|
||||
isApiVersioned(version)
|
||||
? getOnlyApiVersions(version).forEach((apiVersion) => (versions[apiVersion] = {}))
|
||||
: (versions[`${allVersions[version].version}`] = {})
|
||||
})
|
||||
|
||||
return versions
|
||||
}
|
||||
|
||||
function getDifferences(openAPISchemaCheck, contentCheck) {
|
||||
const differences = {}
|
||||
for (const version in openAPISchemaCheck) {
|
||||
const diffOpenApiContent = difference(openAPISchemaCheck[version], contentCheck[version])
|
||||
if (Object.keys(diffOpenApiContent).length > 0) differences[version] = diffOpenApiContent
|
||||
}
|
||||
|
||||
return differences
|
||||
}
|
||||
|
||||
function throughDirectory(directory) {
|
||||
fs.readdirSync(directory).forEach((file) => {
|
||||
const absolute = path.join(directory, file)
|
||||
if (fs.statSync(absolute).isDirectory()) {
|
||||
return throughDirectory(absolute)
|
||||
} else if (
|
||||
!directory.includes('rest/guides') &&
|
||||
!directory.includes('rest/overview') &&
|
||||
!file.includes('index.md') &&
|
||||
!file.includes('quickstart.md') &&
|
||||
!file.includes('README.md')
|
||||
) {
|
||||
return contentFiles.push(absolute)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function difference(obj1, obj2) {
|
||||
const diff = Object.keys(obj1).reduce((result, key) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(obj2, key)) {
|
||||
result.push(key)
|
||||
} else if (_.isEqual(obj1[key], obj2[key])) {
|
||||
const resultKeyIndex = result.indexOf(key)
|
||||
result.splice(resultKeyIndex, 1)
|
||||
}
|
||||
return result
|
||||
}, Object.keys(obj2))
|
||||
|
||||
return diff
|
||||
}
|
||||
169
src/rest/scripts/update-files.js
Executable file
169
src/rest/scripts/update-files.js
Executable file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// [start-readme]
|
||||
//
|
||||
// Run this script to pull openAPI files from github/github, dereference them, and decorate them.
|
||||
//
|
||||
// [end-readme]
|
||||
|
||||
import { stat, readdir } from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { program } from 'commander'
|
||||
import { execSync } from 'child_process'
|
||||
import mkdirp from 'mkdirp'
|
||||
import rimraf from 'rimraf'
|
||||
|
||||
import { decorate } from './utils/decorator.js'
|
||||
import { validateVersionsOptions } from './utils/get-openapi-schemas.js'
|
||||
import { allVersions } from '../../../lib/all-versions.js'
|
||||
|
||||
const TEMP_DOCS_DIR = path.join(process.cwd(), 'openapiTmp')
|
||||
const DOCS_DEREF_OPENAPI_DIR = path.join(process.cwd(), 'src/rest/data/dereferenced')
|
||||
const GITHUB_REP_DIR = path.join(process.cwd(), '../github')
|
||||
|
||||
program
|
||||
.description('Generate dereferenced OpenAPI and decorated schema files.')
|
||||
.option(
|
||||
'--decorate-only',
|
||||
'⚠️ Only used by a 🤖 to generate decorated schema files from existing dereferenced schema files.'
|
||||
)
|
||||
.option(
|
||||
'-v --versions <VERSIONS...>',
|
||||
'A list of undeprecated, published versions to build, separated by a space. Example `-v ghes-3.1` or `-v api.github.com github.ae`'
|
||||
)
|
||||
.option('-d --include-deprecated', 'Includes schemas that are marked as `deprecated: true`')
|
||||
.option('-u --include-unpublished', 'Includes schemas that are marked as `published: false`')
|
||||
.option(
|
||||
'-k --keep-dereferenced-files',
|
||||
'Keeps the dereferenced files after the script runs. You will need to delete them manually.'
|
||||
)
|
||||
.option('-n --next', 'Generate the next OpenAPI calendar-date version.')
|
||||
.option('-s --open-source', 'Generate the OpenAPI schema from github/rest-api-description')
|
||||
.parse(process.argv)
|
||||
|
||||
const {
|
||||
decorateOnly,
|
||||
versions,
|
||||
includeUnpublished,
|
||||
includeDeprecated,
|
||||
keepDereferencedFiles,
|
||||
next,
|
||||
openSource,
|
||||
} = program.opts()
|
||||
|
||||
main()
|
||||
|
||||
async function main() {
|
||||
await validateInputParameters()
|
||||
// Generate the dereferenced OpenAPI schema files
|
||||
if (!decorateOnly) {
|
||||
await getBundledFiles()
|
||||
}
|
||||
|
||||
// When we get the dereferenced OpenAPI files from the open-source
|
||||
// github/rest-api-description repo, we need to remove any versions
|
||||
// that are deprecated.
|
||||
if (openSource) {
|
||||
const currentOpenApiVersions = Object.values(allVersions).map((elem) => elem.openApiVersionName)
|
||||
const allSchemas = await readdir(DOCS_DEREF_OPENAPI_DIR)
|
||||
allSchemas.forEach((schema) => {
|
||||
// if the schema does not start with a current version name, delete it
|
||||
if (!currentOpenApiVersions.some((version) => schema.startsWith(version))) {
|
||||
rimraf.sync(path.join(DOCS_DEREF_OPENAPI_DIR, schema))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const schemas = await readdir(DOCS_DEREF_OPENAPI_DIR)
|
||||
// Decorate the dereferenced files in a format ingestible by docs.github.com
|
||||
await decorate(schemas)
|
||||
console.log(
|
||||
'\n🏁 The static REST API files are now up-to-date with your local `github/github` checkout. To revert uncommitted changes, run `git checkout src/rest/data/*`.\n\n'
|
||||
)
|
||||
if (!keepDereferencedFiles) {
|
||||
rimraf.sync(DOCS_DEREF_OPENAPI_DIR)
|
||||
}
|
||||
}
|
||||
|
||||
async function getBundledFiles() {
|
||||
// Get the github/github repo branch name and pull latest
|
||||
const githubBranch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: GITHUB_REP_DIR })
|
||||
.toString()
|
||||
.trim()
|
||||
|
||||
// Only pull master branch because development mode branches are assumed
|
||||
// to be up-to-date during active work.
|
||||
if (githubBranch === 'master') {
|
||||
execSync('git pull', { cwd: GITHUB_REP_DIR })
|
||||
}
|
||||
|
||||
// Create a tmp directory to store schema files generated from github/github
|
||||
rimraf.sync(TEMP_DOCS_DIR)
|
||||
await mkdirp(TEMP_DOCS_DIR)
|
||||
|
||||
console.log(
|
||||
`\n🏃♀️🏃🏃♀️Running \`bin/openapi bundle\` in branch '${githubBranch}' of your github/github checkout to generate the dereferenced OpenAPI schema files.\n`
|
||||
)
|
||||
// Format the command supplied to the bundle script in `github/github`
|
||||
const bundlerOptions = await getBundlerOptions()
|
||||
const bundleCommand = `bundle -v -w${next ? ' -n' : ''} -o ${TEMP_DOCS_DIR} ${bundlerOptions}`
|
||||
try {
|
||||
console.log(bundleCommand)
|
||||
execSync(`${path.join(GITHUB_REP_DIR, 'bin/openapi')} ${bundleCommand}`, { stdio: 'inherit' })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const errorMsg =
|
||||
'🛑 Whoops! It looks like the `bin/openapi bundle` command failed to run in your `github/github` repository checkout.\n\n✅ Troubleshooting:\n - Make sure you have a codespace with a checkout of `github/github` at the same level as your `github/docs-internal` repo before running this script. See this documentation for details: https://thehub.github.com/epd/engineering/products-and-services/public-apis/rest/openapi/openapi-in-the-docs/#previewing-changes-in-the-docs.\n - Ensure that your OpenAPI schema YAML is formatted correctly. A CI test runs on your `github/github` PR that flags malformed YAML. You can check the PR diff view for comments left by the OpenAPI CI test to find and fix any formatting errors.\n\n'
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
|
||||
// Moving the dereferenced files to the docs directory creates a consistent
|
||||
// place to generate the decorated files from. This is where they will be
|
||||
// delivered in automated pull requests and because of that we move them
|
||||
// to the same location during local development.
|
||||
await mkdirp(DOCS_DEREF_OPENAPI_DIR)
|
||||
execSync(
|
||||
`find ${TEMP_DOCS_DIR} -type f -name "*deref.json" -exec mv '{}' ${DOCS_DEREF_OPENAPI_DIR} ';'`
|
||||
)
|
||||
|
||||
rimraf.sync(TEMP_DOCS_DIR)
|
||||
}
|
||||
|
||||
async function getBundlerOptions() {
|
||||
let includeParams = []
|
||||
|
||||
if (versions) {
|
||||
includeParams = versions
|
||||
}
|
||||
if (includeUnpublished) {
|
||||
includeParams.push('--include_unpublished')
|
||||
}
|
||||
if (includeDeprecated) {
|
||||
includeParams.push('--include_deprecated')
|
||||
}
|
||||
|
||||
return includeParams.join(' ')
|
||||
}
|
||||
|
||||
async function validateInputParameters() {
|
||||
// The `--versions` and `--decorate-only` options cannot be used
|
||||
// with the `--include-deprecated` or `--include-unpublished` options
|
||||
if ((includeDeprecated || includeUnpublished) && (decorateOnly || versions)) {
|
||||
const errorMsg = `🛑 You cannot use the versions option with the include-unpublished or include-deprecated options. This is not currently supported in the bundler.\nYou cannot use the decorate-only option with include-unpublished or include-deprecated because the include-unpublished and include-deprecated options are only available when running the bundler. The decorate-only option skips running the bundler.\nPlease reach out to #docs-engineering if a new use case should be supported.`
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
|
||||
// Check that the github/github repo exists. If the files are only being
|
||||
// decorated, the github/github repo isn't needed.
|
||||
if (!decorateOnly) {
|
||||
try {
|
||||
await stat(GITHUB_REP_DIR)
|
||||
} catch (error) {
|
||||
const errorMsg = `🛑 The ${GITHUB_REP_DIR} does not exist. Make sure you have a codespace with a checkout of \`github/github\` at the same level as your \`github/docs-internal \`repo before running this script. See this documentation for details: https://thehub.github.com/epd/engineering/products-and-services/public-apis/rest/openapi/openapi-in-the-docs/#previewing-changes-in-the-docs.`
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
}
|
||||
if (versions && versions.length) {
|
||||
await validateVersionsOptions(versions)
|
||||
}
|
||||
}
|
||||
359
src/rest/scripts/utils/create-rest-examples.js
Normal file
359
src/rest/scripts/utils/create-rest-examples.js
Normal file
@@ -0,0 +1,359 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// In the case that there are more than one example requests, and
|
||||
// no content responses, a request with an example key that matches the
|
||||
// status code of a response will be matched.
|
||||
const DEFAULT_EXAMPLE_DESCRIPTION = 'Example'
|
||||
const DEFAULT_EXAMPLE_KEY = 'default'
|
||||
const DEFAULT_ACCEPT_HEADER = 'application/vnd.github.v3+json'
|
||||
|
||||
// Retrieves request and response examples and attempts to
|
||||
// merge them to create matching request/response examples
|
||||
// The key used in the media type `examples` property is
|
||||
// used to match requests to responses.
|
||||
export default function getCodeSamples(operation) {
|
||||
const responseExamples = getResponseExamples(operation)
|
||||
const requestExamples = getRequestExamples(operation)
|
||||
|
||||
const mergedExamples = mergeExamples(requestExamples, responseExamples)
|
||||
|
||||
// If there are multiple examples and if the request body
|
||||
// has the same description, add a number to the example
|
||||
if (mergedExamples.length > 1) {
|
||||
const count = {}
|
||||
mergedExamples.forEach((item) => {
|
||||
count[item.request.description] = (count[item.request.description] || 0) + 1
|
||||
})
|
||||
|
||||
const newMergedExamples = mergedExamples.map((example, i) => ({
|
||||
...example,
|
||||
request: {
|
||||
...example.request,
|
||||
description:
|
||||
count[example.request.description] > 1
|
||||
? example.request.description +
|
||||
' ' +
|
||||
(i + 1) +
|
||||
': Status Code ' +
|
||||
example.response.statusCode
|
||||
: example.request.description,
|
||||
},
|
||||
}))
|
||||
|
||||
return newMergedExamples
|
||||
}
|
||||
|
||||
return mergedExamples
|
||||
}
|
||||
|
||||
export function mergeExamples(requestExamples, responseExamples) {
|
||||
// There is always at least one request example, but it won't create
|
||||
// a meaningful example unless it has a response example.
|
||||
if (requestExamples.length === 1 && responseExamples.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// If there is one request and one response example, we don't
|
||||
// need to merge the requests and responses, and we don't need
|
||||
// to match keys directly. This allows falling back in the
|
||||
// case that the existing OpenAPI schema has mismatched example keys.
|
||||
if (requestExamples.length === 1 && responseExamples.length === 1) {
|
||||
return [{ ...requestExamples[0], response: responseExamples[0].response }]
|
||||
}
|
||||
|
||||
// If there is a request with no request body parameters and all of
|
||||
// the responses have no content, then we can create a docs
|
||||
// example for just status codes below 300. All other status codes will
|
||||
// be listed in the status code table in the docs.
|
||||
if (
|
||||
requestExamples.length === 1 &&
|
||||
responseExamples.length > 1 &&
|
||||
!responseExamples.find((ex) => ex.response.example)
|
||||
) {
|
||||
return responseExamples
|
||||
.filter((resp) => parseInt(resp.response.statusCode, 10) < 300)
|
||||
.map((ex) => ({ ...requestExamples[0], ...ex }))
|
||||
}
|
||||
|
||||
// If there is exactly one request example and one or more response
|
||||
// examples, we can make a docs example for the response examples that
|
||||
// have content. All remaining status codes with no content
|
||||
// will be listed in the status code table in the docs.
|
||||
if (
|
||||
requestExamples.length === 1 &&
|
||||
responseExamples.length > 1 &&
|
||||
responseExamples.filter((ex) => ex.response.example).length >= 1
|
||||
) {
|
||||
return responseExamples
|
||||
.filter((ex) => ex.response.example)
|
||||
.map((ex) => ({ ...requestExamples[0], ...ex }))
|
||||
}
|
||||
|
||||
// Finally, we'll attempt to match examples with matching keys.
|
||||
// This iterates through the longer array and compares key values to keys in
|
||||
// the shorter array.
|
||||
const requestsExamplesLarger = requestExamples.length >= responseExamples.length
|
||||
const target = requestsExamplesLarger ? requestExamples : responseExamples
|
||||
const source = requestsExamplesLarger ? responseExamples : requestExamples
|
||||
|
||||
// Iterates over the larger array or "target" (or if equal requests) to see
|
||||
// if there are any matches in the smaller array or "source"
|
||||
// (or if equal responses) that can be added to target array. If a request
|
||||
// example and response example have matching keys they will be merged into
|
||||
// an example. If there is more than one key match, the first match will
|
||||
// be used.
|
||||
return target.filter((targetEx) => {
|
||||
const match = source.find((srcEx) => srcEx.key === targetEx.key)
|
||||
if (match) return Object.assign(targetEx, match)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Create an example object for each example in the requestBody property
|
||||
of the schema. Each requestBody can have more than one content type.
|
||||
Each content type can have more than one example. We create an object
|
||||
for each permutation of content type and example.
|
||||
Returns an array of objects in the format:
|
||||
{
|
||||
key,
|
||||
request: {
|
||||
contentType,
|
||||
description,
|
||||
acceptHeader,
|
||||
bodyParameters,
|
||||
parameters,
|
||||
}
|
||||
}
|
||||
*/
|
||||
export function getRequestExamples(operation) {
|
||||
const requestExamples = []
|
||||
const parameterExamples = getParameterExamples(operation)
|
||||
|
||||
// When no request body or parameters are defined, we create a generic
|
||||
// request example. Not all operations have request bodies or parameters,
|
||||
// but we always want to show at least an example with the path.
|
||||
if (!operation.requestBody && Object.keys(parameterExamples).length === 0) {
|
||||
return [
|
||||
{
|
||||
key: DEFAULT_EXAMPLE_KEY,
|
||||
request: {
|
||||
description: DEFAULT_EXAMPLE_DESCRIPTION,
|
||||
acceptHeader: DEFAULT_ACCEPT_HEADER,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// When no request body exists, we create an example from the parameters
|
||||
if (!operation.requestBody) {
|
||||
return Object.keys(parameterExamples).map((key) => {
|
||||
return {
|
||||
key,
|
||||
request: {
|
||||
description: DEFAULT_EXAMPLE_DESCRIPTION,
|
||||
acceptHeader: DEFAULT_ACCEPT_HEADER,
|
||||
parameters: parameterExamples[key] || parameterExamples.default,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Requests can have multiple content types each with their own set of
|
||||
// examples.
|
||||
Object.keys(operation.requestBody.content).forEach((contentType) => {
|
||||
let examples = {}
|
||||
// This is a fallback to allow using the `example` property in
|
||||
// the schema. If we start to enforce using examples vs. example using
|
||||
// a linter, we can remove the check for `example`.
|
||||
// For now, we'll use the key default, which is a common default
|
||||
// example name in the OpenAPI schema.
|
||||
if (operation.requestBody.content[contentType].example) {
|
||||
examples = {
|
||||
default: {
|
||||
value: operation.requestBody.content[contentType].example,
|
||||
},
|
||||
}
|
||||
} else if (operation.requestBody.content[contentType].examples) {
|
||||
examples = operation.requestBody.content[contentType].examples
|
||||
} else {
|
||||
// Example for this content type doesn't exist so we'll try and create one
|
||||
requestExamples.push({
|
||||
key: DEFAULT_EXAMPLE_KEY,
|
||||
request: {
|
||||
contentType,
|
||||
description: DEFAULT_EXAMPLE_DESCRIPTION,
|
||||
acceptHeader: DEFAULT_ACCEPT_HEADER,
|
||||
parameters: parameterExamples.default,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// There can be more than one example for a given content type. We need to
|
||||
// iterate over the keys of the examples to create individual
|
||||
// example objects
|
||||
Object.keys(examples).forEach((key) => {
|
||||
// A content type that includes `+json` is a custom media type
|
||||
// The default accept header is application/vnd.github.v3+json
|
||||
// Which would have a content type of `application/json`
|
||||
const acceptHeader = contentType.includes('+json')
|
||||
? contentType
|
||||
: 'application/vnd.github.v3+json'
|
||||
|
||||
const example = {
|
||||
key,
|
||||
request: {
|
||||
contentType,
|
||||
description: examples[key].summary || DEFAULT_EXAMPLE_DESCRIPTION,
|
||||
acceptHeader,
|
||||
bodyParameters: examples[key].value,
|
||||
parameters: parameterExamples[key] || parameterExamples.default,
|
||||
},
|
||||
}
|
||||
requestExamples.push(example)
|
||||
})
|
||||
})
|
||||
return requestExamples
|
||||
}
|
||||
|
||||
/*
|
||||
Create an example object for each example in the response property
|
||||
of the schema. Each response can have more than one status code,
|
||||
each with more than one content type. And each content type can
|
||||
have more than one example. We create an object
|
||||
for each permutation of status, content type, and example.
|
||||
Returns an array of objects in the format:
|
||||
{
|
||||
key,
|
||||
response: {
|
||||
statusCode,
|
||||
contentType,
|
||||
description,
|
||||
example,
|
||||
}
|
||||
}
|
||||
*/
|
||||
export function getResponseExamples(operation) {
|
||||
const responseExamples = []
|
||||
Object.keys(operation.responses).forEach((statusCode) => {
|
||||
// We don't want to create examples for error codes
|
||||
// Error codes are displayed in the status table in the docs
|
||||
if (parseInt(statusCode, 10) >= 400) return
|
||||
|
||||
const content = operation.responses[statusCode].content
|
||||
|
||||
// A response doesn't always have content (ex:, status 304)
|
||||
// In this case we create a generic example for the status code
|
||||
// with a key that matches the status code.
|
||||
if (!content) {
|
||||
const example = {
|
||||
key: statusCode,
|
||||
response: {
|
||||
statusCode,
|
||||
description: operation.responses[statusCode].description,
|
||||
},
|
||||
}
|
||||
responseExamples.push(example)
|
||||
return
|
||||
}
|
||||
|
||||
// Responses can have multiple content types each with their own set of
|
||||
// examples.
|
||||
Object.keys(content).forEach((contentType) => {
|
||||
let examples = {}
|
||||
// This is a fallback to allow using the `example` property in
|
||||
// the schema. If we start to enforce using examples vs. example using
|
||||
// a linter, we can remove the check for `example`.
|
||||
// For now, we'll use the key default, which is a common default
|
||||
// example name in the OpenAPI schema.
|
||||
if (operation.responses[statusCode].content[contentType].example) {
|
||||
examples = {
|
||||
default: {
|
||||
value: operation.responses[statusCode].content[contentType].example,
|
||||
},
|
||||
}
|
||||
} else if (operation.responses[statusCode].content[contentType].examples) {
|
||||
examples = operation.responses[statusCode].content[contentType].examples
|
||||
} else if (parseInt(statusCode, 10) < 300) {
|
||||
// Sometimes there are missing examples for say a 200 response and
|
||||
// the operation also has a 304 no content status. If we don't add
|
||||
// the 200 response example, even though it has not example response,
|
||||
// the resulting responseExamples would only contain the 304 response.
|
||||
// That would be confusing in the docs because it's expected to see the
|
||||
// common or success responses by default.
|
||||
const example = {
|
||||
key: statusCode,
|
||||
response: {
|
||||
statusCode,
|
||||
description: operation.responses[statusCode].description,
|
||||
},
|
||||
}
|
||||
responseExamples.push(example)
|
||||
return
|
||||
} else {
|
||||
// Example for this content type doesn't exist.
|
||||
// We could also check if there is a fully populated example
|
||||
// directly in the response schema examples properties.
|
||||
return
|
||||
}
|
||||
|
||||
// There can be more than one example for a given content type. We need to
|
||||
// iterate over the keys of the examples to create individual
|
||||
// example objects
|
||||
Object.keys(examples).forEach((key) => {
|
||||
const example = {
|
||||
key,
|
||||
response: {
|
||||
statusCode,
|
||||
contentType,
|
||||
description: examples[key].summary || operation.responses[statusCode].description,
|
||||
example: examples[key].value,
|
||||
// TODO adding the schema quadruples the JSON file size. Changing
|
||||
// how we write the JSON file helps a lot, but we should revisit
|
||||
// adding the response schema to ensure we have a way to view the
|
||||
// prettified JSON before minimizing it.
|
||||
schema: operation.responses[statusCode].content[contentType].schema,
|
||||
},
|
||||
}
|
||||
responseExamples.push(example)
|
||||
})
|
||||
})
|
||||
})
|
||||
return responseExamples
|
||||
}
|
||||
|
||||
/*
|
||||
Path parameters can have more than one example key. We need to create
|
||||
an example for each and then choose the most appropriate example when
|
||||
we merge requests with responses.
|
||||
Parameter examples are in the format:
|
||||
{
|
||||
[parameter key]: {
|
||||
[parameter name]: value
|
||||
}
|
||||
}
|
||||
*/
|
||||
export function getParameterExamples(operation) {
|
||||
if (!operation.parameters) {
|
||||
return {}
|
||||
}
|
||||
const parameters = operation.parameters.filter((param) => param.in === 'path')
|
||||
const parameterExamples = {}
|
||||
parameters.forEach((parameter) => {
|
||||
const examples = parameter.examples
|
||||
// If there are no examples, create an example from the uppercase parameter
|
||||
// name, so that it is more visible that the value is fake data
|
||||
// in the route path.
|
||||
if (!examples) {
|
||||
if (!parameterExamples.default) parameterExamples.default = {}
|
||||
parameterExamples.default[parameter.name] = parameter.name.toUpperCase()
|
||||
} else {
|
||||
Object.keys(examples).forEach((key) => {
|
||||
if (!parameterExamples[key]) parameterExamples[key] = {}
|
||||
parameterExamples[key][parameter.name] = examples[key].value
|
||||
})
|
||||
}
|
||||
})
|
||||
return parameterExamples
|
||||
}
|
||||
333
src/rest/scripts/utils/decorator.js
Normal file
333
src/rest/scripts/utils/decorator.js
Normal file
@@ -0,0 +1,333 @@
|
||||
import { existsSync, mkdirSync } from 'fs'
|
||||
import { readFile, writeFile } from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { slug } from 'github-slugger'
|
||||
|
||||
import { allVersions } from '../../../../lib/all-versions.js'
|
||||
import { categoriesWithoutSubcategories } from '../../lib/index.js'
|
||||
import getOperations, { getWebhooks } from './get-operations.js'
|
||||
|
||||
const ENABLED_APPS = 'src/github-apps/data/enabled-for-apps.json'
|
||||
const STATIC_REDIRECTS = 'lib/redirects/static/client-side-rest-api-redirects.json'
|
||||
const REST_DECORATED_DIR = 'src/rest/data'
|
||||
const WEBHOOK_DECORATED_DIR = 'src/webhooks/data'
|
||||
const REST_DEREFERENCED_DIR = 'src/rest/data/dereferenced'
|
||||
|
||||
export async function decorate(schemas) {
|
||||
console.log('\n🎄 Decorating the OpenAPI schema files in src/rest/data/dereferenced.\n')
|
||||
const { restSchemas, webhookSchemas } = await getOpenApiSchemaFiles(schemas)
|
||||
const webhookOperations = await getWebhookOperations(webhookSchemas)
|
||||
await createStaticWebhookFiles(webhookOperations)
|
||||
const restOperations = await getRestOperations(restSchemas)
|
||||
await createStaticRestFiles(restOperations)
|
||||
}
|
||||
|
||||
async function getRestOperations(restSchemas) {
|
||||
console.log('\n⏭️ Start generating static REST files\n')
|
||||
const restSchemaData = await getDereferencedFiles(restSchemas)
|
||||
const restOperations = {}
|
||||
for (const [schemaName, schema] of Object.entries(restSchemaData)) {
|
||||
try {
|
||||
// get all of the operations and wehbooks for a particular version of the openapi
|
||||
const operations = await getOperations(schema)
|
||||
// process each operation and webhook, asynchronously rendering markdown and stuff
|
||||
if (operations.length) {
|
||||
console.log(`...processing ${schemaName} rest operations`)
|
||||
await Promise.all(operations.map((operation) => operation.process()))
|
||||
restOperations[schemaName] = operations
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
"🐛 Whoops! It looks like the decorator script wasn't able to parse the dereferenced schema. A recent change may not yet be supported by the decorator. Please reach out in the #docs-engineering slack channel for help."
|
||||
)
|
||||
}
|
||||
}
|
||||
return restOperations
|
||||
}
|
||||
|
||||
async function getWebhookOperations(webhookSchemas) {
|
||||
console.log('⏭️ Start generating static webhook files\n')
|
||||
const webhookSchemaData = await getDereferencedFiles(webhookSchemas)
|
||||
const webhookOperations = {}
|
||||
for (const [schemaName, schema] of Object.entries(webhookSchemaData)) {
|
||||
try {
|
||||
const webhooks = await getWebhooks(schema)
|
||||
if (webhooks.length) {
|
||||
console.log(`...processing ${schemaName} webhook operations`)
|
||||
await Promise.all(webhooks.map((webhook) => webhook.process()))
|
||||
webhookOperations[schemaName] = webhooks
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
"🐛 Whoops! It looks like the decorator script wasn't able to parse the dereferenced schema. A recent change may not yet be supported by the decorator. Please reach out in the #docs-engineering slack channel for help."
|
||||
)
|
||||
}
|
||||
}
|
||||
return webhookOperations
|
||||
}
|
||||
|
||||
async function createStaticRestFiles(restOperations) {
|
||||
const operationsEnabledForGitHubApps = {}
|
||||
const clientSideRedirects = await getCategoryOverrideRedirects()
|
||||
for (const schemaName in restOperations) {
|
||||
const operations = restOperations[schemaName]
|
||||
await addRestClientSideRedirects(operations, clientSideRedirects)
|
||||
|
||||
const categories = [...new Set(operations.map((operation) => operation.category))].sort()
|
||||
|
||||
// Orders the operations by their category and subcategories.
|
||||
// All operations must have a category, but operations don't need
|
||||
// a subcategory. When no subcategory is present, the subcategory
|
||||
// property is an empty string ('').
|
||||
/*
|
||||
Example:
|
||||
{
|
||||
[category]: {
|
||||
'': {
|
||||
"description": "",
|
||||
"operations": []
|
||||
},
|
||||
[subcategory sorted alphabetically]: {
|
||||
"description": "",
|
||||
"operations": []
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const operationsByCategory = {}
|
||||
categories.forEach((category) => {
|
||||
operationsByCategory[category] = {}
|
||||
const categoryOperations = operations.filter((operation) => operation.category === category)
|
||||
categoryOperations
|
||||
.filter((operation) => !operation.subcategory)
|
||||
.map((operation) => (operation.subcategory = operation.category))
|
||||
|
||||
const subcategories = [
|
||||
...new Set(categoryOperations.map((operation) => operation.subcategory)),
|
||||
].sort()
|
||||
// the first item should be the item that has no subcategory
|
||||
// e.g., when the subcategory = category
|
||||
const firstItemIndex = subcategories.indexOf(category)
|
||||
if (firstItemIndex > -1) {
|
||||
const firstItem = subcategories.splice(firstItemIndex, 1)[0]
|
||||
subcategories.unshift(firstItem)
|
||||
}
|
||||
|
||||
subcategories.forEach((subcategory) => {
|
||||
operationsByCategory[category][subcategory] = {}
|
||||
|
||||
const subcategoryOperations = categoryOperations.filter(
|
||||
(operation) => operation.subcategory === subcategory
|
||||
)
|
||||
|
||||
operationsByCategory[category][subcategory] = subcategoryOperations
|
||||
})
|
||||
})
|
||||
|
||||
const restFilename = path.join(REST_DECORATED_DIR, `${schemaName}.json`).replace('.deref', '')
|
||||
|
||||
// write processed operations to disk
|
||||
await writeFile(restFilename, JSON.stringify(operationsByCategory, null, 2))
|
||||
console.log('Wrote', path.relative(process.cwd(), restFilename))
|
||||
|
||||
// Create the enabled-for-apps.json file used for
|
||||
// https://docs.github.com/en/rest/overview/endpoints-available-for-github-apps
|
||||
operationsEnabledForGitHubApps[schemaName] = {}
|
||||
for (const category of categories) {
|
||||
const categoryOperations = operations.filter((operation) => operation.category === category)
|
||||
|
||||
// This is a collection of operations that have `enabledForGitHubApps = true`
|
||||
// It's grouped by resource title to make rendering easier
|
||||
operationsEnabledForGitHubApps[schemaName][category] = categoryOperations
|
||||
.filter((operation) => operation.enabledForGitHubApps)
|
||||
.map((operation) => ({
|
||||
slug: slug(operation.title),
|
||||
subcategory: operation.subcategory,
|
||||
verb: operation.verb,
|
||||
requestPath: operation.requestPath,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile(ENABLED_APPS, JSON.stringify(operationsEnabledForGitHubApps, null, 2))
|
||||
console.log('Wrote', ENABLED_APPS)
|
||||
await writeFile(STATIC_REDIRECTS, JSON.stringify(clientSideRedirects, null, 2), 'utf8')
|
||||
console.log('Wrote', STATIC_REDIRECTS)
|
||||
}
|
||||
|
||||
async function getDereferencedFiles(schemas) {
|
||||
const schemaData = {}
|
||||
for (const filename of schemas) {
|
||||
const file = path.join(REST_DEREFERENCED_DIR, `${filename}.deref.json`)
|
||||
const schema = JSON.parse(await readFile(file))
|
||||
schemaData[filename] = schema
|
||||
}
|
||||
return schemaData
|
||||
}
|
||||
|
||||
async function createStaticWebhookFiles(webhookSchemas) {
|
||||
if (!Object.keys(webhookSchemas).length) {
|
||||
console.log(
|
||||
'🟡 No webhooks exist in the dereferenced files. No static webhook files will be generated.\n'
|
||||
)
|
||||
return
|
||||
}
|
||||
// Create a map of webhooks (e.g. check_run, issues, release) to the
|
||||
// webhook's actions (e.g. created, deleted, etc.).
|
||||
//
|
||||
// Some webhooks like the ping webhook have no action types -- in cases
|
||||
// like this we set a default action of 'default'.
|
||||
//
|
||||
// Example:
|
||||
/*
|
||||
{
|
||||
'branch-protection-rule': {
|
||||
created: Webhook {
|
||||
descriptionHtml: '<p>A branch protection rule was created.</p>',
|
||||
summaryHtml: '<p>This event occurs when there is activity relating to branch protection rules. For more information, see "<a href="https://docs.github.com/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches">About protected branches</a>." For information about the Branch protection APIs, see <a href="https://docs.github.com/graphql/reference/objects#branchprotectionrule">the GraphQL documentation</a> and <a href="https://docs.github.com/rest/branches/branch-protection">the REST API documentation</a>.</p>\n' +
|
||||
'<p>In order to install this event on a GitHub App, the app must have <code>read-only</code> access on repositories administration.</p>',
|
||||
bodyParameters: [Array],
|
||||
availability: [Array],
|
||||
action: 'created',
|
||||
category: 'branch-protection-rule'
|
||||
},
|
||||
deleted: Webhook {
|
||||
descriptionHtml: '<p>A branch protection rule was deleted.</p>',
|
||||
summaryHtml: '<p>This event occurs when there is activity relating to branch protection rules. For more information, see "<a href="https://docs.github.com/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches">About protected branches</a>." For information about the Branch protection APIs, see <a href="https://docs.github.com/graphql/reference/objects#branchprotectionrule">the GraphQL documentation</a> and <a href="https://docs.github.com/rest/branches/branch-protection">the REST API documentation</a>.</p>\n' +
|
||||
'<p>In order to install this event on a GitHub App, the app must have <code>read-only</code> access on repositories administration.</p>',
|
||||
bodyParameters: [Array],
|
||||
availability: [Array],
|
||||
action: 'deleted',
|
||||
category: 'branch-protection-rule'
|
||||
},
|
||||
...
|
||||
}
|
||||
*/
|
||||
const categorizedWebhooks = {}
|
||||
for (const [schemaName, webhooks] of Object.entries(webhookSchemas)) {
|
||||
webhooks.forEach((webhook) => {
|
||||
if (!webhook.action) webhook.action = 'default'
|
||||
|
||||
if (categorizedWebhooks[webhook.category]) {
|
||||
categorizedWebhooks[webhook.category][webhook.action] = webhook
|
||||
} else {
|
||||
categorizedWebhooks[webhook.category] = {}
|
||||
categorizedWebhooks[webhook.category][webhook.action] = webhook
|
||||
}
|
||||
})
|
||||
const webhooksFilename = path
|
||||
.join(WEBHOOK_DECORATED_DIR, `${schemaName}.json`)
|
||||
.replace('.deref', '')
|
||||
if (Object.keys(categorizedWebhooks).length > 0) {
|
||||
if (!existsSync(WEBHOOK_DECORATED_DIR)) {
|
||||
mkdirSync(WEBHOOK_DECORATED_DIR)
|
||||
}
|
||||
await writeFile(webhooksFilename, JSON.stringify(categorizedWebhooks, null, 2))
|
||||
console.log('Wrote', path.relative(process.cwd(), webhooksFilename))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getCategoryOverrideRedirects() {
|
||||
const { operationUrls, sectionUrls } = JSON.parse(
|
||||
await readFile('src/rest/scripts/utils/rest-api-overrides.json', 'utf8')
|
||||
)
|
||||
|
||||
const operationRedirects = {}
|
||||
console.log('\n➡️ Updating REST API redirect exception list.\n')
|
||||
Object.values(operationUrls).forEach((value) => {
|
||||
const oldUrl = value.originalUrl.replace('/rest/reference', '/rest')
|
||||
const anchor = oldUrl.split('#')[1]
|
||||
const subcategory = value.subcategory
|
||||
const redirectTo = subcategory
|
||||
? `/rest/${value.category}/${subcategory}#${anchor}`
|
||||
: `/rest/${value.category}#${anchor}`
|
||||
operationRedirects[oldUrl] = redirectTo
|
||||
})
|
||||
const redirects = {
|
||||
...operationRedirects,
|
||||
...sectionUrls,
|
||||
}
|
||||
return redirects
|
||||
}
|
||||
|
||||
async function addRestClientSideRedirects(operations, clientSideRedirects) {
|
||||
// For each rest operation that doesn't have an override defined
|
||||
// in src/rest/scripts/utils/rest-api-overrides.json,
|
||||
// add a client-side redirect
|
||||
operations.forEach((operation) => {
|
||||
// A handful of operations don't have external docs properties
|
||||
const externalDocs = operation.getExternalDocs()
|
||||
if (!externalDocs) {
|
||||
return
|
||||
}
|
||||
const oldUrl = `/rest${externalDocs.url.replace('/rest/reference', '/rest').split('/rest')[1]}`
|
||||
|
||||
if (!(oldUrl in clientSideRedirects)) {
|
||||
// There are some operations that aren't nested in the sidebar
|
||||
// For these, don't need to add a client-side redirect, the
|
||||
// frontmatter redirect will handle it for us.
|
||||
if (categoriesWithoutSubcategories.includes(operation.category)) {
|
||||
return
|
||||
}
|
||||
const anchor = oldUrl.split('#')[1]
|
||||
const subcategory = operation.subcategory
|
||||
|
||||
// If there is no subcategory, a new page with the same name as the
|
||||
// category was created. That page name may change going forward.
|
||||
const redirectTo = subcategory
|
||||
? `/rest/${operation.category}/${subcategory}#${anchor}`
|
||||
: `/rest/${operation.category}/${operation.category}#${anchor}`
|
||||
clientSideRedirects[oldUrl] = redirectTo
|
||||
}
|
||||
|
||||
// There are a lot of section headings that we'll want to redirect too,
|
||||
// now that subcategories are on their own page. For example,
|
||||
// /rest/reference/actions#artifacts should redirect to
|
||||
// /rest/actions/artifacts
|
||||
if (operation.subcategory) {
|
||||
const sectionRedirectFrom = `/rest/${operation.category}#${operation.subcategory}`
|
||||
const sectionRedirectTo = `/rest/${operation.category}/${operation.subcategory}`
|
||||
if (!(sectionRedirectFrom in clientSideRedirects)) {
|
||||
clientSideRedirects[sectionRedirectFrom] = sectionRedirectTo
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function getOpenApiSchemaFiles(schemas) {
|
||||
const webhookSchemas = []
|
||||
const restSchemas = []
|
||||
|
||||
// All of the schema releases that we store in allVersions
|
||||
// Ex: 'api.github.com', 'ghec', 'ghes-3.6', 'ghes-3.5',
|
||||
// 'ghes-3.4', 'ghes-3.3', 'ghes-3.2', 'github.ae'
|
||||
const openApiVersions = Object.keys(allVersions).map(
|
||||
(elem) => allVersions[elem].openApiVersionName
|
||||
)
|
||||
// The full list of dereferened OpenAPI schemas received from
|
||||
// bundling the OpenAPI in github/github
|
||||
const schemaBaseNames = schemas.map((schema) => path.basename(schema, '.deref.json'))
|
||||
for (const schema of schemaBaseNames) {
|
||||
// catches all of the schemas that are not
|
||||
// calendar date versioned. Ex: ghec, ghes-3.7, and api.github.com
|
||||
if (openApiVersions.includes(schema)) {
|
||||
webhookSchemas.push(schema)
|
||||
// Non-calendar date schemas could also match the calendar date versioned
|
||||
// counterpart.
|
||||
// Ex: api.github.com would match api.github.com and
|
||||
// api.github.com.2022-09-09
|
||||
const filteredMatches = schemaBaseNames.filter((elem) => elem.includes(schema))
|
||||
// If there is only one match then there are no calendar date counterparts
|
||||
// and this is the only schema for this plan and release.
|
||||
if (filteredMatches.length === 1) {
|
||||
restSchemas.push(schema)
|
||||
}
|
||||
// catches all of the calendar date versioned schemas in the
|
||||
// format api.github.com.<year>-<month>-<day>
|
||||
} else {
|
||||
restSchemas.push(schema)
|
||||
}
|
||||
}
|
||||
return { restSchemas, webhookSchemas }
|
||||
}
|
||||
201
src/rest/scripts/utils/get-body-params.js
Normal file
201
src/rest/scripts/utils/get-body-params.js
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env node
|
||||
import renderContent from '../../../../lib/render-content/index.js'
|
||||
|
||||
// If there is a oneOf at the top level, then we have to present just one
|
||||
// in the docs. We don't currently have a convention for showing more than one
|
||||
// set of input parameters in the docs. Having a top-level oneOf is also very
|
||||
// uncommon.
|
||||
// Currently there aren't very many operations that require this treatment.
|
||||
// As an example, the 'Add status check contexts' and 'Set status check contexts'
|
||||
// operations have a top-level oneOf.
|
||||
|
||||
async function getTopLevelOneOfProperty(schema) {
|
||||
if (!schema.oneOf) {
|
||||
throw new Error('Schema does not have a requestBody oneOf property defined')
|
||||
}
|
||||
if (!(Array.isArray(schema.oneOf) && schema.oneOf.length > 0)) {
|
||||
throw new Error('Schema requestBody oneOf property is not an array')
|
||||
}
|
||||
// When a oneOf exists but the `type` differs, the case has historically
|
||||
// been that the alternate option is an array, where the first option
|
||||
// is the array as a property of the object. We need to ensure that the
|
||||
// first option listed is the most comprehensive and preferred option.
|
||||
const firstOneOfObject = schema.oneOf[0]
|
||||
const allOneOfAreObjects = schema.oneOf.every((elem) => elem.type === 'object')
|
||||
let required = firstOneOfObject.required || []
|
||||
let properties = firstOneOfObject.properties || {}
|
||||
|
||||
// When all of the oneOf objects have the `type: object` we
|
||||
// need to display all of the parameters.
|
||||
// This merges all of the properties and required values.
|
||||
if (allOneOfAreObjects) {
|
||||
for (const each of schema.oneOf.slice(1)) {
|
||||
Object.assign(firstOneOfObject.properties, each.properties)
|
||||
required = firstOneOfObject.required.concat(each.required)
|
||||
}
|
||||
properties = firstOneOfObject.properties
|
||||
}
|
||||
return { properties, required }
|
||||
}
|
||||
|
||||
// Gets the body parameters for a given schema recursively.
|
||||
export async function getBodyParams(schema, topLevel = false) {
|
||||
const bodyParametersParsed = []
|
||||
const schemaObject = schema.oneOf && topLevel ? await getTopLevelOneOfProperty(schema) : schema
|
||||
const properties = schemaObject.properties || {}
|
||||
const required = schemaObject.required || []
|
||||
|
||||
// Most operation requestBody schemas are objects. When the type is an array,
|
||||
// there will not be properties on the `schema` object.
|
||||
if (topLevel && schema.type === 'array') {
|
||||
const childParamsGroups = []
|
||||
const arrayType = schema.items.type
|
||||
const paramType = [schema.type]
|
||||
if (arrayType === 'object') {
|
||||
childParamsGroups.push(...(await getBodyParams(schema.items, false)))
|
||||
} else {
|
||||
paramType.splice(paramType.indexOf('array'), 1, `array of ${arrayType}s`)
|
||||
}
|
||||
const paramDecorated = await getTransformedParam(schema, paramType, {
|
||||
required,
|
||||
topLevel,
|
||||
childParamsGroups,
|
||||
})
|
||||
return [paramDecorated]
|
||||
}
|
||||
|
||||
for (const [paramKey, param] of Object.entries(properties)) {
|
||||
// OpenAPI 3.0 only had a single value for `type`. OpenAPI 3.1
|
||||
// will either be a single value or an array of values.
|
||||
// This makes type an array regardless of how many values the array
|
||||
// includes. This allows us to support 3.1 while remaining backwards
|
||||
// compatible with 3.0.
|
||||
const paramType = Array.isArray(param.type) ? param.type : [param.type]
|
||||
const additionalPropertiesType = param.additionalProperties
|
||||
? Array.isArray(param.additionalProperties.type)
|
||||
? param.additionalProperties.type
|
||||
: [param.additionalProperties.type]
|
||||
: []
|
||||
const childParamsGroups = []
|
||||
|
||||
// If the parameter is an array or object there may be child params
|
||||
// If the parameter has oneOf or additionalProperties, they need to be
|
||||
// recursively read too.
|
||||
|
||||
// There are a couple operations with additionalProperties, which allows
|
||||
// the api to define input parameters with the type dictionary. These are the only
|
||||
// two operations (at the time of adding this code) that use additionalProperties
|
||||
// Create a snapshot of dependencies for a repository
|
||||
// Update a gist
|
||||
if (param.additionalProperties && additionalPropertiesType.includes('object')) {
|
||||
const keyParam = {
|
||||
type: 'object',
|
||||
name: 'key',
|
||||
description: await renderContent(
|
||||
`A user-defined key to represent an item in \`${paramKey}\`.`
|
||||
),
|
||||
isRequired: param.required,
|
||||
enum: param.enum,
|
||||
default: param.default,
|
||||
childParamsGroups: [],
|
||||
}
|
||||
keyParam.childParamsGroups.push(...(await getBodyParams(param.additionalProperties, false)))
|
||||
childParamsGroups.push(keyParam)
|
||||
} else if (paramType && paramType.includes('array')) {
|
||||
const arrayType = param.items.type
|
||||
if (arrayType) {
|
||||
paramType.splice(paramType.indexOf('array'), 1, `array of ${arrayType}s`)
|
||||
}
|
||||
if (arrayType === 'object') {
|
||||
childParamsGroups.push(...(await getBodyParams(param.items, false)))
|
||||
}
|
||||
} else if (paramType && paramType.includes('object')) {
|
||||
childParamsGroups.push(...(await getBodyParams(param, false)))
|
||||
} else if (param && param.oneOf) {
|
||||
// get concatenated description and type
|
||||
const descriptions = []
|
||||
for (const childParam of param.oneOf) {
|
||||
paramType.push(childParam.type)
|
||||
// If there is no parent description, create a description from
|
||||
// each type
|
||||
if (!param.description) {
|
||||
if (childParam.type === 'array') {
|
||||
if (childParam.items.description) {
|
||||
descriptions.push({
|
||||
type: childParam.type,
|
||||
description: childParam.items.description,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (childParam.description) {
|
||||
descriptions.push({ type: childParam.type, description: childParam.description })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Occasionally, there is no parent description and the description
|
||||
// is in the first child parameter.
|
||||
const oneOfDescriptions = descriptions.length ? descriptions[0].description : ''
|
||||
if (!param.description) param.description = oneOfDescriptions
|
||||
|
||||
// This is a workaround for an operation that incorrectly defines allOf for a
|
||||
// body parameter. As a workaround, we will use the first object in the list of
|
||||
// the allOf array. Otherwise, fallback to the first item in the array.
|
||||
// This isn't ideal, and in the case of an actual allOf occurrence, we should
|
||||
// handle it differently by merging all of the properties. There is currently
|
||||
// only one occurrence for the operation id repos/update-information-about-pages-site
|
||||
// See Ecosystem API issue number #3332 for future plans to fix this in the OpenAPI
|
||||
} else if (param && param.anyOf && Object.keys(param).length === 1) {
|
||||
const firstObject = Object.values(param.anyOf).find((item) => item.type === 'object')
|
||||
if (firstObject) {
|
||||
paramType.push('object')
|
||||
param.description = firstObject.description
|
||||
param.isRequired = firstObject.required
|
||||
childParamsGroups.push(...(await getBodyParams(firstObject, false)))
|
||||
} else {
|
||||
paramType.push(param.anyOf[0].type)
|
||||
param.description = param.anyOf[0].description
|
||||
param.isRequired = param.anyOf[0].required
|
||||
}
|
||||
}
|
||||
|
||||
const paramDecorated = await getTransformedParam(param, paramType, {
|
||||
paramKey,
|
||||
required,
|
||||
childParamsGroups,
|
||||
topLevel,
|
||||
})
|
||||
bodyParametersParsed.push(paramDecorated)
|
||||
}
|
||||
return bodyParametersParsed
|
||||
}
|
||||
|
||||
async function getTransformedParam(param, paramType, props) {
|
||||
const { paramKey, required, childParamsGroups, topLevel } = props
|
||||
const paramDecorated = {}
|
||||
// Supports backwards compatibility for OpenAPI 3.0
|
||||
// In 3.1 a nullable type is part of the param.type array and
|
||||
// the property param.nullable does not exist.
|
||||
if (param.nullable) paramType.push('null')
|
||||
paramDecorated.type = paramType.filter(Boolean).join(' or ')
|
||||
paramDecorated.name = paramKey
|
||||
if (topLevel) {
|
||||
paramDecorated.in = 'body'
|
||||
}
|
||||
paramDecorated.description = await renderContent(param.description)
|
||||
if (required && required.includes(paramKey)) {
|
||||
paramDecorated.isRequired = true
|
||||
}
|
||||
if (childParamsGroups && childParamsGroups.length > 0) {
|
||||
paramDecorated.childParamsGroups = childParamsGroups
|
||||
}
|
||||
if (param.enum) {
|
||||
paramDecorated.enum = param.enum
|
||||
}
|
||||
|
||||
// we also want to catch default values of `false` for booleans
|
||||
if (param.default !== undefined) {
|
||||
paramDecorated.default = param.default
|
||||
}
|
||||
return paramDecorated
|
||||
}
|
||||
65
src/rest/scripts/utils/get-openapi-schemas.js
Normal file
65
src/rest/scripts/utils/get-openapi-schemas.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { readFile, readdir } from 'fs/promises'
|
||||
import yaml from 'js-yaml'
|
||||
import path from 'path'
|
||||
|
||||
import { allVersions } from '../../../../lib/all-versions.js'
|
||||
const OPEN_API_RELEASES_DIR = path.join('..', 'github', '/app/api/description/config/releases')
|
||||
|
||||
// Gets the full list of unpublished + active, deprecated + active,
|
||||
// or active schemas from the github/github repo
|
||||
// `openApiReleaseDir` is the path to the `app/api/description/config/releases`
|
||||
// directory in `github/github`
|
||||
// You can also specify getting specific versions of schemas.
|
||||
export async function getSchemas(directory = OPEN_API_RELEASES_DIR) {
|
||||
const openAPIConfigs = await readdir(directory)
|
||||
const unpublished = []
|
||||
const deprecated = []
|
||||
const currentReleases = []
|
||||
|
||||
// The file content in the `github/github` repo is YAML before it is
|
||||
// bundled into JSON.
|
||||
for (const file of openAPIConfigs) {
|
||||
const fileBaseName = path.basename(file, '.yaml')
|
||||
const newFileName = `${fileBaseName}.deref.json`
|
||||
const content = await readFile(path.join(directory, file), 'utf8')
|
||||
const yamlContent = yaml.load(content)
|
||||
|
||||
const isDeprecatedInDocs = !Object.keys(allVersions).find(
|
||||
(version) => allVersions[version].openApiVersionName === fileBaseName
|
||||
)
|
||||
if (!yamlContent.published) {
|
||||
unpublished.push(newFileName)
|
||||
}
|
||||
// If it's deprecated, it must have been published at some point in the past
|
||||
// This checks if the schema is deprecated in github/github and
|
||||
// github/docs-internal. Sometimes deprecating in github/github lags
|
||||
// behind deprecating in github/docs-internal a few days
|
||||
if (
|
||||
(yamlContent.deprecated && yamlContent.published) ||
|
||||
(isDeprecatedInDocs && yamlContent.published)
|
||||
) {
|
||||
deprecated.push(newFileName)
|
||||
}
|
||||
if (!yamlContent.deprecated && !isDeprecatedInDocs && yamlContent.published) {
|
||||
currentReleases.push(newFileName)
|
||||
}
|
||||
}
|
||||
|
||||
return { currentReleases, unpublished, deprecated }
|
||||
}
|
||||
|
||||
export async function validateVersionsOptions(versions) {
|
||||
const schemas = await getSchemas()
|
||||
// Validate individual versions provided
|
||||
versions.forEach((version) => {
|
||||
if (
|
||||
schemas.deprecated.includes(`${version}.deref.json`) ||
|
||||
schemas.unpublished.includes(`${version}.deref.json`)
|
||||
) {
|
||||
const errorMsg = `🛑 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.`
|
||||
throw new Error(errorMsg)
|
||||
} else if (!schemas.currentReleases.includes(`${version}.deref.json`)) {
|
||||
throw new Error(`🛑 The version (${version}) you specified is not valid.`)
|
||||
}
|
||||
})
|
||||
}
|
||||
33
src/rest/scripts/utils/get-operations.js
Normal file
33
src/rest/scripts/utils/get-operations.js
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
import Operation from './operation.js'
|
||||
import Webhook from './webhook.js'
|
||||
|
||||
// The module accepts a JSON schema object as input
|
||||
// and returns an array of its operation objects with their
|
||||
// HTTP verb and requestPath attached as properties
|
||||
|
||||
export default async function getOperations(schema) {
|
||||
const operations = []
|
||||
|
||||
for (const [requestPath, operationsAtPath] of Object.entries(schema.paths)) {
|
||||
for (const [verb, operationProps] of Object.entries(operationsAtPath)) {
|
||||
const operation = new Operation(verb, requestPath, operationProps, schema.servers)
|
||||
operations.push(operation)
|
||||
}
|
||||
}
|
||||
|
||||
return operations
|
||||
}
|
||||
|
||||
export async function getWebhooks(schema) {
|
||||
// In OpenAPI version 3.1, the schema data is under the `webhooks` key, but
|
||||
// in 3.0 the schema data was in `x-webhooks`. We just fallback to
|
||||
// `x-webhooks` for now since there's currently otherwise no difference with
|
||||
// the schema data so we can handle either version.
|
||||
const webhooks = schema.webhooks ?? schema['x-webhooks']
|
||||
if (webhooks) {
|
||||
return Object.values(webhooks).map((webhook) => new Webhook(webhook.post))
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
68
src/rest/scripts/utils/operation-schema.js
Normal file
68
src/rest/scripts/utils/operation-schema.js
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
// This schema is used to validate each generated operation object at build time
|
||||
|
||||
export default {
|
||||
type: 'object',
|
||||
required: [
|
||||
'title',
|
||||
'verb',
|
||||
'requestPath',
|
||||
'category',
|
||||
'parameters',
|
||||
'statusCodes',
|
||||
'codeExamples',
|
||||
],
|
||||
properties: {
|
||||
// Properties from the source OpenAPI schema that this module depends on
|
||||
title: {
|
||||
description: 'The title of the operation',
|
||||
type: 'string',
|
||||
},
|
||||
verb: {
|
||||
description: 'The HTTP method',
|
||||
type: 'string',
|
||||
enum: ['get', 'put', 'post', 'delete', 'patch', 'head'],
|
||||
},
|
||||
requestPath: {
|
||||
description: 'The URL path',
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
},
|
||||
descriptionHTML: {
|
||||
description: 'The rendered HTML version of the markdown `description` property',
|
||||
type: 'string',
|
||||
},
|
||||
category: {
|
||||
description: 'the `issues` in `/v3/issues/events/`; supports legacy developer site URLs',
|
||||
type: 'string',
|
||||
},
|
||||
subcategory: {
|
||||
description: 'the `events` in `/v3/issues/events/`; supports legacy developer site URLs',
|
||||
type: 'string',
|
||||
},
|
||||
parameters: {
|
||||
description: 'Parameters to the operation that can be present in the URL path or query',
|
||||
type: 'array',
|
||||
},
|
||||
codeSamples: {
|
||||
description: 'Code samples for the operation',
|
||||
type: 'array',
|
||||
},
|
||||
statusCodes: {
|
||||
description: 'The possible HTTP status codes for the operation',
|
||||
type: 'array',
|
||||
},
|
||||
previews: {
|
||||
description: 'The information about the preview headers',
|
||||
type: 'array',
|
||||
},
|
||||
enabledForGitHubApps: {
|
||||
description: 'Whether the operation is enabled for server-to-server GitHub Apps',
|
||||
type: 'boolean',
|
||||
},
|
||||
bodyParameters: {
|
||||
description: 'The request body parameters for the operation',
|
||||
type: 'array',
|
||||
},
|
||||
},
|
||||
}
|
||||
187
src/rest/scripts/utils/operation.js
Normal file
187
src/rest/scripts/utils/operation.js
Normal file
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env node
|
||||
import Ajv from 'ajv'
|
||||
import httpStatusCodes from 'http-status-code'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { get, isPlainObject } from 'lodash-es'
|
||||
import { parseTemplate } from 'url-template'
|
||||
|
||||
import renderContent from '../../../../lib/render-content/index.js'
|
||||
import getCodeSamples from './create-rest-examples.js'
|
||||
import operationSchema from './operation-schema.js'
|
||||
import { getBodyParams } from './get-body-params.js'
|
||||
|
||||
const { operationUrls } = JSON.parse(
|
||||
await readFile('src/rest/scripts/utils/rest-api-overrides.json', 'utf8')
|
||||
)
|
||||
|
||||
export default class Operation {
|
||||
#operation
|
||||
constructor(verb, requestPath, operation, globalServers) {
|
||||
this.#operation = operation
|
||||
// The global server object sets metadata including the base url for
|
||||
// all operations in a version. Individual operations can override
|
||||
// the global server url at the operation level.
|
||||
this.serverUrl = operation.servers ? operation.servers[0].url : globalServers[0].url
|
||||
|
||||
const serverVariables = operation.servers
|
||||
? operation.servers[0].variables
|
||||
: globalServers[0].variables
|
||||
if (serverVariables) {
|
||||
const templateVariables = {}
|
||||
Object.keys(serverVariables).forEach(
|
||||
(key) => (templateVariables[key] = serverVariables[key].default)
|
||||
)
|
||||
this.serverUrl = parseTemplate(this.serverUrl).expand(templateVariables)
|
||||
}
|
||||
|
||||
this.serverUrl = this.serverUrl.replace('http:', 'http(s):')
|
||||
|
||||
// Attach some global properties to the operation object to use
|
||||
// during processing
|
||||
this.#operation.serverUrl = this.serverUrl
|
||||
this.#operation.requestPath = requestPath
|
||||
this.#operation.verb = verb
|
||||
|
||||
this.verb = verb
|
||||
this.requestPath = requestPath
|
||||
this.title = operation.summary
|
||||
this.setCategories()
|
||||
this.parameters = operation.parameters || []
|
||||
this.bodyParameters = []
|
||||
this.enabledForGitHubApps = operation['x-github'].enabledForGitHubApps
|
||||
this.codeExamples = getCodeSamples(this.#operation)
|
||||
return this
|
||||
}
|
||||
|
||||
setCategories() {
|
||||
const operationId = this.#operation.operationId
|
||||
const xGithub = this.#operation['x-github']
|
||||
// Set category
|
||||
// A temporary override file allows us to override the category defined in
|
||||
// the openapi schema. Without it, we'd have to update several
|
||||
// @documentation_urls in the api code every time we move
|
||||
// an endpoint to a new page.
|
||||
this.category = operationUrls[operationId]
|
||||
? operationUrls[operationId].category
|
||||
: xGithub.category
|
||||
|
||||
// Set subcategory
|
||||
// A temporary override file allows us to override the subcategory
|
||||
// defined in the openapi schema. Without it, we'd have to update several
|
||||
// @documentation_urls in the api code every time we move
|
||||
// an endpoint to a new page.
|
||||
if (operationUrls[operationId]) {
|
||||
if (operationUrls[operationId].subcategory) {
|
||||
this.subcategory = operationUrls[operationId].subcategory
|
||||
}
|
||||
} else if (xGithub.subcategory) {
|
||||
this.subcategory = xGithub.subcategory
|
||||
}
|
||||
}
|
||||
|
||||
async process() {
|
||||
await Promise.all([
|
||||
this.renderDescription(),
|
||||
this.renderStatusCodes(),
|
||||
this.renderParameterDescriptions(),
|
||||
this.renderBodyParameterDescriptions(),
|
||||
this.renderExampleResponseDescriptions(),
|
||||
this.renderPreviewNotes(),
|
||||
])
|
||||
|
||||
const ajv = new Ajv()
|
||||
const valid = ajv.validate(operationSchema, this)
|
||||
if (!valid) {
|
||||
console.error(JSON.stringify(ajv.errors, null, 2))
|
||||
throw new Error('Invalid OpenAPI operation found')
|
||||
}
|
||||
}
|
||||
|
||||
getExternalDocs() {
|
||||
return this.#operation.externalDocs
|
||||
}
|
||||
|
||||
async renderDescription() {
|
||||
this.descriptionHTML = await renderContent(this.#operation.description)
|
||||
return this
|
||||
}
|
||||
|
||||
async renderExampleResponseDescriptions() {
|
||||
return Promise.all(
|
||||
this.codeExamples.map(async (codeExample) => {
|
||||
codeExample.response.description = await renderContent(codeExample.response.description)
|
||||
return codeExample
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async renderStatusCodes() {
|
||||
const responses = this.#operation.responses
|
||||
const responseKeys = Object.keys(responses)
|
||||
if (responseKeys.length === 0) return []
|
||||
|
||||
this.statusCodes = await Promise.all(
|
||||
responseKeys.map(async (responseCode) => {
|
||||
const response = responses[responseCode]
|
||||
const httpStatusCode = responseCode
|
||||
const httpStatusMessage = httpStatusCodes.getMessage(Number(responseCode), 'HTTP/2')
|
||||
// The OpenAPI should be updated to provide better descriptions, but
|
||||
// until then, we can catch some known generic descriptions and replace
|
||||
// them with the default http status message.
|
||||
const responseDescription =
|
||||
response.description.toLowerCase() === 'response'
|
||||
? await renderContent(httpStatusMessage)
|
||||
: await renderContent(response.description)
|
||||
|
||||
return {
|
||||
httpStatusCode,
|
||||
description: responseDescription,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async renderParameterDescriptions() {
|
||||
return Promise.all(
|
||||
this.parameters.map(async (param) => {
|
||||
param.description = await renderContent(param.description)
|
||||
return param
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async renderBodyParameterDescriptions() {
|
||||
if (!this.#operation.requestBody) return []
|
||||
// There is currently only one operation with more than one content type
|
||||
// and the request body parameter types are the same for both.
|
||||
// Operation Id: markdown/render-raw
|
||||
const contentType = Object.keys(this.#operation.requestBody.content)[0]
|
||||
const schema = get(this.#operation, `requestBody.content.${contentType}.schema`, {})
|
||||
// TODO: Remove this check
|
||||
if (this.#operation.operationId === 'checks/create') {
|
||||
delete schema.oneOf
|
||||
}
|
||||
|
||||
this.bodyParameters = isPlainObject(schema) ? await getBodyParams(schema, true) : []
|
||||
}
|
||||
|
||||
async renderPreviewNotes() {
|
||||
const previews = get(this.#operation, 'x-github.previews', [])
|
||||
this.previews = await Promise.all(
|
||||
previews.map(async (preview) => {
|
||||
const note = preview.note
|
||||
// remove extra leading and trailing newlines
|
||||
.replace(/```\n\n\n/gm, '```\n')
|
||||
.replace(/```\n\n/gm, '```\n')
|
||||
.replace(/\n\n\n```/gm, '\n```')
|
||||
.replace(/\n\n```/gm, '\n```')
|
||||
|
||||
// convert single-backtick code snippets to fully fenced triple-backtick blocks
|
||||
// example: This is the description.\n\n`application/vnd.github.machine-man-preview+json`
|
||||
.replace(/\n`application/, '\n```\napplication')
|
||||
.replace(/json`$/, 'json\n```')
|
||||
return await renderContent(note)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
794
src/rest/scripts/utils/rest-api-overrides.json
Normal file
794
src/rest/scripts/utils/rest-api-overrides.json
Normal file
@@ -0,0 +1,794 @@
|
||||
{
|
||||
"operationUrls": {
|
||||
"repos/list-collaborators": {
|
||||
"category": "collaborators",
|
||||
"subcategory": "collaborators",
|
||||
"originalUrl": "/rest/reference/repos#list-repository-collaborators"
|
||||
},
|
||||
"repos/check-collaborator": {
|
||||
"category": "collaborators",
|
||||
"subcategory": "collaborators",
|
||||
"originalUrl": "/rest/reference/repos#check-if-a-user-is-a-repository-collaborator"
|
||||
},
|
||||
"repos/add-collaborator": {
|
||||
"category": "collaborators",
|
||||
"subcategory": "collaborators",
|
||||
"originalUrl": "/rest/reference/repos#add-a-repository-collaborator"
|
||||
},
|
||||
"repos/remove-collaborator": {
|
||||
"category": "collaborators",
|
||||
"subcategory": "collaborators",
|
||||
"originalUrl": "/rest/reference/repos#remove-a-repository-collaborator"
|
||||
},
|
||||
"repos/get-collaborator-permission-level": {
|
||||
"category": "collaborators",
|
||||
"subcategory": "collaborators",
|
||||
"originalUrl": "/rest/reference/repos#get-repository-permissions-for-a-user"
|
||||
},
|
||||
"repos/list-commit-comments-for-repo": {
|
||||
"category": "commits",
|
||||
"subcategory": "comments",
|
||||
"originalUrl": "/rest/reference/repos#list-commit-comments-for-a-repository"
|
||||
},
|
||||
"repos/get-commit-comment": {
|
||||
"category": "commits",
|
||||
"subcategory": "comments",
|
||||
"originalUrl": "/rest/reference/repos#get-a-commit-comment"
|
||||
},
|
||||
"repos/update-commit-comment": {
|
||||
"category": "commits",
|
||||
"subcategory": "comments",
|
||||
"originalUrl": "/rest/reference/repos#update-a-commit-comment"
|
||||
},
|
||||
"repos/delete-commit-comment": {
|
||||
"category": "commits",
|
||||
"subcategory": "comments",
|
||||
"originalUrl": "/rest/reference/repos#delete-a-commit-comment"
|
||||
},
|
||||
"repos/list-commits": {
|
||||
"category": "commits",
|
||||
"subcategory": "commits",
|
||||
"originalUrl": "/rest/reference/repos#list-commits"
|
||||
},
|
||||
"repos/list-branches-for-head-commit": {
|
||||
"category": "commits",
|
||||
"subcategory": "commits",
|
||||
"originalUrl": "/rest/reference/repos#list-branches-for-head-commit"
|
||||
},
|
||||
"repos/list-comments-for-commit": {
|
||||
"category": "commits",
|
||||
"subcategory": "comments",
|
||||
"originalUrl": "/rest/reference/repos#list-commit-comments"
|
||||
},
|
||||
"repos/create-commit-comment": {
|
||||
"category": "commits",
|
||||
"subcategory": "comments",
|
||||
"originalUrl": "/rest/reference/repos#create-a-commit-comment"
|
||||
},
|
||||
"repos/list-pull-requests-associated-with-commit": {
|
||||
"category": "commits",
|
||||
"subcategory": "commits",
|
||||
"originalUrl": "/rest/reference/repos#list-pull-requests-associated-with-a-commit"
|
||||
},
|
||||
"repos/get-commit": {
|
||||
"category": "commits",
|
||||
"subcategory": "commits",
|
||||
"originalUrl": "/rest/reference/repos#get-a-commit"
|
||||
},
|
||||
"repos/get-combined-status-for-ref": {
|
||||
"category": "commits",
|
||||
"subcategory": "statuses",
|
||||
"originalUrl": "/rest/reference/repos#get-the-combined-status-for-a-specific-reference"
|
||||
},
|
||||
"repos/list-commit-statuses-for-ref": {
|
||||
"category": "commits",
|
||||
"subcategory": "statuses",
|
||||
"originalUrl": "/rest/reference/repos#list-commit-statuses-for-a-reference"
|
||||
},
|
||||
"repos/get-community-profile-metrics": {
|
||||
"category": "metrics",
|
||||
"subcategory": "community",
|
||||
"originalUrl": "/rest/reference/repos#get-community-profile-metrics"
|
||||
},
|
||||
"repos/compare-commits": {
|
||||
"category": "commits",
|
||||
"subcategory": "commits",
|
||||
"originalUrl": "/rest/reference/repos#compare-two-commits"
|
||||
},
|
||||
"repos/get-all-environments": {
|
||||
"category": "deployments",
|
||||
"subcategory": "environments",
|
||||
"originalUrl": "/rest/reference/repos#get-all-environments"
|
||||
},
|
||||
"repos/get-environment": {
|
||||
"category": "deployments",
|
||||
"subcategory": "environments",
|
||||
"originalUrl": "/rest/reference/repos#get-an-environment"
|
||||
},
|
||||
"repos/create-or-update-environment": {
|
||||
"category": "deployments",
|
||||
"subcategory": "environments",
|
||||
"originalUrl": "/rest/reference/repos#create-or-update-an-environment"
|
||||
},
|
||||
"repos/delete-an-environment": {
|
||||
"category": "deployments",
|
||||
"subcategory": "environments",
|
||||
"originalUrl": "/rest/reference/repos#delete-an-environment"
|
||||
},
|
||||
"repos/list-invitations": {
|
||||
"category": "collaborators",
|
||||
"subcategory": "invitations",
|
||||
"originalUrl": "/rest/reference/repos#list-repository-invitations"
|
||||
},
|
||||
"repos/update-invitation": {
|
||||
"category": "collaborators",
|
||||
"subcategory": "invitations",
|
||||
"originalUrl": "/rest/reference/repos#update-a-repository-invitation"
|
||||
},
|
||||
"repos/delete-invitation": {
|
||||
"category": "collaborators",
|
||||
"subcategory": "invitations",
|
||||
"originalUrl": "/rest/reference/repos#delete-a-repository-invitation"
|
||||
},
|
||||
"repos/list-deploy-keys": {
|
||||
"category": "deploy-keys",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#list-deploy-keys"
|
||||
},
|
||||
"repos/create-deploy-key": {
|
||||
"category": "deploy-keys",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#create-a-deploy-key"
|
||||
},
|
||||
"repos/get-deploy-key": {
|
||||
"category": "deploy-keys",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#get-a-deploy-key"
|
||||
},
|
||||
"repos/delete-deploy-key": {
|
||||
"category": "deploy-keys",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#delete-a-deploy-key"
|
||||
},
|
||||
"repos/get-pages": {
|
||||
"category": "pages",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#get-a-github-pages-site"
|
||||
},
|
||||
"repos/create-pages-site": {
|
||||
"category": "pages",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#create-a-github-pages-site"
|
||||
},
|
||||
"repos/update-information-about-pages-site": {
|
||||
"category": "pages",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#update-information-about-a-github-pages-site"
|
||||
},
|
||||
"repos/delete-pages-site": {
|
||||
"category": "pages",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#delete-a-github-pages-site"
|
||||
},
|
||||
"repos/list-pages-builds": {
|
||||
"category": "pages",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#list-github-pages-builds"
|
||||
},
|
||||
"repos/request-pages-build": {
|
||||
"category": "pages",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#request-a-github-pages-build"
|
||||
},
|
||||
"repos/get-latest-pages-build": {
|
||||
"category": "pages",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#get-latest-pages-build"
|
||||
},
|
||||
"repos/get-pages-build": {
|
||||
"category": "pages",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#get-github-pages-build"
|
||||
},
|
||||
"repos/get-pages-health-check": {
|
||||
"category": "pages",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#get-a-dns-health-check-for-github-pages"
|
||||
},
|
||||
"repos/create-pages-deployment": {
|
||||
"category": "pages",
|
||||
"subcategory": null,
|
||||
"originalUrl": "/rest/reference/repos#create-a-github-pages-deployment"
|
||||
},
|
||||
"repos/get-code-frequency-stats": {
|
||||
"category": "metrics",
|
||||
"subcategory": "statistics",
|
||||
"originalUrl": "/rest/reference/repos#get-the-weekly-commit-activity"
|
||||
},
|
||||
"repos/get-commit-activity-stats": {
|
||||
"category": "metrics",
|
||||
"subcategory": "statistics",
|
||||
"originalUrl": "/rest/reference/repos#get-the-last-year-of-commit-activity"
|
||||
},
|
||||
"repos/get-contributors-stats": {
|
||||
"category": "metrics",
|
||||
"subcategory": "statistics",
|
||||
"originalUrl": "/rest/reference/repos#get-all-contributor-commit-activity"
|
||||
},
|
||||
"repos/get-participation-stats": {
|
||||
"category": "metrics",
|
||||
"subcategory": "statistics",
|
||||
"originalUrl": "/rest/reference/repos#get-the-weekly-commit-count"
|
||||
},
|
||||
"repos/get-punch-card-stats": {
|
||||
"category": "metrics",
|
||||
"subcategory": "statistics",
|
||||
"originalUrl": "/rest/reference/repos#get-the-hourly-commit-count-for-each-day"
|
||||
},
|
||||
"repos/create-commit-status": {
|
||||
"category": "commits",
|
||||
"subcategory": "statuses",
|
||||
"originalUrl": "/rest/reference/repos#create-a-commit-status"
|
||||
},
|
||||
"repos/get-clones": {
|
||||
"category": "metrics",
|
||||
"subcategory": "traffic",
|
||||
"originalUrl": "/rest/reference/repos#get-repository-clones"
|
||||
},
|
||||
"repos/get-top-paths": {
|
||||
"category": "metrics",
|
||||
"subcategory": "traffic",
|
||||
"originalUrl": "/rest/reference/repos#get-top-referral-paths"
|
||||
},
|
||||
"repos/get-top-referrers": {
|
||||
"category": "metrics",
|
||||
"subcategory": "traffic",
|
||||
"originalUrl": "/rest/reference/repos#get-top-referral-sources"
|
||||
},
|
||||
"repos/get-views": {
|
||||
"category": "metrics",
|
||||
"subcategory": "traffic",
|
||||
"originalUrl": "/rest/reference/repos#get-page-views"
|
||||
},
|
||||
"repos/list-invitations-for-authenticated-user": {
|
||||
"category": "collaborators",
|
||||
"subcategory": "invitations",
|
||||
"originalUrl": "/rest/reference/repos#list-repository-invitations-for-the-authenticated-user"
|
||||
},
|
||||
"repos/accept-invitation-for-authenticated-user": {
|
||||
"category": "collaborators",
|
||||
"subcategory": "invitations",
|
||||
"originalUrl": "/rest/reference/repos#accept-a-repository-invitation"
|
||||
},
|
||||
"repos/decline-invitation-for-authenticated-user": {
|
||||
"category": "collaborators",
|
||||
"subcategory": "invitations",
|
||||
"originalUrl": "/rest/reference/repos#decline-a-repository-invitation"
|
||||
},
|
||||
"repos/list-branches": {
|
||||
"category": "branches",
|
||||
"subcategory": "branches",
|
||||
"originalUrl": "/rest/reference/repos#list-branches"
|
||||
},
|
||||
"repos/get-branch": {
|
||||
"category": "branches",
|
||||
"subcategory": "branches",
|
||||
"originalUrl": "/rest/reference/repos#get-a-branch"
|
||||
},
|
||||
"repos/rename-branch": {
|
||||
"category": "branches",
|
||||
"subcategory": "branches",
|
||||
"originalUrl": "/rest/reference/repos#rename-a-branch"
|
||||
},
|
||||
"repos/merge-upstream": {
|
||||
"category": "branches",
|
||||
"subcategory": "branches",
|
||||
"originalUrl": "/rest/reference/repos#sync-a-fork-branch-with-the-upstream-repository"
|
||||
},
|
||||
"repos/merge": {
|
||||
"category": "branches",
|
||||
"subcategory": "branches",
|
||||
"originalUrl": "/rest/reference/repos#merge-a-branch"
|
||||
},
|
||||
"repos/add-app-access-restrictions": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#add-app-access-restrictions"
|
||||
},
|
||||
"repos/add-status-check-contexts": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#add-status-check-contexts"
|
||||
},
|
||||
"repos/add-team-access-restrictions": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#add-team-access-restrictions"
|
||||
},
|
||||
"repos/add-user-access-restrictions": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#add-user-access-restrictions"
|
||||
},
|
||||
"repos/create-commit-signature-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#create-commit-signature-protection"
|
||||
},
|
||||
"repos/delete-access-restrictions": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#delete-access-restrictions"
|
||||
},
|
||||
"repos/delete-admin-branch-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#delete-admin-branch-protection"
|
||||
},
|
||||
"repos/delete-branch-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#delete-branch-protection"
|
||||
},
|
||||
"repos/delete-commit-signature-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#delete-commit-signature-protection"
|
||||
},
|
||||
"repos/delete-pull-request-review-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#delete-pull-request-review-protection"
|
||||
},
|
||||
"repos/get-access-restrictions": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#get-access-restrictions"
|
||||
},
|
||||
"repos/get-admin-branch-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#get-admin-branch-protection"
|
||||
},
|
||||
"repos/get-all-status-check-contexts": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#get-all-status-check-contexts"
|
||||
},
|
||||
"repos/get-apps-with-access-to-protected-branch": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#list-apps-with-access-to-the-protected-branch"
|
||||
},
|
||||
"repos/update-pull-request-review-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#update-pull-request-review-protection"
|
||||
},
|
||||
"repos/get-branch-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#get-branch-protection"
|
||||
},
|
||||
"repos/get-commit-signature-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#get-commit-signature-protection"
|
||||
},
|
||||
"repos/get-pull-request-review-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#get-pull-request-review-protection"
|
||||
},
|
||||
"repos/get-status-checks-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#get-status-checks-protection"
|
||||
},
|
||||
"repos/get-teams-with-access-to-protected-branch": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#list-teams-with-access-to-the-protected-branch"
|
||||
},
|
||||
"repos/get-users-with-access-to-protected-branch": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#list-users-with-access-to-the-protected-branch"
|
||||
},
|
||||
"repos/remove-app-access-restrictions": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#remove-app-access-restrictions"
|
||||
},
|
||||
"repos/remove-status-check-contexts": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#remove-status-check-contexts"
|
||||
},
|
||||
"repos/remove-status-check-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#remove-status-check-protection"
|
||||
},
|
||||
"repos/remove-team-access-restrictions": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#remove-team-access-restrictions"
|
||||
},
|
||||
"repos/remove-user-access-restrictions": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#remove-user-access-restrictions"
|
||||
},
|
||||
"repos/set-admin-branch-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#set-admin-branch-protection"
|
||||
},
|
||||
"repos/set-app-access-restrictions": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#set-app-access-restrictions"
|
||||
},
|
||||
"repos/set-status-check-contexts": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#set-status-check-contexts"
|
||||
},
|
||||
"repos/set-team-access-restrictions": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#set-team-access-restrictions"
|
||||
},
|
||||
"repos/set-user-access-restrictions": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#set-user-access-restrictions"
|
||||
},
|
||||
"repos/update-branch-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#update-branch-protection"
|
||||
},
|
||||
"repos/update-status-check-protection": {
|
||||
"category": "branches",
|
||||
"subcategory": "branch-protection",
|
||||
"originalUrl": "/rest/reference/repos#update-status-check-protection"
|
||||
},
|
||||
"repos/create-deployment-status": {
|
||||
"category": "deployments",
|
||||
"subcategory": "statuses",
|
||||
"originalUrl": "/rest/reference/repos#create-a-deployment-status"
|
||||
},
|
||||
"repos/list-deployment-statuses": {
|
||||
"category": "deployments",
|
||||
"subcategory": "statuses",
|
||||
"originalUrl": "/rest/reference/repos#list-deployment-statuses"
|
||||
},
|
||||
"repos/get-deployment-status": {
|
||||
"category": "deployments",
|
||||
"subcategory": "statuses",
|
||||
"originalUrl": "/rest/reference/repos#get-a-deployment-status"
|
||||
},
|
||||
"repos/list-deployments": {
|
||||
"category": "deployments",
|
||||
"subcategory": "deployments",
|
||||
"originalUrl": "/rest/reference/repos#list-deployments"
|
||||
},
|
||||
"repos/create-deployment": {
|
||||
"category": "deployments",
|
||||
"subcategory": "deployments",
|
||||
"originalUrl": "/rest/reference/repos#create-a-deployment"
|
||||
},
|
||||
"repos/get-deployment": {
|
||||
"category": "deployments",
|
||||
"subcategory": "deployments",
|
||||
"originalUrl": "/rest/reference/repos#get-a-deployment"
|
||||
},
|
||||
"repos/delete-deployment": {
|
||||
"category": "deployments",
|
||||
"subcategory": "deployments",
|
||||
"originalUrl": "/rest/reference/repos#delete-a-deployment"
|
||||
},
|
||||
"repos/list-releases": {
|
||||
"category": "releases",
|
||||
"subcategory": "releases",
|
||||
"originalUrl": "/rest/reference/repos#list-releases"
|
||||
},
|
||||
"repos/create-release": {
|
||||
"category": "releases",
|
||||
"subcategory": "releases",
|
||||
"originalUrl": "/rest/reference/repos#create-a-release"
|
||||
},
|
||||
"repos/generate-release-notes": {
|
||||
"category": "releases",
|
||||
"subcategory": "releases",
|
||||
"originalUrl": "/rest/reference/repos#generate-release-notes"
|
||||
},
|
||||
"repos/get-latest-release": {
|
||||
"category": "releases",
|
||||
"subcategory": "releases",
|
||||
"originalUrl": "/rest/reference/repos#get-the-latest-release"
|
||||
},
|
||||
"repos/get-release-by-tag": {
|
||||
"category": "releases",
|
||||
"subcategory": "releases",
|
||||
"originalUrl": "/rest/reference/repos#get-a-release-by-tag-name"
|
||||
},
|
||||
"repos/get-release": {
|
||||
"category": "releases",
|
||||
"subcategory": "releases",
|
||||
"originalUrl": "/rest/reference/repos#get-a-release"
|
||||
},
|
||||
"repos/update-release": {
|
||||
"category": "releases",
|
||||
"subcategory": "releases",
|
||||
"originalUrl": "/rest/reference/repos#update-a-release"
|
||||
},
|
||||
"repos/delete-release": {
|
||||
"category": "releases",
|
||||
"subcategory": "releases",
|
||||
"originalUrl": "/rest/reference/repos#delete-a-release"
|
||||
},
|
||||
"repos/delete-release-asset": {
|
||||
"category": "releases",
|
||||
"subcategory": "assets",
|
||||
"originalUrl": "/rest/reference/repos#delete-a-release-asset"
|
||||
},
|
||||
"repos/get-release-asset": {
|
||||
"category": "releases",
|
||||
"subcategory": "assets",
|
||||
"originalUrl": "/rest/reference/repos#get-a-release-asset"
|
||||
},
|
||||
"repos/list-release-assets": {
|
||||
"category": "releases",
|
||||
"subcategory": "assets",
|
||||
"originalUrl": "/rest/reference/repos#list-release-assets"
|
||||
},
|
||||
"repos/update-release-asset": {
|
||||
"category": "releases",
|
||||
"subcategory": "assets",
|
||||
"originalUrl": "/rest/reference/repos#update-a-release-asset"
|
||||
},
|
||||
"repos/upload-release-asset": {
|
||||
"category": "releases",
|
||||
"subcategory": "assets",
|
||||
"originalUrl": "/rest/reference/repos#upload-a-release-asset"
|
||||
},
|
||||
"repos/list-webhooks": {
|
||||
"category": "webhooks",
|
||||
"subcategory": "repos",
|
||||
"originalUrl": "/rest/reference/repos#list-repository-webhooks"
|
||||
},
|
||||
"repos/create-webhook": {
|
||||
"category": "webhooks",
|
||||
"subcategory": "repos",
|
||||
"originalUrl": "/rest/reference/repos#create-a-repository-webhook"
|
||||
},
|
||||
"repos/get-webhook": {
|
||||
"category": "webhooks",
|
||||
"subcategory": "repos",
|
||||
"originalUrl": "/rest/reference/repos#get-a-repository-webhook"
|
||||
},
|
||||
"repos/update-webhook": {
|
||||
"category": "webhooks",
|
||||
"subcategory": "repos",
|
||||
"originalUrl": "/rest/reference/repos#update-a-repository-webhook"
|
||||
},
|
||||
"repos/delete-webhook": {
|
||||
"category": "webhooks",
|
||||
"subcategory": "repos",
|
||||
"originalUrl": "/rest/reference/repos#delete-a-repository-webhook"
|
||||
},
|
||||
"repos/ping-webhook": {
|
||||
"category": "webhooks",
|
||||
"subcategory": "repos",
|
||||
"originalUrl": "/rest/reference/repos#ping-a-repository-webhook"
|
||||
},
|
||||
"repos/test-push-webhook": {
|
||||
"category": "webhooks",
|
||||
"subcategory": "repos",
|
||||
"originalUrl": "/rest/reference/repos#test-the-push-repository-webhook"
|
||||
},
|
||||
"repos/get-webhook-config-for-repo": {
|
||||
"category": "webhooks",
|
||||
"subcategory": "repo-config",
|
||||
"originalUrl": "/rest/reference/repos#get-a-webhook-configuration-for-a-repository"
|
||||
},
|
||||
"repos/update-webhook-config-for-repo": {
|
||||
"category": "webhooks",
|
||||
"subcategory": "repo-config",
|
||||
"originalUrl": "/rest/reference/repos#update-a-webhook-configuration-for-a-repository"
|
||||
},
|
||||
"repos/get-webhook-delivery": {
|
||||
"category": "webhooks",
|
||||
"subcategory": "repo-deliveries",
|
||||
"originalUrl": "/rest/reference/repos#get-a-delivery-for-a-repository-webhook"
|
||||
},
|
||||
"repos/list-webhook-deliveries": {
|
||||
"category": "webhooks",
|
||||
"subcategory": "repo-deliveries",
|
||||
"originalUrl": "/rest/reference/repos#list-deliveries-for-a-repository-webhook"
|
||||
},
|
||||
"repos/redeliver-webhook-delivery": {
|
||||
"category": "webhooks",
|
||||
"subcategory": "repo-deliveries",
|
||||
"originalUrl": "/rest/reference/repos#redeliver-a-delivery-for-a-repository-webhook"
|
||||
},
|
||||
"enterprise-admin/add-custom-labels-to-self-hosted-runner-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runners",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#add-custom-labels-to-a-self-hosted-runner-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/add-org-access-to-self-hosted-runner-group-in-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runner-groups",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#add-organization-access-to-a-self-hosted-runner-group-in-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/add-self-hosted-runner-to-group-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runner-groups",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#add-custom-labels-to-a-self-hosted-runner-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/create-registration-token-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runners",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#create-a-registration-token-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/create-remove-token-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runners",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#create-a-remove-token-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/create-self-hosted-runner-group-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runner-groups",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#create-self-hosted-runner-group-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/delete-self-hosted-runner-from-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runners",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#delete-self-hosted-runner-from-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/delete-self-hosted-runner-group-from-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runner-groups",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#delete-a-self-hosted-runner-group-from-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/disable-selected-organization-github-actions-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "permissions",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#disable-a-selected-organization-for-github-actions-in-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/enable-selected-organization-github-actions-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "permissions",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#enable-a-selected-organization-for-github-actions-in-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/get-allowed-actions-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "permissions",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#get-allowed-actions-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/get-github-actions-permissions-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "permissions",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#get-github-actions-permissions-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/get-self-hosted-runner-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runners",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#get-a-self-hosted-runner-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/get-self-hosted-runner-group-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runner-groups",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#get-a-self-hosted-runner-group-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/list-labels-for-self-hosted-runner-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runners",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#list-labels-for-a-self-hosted-runner-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/list-org-access-to-self-hosted-runner-group-in-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runner-groups",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#list-organization-access-to-a-self-hosted-runner-group-in-a-enterprise"
|
||||
},
|
||||
"enterprise-admin/list-runner-applications-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runners",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#list-runner-applications-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/list-selected-organizations-enabled-github-actions-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "permissions",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#list-selected-organizations-enabled-for-github-actions-in-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/list-self-hosted-runner-groups-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runner-groups",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#list-self-hosted-runner-groups-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/list-self-hosted-runners-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runners",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#list-self-hosted-runners-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/list-self-hosted-runners-in-group-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runner-groups",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#list-self-hosted-runners-in-a-group-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/remove-all-custom-labels-from-self-hosted-runner-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runners",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#remove-all-custom-labels-from-a-self-hosted-runner-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/remove-custom-label-from-self-hosted-runner-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runners",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#remove-a-custom-label-from-a-self-hosted-runner-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/remove-org-access-to-self-hosted-runner-group-in-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runner-groups",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#remove-organization-access-to-a-self-hosted-runner-group-in-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/remove-self-hosted-runner-from-group-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runner-groups",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#remove-a-self-hosted-runner-from-a-group-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/set-allowed-actions-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "permissions",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#set-allowed-actions-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/set-custom-labels-for-self-hosted-runner-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runners",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#set-custom-labels-for-a-self-hosted-runner-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/set-github-actions-permissions-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "permissions",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#set-github-actions-permissions-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/set-org-access-to-self-hosted-runner-group-in-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runner-groups",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#set-organization-access-to-a-self-hosted-runner-group-in-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/set-selected-organizations-enabled-github-actions-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "permissions",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#set-selected-organizations-enabled-for-github-actions-in-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/set-self-hosted-runners-in-group-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runner-groups",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#set-self-hosted-runners-in-a-group-for-an-enterprise"
|
||||
},
|
||||
"enterprise-admin/update-self-hosted-runner-group-for-enterprise": {
|
||||
"category": "actions",
|
||||
"subcategory": "self-hosted-runner-groups",
|
||||
"originalUrl": "/rest/reference/enterprise-admin#update-a-self-hosted-runner-group-for-an-enterprise"
|
||||
},
|
||||
"orgs/list-custom-roles": {
|
||||
"category": "orgs",
|
||||
"subcategory": "custom-roles",
|
||||
"originalUrl": "/rest/reference/orgs#list-custom-repository-roles-in-an-organization"
|
||||
},
|
||||
"apps/oauth-applications": {
|
||||
"category": "apps",
|
||||
"subcategory": "apps",
|
||||
"originalUrl": "/rest/apps/oauth-applications#create-a-scoped-access-token"
|
||||
}
|
||||
},
|
||||
"sectionUrls": {
|
||||
"/rest/repos#deploy-keys": "/rest/deploy-keys",
|
||||
"/rest/deployments#deploy-keys": "/rest/deploy-keys",
|
||||
"/rest/repos#statuses": "/rest/commits/statuses"
|
||||
}
|
||||
}
|
||||
34
src/rest/scripts/utils/webhook-schema.js
Normal file
34
src/rest/scripts/utils/webhook-schema.js
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
// This schema is used to validate each generated webhook object at build time
|
||||
|
||||
export default {
|
||||
type: 'object',
|
||||
required: ['availability', 'bodyParameters', 'category', 'descriptionHtml', 'summaryHtml'],
|
||||
properties: {
|
||||
// Properties from the source OpenAPI schema that this module depends on
|
||||
action: {
|
||||
description: 'The webhook action type',
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
availability: {
|
||||
description: 'The supported origins of the webhook',
|
||||
type: 'array',
|
||||
},
|
||||
bodyParameters: {
|
||||
description: 'The request body parameters for the webhook',
|
||||
type: 'array',
|
||||
},
|
||||
category: {
|
||||
description: 'The name of the webhook and also the subcategory of the OpenAPI operation.',
|
||||
type: 'string',
|
||||
},
|
||||
descriptionHtml: {
|
||||
description: 'The description of the action property in the requestBody',
|
||||
type: 'string',
|
||||
},
|
||||
summaryHtml: {
|
||||
description: 'The description of the webhook',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
}
|
||||
79
src/rest/scripts/utils/webhook.js
Normal file
79
src/rest/scripts/utils/webhook.js
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env node
|
||||
import Ajv from 'ajv'
|
||||
import { get, isPlainObject } from 'lodash-es'
|
||||
|
||||
import renderContent from '../../../../lib/render-content/index.js'
|
||||
import webhookSchema from './webhook-schema.js'
|
||||
import { getBodyParams } from './get-body-params.js'
|
||||
|
||||
const NO_CHILD_PROPERTIES = [
|
||||
'action',
|
||||
'enterprise',
|
||||
'installation',
|
||||
'organization',
|
||||
'repository',
|
||||
'sender',
|
||||
]
|
||||
|
||||
export default class Webhook {
|
||||
#webhook
|
||||
constructor(webhook) {
|
||||
this.#webhook = webhook
|
||||
this.descriptionHtml = ''
|
||||
this.summaryHtml = ''
|
||||
this.bodyParameters = []
|
||||
this.availability = webhook['x-github']['supported-webhook-types']
|
||||
this.action = get(
|
||||
webhook,
|
||||
`requestBody.content['application/json'].schema.properties.action.enum[0]`,
|
||||
null
|
||||
)
|
||||
|
||||
// for some webhook action types (like some pull-request webhook types) the
|
||||
// schema properties are under a oneOf so we try and take the action from
|
||||
// the first one (the action will be the same across oneOf items)
|
||||
if (!this.action) {
|
||||
this.action = get(
|
||||
webhook,
|
||||
`requestBody.content['application/json'].schema.oneOf[0].properties.action.enum[0]`,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
// The OpenAPI uses hyphens for the webhook names, but the webhooks
|
||||
// are sent using underscores (e.g. `branch_protection_rule` instead
|
||||
// of `branch-protection-rule`)
|
||||
this.category = webhook['x-github'].subcategory.replaceAll('-', '_')
|
||||
return this
|
||||
}
|
||||
|
||||
async process() {
|
||||
await Promise.all([this.renderDescription(), this.renderBodyParameterDescriptions()])
|
||||
|
||||
const ajv = new Ajv()
|
||||
const valid = ajv.validate(webhookSchema, this)
|
||||
if (!valid) {
|
||||
console.error(JSON.stringify(ajv.errors, null, 2))
|
||||
throw new Error(`Invalid OpenAPI webhook found: ${this.category}`)
|
||||
}
|
||||
}
|
||||
|
||||
async renderDescription() {
|
||||
this.descriptionHtml = await renderContent(this.#webhook.description)
|
||||
this.summaryHtml = await renderContent(this.#webhook.summary)
|
||||
return this
|
||||
}
|
||||
|
||||
async renderBodyParameterDescriptions() {
|
||||
if (!this.#webhook.requestBody) return []
|
||||
const schema = get(this.#webhook, `requestBody.content.['application/json'].schema`, {})
|
||||
this.bodyParameters = isPlainObject(schema) ? await getBodyParams(schema, true, this.title) : []
|
||||
|
||||
// Removes the children of the common properties
|
||||
this.bodyParameters.forEach((param) => {
|
||||
if (NO_CHILD_PROPERTIES.includes(param.name)) {
|
||||
param.childParamsGroups = []
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
44
src/webhooks/README.md
Normal file
44
src/webhooks/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Webhooks
|
||||
|
||||
## About this directory
|
||||
|
||||
* `src/webhooks/lib/index.js` is human-editable.
|
||||
* `src/rest/data/**/*.payload.json` are manually edited and copied. When a new GHES release is created, the static webhook files from the previous version's directory are copied to a new version directory.
|
||||
|
||||
## Editable files
|
||||
|
||||
* `src/webhooks/lib/index.js` consumes the static JSON files in `src/webhooks/data` and exports the data used by the REST middleware contextualizer.
|
||||
|
||||
## Static files
|
||||
|
||||
Generated by `src/rest/scripts/update-files.js`:
|
||||
|
||||
* `src/rest/data` - files generated from the dereferenced OpenAPI schema with the Markdown descriptions rendered in HTML
|
||||
* `src/rest/data/dereferenced` - **NOTE** These are only generated if you pass the --keep-dereferenced-files option and are not checked into the repository. This option is for debug only.
|
||||
|
||||
## Rendering docs
|
||||
|
||||
When the server starts, `middleware/contextualizers/webhooks.js` accesses the data exported from the static webhook JSON files, fetches the data for the current version and requested path, and adds it to the `context` object. The added property is:
|
||||
|
||||
* `req.context.webhookPayloadsForCurrentVersion` - all webhook payloads with a version matching the current version
|
||||
|
||||
Markdown files in `content/developers/webhooks-and-events/webhooks/webhook-events-and-payloads.md` use Liquid to display the webhook payloads in `req.context.webhookPayloadsForCurrentVersion`. For example `{{ webhookPayloadsForCurrentVersion.user.created }}` references the payload file `user.created.payload.json` for the version being viewed.
|
||||
|
||||
**Note** Payload files either contain the webhook action type or no action type at all. For example, `user.created.payload.json` is the webhook `user` with the action type of `created`. Not all webhooks have action types. If a file exists with no action type (e.g., `user.payload.json`) and the action types (e.g., `user.created.payload.json` and `user.deleted.payload.json`), the entry in the context for the file with no action type will be `default`. For example, for the three static file mentioned, the object would be:
|
||||
|
||||
```
|
||||
{
|
||||
user: {
|
||||
default: "STRING VALUE",
|
||||
created: "STRING VALUE",
|
||||
deleted: "STRING VALUE"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If no action types exist, and only `user.payload.json` exists, the object would be:
|
||||
|
||||
```
|
||||
{
|
||||
user: "STRING VALUE"
|
||||
}
|
||||
169382
src/webhooks/data/api.github.com.json
Normal file
169382
src/webhooks/data/api.github.com.json
Normal file
File diff suppressed because it is too large
Load Diff
169382
src/webhooks/data/ghec.json
Normal file
169382
src/webhooks/data/ghec.json
Normal file
File diff suppressed because it is too large
Load Diff
11447
src/webhooks/data/ghes-3.3.json
Normal file
11447
src/webhooks/data/ghes-3.3.json
Normal file
File diff suppressed because it is too large
Load Diff
11704
src/webhooks/data/ghes-3.4.json
Normal file
11704
src/webhooks/data/ghes-3.4.json
Normal file
File diff suppressed because it is too large
Load Diff
11881
src/webhooks/data/ghes-3.5.json
Normal file
11881
src/webhooks/data/ghes-3.5.json
Normal file
File diff suppressed because it is too large
Load Diff
12135
src/webhooks/data/ghes-3.6.json
Normal file
12135
src/webhooks/data/ghes-3.6.json
Normal file
File diff suppressed because it is too large
Load Diff
168292
src/webhooks/data/ghes-3.7.json
Normal file
168292
src/webhooks/data/ghes-3.7.json
Normal file
File diff suppressed because it is too large
Load Diff
169996
src/webhooks/data/ghes-3.8.json
Normal file
169996
src/webhooks/data/ghes-3.8.json
Normal file
File diff suppressed because it is too large
Load Diff
169196
src/webhooks/data/github.ae.json
Normal file
169196
src/webhooks/data/github.ae.json
Normal file
File diff suppressed because it is too large
Load Diff
83
src/webhooks/lib/index.js
Normal file
83
src/webhooks/lib/index.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import path from 'path'
|
||||
|
||||
import { allVersions } from '../../../lib/all-versions.js'
|
||||
import { readCompressedJsonFileFallback } from '../../../lib/read-json-file.js'
|
||||
|
||||
const schemasPath = 'src/webhooks/data'
|
||||
|
||||
// cache for webhook data per version
|
||||
const webhooksCache = new Map()
|
||||
// cache for webhook data for when you first visit the webhooks page where we
|
||||
// show all webhooks for the current version but only 1 action type per webhook
|
||||
// and also no nested parameters
|
||||
const initialWebhooksCache = new Map()
|
||||
|
||||
// return the webhoook data as described for `initialWebhooksCache` for the given
|
||||
// version
|
||||
export async function getInitialPageWebhooks(version) {
|
||||
if (initialWebhooksCache.has(version)) {
|
||||
return initialWebhooksCache.get(version)
|
||||
}
|
||||
const allWebhooks = await getWebhooks(version)
|
||||
const initialWebhooks = []
|
||||
|
||||
// The webhooks page shows all webhooks but for each webhook only a single
|
||||
// webhook action type at a time. We pick the first webhook type from each
|
||||
// webhook's set of action types to show.
|
||||
for (const [key, webhook] of Object.entries(allWebhooks)) {
|
||||
const actionTypes = Object.keys(webhook)
|
||||
const defaultAction = actionTypes ? actionTypes[0] : null
|
||||
|
||||
const initialWebhook = {
|
||||
name: key,
|
||||
actionTypes,
|
||||
data: webhook[defaultAction],
|
||||
}
|
||||
|
||||
// remove all nested params for the initial webhooks page, we'll load
|
||||
// them by request
|
||||
if (initialWebhook.data.bodyParameters) {
|
||||
initialWebhook.data.bodyParameters.forEach((bodyParam) => {
|
||||
if (bodyParam.childParamsGroups) {
|
||||
bodyParam.childParamsGroups = []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
initialWebhooks.push({ ...initialWebhook })
|
||||
}
|
||||
initialWebhooksCache.set(version, initialWebhooks)
|
||||
return initialWebhooks
|
||||
}
|
||||
|
||||
// returns the webhook data for the given version and webhook category (e.g.
|
||||
// `check_run`) -- this includes all the data per webhook action type and all
|
||||
// nested parameters
|
||||
export async function getWebhook(version, webhookCategory) {
|
||||
const webhooks = await getWebhooks(version)
|
||||
return webhooks[webhookCategory]
|
||||
}
|
||||
|
||||
// returns all the webhook data for the given version
|
||||
export async function getWebhooks(version) {
|
||||
const openApiVersion = getOpenApiVersion(version)
|
||||
if (!webhooksCache.has(openApiVersion)) {
|
||||
const filename = `${openApiVersion}.json`
|
||||
|
||||
// The `readCompressedJsonFileFallback()` function
|
||||
// will check for both a .br and .json extension.
|
||||
webhooksCache.set(
|
||||
openApiVersion,
|
||||
readCompressedJsonFileFallback(path.join(schemasPath, filename))
|
||||
)
|
||||
}
|
||||
|
||||
return webhooksCache.get(openApiVersion)
|
||||
}
|
||||
|
||||
function getOpenApiVersion(version) {
|
||||
if (!(version in allVersions)) {
|
||||
throw new Error(`Unrecognized version '${version}'. Not found in ${Object.keys(allVersions)}`)
|
||||
}
|
||||
return allVersions[version].openApiVersionName
|
||||
}
|
||||
Reference in New Issue
Block a user