@redocly/cli
Version:
[@Redocly](https://redocly.com) CLI is your all-in-one API documentation utility. It builds, manages, improves, and quality-checks your API descriptions, all of which comes in handy for various phases of the API Lifecycle. Create your own rulesets to make
119 lines • 5.67 kB
JavaScript
import { BaseResolver, logger } from '@redocly/openapi-core';
import { blue, bold, cyan, gray, green, white } from 'colorette';
import { AbortFlowError, exitWithError } from '../../utils/error.js';
import { formatPath, getExecutionTime, getFallbackApisOrExit, getAliasOrPath, } from '../../utils/miscellaneous.js';
import { handleLoginAndFetchToken } from './auth/login-handler.js';
import { printScorecardResultsAsCheckstyle } from './formatters/checkstyle-formatter.js';
import { printScorecardResultsAsJson } from './formatters/json-formatter.js';
import { printScorecardResults } from './formatters/stylish-formatter.js';
import { fetchRemoteScorecardAndPlugins } from './remote/fetch-scorecard.js';
import { getTarget } from './targets-handler/targets-handler.js';
import { isAllowedScorecardProjectUrl } from './validation/project-url.js';
import { validateScorecard } from './validation/validate-scorecard.js';
export async function handleScorecardClassic({ argv, config, version, collectSpecData, }) {
const startedAt = performance.now();
const apis = await getFallbackApisOrExit(argv.api ? [argv.api] : [], config);
if (!apis.length) {
exitWithError('No APIs were provided.');
}
const projectUrl = argv['project-url'] ||
config.resolvedConfig.scorecardClassic?.fromProjectUrl ||
config.resolvedConfig.scorecard?.fromProjectUrl;
if (!projectUrl) {
exitWithError('Scorecard is not configured. Please provide it via --project-url flag or configure it in redocly.yaml. Learn more: https://redocly.com/docs/realm/config/scorecard#fromprojecturl-example');
}
if (!isAllowedScorecardProjectUrl(projectUrl)) {
exitWithError(`Project URL must be from the .redocly.com domain. Received: ${projectUrl}`);
}
const apiKey = process.env.REDOCLY_AUTHORIZATION;
if (isNonInteractiveEnvironment() && !apiKey) {
exitWithError('Please provide an API key using the REDOCLY_AUTHORIZATION environment variable.\n');
}
const auth = apiKey || (await handleLoginAndFetchToken(config, argv.verbose));
if (!auth) {
exitWithError('Failed to obtain access token or API key.');
}
const { scorecard, plugins } = await fetchRemoteScorecardAndPlugins({
projectUrl,
auth,
isApiKey: !!apiKey,
verbose: argv.verbose,
});
const { path, alias } = apis[0];
const matchedEntry = getAliasOrPath(config, path);
const matchedAlias = matchedEntry.alias || alias;
const apiConfigMetadata = matchedAlias
? config.resolvedConfig.apis?.[matchedAlias]?.metadata
: undefined;
if (argv.verbose && matchedAlias && apiConfigMetadata) {
logger.info(`\n✓ Matched API "${cyan(matchedAlias)}" from config\n`);
}
if (argv.verbose) {
logger.info(`Processing API: ${cyan(matchedAlias || 'default')}\n`);
logger.info(`Path: ${formatPath(path)}\n`);
if (apiConfigMetadata) {
logger.info(`Config Metadata: ${JSON.stringify(apiConfigMetadata, null, 2)}\n`);
}
logger.info(`Project URL: ${projectUrl}\n`);
}
const externalRefResolver = new BaseResolver(config.resolve);
const document = (await externalRefResolver.resolveDocument(null, path, true));
collectSpecData?.(document.parsed);
const documentInfo = document.parsed?.info;
const builtInMetadata = documentInfo?.['x-metadata'] || {};
const metadata = {
title: documentInfo?.title,
version: documentInfo?.version,
...builtInMetadata,
...apiConfigMetadata,
};
if (argv.verbose) {
logger.info(`Combined Metadata for target matching: ${JSON.stringify(metadata, null, 2)}\n`);
}
const targetLevel = argv['target-level'] || getTarget(scorecard?.targets, metadata)?.minimumLevel;
logger.info(gray(`\nRunning scorecard for ${formatPath(path)}...\n`));
const { problems: result, achievedLevel, targetLevelAchieved, } = await validateScorecard({
apiPath: path,
document,
externalRefResolver,
scorecardConfig: scorecard,
configPath: config.configPath,
pluginsCodeOrPlugins: plugins,
targetLevel,
metadata,
verbose: argv.verbose,
});
if (result.length === 0) {
logger.output(white(bold(`\n ☑️ Achieved Level: ${cyan(achievedLevel)}\n`)));
logger.output(green(`✅ No issues found for ${blue(formatPath(path))}. Your API meets all scorecard requirements.\n`));
return;
}
if (targetLevel && !targetLevelAchieved) {
logger.error(`\n❌ Your API specification does not satisfy the target scorecard level "${targetLevel}".\n`);
}
if (argv.format === 'json') {
printScorecardResultsAsJson(result, achievedLevel, targetLevelAchieved, version);
}
else if (argv.format === 'checkstyle') {
printScorecardResultsAsCheckstyle(path, result, achievedLevel, targetLevelAchieved);
}
else {
printScorecardResults(result, achievedLevel, targetLevelAchieved);
}
const elapsed = getExecutionTime(startedAt);
logger.info(`📊 Scorecard results for ${blue(formatPath(path))} at ${blue(path || 'stdout')} ${green(elapsed)}.\n`);
if (targetLevel && !targetLevelAchieved) {
throw new AbortFlowError('Target scorecard level not achieved.');
}
else if (achievedLevel !== 'Non Conformant') {
return;
}
throw new AbortFlowError('Scorecard validation failed.');
}
function isNonInteractiveEnvironment() {
if (process.env.CI || !process.stdin.isTTY) {
return true;
}
return false;
}
//# sourceMappingURL=index.js.map