diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..a716ac907 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +/.* +/node_modules +/appscan-config.xml +/*.md +/LICENSE diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..782e58963 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,15 @@ +image-build: + image: + name: quay.io/buildah/stable:latest + stage: build + before_script: + - 'echo "$CI_REGISTRY_PASSWORD" | buildah login --username "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY' + script: + - buildah bud + -f "Containerfile" + -t "$CI_REGISTRY_IMAGE/codeclimate-openapi:latest" + --layers + --cache-from=$CI_REGISTRY_IMAGE/codeclimate-openapi/cache + --cache-to=$CI_REGISTRY_IMAGE/codeclimate-openapi/cache + . + - buildah push "$CI_REGISTRY_IMAGE/codeclimate-openapi:latest" diff --git a/Containerfile b/Containerfile new file mode 100644 index 000000000..b664ef0d3 --- /dev/null +++ b/Containerfile @@ -0,0 +1,19 @@ +FROM node:current-alpine3.17 AS builder + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . ./ +RUN cd packages/validator && npm pack && mv ibm-openapi-validator-*.tgz /tmp/ibm-openapi-validator-latest.tgz + +FROM node:current-alpine3.17 + +RUN apk add --no-cache git + +COPY --from=builder /tmp/ibm-openapi-validator-latest.tgz /tmp +# not possible to install from github directly https://github.com/npm/npm/issues/2974 +RUN npm install -g /tmp/ibm-openapi-validator-latest.tgz \ + && npm cache clean --force + +WORKDIR /code +ENTRYPOINT ["lint-openapi", "--config", "/config.json", "--codeclimate"] diff --git a/README.md b/README.md index 9e49ec3e5..7eb7fb753 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ The IBM OpenAPI Validator lets you validate OpenAPI 3.x documents according to t - [Validator Output](#validator-output) * [Text](#text) * [JSON](#json) + * [CodeClimate](#codeclimate) - [Logging](#logging) - [Contributing](#contributing) - [License](#license) @@ -115,6 +116,7 @@ Options: -e, --errors-only include only errors in the output and skip warnings (default is false) -i, --ignore avoid validating (e.g. -i /dir1/ignore-file1.json --ignore /dir2/ignore-file2.yaml ...) (default is []) (default: []) -j, --json produce JSON output (default is text) + --codeclimate produce JSON output according to CodeClimate spec -l, --log-level set the log level for one or more loggers (e.g. -l root=info -l ibm-schema-description-exists=debug ...) (default: []) -n, --no-colors disable colorizing of the output (default is false) -r, --ruleset use Spectral ruleset contained in `` ("default" forces use of default IBM Cloud Validation Ruleset) @@ -130,18 +132,18 @@ The validator supports OpenAPI documents in either JSON or YAML format, using th .yaml .yml ``` -Assuming your command shell supports the use of wildcards, you can use wildcards when specifying the names of files to be validated. +If the string ends with '/', it will be searched recursively for supported files. For example, to run the validator on all `.yaml` files contained in the `/my/apis` directory, you could use this command: ```bash -lint-openapi /my/apis/*.yaml +lint-openapi /my/apis/ ``` -Note that the `-i`/`--ignore` option can be particularly useful when using wildcards because it allows you to skip the +Note that the `-i`/`--ignore` option can be particularly useful when using wildcards or directories because it allows you to skip the validation of specific files which might otherwise be included in a validation run. For example, to validate all `.yaml` files in the `/my/apis` directory, except for `/my/apis/broken-api.yaml` use the command: ```bash -lint-openapi /my/apis/*.yaml -i /my/apis/broken-api.yaml +lint-openapi /my/apis/ -i /my/apis/broken-api.yaml ``` ### Configuration @@ -269,7 +271,8 @@ module.exports = { The files configuration property corresponds to positional command-line arguments (i.e. [file...]). You can set this property to the names of the OpenAPI documents to be validated. If any filenames are also entered as positional arguments -on the command-line, they will override any values specified in this configuration property. +on the command-line, they will override any values specified in this configuration property. +`input_path` is an alternative key for `files`. If both are set, `input_paths` will be used. [](empty list) @@ -482,9 +485,9 @@ module.exports = { Default -You can set the outputFormat configuration property to either text or json +You can set the outputFormat configuration property to either text, json or codeclimate to indicate the type of output you want the validator to produce. -This property corresponds to the -j/--json command-line option. +This property corresponds to the -j/--json/--codeclimate command-line option. text @@ -621,7 +624,7 @@ module.exports = { ## Validator Output The validator can produce output in either text or JSON format. The default is `text` output, and this can be -controlled with the `-j`/`--json` command-line option or `outputFormat` configuration property. +controlled with the `-j`/`--json`/`--codeclimate` command-line option or `outputFormat` configuration property. ### Text Here is an example of text output: @@ -753,6 +756,10 @@ Here is an example of JSON output: The JSON output is also affected by the `-s`/`--summary-only` and `-e`/`--errors-only` options as well as the `summaryOnly` and `errorsOnly` configuration properties. +### CodeClimate +When displaying CodeClimate JSON output, the validator will produce a null-byte separated stream of JSON objects +which complies with [the CodeClimate Output format](https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#output). + ## Logging The validator uses a *logger* for displaying messages on the console. The core validator uses a single logger named `root`, while each of the rules contained in the diff --git a/packages/validator/src/cli-validator/run-validator.js b/packages/validator/src/cli-validator/run-validator.js index a62d7c9bb..f5491bfe6 100644 --- a/packages/validator/src/cli-validator/run-validator.js +++ b/packages/validator/src/cli-validator/run-validator.js @@ -18,6 +18,7 @@ const ext = require('./utils/file-extension-validator'); const preprocessFile = require('./utils/preprocess-file'); const print = require('./utils/print-results'); const { printJson } = require('./utils/json-results'); +const { printCCJson } = require('./utils/codeclimate-results'); const { runSpectral } = require('../spectral/spectral-validator'); const getCopyrightString = require('./utils/get-copyright-string'); @@ -72,7 +73,7 @@ async function runValidator(cliArgs, parseOptions = {}) { context.chalk = chalk; - if (context.config.outputFormat !== 'json') { + if (context.config.outputFormat === 'text') { console.log(getCopyrightString()); } @@ -80,6 +81,9 @@ async function runValidator(cliArgs, parseOptions = {}) { // Run the validator on the files specified via command-line or config file. // + // TODO: implement recursive filesearch when arg ends with '/' + // must be done before filtering + // Ignore files listed in the config object's "ignoreFiles" field // by comparing absolute paths. // "filteredArgs" will be "args" minus any ignored files. @@ -97,8 +101,6 @@ async function runValidator(cliArgs, parseOptions = {}) { // At this point, "args" is an array of file names passed in by the user, // but with the ignored files removed. - // Nothing in "args" will be a glob type, as glob types are automatically - // converted to arrays of matching file names by the shell. const supportedFileTypes = ['json', 'yml', 'yaml']; const filesWithValidExtensions = []; let unsupportedExtensionsFound = false; @@ -157,7 +159,7 @@ async function runValidator(cliArgs, parseOptions = {}) { let originalFile; let input; - if (context.config.outputFormat != 'json') { + if (context.config.outputFormat === 'text') { console.log(''); console.log(chalk.underline(`Validation Results for ${validFile}:\n`)); } @@ -202,15 +204,15 @@ async function runValidator(cliArgs, parseOptions = {}) { // Check to see if we should be passing back a non-zero exit code. if (results.error.summary.total) { - // If we have any errors, then exit code 1 is returned. - exitCode = 1; + // If we have any errors, then exit code 1 is returned, except when running for codeclimate. + exitCode = context.config.outputFormat === 'codeclimate' ? 0 : 1; } // If the # of warnings exceeded the warnings limit, then this is an error. const numWarnings = results.warning.summary.total; const warningsLimit = context.config.limits.warnings; if (warningsLimit >= 0 && numWarnings > warningsLimit) { - exitCode = 1; + exitCode = context.config.outputFormat === 'codeclimate' ? 0 : 1; logger.error( `Number of warnings (${numWarnings}) exceeds warnings limit (${warningsLimit}).` ); @@ -219,6 +221,8 @@ async function runValidator(cliArgs, parseOptions = {}) { // Now print the results, either JSON or text. if (context.config.outputFormat === 'json') { printJson(context, results); + } else if (context.config.outputFormat === 'codeclimate') { + printCCJson(validFile, results); } else { if (results.hasResults) { print(context, results); diff --git a/packages/validator/src/cli-validator/utils/cli-options.js b/packages/validator/src/cli-validator/utils/cli-options.js index 328dd88fb..ad895d75c 100644 --- a/packages/validator/src/cli-validator/utils/cli-options.js +++ b/packages/validator/src/cli-validator/utils/cli-options.js @@ -58,6 +58,10 @@ function createCLIOptions() { [] ) .option('-j, --json', 'produce JSON output (default is text)') + .option( + '--codeclimate', + 'produce JSON output according to CodeClimate spec' + ) .option( '-l, --log-level ', 'set the log level for one or more loggers (e.g. -l root=info -l ibm-schema-description-exists=debug ...) ', diff --git a/packages/validator/src/cli-validator/utils/codeclimate-results.js b/packages/validator/src/cli-validator/utils/codeclimate-results.js new file mode 100644 index 000000000..7a16846ff --- /dev/null +++ b/packages/validator/src/cli-validator/utils/codeclimate-results.js @@ -0,0 +1,47 @@ +/** + * Copyright 2023 IBM Corporation, Matthias Blümel. + * SPDX-License-Identifier: Apache2.0 + */ + +const each = require('lodash/each'); + +function printCCJson(validFile, results) { + const types = ['error', 'warning', 'info', 'hint']; + const ccTypeMap = { + error: 'critical', + warning: 'major', + info: 'minor', + hint: 'info', + }; + + types.forEach(type => { + each(results[type].results, result => { + let content; + if (result.path.length !== 0) { + let markdown = ''; + each(result.path, pathItem => { + markdown += '* ' + pathItem + '\n'; + }); + content = { body: markdown }; + } + const ccResult = { + type: 'issue', + check_name: result.rule, + description: result.message, + content: content, + categories: ['Style'], // required by codeclimate, ignored by gitlab; has to be defined by the rule. + location: { + path: validFile, + lines: { + begin: result.line, + end: result.line, + }, + }, + severity: ccTypeMap[type], + }; + console.log(JSON.stringify(ccResult) + '\0\n'); + }); + }); +} + +module.exports.printCCJson = printCCJson; diff --git a/packages/validator/src/cli-validator/utils/configuration-manager.js b/packages/validator/src/cli-validator/utils/configuration-manager.js index 8be68630a..3a773beb1 100644 --- a/packages/validator/src/cli-validator/utils/configuration-manager.js +++ b/packages/validator/src/cli-validator/utils/configuration-manager.js @@ -29,6 +29,9 @@ const defaultConfig = { files: [ // 'my-api.yaml' ], + input_paths: [ + // alternative to files for CodeClimate compatibility + ], limits: { warnings: -1, }, @@ -169,9 +172,9 @@ async function processArgs(args, cliParseOptions) { // so overlay CLI options onto our config object. // Filenames specified on the command-line will be in the "args" field. - const cliFiles = command.args || []; - if (cliFiles.length) { - configObj.files = cliFiles; + const prioFiles = command.args || configObj.input_paths || []; + if (prioFiles.length) { + configObj.files = prioFiles; } // Process each loglevel entry supplied on the command line. @@ -224,6 +227,10 @@ async function processArgs(args, cliParseOptions) { configObj.outputFormat = 'json'; } + if ('codeclimate' in opts) { + configObj.outputFormat = 'codeclimate'; + } + if ('ruleset' in opts) { configObj.ruleset = opts.ruleset; } diff --git a/packages/validator/src/schemas/config-file.yaml b/packages/validator/src/schemas/config-file.yaml index fa37fb835..ea1b99c10 100644 --- a/packages/validator/src/schemas/config-file.yaml +++ b/packages/validator/src/schemas/config-file.yaml @@ -3,7 +3,8 @@ title: IBM OpenAPI Validator Configuration File Schema description: > The structure of the configuration file supported by the IBM OpenAPI Validator type: object -additionalProperties: false +# allow additionalProperties on root-level to be compatible with CodeClimate config.json +additionalProperties: true properties: colorizeOutput: description: A flag that indicates whether the validator should colorize its output @@ -16,9 +17,16 @@ properties: files: description: > The names of the files to be validated. - Each element of the array is a glob-like string that will be evaluated relative + Each element of the array is a string that will be evaluated relative to the current directory at the time the validator is being run. - Examples: 'a.yaml', '../b.json', '../../c/foo.yml' + If the string ends with '/', it will be searched recursively for supported files. + Each file must end with the extension '.json', '.yaml' or '.yml' and contain a key 'openapi' in it's root level. + Examples: 'a.yaml', '../b.json', '../../c/foo/' + type: array + items: + type: string + include_paths: + description: replaces 'files' when used type: array items: type: string @@ -59,6 +67,7 @@ properties: type: string enum: - json + - codeclimate - text default: text ruleset: diff --git a/packages/validator/test/cli-validator/mock-files/config/invalid-values.json b/packages/validator/test/cli-validator/mock-files/config/invalid-values.json index 5b5049bf5..b00ca5f45 100644 --- a/packages/validator/test/cli-validator/mock-files/config/invalid-values.json +++ b/packages/validator/test/cli-validator/mock-files/config/invalid-values.json @@ -1,5 +1,6 @@ { "errors": 0, "warnings": "text", - "population": 10 + "population": 10, + "errorsOnly": "yes" } diff --git a/packages/validator/test/cli-validator/tests/run-validator.test.js b/packages/validator/test/cli-validator/tests/run-validator.test.js index 77ed8813b..0fcda123b 100644 --- a/packages/validator/test/cli-validator/tests/run-validator.test.js +++ b/packages/validator/test/cli-validator/tests/run-validator.test.js @@ -71,7 +71,7 @@ describe('run-validator tests', function () { expect(allOutput).toMatch(/Invalid configuration file/); expect(allOutput).toMatch( - /schema validation error: must NOT have additional properties/ + /schema validation error: '\/errorsOnly': must be boolean/ ); expect(allOutput).toMatch(/validator will use a default config/); }); diff --git a/packages/validator/test/cli-validator/tests/utils/schema-validator.test.js b/packages/validator/test/cli-validator/tests/utils/schema-validator.test.js index 7ceb82c70..3de814797 100644 --- a/packages/validator/test/cli-validator/tests/utils/schema-validator.test.js +++ b/packages/validator/test/cli-validator/tests/utils/schema-validator.test.js @@ -76,9 +76,6 @@ describe('Schema validator tests', function () { it('invalid config object should return errors', function () { const configObj = { - xlogLevels: { - root: 'debug', - }, limits: { xwarnings: 10, }, @@ -88,7 +85,7 @@ describe('Schema validator tests', function () { const results = validate(configObj, configFileSchema); expect(results).toHaveLength(1); expect(results[0]).toBe( - `schema validation error: must NOT have additional properties` + `schema validation error: '/limits': must have required property 'warnings'` ); }); });