Skip to content

Commit

Permalink
Merge pull request #129 from stoplightio/custom-rulesets
Browse files Browse the repository at this point in the history
feat: custom rulesets
  • Loading branch information
nulltoken authored Apr 28, 2020
2 parents 8fe0708 + 13042a4 commit 5c59284
Show file tree
Hide file tree
Showing 16 changed files with 325 additions and 100 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/spectral.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ jobs:
name: Spectral checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v2
- name: Spectral checks
uses: ./
with:
file_glob: ./**/*.yml
repo_token: ${{ secrets.GITHUB_TOKEN }}
spectral_ruleset: 'spectral:oas'
spectral_ruleset: .my-custom.spectral.yml
15 changes: 15 additions & 0 deletions .my-custom.spectral.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
extends: spectral:oas

except:
'fixtures/test.oas.yml#/paths/~1todos/get':
- operation-description

'fixtures/non_existent.yml#':
- oas3-api-servers
- openapi-tags

'fixtures/non_existent.yml#/info':
- info-contact

'fixtures/non_existent.yml#/paths/~1todos/get':
- operation-description
5 changes: 5 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": ["editorconfig.editorconfig"]
}
25 changes: 18 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
FROM node:13 as builder
FROM node:12 as builder

COPY package* ./
COPY package.json yarn.lock ./
RUN yarn

COPY src ./src
COPY tsconfig.json tsconfig.json

RUN ./node_modules/.bin/tsc || true
RUN yarn
RUN yarn build || true

###############################################################

FROM node:13 as installer
FROM node:12 as dependencies

ENV NODE_ENV production
COPY package.json package.json
COPY package.json yarn.lock ./
RUN yarn --production

FROM node:13-alpine as runtime
RUN curl -sfL https://install.goreleaser.com/github.com/tj/node-prune.sh | bash
RUN ./bin/node-prune

###############################################################

FROM node:12-alpine as runtime

ENV NODE_ENV production

COPY package.json /action/package.json

COPY --from=builder dist /action/dist
COPY --from=installer node_modules /action/node_modules
COPY --from=dependencies node_modules /action/node_modules

ENTRYPOINT ["node", "/action/dist/index.js"]
39 changes: 35 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
# Spectral Lint Action
# Spectral Linter Action

![](https://raw.githubusercontent.com/stoplightio/spectral/master/img/spectral-banner.png)

This action uses [Spectral 4.x](https://github.com/stoplightio/spectral) to lint your OpenAPI files.
This action uses [Spectral](https://github.com/stoplightio/spectral) from [Stoplight](https://stoplight.io/) to lint your OpenAPI documents, or any other JSON/YAML files.

![](./image.png)

## Usage

See [action.yml](action.yml)

```yaml
name: Run Spectral on Pull Requests

on:
- pull_request

jobs:
build:
name: Run Spectral
runs-on: ubuntu-latest
steps:
# Check out the repository
- uses: actions/checkout@v2

# Run Spectral
- uses: stoplightio/[email protected]
with:
file_glob: 'doc/api/*.yaml'
```
### Inputs
- **file_glob:** Pattern describing the set of files to lint. Defaults to `*.oas.{json,yml,yaml}`. (_Note:_ Pattern syntax is documented in the [fast-glob](https://www.npmjs.com/package/fast-glob) package documentation)
- **spectral_ruleset:** 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.

## Configuration

Spectral Action will respect your [Spectral Rulesets](https://stoplight.io/p/docs/gh/stoplightio/spectral/docs/getting-started/rulesets.md), which can be defined, extended, and overriden by placing `.spectral.yml` in the root of your repository.
22 changes: 17 additions & 5 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,27 @@ description: Lint your files with Spectral
inputs:
file_glob:
required: true
description: The file path to lint with Spectral
description: The pattern describing the file paths to lint with Spectral
default: '*.oas.{json,yml,yaml}'
spectral_ruleset:
required: true
description: Ruleset file to load in Spectral
default: 'spectral:oas'
required: false
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.
repo_token:
required: true
description: Ruleset file to load in Spectral
description: |
The GitHub App installation access token.
[Learn more about `GITHUB_TOKEN`](https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#about-the-github_token-secret)
default: ${{ github.token }}
event_name:
required: true
description: |
The name of the event that triggered the workflow
default: ${{ github.event_name }}
runs:
using: docker
image: Dockerfile
Expand Down
File renamed without changes.
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"main": "src/index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node dist/index.js"
"start": "node dist/index.js",
"build": "tsc -p tsconfig.json"
},
"repository": {
"type": "git",
Expand All @@ -18,9 +19,6 @@
},
"homepage": "https://github.com/XVincentX/spectral-action#readme",
"devDependencies": {
"@octokit/graphql": "^4.3.1",
"@octokit/request": "^5.3.2",
"@octokit/rest": "^16.35.0",
"@types/lodash": "^4.14.149",
"@types/node": "^13.7.7",
"@types/urijs": "^1.19.6",
Expand All @@ -32,8 +30,11 @@
"dependencies": {
"@actions/core": "^1.2.3",
"@actions/github": "^2.1.1",
"@octokit/graphql": "^4.3.1",
"@octokit/request": "^5.3.2",
"@octokit/rest": "^16.35.0",
"@stoplight/json": "^3.5.1",
"@stoplight/spectral": "^5.1.0",
"@stoplight/spectral": "^5.3.0",
"fast-glob": "^3.2.2",
"fp-ts": "^2.5.3",
"io-ts": "^2.1.2",
Expand Down
10 changes: 10 additions & 0 deletions src/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
root = true

[*]
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.{ts,yaml,yml,json,md}]
indent_style = space
indent_size = 2
3 changes: 1 addition & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import * as t from 'io-ts';
export const Config = t.strict({
GITHUB_EVENT_PATH: t.string,
INPUT_REPO_TOKEN: t.string,
GITHUB_SHA: t.string,
GITHUB_WORKSPACE: t.string,
INPUT_FILE_GLOB: t.string,
GITHUB_ACTION: t.string,
INPUT_EVENT_NAME: t.string,
INPUT_SPECTRAL_RULESET: t.string,
});

Expand Down
81 changes: 58 additions & 23 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { promises as fs } from 'fs';
import { array } from 'fp-ts/lib/Array';
import { flatten } from 'lodash';
import { Config } from './config';
import { runSpectral, createSpectral } from './spectral';
import { runSpectral, createSpectral, fileWithContent } from './spectral';
import { pluralizer } from './utils';
import { createGithubCheck, createOctokitInstance, getRepositoryInfoFromEvent, updateGithubCheck } from './octokit';
import glob from 'fast-glob';
import { error, info, setFailed } from '@actions/core';
Expand All @@ -23,24 +24,33 @@ import * as path from 'path';

const CHECK_NAME = 'Lint';
const traverseTask = array.traverse(T.task);
type fileWithContent = { file: string; content: string };

const createSpectralAnnotations = (ruleset: string, parsed: fileWithContent[], basePath: string) =>
pipe(
createSpectral(ruleset),
TE.chain(spectral => {
const spectralRuns = parsed.map(v =>
pipe(
runSpectral(spectral, v.content),
TE.map(rules => ({ path: v.file, rules }))
runSpectral(spectral, v),
TE.map(results => {
info(`Done linting '${v.path}'`);

if (results.length === 0) {
info(' No issue detected');
} else {
info(` /!\\ ${pluralizer(results.length, 'issue')} detected`);
}

return { path: v.path, results };
})
)
);
return array.sequence(TE.taskEither)(spectralRuns);
}),
TE.map(results =>
flatten(
results.map(validationResult => {
return validationResult.rules.map<ChecksUpdateParamsOutputAnnotations>(vl => {
return validationResult.results.map<ChecksUpdateParamsOutputAnnotations>(vl => {
const annotation_level: ChecksUpdateParamsOutputAnnotations['annotation_level'] =
vl.severity === DiagnosticSeverity.Error
? 'failure'
Expand All @@ -66,21 +76,23 @@ const createSpectralAnnotations = (ruleset: string, parsed: fileWithContent[], b
)
);

const readFilesToAnalyze = (path: string) => {
const readFilesToAnalyze = (pattern: string, workingDir: string) => {
const path = join(workingDir, pattern);

const readFile = (file: string) => TE.tryCatch(() => fs.readFile(file, { encoding: 'utf8' }), E.toError);

return pipe(
TE.tryCatch(() => glob(path), E.toError),
TE.map(fileList => {
info(`Files to check: ${fileList.join(',')}`);
info(`Using glob '${pattern}' under '${workingDir}', found ${pluralizer(fileList.length, 'file')} to lint`);
return fileList;
}),
TE.chain(fileList =>
pipe(
traverseTask(fileList, file =>
traverseTask(fileList, path =>
pipe(
readFile(file),
TE.map(content => ({ file, content }))
readFile(path),
TE.map<string, fileWithContent>(content => ({ path, content }))
)
),
T.map(e => {
Expand Down Expand Up @@ -109,9 +121,16 @@ const createConfigFromEnv = pipe(
const program = pipe(
TE.fromIOEither(createConfigFromEnv),
TE.chain(
({ GITHUB_EVENT_PATH, INPUT_REPO_TOKEN, GITHUB_SHA, GITHUB_WORKSPACE, INPUT_FILE_GLOB, INPUT_SPECTRAL_RULESET }) =>
({
INPUT_EVENT_NAME,
GITHUB_EVENT_PATH,
INPUT_REPO_TOKEN,
GITHUB_WORKSPACE,
INPUT_FILE_GLOB,
INPUT_SPECTRAL_RULESET,
}) =>
pipe(
getRepositoryInfoFromEvent(GITHUB_EVENT_PATH),
getRepositoryInfoFromEvent(GITHUB_EVENT_PATH, INPUT_EVENT_NAME),
TE.chain(event =>
pipe(
createOctokitInstance(INPUT_REPO_TOKEN),
Expand All @@ -120,27 +139,43 @@ const program = pipe(
),
TE.chain(({ octokit, event }) =>
pipe(
createGithubCheck(octokit, event, CHECK_NAME, GITHUB_SHA),
createGithubCheck(octokit, event, `${CHECK_NAME} (${event.eventName})`),
TE.map(check => ({ octokit, event, check }))
)
),
TE.chain(({ octokit, event, check }) =>
pipe(
readFilesToAnalyze(join(GITHUB_WORKSPACE, INPUT_FILE_GLOB)),
readFilesToAnalyze(INPUT_FILE_GLOB, GITHUB_WORKSPACE),
TE.chain(fileContents => createSpectralAnnotations(INPUT_SPECTRAL_RULESET, fileContents, GITHUB_WORKSPACE)),
TE.chain(annotations =>
updateGithubCheck(
octokit,
CHECK_NAME,
check,
event,
annotations,
annotations.findIndex(f => f.annotation_level === 'failure') === -1 ? 'success' : 'failure'
pipe(
updateGithubCheck(
octokit,
check,
event,
annotations,
annotations.findIndex(f => f.annotation_level === 'failure') === -1 ? 'success' : 'failure'
),
TE.map(checkResponse => {
info(
`Check run '${checkResponse.data.name}' concluded with '${checkResponse.data.conclusion}' (${checkResponse.data.html_url})`
);
info(
`Commit ${event.sha} has been annotated (https://github.com/${event.owner}/${event.repo}/commit/${event.sha})`
);

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

return checkResponse;
})
)
),
TE.orElse(e => {
setFailed(e.message);
return updateGithubCheck(octokit, CHECK_NAME, check, event, [], 'failure', e.message);
return updateGithubCheck(octokit, check, event, [], 'failure', e.message);
})
)
)
Expand All @@ -153,7 +188,7 @@ program().then(result =>
result,
E.fold(
e => error(e.message),
() => info('Worked fine')
() => info('Analysis is complete')
)
)
);
Loading

0 comments on commit 5c59284

Please sign in to comment.