From 06b016e9b8760536955b5fefee73da45a431328b Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 7 Mar 2023 16:49:01 -0500 Subject: [PATCH] Add generate and verify commands. Signed-off-by: Aramis Sennyey --- .github/workflows/ci.yml | 3 + packages/cli/package.json | 1 + packages/cli/src/commands/index.ts | 21 +++++ packages/cli/src/commands/openapi/generate.ts | 41 +++++++++ packages/cli/src/commands/openapi/verify.ts | 91 +++++++++++++++++++ .../{src/service => }/schema/openapi.ts | 0 .../src/service/createRouter.ts | 2 +- yarn.lock | 1 + 8 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/commands/openapi/generate.ts create mode 100644 packages/cli/src/commands/openapi/verify.ts rename plugins/catalog-backend/{src/service => }/schema/openapi.ts (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd67d2742a..4bd7622796 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,9 @@ jobs: - name: verify api reference run: node scripts/verify-api-reference.js + - name: verify openapi yaml <> ts files + run: yarn backstage-cli schema:openapi:verify + - name: verify doc links run: node scripts/verify-links.js diff --git a/packages/cli/package.json b/packages/cli/package.json index 5974116843..5e99a3cba0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -96,6 +96,7 @@ "jest-css-modules": "^2.1.0", "jest-environment-jsdom": "^29.0.2", "jest-runtime": "^29.0.2", + "js-yaml": "^4.1.0", "json-schema": "^0.4.0", "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.4.2", diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index ba11a53cfd..6c29741060 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -80,6 +80,13 @@ export function registerRepoCommand(program: Command) { lazy(() => import('./repo/list-deprecations').then(m => m.command)), ); + command + .command('schema:openapi:verify') + .description( + 'Verify that all OpenAPI schemas are valid and have a matching `schemas/openapi.ts` file.', + ) + .action(lazy(() => import('./openapi/verify').then(m => m.bulkCommand))); + command .command('test') .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args @@ -179,6 +186,20 @@ export function registerScriptCommand(program: Command) { .command('postpack') .description('Restores the changes made by the prepack command') .action(lazy(() => import('./pack').then(m => m.post))); + + command + .command('schema:openapi:verify') + .description( + 'Verifies that an OpenAPI spec is defined in both an `openapi.yaml` file and a `schema/openapi.ts` file.', + ) + .action(lazy(() => import('./openapi/verify').then(m => m.command))); + + command + .command('schema:openapi:generate') + .description( + 'Generates a `schema/openapi.ts` file from an OpenAPI spec defined in `openapi.yaml`.', + ) + .action(lazy(() => import('./openapi/generate').then(m => m.command))); } export function registerMigrateCommand(program: Command) { diff --git a/packages/cli/src/commands/openapi/generate.ts b/packages/cli/src/commands/openapi/generate.ts new file mode 100644 index 0000000000..f90f152b09 --- /dev/null +++ b/packages/cli/src/commands/openapi/generate.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import { paths } from '../../lib/paths'; +import YAML from 'js-yaml'; + +export async function command() { + const openapiPath = paths.resolveTarget('openapi.yaml'); + if (!(await fs.pathExists(openapiPath))) { + console.warn('Could not find openapi.yaml in root of directory.'); + process.exit(1); + } + try { + const yaml = YAML.load( + await fs.readFile(paths.resolveTarget('openapi.yaml'), 'utf8'), + ); + + // For now, we're not adding a header or linting after pasting. + await fs.writeFile( + paths.resolveTarget('schema/openapi.ts'), + `export default ${JSON.stringify(yaml, null, 2)} as const`, + ); + } catch (err) { + console.error(err); + process.exit(1); + } +} diff --git a/packages/cli/src/commands/openapi/verify.ts b/packages/cli/src/commands/openapi/verify.ts new file mode 100644 index 0000000000..7d3de8e7ee --- /dev/null +++ b/packages/cli/src/commands/openapi/verify.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import { paths } from '../../lib/paths'; +import YAML from 'js-yaml'; +import { isEqual } from 'lodash'; +import { join } from 'path'; +import chalk from 'chalk'; +import { relative as relativePath } from 'path'; +import { PackageGraph } from '../../lib/monorepo'; + +async function verify(directoryPath: string) { + const openapiPath = join(directoryPath, 'openapi.yaml'); + if (!(await fs.pathExists(openapiPath))) { + return; + } + const yaml = YAML.load(await fs.readFile(openapiPath, 'utf8')); + + const schemaPath = join(directoryPath, 'schema/openapi.ts'); + if (!(await fs.pathExists(schemaPath))) { + throw new Error('No `schema/openapi.ts` file found.'); + } + const schema = await import(join(directoryPath, 'schema/openapi')); + if (schema.default) { + if (!isEqual(schema.default, yaml)) { + throw new Error( + '`openapi.yaml` and `schema/openapi.ts` do not match. Please run `yarn build:openapi` to generate the `schema/openapi.ts` file from the `openapi.yaml` file.', + ); + } + } else { + throw new Error('`schemas/openapi.ts` needs to have a default export.'); + } +} + +export async function command() { + try { + await verify(paths.resolveTarget('.')); + } catch (err) { + console.error(chalk.red(err.message)); + process.exit(1); + } +} + +export async function bulkCommand(): Promise { + const packages = await PackageGraph.listTargetPackages(); + + const resultsList = await Promise.all( + packages.map(async pkg => { + let resultText = ''; + try { + await verify(pkg.dir); + } catch (err) { + resultText = err.message; + } + + return { + relativeDir: relativePath(paths.targetRoot, pkg.dir), + resultText, + }; + }), + ); + + let failed = false; + for (const { relativeDir, resultText } of resultsList) { + if (resultText) { + console.log(); + console.log(chalk.red(`Lint failed in ${relativeDir}:`)); + console.log(resultText.trimStart()); + + failed = true; + } + } + + if (failed) { + process.exit(1); + } +} diff --git a/plugins/catalog-backend/src/service/schema/openapi.ts b/plugins/catalog-backend/schema/openapi.ts similarity index 100% rename from plugins/catalog-backend/src/service/schema/openapi.ts rename to plugins/catalog-backend/schema/openapi.ts diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index db2f569d54..77fe4a24e8 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -51,7 +51,7 @@ import { validateRequestBody, } from './util'; import { ApiRouter } from '@backstage/backend-openapi-utils'; -import spec from './schema/openapi'; +import spec from '../../schema/openapi'; /** * Options used by {@link createRouter}. diff --git a/yarn.lock b/yarn.lock index 0069effc02..78e5acf047 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4157,6 +4157,7 @@ __metadata: jest-css-modules: ^2.1.0 jest-environment-jsdom: ^29.0.2 jest-runtime: ^29.0.2 + js-yaml: ^4.1.0 json-schema: ^0.4.0 lodash: ^4.17.21 mini-css-extract-plugin: ^2.4.2