Skip to content

Commit

Permalink
Merge pull request #615 from stoplightio/feat/newer-spectral
Browse files Browse the repository at this point in the history
feat: use newer Spectral
  • Loading branch information
P0lip committed Sep 6, 2021
2 parents a081f4c + 719cf35 commit a1a5b82
Show file tree
Hide file tree
Showing 9 changed files with 382 additions and 749 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,6 @@ typings/
.next

dist

# JetBrains IDEs
.idea/
7 changes: 1 addition & 6 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ inputs:
description: |
Custom ruleset to load in Spectral.
When unspecified, will try to load the default `.spectral.yaml` ruleset if it exists.
Otherwise, the default built-in Spectral rulesets will be loaded.
When unspecified, will try to load the default ruleset (matching .spectral.{yaml,yml,js,json}) if it exists.
repo_token:
required: true
description: |
Expand All @@ -25,10 +24,6 @@ inputs:
description: |
The name of the event that triggered the workflow
default: ${{ github.event_name }}
use_nimma:
required: false
description: Use the experimental JSON Path engine
default: 'false'
runs:
using: docker
image: Dockerfile
Expand Down
18 changes: 14 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,39 @@
"bugs": {
"url": "https://github.com/XVincentX/spectral-action/issues"
},
"engines": {
"node": ">=12.20"
},
"homepage": "https://github.com/XVincentX/spectral-action#readme",
"devDependencies": {
"@octokit/types": "^6.8.2",
"@types/lodash": "^4.14.165",
"@types/node": "^14.14.7",
"@types/pluralize": "^0.0.29",
"@types/urijs": "^1.19.13",
"husky": "^4.3.0",
"prettier": "^2.1.2",
"pretty-quick": "^3.1.0",
"typescript": "^4.0.5"
},
"dependencies": {
"@actions/core": "^1.2.6",
"@actions/core": "^1.5.0",
"@actions/github": "^4.0.0",
"@octokit/graphql": "^4.6.0",
"@octokit/request": "^5.4.14",
"@octokit/rest": "^18.1.0",
"@stoplight/spectral": "^5.9.1",
"fast-glob": "^3.2.5",
"@stoplight/spectral-core": "^1.4.0",
"@stoplight/spectral-parsers": "^1.0.0",
"@stoplight/spectral-ref-resolver": "^1.0.0",
"@stoplight/spectral-ruleset-migrator": "^1.4.2",
"@stoplight/spectral-rulesets": ">=1",
"@stoplight/types": "^12.3.0",
"fast-glob": "^3.2.7",
"fp-ts": "^2.9.5",
"io-ts": "^2.2.16",
"lodash": "^4.17.21",
"tslib": "^2.1.0"
"pluralize": "^8.0.0",
"tslib": "^2.3.1"
},
"husky": {
"hooks": {
Expand Down
1 change: 0 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export const Config = D.type({
INPUT_FILE_GLOB: D.string,
INPUT_EVENT_NAME: D.string,
INPUT_SPECTRAL_RULESET: D.string,
INPUT_USE_NIMMA: D.boolean,
});

export type Config = D.TypeOf<typeof Config>;
71 changes: 71 additions & 0 deletions src/getRuleset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Optional } from '@stoplight/types';
import { Ruleset, RulesetDefinition } from '@stoplight/spectral-core';
import * as fs from 'fs';
import * as path from 'path';
import * as process from 'process';
import { migrateRuleset } from '@stoplight/spectral-ruleset-migrator';
import { info, error } from '@actions/core';

// eslint-disable-next-line @typescript-eslint/require-await
const AsyncFunction = (async (): Promise<void> => void 0).constructor as FunctionConstructor;

async function getDefaultRulesetFile(): Promise<Optional<string>> {
const cwd = process.cwd();
for (const filename of await fs.promises.readdir(cwd)) {
if (Ruleset.isDefaultRulesetFile(filename)) {
return path.join(cwd, filename);
}
}

return;
}

export async function getRuleset(rulesetFile: Optional<string>): Promise<Ruleset> {
if (!rulesetFile) {
rulesetFile = await getDefaultRulesetFile();
} else if (!path.isAbsolute(rulesetFile)) {
rulesetFile = path.join(process.cwd(), rulesetFile);
}

if (!rulesetFile) {
throw new Error(
'No ruleset has been found. Please provide a ruleset using the --ruleset CLI argument, or make sure your ruleset file matches .?spectral.(js|ya?ml|json)'
);
}

info(`Loading ruleset '${rulesetFile}'...`);

let ruleset;

try {
if (/(json|ya?ml)$/.test(path.extname(rulesetFile))) {
const m: { exports?: RulesetDefinition } = {};
const paths = [path.dirname(rulesetFile), __dirname];

await AsyncFunction(
'module, require',
await migrateRuleset(rulesetFile, {
format: 'commonjs',
fs,
})
// eslint-disable-next-line @typescript-eslint/no-var-requires
)(m, (id: string) => require(require.resolve(id, { paths })) as unknown);

ruleset = m.exports;
} else {
const imported = (await import(rulesetFile)) as { default: unknown } | unknown;
ruleset =
typeof imported === 'object' && imported !== null && 'default' in imported
? (imported as Record<'default', unknown>).default
: imported;
}
} catch (e) {
error(`Failed to load ruleset '${rulesetFile}'... Error: ${e.message}`);
throw e;
}

return new Ruleset(ruleset, {
severity: 'recommended',
source: rulesetFile,
});
}
19 changes: 7 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { promises as fs } from 'fs';
import { array } from 'fp-ts/Array';
import { Config } from './config';
import { runSpectral, createSpectral, FileWithContent } from './spectral';
import { pluralizer } from './utils';
import pluralize from 'pluralize';
import {
Annotations,
createGithubCheck,
Expand All @@ -28,9 +28,9 @@ import * as path from 'path';
const CHECK_NAME = 'Lint';
const traverseTask = array.traverse(T.task);

const createSpectralAnnotations = (ruleset: string, parsed: FileWithContent[], basePath: string, useNimma: boolean) =>
const createSpectralAnnotations = (ruleset: string, parsed: FileWithContent[], basePath: string) =>
pipe(
createSpectral(ruleset, useNimma),
createSpectral(ruleset),
TE.chain(spectral => {
const spectralRuns = parsed.map(v =>
pipe(
Expand All @@ -41,7 +41,7 @@ const createSpectralAnnotations = (ruleset: string, parsed: FileWithContent[], b
if (results.length === 0) {
info(' No issue detected');
} else {
info(` /!\\ ${pluralizer(results.length, 'issue')} detected`);
info(` /!\\ ${pluralize('issue', results.length)} detected`);
}

return { path: v.path, results };
Expand Down Expand Up @@ -87,7 +87,7 @@ const readFilesToAnalyze = (pattern: string, workingDir: string) => {
return pipe(
TE.tryCatch(() => glob(path), E.toError),
TE.map(fileList => {
info(`Using glob '${pattern}' under '${workingDir}', found ${pluralizer(fileList.length, 'file')} to lint`);
info(`Using glob '${pattern}' under '${workingDir}', found ${pluralize('file', fileList.length)} to lint`);
return fileList;
}),
TE.chain(fileList =>
Expand Down Expand Up @@ -138,12 +138,7 @@ const program = pipe(
),
TE.bind('fileContents', ({ config }) => readFilesToAnalyze(config.INPUT_FILE_GLOB, config.GITHUB_WORKSPACE)),
TE.bind('annotations', ({ fileContents, config }) =>
createSpectralAnnotations(
config.INPUT_SPECTRAL_RULESET,
fileContents,
config.GITHUB_WORKSPACE,
config.INPUT_USE_NIMMA
)
createSpectralAnnotations(config.INPUT_SPECTRAL_RULESET, fileContents, config.GITHUB_WORKSPACE)
),
TE.bind('checkResponse', ({ octokit, check, repositoryInfo, annotations }) =>
updateGithubCheck(
Expand All @@ -164,7 +159,7 @@ const program = pipe(

const fatalErrors = annotations.filter(a => a.annotation_level === 'failure');
if (fatalErrors.length > 0) {
setFailed(`${pluralizer(fatalErrors.length, 'fatal issue')} detected. Failing the process.`);
setFailed(`${pluralize('fatal issue', fatalErrors.length)} detected. Failing the process.`);
}

return checkResponse;
Expand Down
83 changes: 13 additions & 70 deletions src/spectral.ts
Original file line number Diff line number Diff line change
@@ -1,90 +1,34 @@
import { getRuleset } from '@stoplight/spectral/dist/cli/services/linter/utils';
import { httpAndFileResolver } from '@stoplight/spectral/dist/resolvers/http-and-file';
import {
Spectral,
isJSONSchema,
isJSONSchemaDraft4,
isJSONSchemaDraft6,
isJSONSchemaDraft7,
isJSONSchemaDraft2019_09,
isJSONSchemaLoose,
isOpenApiv2,
isOpenApiv3,
} from '@stoplight/spectral';
import { Spectral, Document } from '@stoplight/spectral-core';
import * as Parsers from '@stoplight/spectral-parsers';
import { httpAndFileResolver } from '@stoplight/spectral-ref-resolver';
import pluralize from 'pluralize';

import * as IOEither from 'fp-ts/IOEither';
import * as TE from 'fp-ts/TaskEither';
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/pipeable';

import { info } from '@actions/core';
import { pluralizer } from './utils';

const evaluateNumberOfExceptions = (exceptions: Record<string, string[]>) => {
const reduced = Object.keys(exceptions).reduce(
(acc, cur) => {
acc.uniqueFilePaths.add(cur.split('#')[0]);
acc.numberOfExceptions += exceptions[cur].length;
return acc;
},
{ numberOfExceptions: 0, uniqueFilePaths: new Set() }
);

return {
numberOfExceptions: reduced.numberOfExceptions,
numberOfFiles: reduced.uniqueFilePaths.size,
};
};

const normalizeRulesetPath = (rulesetPath: string): [string] | undefined => {
if (rulesetPath.length === 0) {
info(`Loading built-in rulesets...`);
return undefined;
}

info(`Loading ruleset '${rulesetPath}'...`);
return [rulesetPath];
};
import { getRuleset } from './getRuleset';

const retrieveSpectralPackageVersion = (): IOEither.IOEither<Error, string> =>
IOEither.tryCatch<Error, string>(() => {
const x = require('../node_modules/@stoplight/spectral/package.json');
const x = require('../node_modules/@stoplight/spectral-core/package.json');
return String(x.version);
}, E.toError);

export const createSpectral = (rulesetPath: string, useNimma: boolean) =>
export const createSpectral = (rulesetPath: string) =>
pipe(
TE.fromIOEither(retrieveSpectralPackageVersion()),
TE.chain(spectralPackageVersion =>
TE.tryCatch(async () => {
info(`Running Spectral v${spectralPackageVersion}`);

const spectral = new Spectral({ resolver: httpAndFileResolver, useNimma: useNimma });
spectral.registerFormat('oas2', isOpenApiv2);
spectral.registerFormat('oas3', isOpenApiv3);
spectral.registerFormat('json-schema', isJSONSchema);
spectral.registerFormat('json-schema-loose', isJSONSchemaLoose);
spectral.registerFormat('json-schema-draft4', isJSONSchemaDraft4);
spectral.registerFormat('json-schema-draft6', isJSONSchemaDraft6);
spectral.registerFormat('json-schema-draft7', isJSONSchemaDraft7);
spectral.registerFormat('json-schema-2019-09', isJSONSchemaDraft2019_09);

const normRuleSetPath = normalizeRulesetPath(rulesetPath);
const ruleset = await getRuleset(normRuleSetPath, {});
info(`Loading ruleset '${rulesetPath}'...`);
info(`Loading built-in rulesets...`);
spectral.setRuleset(ruleset);
info(`Running @stoplight/spectral-core v${spectralPackageVersion}`);

const loadedRules = Object.values(spectral.rules);
info(` - ${pluralizer(loadedRules.length, 'rule')} (${loadedRules.filter(r => r.enabled).length} enabled)`);
const spectral = new Spectral({ resolver: httpAndFileResolver });
spectral.setRuleset(await getRuleset(rulesetPath));

const exceptionsStats = evaluateNumberOfExceptions(ruleset.exceptions);
info(
` - ${pluralizer(exceptionsStats.numberOfExceptions, 'exception')} (spanning ${pluralizer(
exceptionsStats.numberOfFiles,
'file'
)})`
);
const loadedRules = Object.values(spectral.ruleset!.rules);
info(` - ${pluralize('rule', loadedRules.length)} (${loadedRules.filter(r => r.enabled).length} enabled)`);

return spectral;
}, E.toError)
Expand All @@ -96,9 +40,8 @@ export type FileWithContent = { path: string; content: string };
export const runSpectral = (spectral: Spectral, fileDescription: FileWithContent) => {
return TE.tryCatch(
() =>
spectral.run(fileDescription.content, {
spectral.run(new Document(fileDescription.content, Parsers.Yaml, fileDescription.path), {
ignoreUnknownFormat: false,
resolve: { documentUri: fileDescription.path },
}),
E.toError
);
Expand Down
5 changes: 0 additions & 5 deletions src/utils.ts

This file was deleted.

Loading

0 comments on commit a1a5b82

Please sign in to comment.