diff --git a/docs/guides/2-cli.md b/docs/guides/2-cli.md index f38944c80..7bfd2e5ab 100644 --- a/docs/guides/2-cli.md +++ b/docs/guides/2-cli.md @@ -33,7 +33,8 @@ Other options include: [string] [choices: "utf8", "ascii", "utf-8", "utf16le", "ucs2", "ucs-2", "base64", "latin1"] [default: "utf8"] -f, --format formatters to use for outputting results, more than one can be given joining them with a comma - [string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty"] [default: "stylish"] + [string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty", "github-actions"] [default: + "stylish"] -o, --output where to output results, can be a single file name, multiple "output." or missing to print to stdout [string] --stdin-filepath path to a file to pretend that stdin comes from [string] diff --git a/packages/formatters/README.md b/packages/formatters/README.md index cc2dfa53c..ebade72f6 100644 --- a/packages/formatters/README.md +++ b/packages/formatters/README.md @@ -32,3 +32,4 @@ console.error(output); ### Node.js only - pretty +- github-actions diff --git a/packages/formatters/src/__tests__/github-actions.test.ts b/packages/formatters/src/__tests__/github-actions.test.ts new file mode 100644 index 000000000..c81d76fec --- /dev/null +++ b/packages/formatters/src/__tests__/github-actions.test.ts @@ -0,0 +1,50 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import type { IRuleResult } from '@stoplight/spectral-core'; +import { githubActions } from '../github-actions'; + +const cwd = process.cwd(); +const results: IRuleResult[] = [ + { + code: 'operation-description', + message: 'paths./pets.get.description is not truthy\nMessage can have\nmultiple lines', + path: ['paths', '/pets', 'get', 'description'], + severity: 1, + source: `${cwd}/__tests__/fixtures/petstore.oas2.yaml`, + range: { + start: { + line: 60, + character: 8, + }, + end: { + line: 71, + character: 60, + }, + }, + }, + { + code: 'operation-tags', + message: 'paths./pets.get.tags is not truthy', + path: ['paths', '/pets', 'get', 'tags'], + severity: 1, + source: `${cwd}/__tests__/fixtures/petstore.oas2.yaml`, + range: { + start: { + line: 60, + character: 8, + }, + end: { + line: 71, + character: 60, + }, + }, + }, +]; + +describe('GitHub Actions formatter', () => { + test('should be formatted correctly', () => { + expect(githubActions(results, { failSeverity: DiagnosticSeverity.Error }).split('\n')).toEqual([ + '::warning title=operation-description,file=__tests__/fixtures/petstore.oas2.yaml,col=9,endColumn=61,line=61,endLine=72::paths./pets.get.description is not truthy%0AMessage can have%0Amultiple lines', + '::warning title=operation-tags,file=__tests__/fixtures/petstore.oas2.yaml,col=9,endColumn=61,line=61,endLine=72::paths./pets.get.tags is not truthy', + ]); + }); +}); diff --git a/packages/formatters/src/github-actions.ts b/packages/formatters/src/github-actions.ts new file mode 100644 index 000000000..f09b43adf --- /dev/null +++ b/packages/formatters/src/github-actions.ts @@ -0,0 +1,47 @@ +import { relative } from '@stoplight/path'; +import { DiagnosticSeverity, Dictionary } from '@stoplight/types'; +import { Formatter } from './types'; + +const OUTPUT_TYPES: Dictionary = { + [DiagnosticSeverity.Error]: 'error', + [DiagnosticSeverity.Warning]: 'warning', + [DiagnosticSeverity.Information]: 'notice', + [DiagnosticSeverity.Hint]: 'notice', +}; + +type OutputParams = { + title?: string; + file: string; + col?: number; + endColumn?: number; + line?: number; + endLine?: number; +}; + +export const githubActions: Formatter = results => { + return results + .map(result => { + // GitHub Actions requires relative path for annotations, determining from working directory here + const file = relative(process.cwd(), result.source ?? ''); + const params: OutputParams = { + title: result.code.toString(), + file, + col: result.range.start.character + 1, + endColumn: result.range.end.character + 1, + line: result.range.start.line + 1, + endLine: result.range.end.line + 1, + }; + + const paramsString = Object.entries(params) + .map(p => p.join('=')) + .join(','); + + // As annotated messages must be one-line due to GitHub's limitation, replacing all LF to %0A here. + // see: https://github.com/actions/toolkit/issues/193 + // FIXME: Use replaceAll instead after removing Node.js 14 support. + const message = result.message.replace(/\n/g, '%0A'); + + return `::${OUTPUT_TYPES[result.severity]} ${paramsString}::${message}`; + }) + .join('\n'); +}; diff --git a/packages/formatters/src/index.node.ts b/packages/formatters/src/index.node.ts index bbb40b065..d9e9b3ce2 100644 --- a/packages/formatters/src/index.node.ts +++ b/packages/formatters/src/index.node.ts @@ -1,3 +1,4 @@ export { html, json, junit, text, stylish, teamcity } from './index'; export type { Formatter, FormatterOptions } from './index'; export { pretty } from './pretty'; +export { githubActions } from './github-actions'; diff --git a/packages/formatters/src/index.ts b/packages/formatters/src/index.ts index 4c3b90e34..8caf77087 100644 --- a/packages/formatters/src/index.ts +++ b/packages/formatters/src/index.ts @@ -10,3 +10,7 @@ export type { Formatter, FormatterOptions } from './types'; export const pretty: Formatter = () => { throw Error('pretty formatter is available only in Node.js'); }; + +export const githubActions: Formatter = () => { + throw Error('github-actions formatter is available only in Node.js'); +};