### What
Unhandled OAS circular references shall fail `openapi:generate` until they have been patched in `docs-util/redocly/config.yaml`
### Why
Prevent developers to commit new OAS changes that would cause to API documentation to crash.
Our API documentation rendering library will crash and not load if our OAS contains circular references. We have an automated mechanism to patch offending references but they must be identified and configured by hand.
### How
Let the `openapi:generate --dry-run` command reach OAS sanitization and circular reference check operations.
Fail the build script if unhandled circular references are detected.
Output the offending references to the stdout in order to help the developers identify and configure the required patch.
Since some the tooling involved only work with files, use the host system's temporary directory mechanism when using the `--dry-run` flag.
### Test
* Introduce a bug by removing an entry in `docs-util/redocly/config.yaml`
* Run `yarn openapi:generate --dry-run`
* Expect the script to exit before completing
* Expect the logs to contain `🔴 Unhandled circular references.`
* Expect the logs to contain an array of the offending references
110 lines
2.9 KiB
JavaScript
Executable File
110 lines
2.9 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
const fs = require("fs/promises")
|
|
const os = require("os")
|
|
const path = require("path")
|
|
const execa = require("execa")
|
|
const yaml = require("js-yaml")
|
|
const OpenAPIParser = require("@readme/openapi-parser")
|
|
|
|
const isDryRun = process.argv.indexOf("--dry-run") !== -1
|
|
const basePath = path.resolve(__dirname, `../`)
|
|
const docsApiPath = path.resolve(basePath, "docs/api/")
|
|
|
|
const run = async () => {
|
|
const outputPath = isDryRun ? await getTmpDirectory() : docsApiPath
|
|
|
|
await generateOASSources(outputPath)
|
|
|
|
for (const apiType of ["store", "admin"]) {
|
|
const inputJsonFile = path.resolve(outputPath, `${apiType}.oas.json`)
|
|
const outputYamlFile = path.resolve(outputPath, `${apiType}.oas.yaml`)
|
|
|
|
await jsonFileToYamlFile(inputJsonFile, outputYamlFile)
|
|
await sanitizeOAS(outputYamlFile)
|
|
await circularReferenceCheck(outputYamlFile)
|
|
if (!isDryRun) {
|
|
await generateReference(outputYamlFile, apiType)
|
|
}
|
|
}
|
|
}
|
|
|
|
const generateOASSources = async (outDir, isDryRun) => {
|
|
const params = ["oas", `--out-dir=${outDir}`]
|
|
if (isDryRun) {
|
|
params.push("--dry-run")
|
|
}
|
|
const { all: logs } = await execa("medusa-oas", params, {
|
|
cwd: basePath,
|
|
all: true,
|
|
})
|
|
console.log(logs)
|
|
}
|
|
|
|
const jsonFileToYamlFile = async (inputJsonFile, outputYamlFile) => {
|
|
const jsonString = await fs.readFile(inputJsonFile, "utf8")
|
|
const jsonObject = JSON.parse(jsonString)
|
|
const yamlString = yaml.dump(jsonObject)
|
|
await fs.writeFile(outputYamlFile, yamlString, "utf8")
|
|
}
|
|
|
|
const sanitizeOAS = async (srcFile) => {
|
|
const { all: logs } = await execa(
|
|
"redocly",
|
|
[
|
|
"bundle",
|
|
srcFile,
|
|
`--output=${srcFile}`,
|
|
"--config=docs-util/redocly/config.yaml",
|
|
],
|
|
{ cwd: basePath, all: true }
|
|
)
|
|
console.log(logs)
|
|
}
|
|
|
|
const circularReferenceCheck = async (srcFile) => {
|
|
const parser = new OpenAPIParser()
|
|
await parser.validate(srcFile, {
|
|
dereference: {
|
|
circular: "ignore",
|
|
},
|
|
})
|
|
if (parser.$refs.circular) {
|
|
const fileName = path.basename(srcFile)
|
|
const circularRefs = [...parser.$refs.circularRefs]
|
|
circularRefs.sort()
|
|
console.log(circularRefs)
|
|
throw new Error(
|
|
`🔴 Unhandled circular references - ${fileName} - Please patch in docs-util/redocly/config.yaml`
|
|
)
|
|
}
|
|
}
|
|
|
|
const generateReference = async (srcFile, apiType) => {
|
|
const outDir = path.resolve(docsApiPath, `${apiType}`)
|
|
await fs.rm(outDir, { recursive: true, force: true })
|
|
const { all: logs } = await execa(
|
|
"redocly",
|
|
["split", srcFile, `--outDir=${outDir}`],
|
|
{ cwd: basePath, all: true }
|
|
)
|
|
console.log(logs)
|
|
}
|
|
|
|
const getTmpDirectory = async () => {
|
|
/**
|
|
* RUNNER_TEMP: GitHub action, the path to a temporary directory on the runner.
|
|
*/
|
|
const tmpDir = process.env["RUNNER_TEMP"] ?? os.tmpdir()
|
|
return await fs.mkdtemp(tmpDir)
|
|
}
|
|
|
|
void (async () => {
|
|
try {
|
|
await run()
|
|
} catch (err) {
|
|
console.log(err)
|
|
process.exit(1)
|
|
}
|
|
})()
|