Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom rulesets #129

Merged
merged 14 commits into from
Apr 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this file in the repository?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@XVincentX This will pop up a dialog driving the user to install the editorconfig extension to try and keep the style clean by automatically applying fixups on save (trimming trailing whitespaces, ...). Of course, that will only work with vscode.

cf. https://code.visualstudio.com/docs/editor/extension-gallery#_workspace-recommended-extensions

If you don't think that adds any value, I'll drop it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I am aware of the feature. I have always been against IDE specific files in the repository, but I do not mind if you want to keep it here.

I would personally remove it.

// 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 ./
XVincentX marked this conversation as resolved.
Show resolved Hide resolved
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/spectral-action@v0.5.5
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.
philsturgeon marked this conversation as resolved.
Show resolved Hide resolved
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}'`);

Comment on lines +36 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really worth to add logging here? It's a side effect and it should be encapsulated in an IO .

It would better to use a Writer or simply hold off on logging here

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`);
philsturgeon marked this conversation as resolved.
Show resolved Hide resolved
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