-
Notifications
You must be signed in to change notification settings - Fork 46
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
Properly extract commit sha from PullRequests #186
Changes from all commits
9898643
995ee8d
cb92023
bcc3ea9
7c10d15
0575095
a9669f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,11 +6,11 @@ import { promises as fs } from 'fs'; | |
import { array } from 'fp-ts/lib/Array'; | ||
import { flatten } from 'lodash'; | ||
import { Config } from './config'; | ||
import { runSpectral, createSpectral, fileWithContent } from './spectral'; | ||
import { runSpectral, createSpectral, fileWithContent, logTextualReport } 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'; | ||
import { info, setFailed } from '@actions/core'; | ||
import * as IOEither from 'fp-ts/lib/IOEither'; | ||
import * as IO from 'fp-ts/lib/IO'; | ||
import * as TE from 'fp-ts/lib/TaskEither'; | ||
|
@@ -29,51 +29,35 @@ const createSpectralAnnotations = (ruleset: string, parsed: fileWithContent[], b | |
pipe( | ||
createSpectral(ruleset), | ||
TE.chain(spectral => { | ||
const spectralRuns = parsed.map(v => | ||
pipe( | ||
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 }; | ||
}) | ||
) | ||
); | ||
const spectralRuns = parsed.map(v => pipe(runSpectral(spectral, v))); | ||
return array.sequence(TE.taskEither)(spectralRuns); | ||
}), | ||
TE.map(results => | ||
flatten( | ||
results.map(validationResult => { | ||
return validationResult.results.map<ChecksUpdateParamsOutputAnnotations>(vl => { | ||
const annotation_level: ChecksUpdateParamsOutputAnnotations['annotation_level'] = | ||
vl.severity === DiagnosticSeverity.Error | ||
? 'failure' | ||
: vl.severity === DiagnosticSeverity.Warning | ||
? 'warning' | ||
: 'notice'; | ||
|
||
const sameLine = vl.range.start.line === vl.range.end.line; | ||
|
||
return { | ||
annotation_level, | ||
message: vl.message, | ||
title: vl.code as string, | ||
start_line: 1 + vl.range.start.line, | ||
end_line: 1 + vl.range.end.line, | ||
start_column: sameLine ? vl.range.start.character : undefined, | ||
end_column: sameLine ? vl.range.end.character : undefined, | ||
path: path.relative(basePath, validationResult.path), | ||
}; | ||
}); | ||
TE.map(results => { | ||
const flattened = flatten(results); | ||
logTextualReport(flattened); | ||
|
||
return flattened | ||
.map<ChecksUpdateParamsOutputAnnotations>(vl => { | ||
const annotation_level: ChecksUpdateParamsOutputAnnotations['annotation_level'] = | ||
vl.severity === DiagnosticSeverity.Error | ||
? 'failure' | ||
: vl.severity === DiagnosticSeverity.Warning | ||
? 'warning' | ||
: 'notice'; | ||
const sameLine = vl.range.start.line === vl.range.end.line; | ||
return { | ||
annotation_level, | ||
message: vl.message, | ||
title: vl.code as string, | ||
start_line: 1 + vl.range.start.line, | ||
end_line: 1 + vl.range.end.line, | ||
start_column: sameLine ? vl.range.start.character : undefined, | ||
end_column: sameLine ? vl.range.end.character : undefined, | ||
path: path.relative(basePath, vl.source!), | ||
}; | ||
}) | ||
).sort((a, b) => (a.start_line > b.start_line ? 1 : -1)) | ||
) | ||
.sort((a, b) => (a.start_line > b.start_line ? 1 : -1)); | ||
}) | ||
); | ||
|
||
const readFilesToAnalyze = (pattern: string, workingDir: string) => { | ||
|
@@ -130,51 +114,58 @@ const program = pipe( | |
INPUT_SPECTRAL_RULESET, | ||
}) => | ||
pipe( | ||
getRepositoryInfoFromEvent(GITHUB_EVENT_PATH, INPUT_EVENT_NAME), | ||
TE.chain(event => | ||
pipe( | ||
getRepositoryInfoFromEvent(GITHUB_EVENT_PATH, INPUT_EVENT_NAME), | ||
TE.map(event => ({ event })) | ||
), | ||
TE.chain(({ event }) => | ||
pipe( | ||
readFilesToAnalyze(INPUT_FILE_GLOB, GITHUB_WORKSPACE), | ||
TE.chain(fileContents => createSpectralAnnotations(INPUT_SPECTRAL_RULESET, fileContents, GITHUB_WORKSPACE)), | ||
TE.map(annotations => ({ event, annotations })) | ||
) | ||
), | ||
TE.chain(({ event, annotations }) => | ||
Comment on lines
+117
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm I'd say this is going in the wrong direction. I'll probably have to check this out. |
||
pipe( | ||
createOctokitInstance(INPUT_REPO_TOKEN), | ||
TE.map(octokit => ({ octokit, event })) | ||
TE.map(octokit => ({ octokit, event, annotations })) | ||
) | ||
), | ||
TE.chain(({ octokit, event }) => | ||
TE.chain(({ octokit, event, annotations }) => | ||
pipe( | ||
createGithubCheck(octokit, event, `${CHECK_NAME} (${event.eventName})`), | ||
TE.map(check => ({ octokit, event, check })) | ||
TE.map(check => ({ octokit, event, check, annotations })) | ||
) | ||
), | ||
TE.chain(({ octokit, event, check }) => | ||
TE.chain(({ octokit, event, check, annotations }) => | ||
pipe( | ||
readFilesToAnalyze(INPUT_FILE_GLOB, GITHUB_WORKSPACE), | ||
TE.chain(fileContents => createSpectralAnnotations(INPUT_SPECTRAL_RULESET, fileContents, GITHUB_WORKSPACE)), | ||
TE.chain(annotations => | ||
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; | ||
}) | ||
) | ||
updateGithubCheck( | ||
octokit, | ||
check, | ||
event, | ||
annotations, | ||
annotations.findIndex(f => f.annotation_level === 'failure') === -1 ? 'success' : 'failure' | ||
), | ||
TE.map(checkResponse => { | ||
if (checkResponse !== undefined) { | ||
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); | ||
setFailed(`${e.message}\n${e.stack}`); | ||
return updateGithubCheck(octokit, check, event, [], 'failure', e.message); | ||
}) | ||
) | ||
|
@@ -187,7 +178,7 @@ program().then(result => | |
pipe( | ||
result, | ||
E.fold( | ||
e => error(e.message), | ||
e => setFailed(`${e.message}\n${e.stack}`), | ||
() => info('Analysis is complete') | ||
) | ||
) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,15 @@ | ||
import { GitHub } from '@actions/github'; | ||
import { info, warning } from '@actions/core'; | ||
import * as TE from 'fp-ts/lib/TaskEither'; | ||
import * as E from 'fp-ts/lib/Either'; | ||
import { pipe } from 'fp-ts/lib/pipeable'; | ||
import { ChecksCreateResponse, ChecksUpdateParamsOutputAnnotations, ChecksUpdateParams, Response } from '@octokit/rest'; | ||
import { | ||
ChecksCreateResponse, | ||
ChecksUpdateParamsOutputAnnotations, | ||
ChecksUpdateParams, | ||
Response, | ||
ChecksUpdateResponse, | ||
} from '@octokit/rest'; | ||
|
||
type Event = { | ||
after: string; | ||
|
@@ -16,18 +23,33 @@ type Event = { | |
|
||
export const createOctokitInstance = (token: string) => TE.fromEither(E.tryCatch(() => new GitHub(token), E.toError)); | ||
|
||
const tryCreateGithubCheck = async ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @XVincentX Could you please help me rewrite this in a more fp-ts way? I haven't found the proper way to do this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You really can't, since the underlying API has side-effects. You can wrap it (as you're doing) with |
||
octokit: GitHub, | ||
event: IRepositoryInfo, | ||
name: string | ||
): Promise<Response<ChecksCreateResponse> | undefined> => { | ||
try { | ||
return await octokit.checks.create({ | ||
owner: event.owner, | ||
repo: event.repo, | ||
name, | ||
head_sha: event.sha, | ||
status: 'in_progress', | ||
}); | ||
} catch (e) { | ||
if (e.message === 'Resource not accessible by integration') { | ||
// not enough permissions to create annotations | ||
|
||
warning('Skipping creation of checkrun because insufficient rights.'); | ||
return undefined; | ||
} | ||
|
||
throw e; | ||
} | ||
}; | ||
|
||
export const createGithubCheck = (octokit: GitHub, event: IRepositoryInfo, name: string) => | ||
TE.tryCatch( | ||
() => | ||
octokit.checks.create({ | ||
owner: event.owner, | ||
repo: event.repo, | ||
name, | ||
head_sha: event.sha, | ||
status: 'in_progress', | ||
}), | ||
E.toError | ||
); | ||
TE.tryCatch(() => tryCreateGithubCheck(octokit, event, name), E.toError); | ||
|
||
export interface IRepositoryInfo { | ||
owner: string; | ||
|
@@ -36,54 +58,81 @@ export interface IRepositoryInfo { | |
sha: string; | ||
} | ||
|
||
const extractSha = (eventName: string, event: any): string => { | ||
switch (eventName) { | ||
case 'pull_request': | ||
return event.pull_request.head.sha; | ||
case 'push': | ||
return event.after; | ||
default: | ||
warning(event); | ||
throw new Error(`Unsupported event '${eventName}'`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, the idea of fp-ts is not to throw exception, so this is not ok. This should return an Either and a Left instance when something goes wrong. |
||
} | ||
}; | ||
|
||
export const getRepositoryInfoFromEvent = ( | ||
eventPath: string, | ||
eventName: string | ||
): TE.TaskEither<Error, IRepositoryInfo> => | ||
pipe( | ||
TE.fromEither(E.tryCatch<Error, Event>(() => require(eventPath), E.toError)), | ||
TE.map(event => { | ||
const { repository, after } = event; | ||
info(`Responding to event '${eventName}'`); | ||
const sha = extractSha(eventName, event); | ||
info(`Processing commit '${sha}'`); | ||
|
||
const { repository } = event; | ||
const { | ||
owner: { login: owner }, | ||
} = repository; | ||
const { name: repo } = repository; | ||
return { owner, repo, eventName, sha: after }; | ||
|
||
return { owner, repo, eventName, sha }; | ||
}) | ||
); | ||
|
||
export const updateGithubCheck = ( | ||
export const tryUpdateGithubCheck = async ( | ||
octokit: GitHub, | ||
check: Response<ChecksCreateResponse>, | ||
check: Response<ChecksCreateResponse> | undefined, | ||
event: IRepositoryInfo, | ||
annotations: ChecksUpdateParamsOutputAnnotations[], | ||
conclusion: ChecksUpdateParams['conclusion'], | ||
message?: string | ||
) => | ||
TE.tryCatch( | ||
() => | ||
octokit.checks.update({ | ||
check_run_id: check.data.id, | ||
owner: event.owner, | ||
name: check.data.name, | ||
repo: event.repo, | ||
status: 'completed', | ||
conclusion, | ||
completed_at: new Date().toISOString(), | ||
output: { | ||
title: check.data.name, | ||
summary: message | ||
? message | ||
: conclusion === 'success' | ||
? 'Lint completed successfully' | ||
: 'Lint completed with some errors', | ||
): Promise<Response<ChecksUpdateResponse> | undefined> => { | ||
if (check === undefined) { | ||
return undefined; | ||
} | ||
|
||
// TODO: Split calls when annotations.length > 50 | ||
// From https://octokit.github.io/rest.js/v17#checks-update | ||
// => "The Checks API limits the number of annotations to a maximum of 50 per API request. | ||
// To create more than 50 annotations, you have to make multiple requests to the Update a check run endpoint." | ||
annotations, | ||
}, | ||
}), | ||
E.toError | ||
); | ||
return octokit.checks.update({ | ||
check_run_id: check.data.id, | ||
owner: event.owner, | ||
name: check.data.name, | ||
repo: event.repo, | ||
status: 'completed', | ||
conclusion, | ||
completed_at: new Date().toISOString(), | ||
output: { | ||
title: check.data.name, | ||
summary: message | ||
? message | ||
: conclusion === 'success' | ||
? 'Lint completed successfully' | ||
: 'Lint completed with some errors', | ||
|
||
// TODO: Split calls when annotations.length > 50 | ||
// From https://octokit.github.io/rest.js/v17#checks-update | ||
// => "The Checks API limits the number of annotations to a maximum of 50 per API request. | ||
// To create more than 50 annotations, you have to make multiple requests to the Update a check run endpoint." | ||
annotations, | ||
}, | ||
}); | ||
}; | ||
|
||
export const updateGithubCheck = ( | ||
octokit: GitHub, | ||
check: Response<ChecksCreateResponse> | undefined, | ||
event: IRepositoryInfo, | ||
annotations: ChecksUpdateParamsOutputAnnotations[], | ||
conclusion: ChecksUpdateParams['conclusion'], | ||
message?: string | ||
) => TE.tryCatch(() => tryUpdateGithubCheck(octokit, check, event, annotations, conclusion, message), E.toError); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're running only a single operation, you do not need to pipe