Skip to content
This repository was archived by the owner on Feb 11, 2022. It is now read-only.

Commit 21e0465

Browse files
authored
Merge pull request #548 from greenkeeper-keeper/check-runs
Check runs
2 parents ff7d8d0 + 5ec4c29 commit 21e0465

20 files changed

+776
-190
lines changed

.npmignore

-17
This file was deleted.

package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@
2626
"coverage": "nyc run-s tests:unit:no-coverage",
2727
"coverage:report": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
2828
"test:unit": "run-s coverage",
29-
"test:integration": "run-s test:integration:base -- --tags 'not @wip'",
29+
"test:integration": "run-s 'test:integration:base -- --tags \"not @wip\"'",
3030
"test:integration:base": "cucumber-js test/integration --require-module babel-register --format-options '{\"snippetInterface\": \"async-await\"}'",
3131
"test:integration:debug": "DEBUG=test run-s test:integration",
32-
"test:integration:wip": "run-s test:integration:base -- --tags @wip",
33-
"test:integration:focus": "run-s test:integration:base -- --tags @focus",
32+
"test:integration:wip": "run-s 'test:integration:base -- --tags @wip'",
33+
"test:integration:focus": "run-s 'test:integration:base -- --tags @focus'",
3434
"test": "npm-run-all --print-label --parallel lint:* --parallel test:*",
3535
"commitmsg": "commitlint -e",
3636
"precommit": "npm test",
@@ -48,6 +48,7 @@
4848
"publishConfig": {
4949
"access": "public"
5050
},
51+
"files": ["lib/"],
5152
"devDependencies": {
5253
"@travi/any": "^1.8.2",
5354
"@travi/eslint-config-travi": "^1.6.18",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default class FailedCheckRunFoundError extends Error {
2+
constructor(message = 'A failed check_run was found for this PR.') {
3+
super(message);
4+
this.message = message;
5+
this.name = 'FailedCheckRunFoundError';
6+
}
7+
}

src/errors/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export {default as FailedStatusFoundError} from './failed-status-found-error';
2+
export {default as FailedCheckRunFoundError} from './failed-check-run-found-error';
23
export {default as InvalidStatusFoundError} from './invalid-status-found-error';
34
export {default as MergeFailureError} from './merge-failure-error';

src/github/actions.js

+59-26
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,71 @@
11
import octokitFactory from './octokit-factory-wrapper';
2-
import {FailedStatusFoundError, InvalidStatusFoundError, MergeFailureError} from '../errors';
2+
import {FailedCheckRunFoundError, FailedStatusFoundError, InvalidStatusFoundError, MergeFailureError} from '../errors';
3+
4+
function allStatusesAreSuccessful(statusesResponse, url, log) {
5+
const {state, statuses} = statusesResponse.data;
6+
7+
switch (state) {
8+
case 'pending': {
9+
if (!statuses.length) {
10+
log(['info', 'PR', 'no commit statuses'], 'no commit statuses listed, continuing...');
11+
return true;
12+
}
13+
14+
log(['info', 'PR', 'pending-status'], `commit status checks have not completed yet: ${url}`);
15+
throw new Error('pending');
16+
}
17+
case 'success': {
18+
log(['info', 'PR', 'passing-status'], 'statuses verified, continuing...');
19+
return true;
20+
}
21+
case 'failure': {
22+
log(['error', 'PR', 'failure status'], 'found failed status, rejecting...');
23+
throw new FailedStatusFoundError();
24+
}
25+
default:
26+
throw new InvalidStatusFoundError();
27+
}
28+
}
29+
30+
function allCheckRunsAreSuccessful(checkRunsResponse, url, log) {
31+
const {total_count: totalCount, check_runs: checkRuns} = checkRunsResponse.data;
32+
33+
if (!totalCount) {
34+
log(['info', 'PR', 'no check-runs'], 'no check_runs listed, continuing...');
35+
return true;
36+
}
37+
38+
checkRuns.forEach(({status, conclusion}) => {
39+
if ('completed' !== status) {
40+
log(['info', 'PR', 'pending-check_runs'], `check_runs have not completed yet: ${url}`);
41+
throw new Error('pending');
42+
}
43+
44+
if (['failure', 'cancelled', 'timed_out', 'action_required'].includes(conclusion)) {
45+
log(['error', 'PR', 'failure check_run'], 'found failed check_run, rejecting...');
46+
throw new FailedCheckRunFoundError();
47+
}
48+
});
49+
50+
return true;
51+
}
352

453
export default function (githubCredentials) {
554
const octokit = octokitFactory();
655
const {token} = githubCredentials;
756
octokit.authenticate({type: 'token', token});
857

9-
function ensureAcceptability({repo, sha, url}, log) {
58+
async function ensureAcceptability({repo, sha, url}, log) {
1059
log(['info', 'PR', 'validating'], url);
60+
const {name: repoName, owner: {login: repoOwner}} = repo;
1161

12-
return octokit.repos.getCombinedStatusForRef({owner: repo.owner.login, repo: repo.name, ref: sha})
13-
.then(response => response.data)
14-
.then(({state}) => {
15-
switch (state) {
16-
case 'pending': {
17-
log(['info', 'PR', 'pending-status'], `commit status checks have not completed yet: ${url}`);
18-
return Promise.reject(new Error('pending'));
19-
}
20-
case 'success':
21-
return Promise.resolve('All commit statuses passed')
22-
.then(message => {
23-
log(['info', 'PR', 'passing-status'], 'statuses verified, continuing...');
24-
return message;
25-
});
26-
case 'failure':
27-
return Promise.reject(new FailedStatusFoundError())
28-
.catch(err => {
29-
log(['error', 'PR', 'failure status'], 'found failed, rejecting...');
30-
return Promise.reject(err);
31-
});
32-
default:
33-
return Promise.reject(new InvalidStatusFoundError());
34-
}
35-
});
62+
const [statusesResponse, checkRunsResponse] = await Promise.all([
63+
octokit.repos.getCombinedStatusForRef({owner: repoOwner, repo: repoName, ref: sha}),
64+
octokit.checks.listForRef({owner: repoOwner, repo: repoName, ref: sha})
65+
]);
66+
67+
return allStatusesAreSuccessful(statusesResponse, url, log) &&
68+
allCheckRunsAreSuccessful(checkRunsResponse, url, log);
3669
}
3770

3871
function acceptPR(repo, sha, prNumber, acceptAction, log) {

src/handler.js

+92-25
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
1-
import {ACCEPTED, NO_CONTENT, BAD_REQUEST, UNSUPPORTED_MEDIA_TYPE} from 'http-status-codes';
1+
import {ACCEPTED, BAD_REQUEST, NO_CONTENT, UNSUPPORTED_MEDIA_TYPE} from 'http-status-codes';
22
import boom from 'boom';
33
import openedByGreenkeeperBot from './greenkeeper';
44
import createActions from './github/actions';
55
import process from './process';
66

7-
function successfulStatusCouldBeForGreenkeeperPR(event, state, branches, log) {
8-
if ('status' !== event) {
9-
log(['PR'], `event was \`${event}\` instead of \`status\``);
7+
function determineIfWebhookConfigIsCorrect(hook, responseToolkit) {
8+
if ('json' !== hook.config.content_type) {
9+
return responseToolkit
10+
.response('please update your webhook configuration to send application/json')
11+
.code(UNSUPPORTED_MEDIA_TYPE);
12+
}
13+
14+
return responseToolkit.response('successfully configured the webhook for greenkeeper-keeper').code(NO_CONTENT);
15+
}
16+
17+
function branchIsNotMaster(branchName, log) {
18+
if ('master' === branchName) {
19+
log(['PR'], `branch name \`${branchName}\` should not be \`master\``);
1020
return false;
1121
}
1222

23+
return true;
24+
}
25+
26+
function statusEventIsSuccessfulAndCouldBeForGreenkeeperPR(state, branches, log) {
1327
if ('success' !== state) {
1428
log(['PR'], `state was \`${state}\` instead of \`success\``);
1529
return false;
@@ -20,52 +34,105 @@ function successfulStatusCouldBeForGreenkeeperPR(event, state, branches, log) {
2034
return false;
2135
}
2236

23-
const branchName = branches[0].name;
24-
if ('master' === branchName) {
25-
log(['PR'], `branch name \`${branchName}\` should not be \`master\``);
37+
return branchIsNotMaster(branches[0].name, log);
38+
}
39+
40+
function checkRunEventIsSuccessfulAndCouldBeForGreenkeeperPR(checkRun, log) {
41+
const {status, conclusion, check_suite: checkSuite} = checkRun;
42+
const {head_branch: headBranch} = checkSuite;
43+
44+
if ('completed' !== status) {
45+
log(['PR'], `check_run status was \`${status}\` instead of \`completed\``);
2646
return false;
2747
}
2848

29-
return true;
49+
if ('success' !== conclusion) {
50+
log(['PR'], `check_run conclusion was \`${conclusion}\` instead of \`success\``);
51+
return false;
52+
}
53+
54+
return branchIsNotMaster(headBranch, log);
3055
}
3156

32-
export default async function (request, responseToolkit, settings) {
57+
async function processStatusEvent(payload, settings, request, responseToolkit, log) {
3358
const {state, repository, branches, sha} = request.payload;
34-
const event = request.headers['x-github-event'];
35-
36-
if ('ping' === event) {
37-
if ('json' !== request.payload.hook.config.content_type) {
38-
return responseToolkit
39-
.response('please update your webhook configuration to send application/json')
40-
.code(UNSUPPORTED_MEDIA_TYPE);
41-
}
4259

43-
return responseToolkit.response('successfully configured the webhook for greenkeeper-keeper').code(NO_CONTENT);
44-
}
45-
46-
if (successfulStatusCouldBeForGreenkeeperPR(event, state, branches, (...args) => request.log(...args))) {
60+
if (statusEventIsSuccessfulAndCouldBeForGreenkeeperPR(state, branches, log)) {
4761
const {getPullRequestsForCommit, getPullRequest} = createActions(settings.github);
4862

4963
return getPullRequestsForCommit({ref: sha})
5064
.then(async pullRequests => {
5165
if (!pullRequests.length) return responseToolkit.response('no PRs for this commit').code(BAD_REQUEST);
5266

5367
if (1 < pullRequests.length) {
54-
return responseToolkit.response(boom.internal('too many PRs exist for this commit'));
68+
throw boom.internal('too many PRs exist for this commit');
5569
}
5670

5771
const senderUrl = pullRequests[0].user.html_url;
5872
if (openedByGreenkeeperBot(senderUrl)) {
59-
process(request, await getPullRequest(repository, pullRequests[0].number), settings);
60-
return responseToolkit.response('ok').code(ACCEPTED);
73+
process(await getPullRequest(repository, pullRequests[0].number), settings, log);
74+
return responseToolkit.response('status event will be processed').code(ACCEPTED);
6175
}
6276

6377
return responseToolkit.response(`PR is not from greenkeeper, but from ${senderUrl}`).code(BAD_REQUEST);
6478
})
6579
.catch(e => boom.internal('failed to fetch PRs', e));
6680
}
6781

68-
request.log(['PR'], 'skipping');
82+
log(['PR'], 'skipping');
6983

7084
return responseToolkit.response('skipping').code(BAD_REQUEST);
7185
}
86+
87+
async function processCheckRunEvent(request, responseToolkit, settings, log) {
88+
const {repository, check_run: checkRun, sender} = request.payload;
89+
90+
if (checkRunEventIsSuccessfulAndCouldBeForGreenkeeperPR(checkRun, log)) {
91+
const {check_suite: {pull_requests: pullRequests}} = checkRun;
92+
const {getPullRequest} = createActions(settings.github);
93+
94+
if (!pullRequests.length) return responseToolkit.response('no PRs for this commit').code(BAD_REQUEST);
95+
if (1 < pullRequests.length) return responseToolkit.response(boom.internal('too many PRs exist for this commit'));
96+
97+
const senderUrl = sender.html_url;
98+
if (!openedByGreenkeeperBot(senderUrl)) {
99+
return responseToolkit.response(`PR is not from greenkeeper, but from ${senderUrl}`).code(BAD_REQUEST);
100+
}
101+
102+
let pullRequest;
103+
try {
104+
pullRequest = await getPullRequest(repository, pullRequests[0].number);
105+
} catch (err) {
106+
throw boom.internal('failed to fetch PRs', err);
107+
}
108+
109+
process(pullRequest, settings, log);
110+
return responseToolkit.response('check_run event will be processed').code(ACCEPTED);
111+
}
112+
113+
log(['PR'], 'skipping');
114+
115+
return responseToolkit.response('skipping').code(BAD_REQUEST);
116+
}
117+
118+
export default async function (request, responseToolkit, settings) {
119+
const {hook} = request.payload;
120+
const event = request.headers['x-github-event'];
121+
122+
function logger(...args) {
123+
request.log(...args);
124+
}
125+
126+
switch (event) {
127+
case 'ping':
128+
return determineIfWebhookConfigIsCorrect(hook, responseToolkit);
129+
case 'status':
130+
return processStatusEvent(request.payload, settings, request, responseToolkit, logger);
131+
case 'check_run':
132+
return processCheckRunEvent(request, responseToolkit, settings, logger);
133+
default:
134+
return responseToolkit
135+
.response(`event was \`${event}\` instead of \`status\` or \`check_run\``)
136+
.code(BAD_REQUEST);
137+
}
138+
}

src/process.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import createActions from './github/actions';
22

3-
export default function (request, {head, url, number}, {github, acceptAction}) {
3+
export default function ({head, url, number}, {github, acceptAction}, log) {
44
const {ensureAcceptability, acceptPR, postErrorComment} = createActions(github);
55

6-
return ensureAcceptability({repo: head.repo, sha: head.sha, url}, (...args) => request.log(...args))
7-
.then(() => acceptPR(head.repo, head.sha, number, acceptAction, (...args) => request.log(...args)))
6+
return ensureAcceptability({repo: head.repo, sha: head.sha, url}, log)
7+
.then(() => acceptPR(head.repo, head.sha, number, acceptAction, log))
88
.catch(err => {
99
if ('pending' !== err.message) {
10-
request.log(['error', 'PR'], err);
10+
log(['error', 'PR'], err);
1111

1212
return postErrorComment(head.repo, number, err)
13-
.catch(e => request.log(`failed to log comment against the PR: ${e}`));
13+
.catch(e => log(`failed to log comment against the PR: ${e}`));
1414
}
1515

1616
return Promise.resolve();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Feature: Check run event webhook
2+
3+
Scenario: Success check_run event for head commit of greenkeeper PR for project with check_runs and statuses
4+
Given the server is configured
5+
And the webhook is for a check_run event, a completed status, and a success conclusion
6+
And the commit is only on one, non-master branch
7+
And the PR was submitted by the greenkeeper integration
8+
And an open PR exists for the commit
9+
And the check_run results resolve to success
10+
And the commit statuses resolve to success
11+
And the PR can be accepted
12+
When the webhook is received
13+
Then the webhook response confirms that it will be processed
14+
And the PR is merged
15+
16+
Scenario: Success check_run event for head commit of greenkeeper PR for project with check_runs but no statuses
17+
Given the server is configured
18+
And the webhook is for a check_run event, a completed status, and a success conclusion
19+
And the commit is only on one, non-master branch
20+
And the PR was submitted by the greenkeeper integration
21+
And an open PR exists for the commit
22+
And the check_run results resolve to success
23+
But there are no statuses
24+
And the PR can be accepted
25+
When the webhook is received
26+
Then the webhook response confirms that it will be processed
27+
And the PR is merged
28+
29+
Scenario: Success check_run event for head commit of greenkeeper PR for project with statuses but no check_runs
30+
Given the server is configured
31+
And the webhook is for a check_run event, a completed status, and a success conclusion
32+
And the commit is only on one, non-master branch
33+
And the PR was submitted by the greenkeeper integration
34+
And an open PR exists for the commit
35+
And the commit statuses resolve to success
36+
But there are no check_runs
37+
And the PR can be accepted
38+
When the webhook is received
39+
Then the webhook response confirms that it will be processed
40+
And the PR is merged

0 commit comments

Comments
 (0)