Skip to content

Commit 81cfc18

Browse files
committed
git-node: add GitHub status as a CI option
Some project in the org don't use Jenkins, which means PRChecker will never succeed for pull requests on those projects. These projects usually have Travis, AppVeyor or other CI systems in place, and those systems will publish the status to GitHub, which can be retrieved via API. This commit adds GitHub status as an optional way to validate if a PR satisfies the CI requirement. We need to check for the CI status in two fields returned by our GraphQL query: commit.status for services using the old GitHub integration, and commits.checkSuites for services using the new GitHub integration via GitHub Apps.
1 parent e60bc6a commit 81cfc18

19 files changed

+610
-3
lines changed

lib/pr_checker.js

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,20 @@ class PRChecker {
184184
return cis.find(ci => isFullCI(ci) || isLiteCI(ci));
185185
}
186186

187+
checkCI() {
188+
const ciType = this.argv.ciType || 'jenkins';
189+
if (ciType === 'jenkins') {
190+
return this.checkJenkinsCI();
191+
} else if (ciType === 'github-check') {
192+
return this.checkGitHubCI();
193+
}
194+
this.cli.error(`Invalid ciType: ${ciType}`);
195+
return false;
196+
}
197+
187198
// TODO: we might want to check CI status when it's less flaky...
188199
// TODO: not all PR requires CI...labels?
189-
checkCI() {
200+
checkJenkinsCI() {
190201
const { cli, commits, argv } = this;
191202
const { maxCommits } = argv;
192203
const thread = this.data.getThread();
@@ -248,6 +259,57 @@ class PRChecker {
248259
return status;
249260
}
250261

262+
checkGitHubCI() {
263+
const { cli, commits } = this;
264+
265+
if (!commits) {
266+
cli.error('No commits detected');
267+
return false;
268+
}
269+
270+
// NOTE(mmarchini): we only care about the last commit. Maybe in the future
271+
// we'll want to check all commits for a successful CI.
272+
const { commit } = commits[commits.length - 1];
273+
274+
this.CIStatus = false;
275+
const checkSuites = commit.checkSuites || { nodes: [] };
276+
if (!commit.status && checkSuites.nodes.length === 0) {
277+
cli.error('No CI runs detected');
278+
return false;
279+
}
280+
281+
// GitHub new Check API
282+
for (let { status, conclusion } of checkSuites.nodes) {
283+
if (status !== 'COMPLETED') {
284+
cli.error('CI is still running');
285+
return false;
286+
}
287+
288+
if (!['SUCCESS', 'NEUTRAL'].includes(conclusion)) {
289+
cli.error('Last CI failed');
290+
return false;
291+
}
292+
}
293+
294+
// GitHub old commit status API
295+
if (commit.status) {
296+
const { state } = commit.status;
297+
if (state === 'PENDING') {
298+
cli.error('CI is still running');
299+
return false;
300+
}
301+
302+
if (!['SUCCESS', 'EXPECTED'].includes(state)) {
303+
cli.error('Last CI failed');
304+
return false;
305+
}
306+
}
307+
308+
cli.info('Last CI run was successful');
309+
this.CIStatus = true;
310+
return true;
311+
}
312+
251313
checkAuthor() {
252314
const { cli, commits, pr } = this;
253315

lib/queries/PRCommits.gql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ query Commits($prid: Int!, $owner: String!, $repo: String!, $after: String) {
2525
message
2626
messageHeadline
2727
authoredByCommitter
28+
checkSuites(first: 10) {
29+
nodes {
30+
conclusion,
31+
status
32+
}
33+
}
34+
status {
35+
state
36+
}
2837
}
2938
}
3039
}

lib/request.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ class Request {
6868
method: 'POST',
6969
headers: {
7070
'Authorization': `Basic ${githubCredentials}`,
71-
'User-Agent': 'node-core-utils'
71+
'User-Agent': 'node-core-utils',
72+
'Accept': 'application/vnd.github.antiope-preview+json'
7273
},
7374
body: JSON.stringify({
7475
query: query,

lib/session.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class Session {
5050
upstream: this.upstream,
5151
branch: this.branch,
5252
readme: this.readme,
53+
ciType: this.ciType,
5354
prid: this.prid
5455
};
5556
}
@@ -78,6 +79,10 @@ class Session {
7879
return this.config.readme;
7980
}
8081

82+
get ciType() {
83+
return this.config.ciType || 'jenkins';
84+
}
85+
8186
get pullName() {
8287
return `${this.owner}/${this.repo}/pulls/${this.prid}`;
8388
}

test/fixtures/data.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
'use strict';
22

3-
const { readJSON, patchPrototype, readFile } = require('./index');
3+
const { basename } = require('path');
4+
const { readdirSync } = require('fs');
5+
6+
const { readJSON, patchPrototype, readFile, path } = require('./index');
47
const { Collaborator } = require('../../lib/collaborators');
58
const { Review } = require('../../lib/reviews');
69

@@ -82,6 +85,15 @@ const readmeNoCollaborators = readFile('./README/README_no_collaborators.md');
8285
const readmeNoCollaboratorE = readFile('./README/README_no_collaboratorE.md');
8386
const readmeUnordered = readFile('./README/README_unordered.md');
8487

88+
const githubCI = {};
89+
90+
for (let item of readdirSync(path('./github-ci'))) {
91+
if (!item.endsWith('.json')) {
92+
continue;
93+
}
94+
githubCI[basename(item, '.json')] = readJSON(`./github-ci/${item}`);
95+
};
96+
8597
module.exports = {
8698
approved,
8799
requestedChanges,
@@ -94,6 +106,7 @@ module.exports = {
94106
commentsWithLiteCI,
95107
commentsWithLGTM,
96108
oddCommits,
109+
githubCI,
97110
incorrectGitConfigCommits,
98111
simpleCommits,
99112
singleCommitAfterReview,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[
2+
{
3+
"commit": {
4+
"committedDate": "2017-10-26T12:10:20Z",
5+
"oid": "9d098ssiskj8dhd39js0sjd0cn2ng4is9n40sj12d",
6+
"messageHeadline": "doc: add api description README",
7+
"author": {
8+
"login": "foo"
9+
},
10+
"status": {
11+
"state": "FAILURE"
12+
},
13+
"checkSuites": {
14+
"nodes": [
15+
{
16+
"status": "COMPLETED",
17+
"conclusion": "FAILURE"
18+
}
19+
]
20+
}
21+
}
22+
}
23+
]
24+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[
2+
{
3+
"commit": {
4+
"committedDate": "2017-10-26T12:10:20Z",
5+
"oid": "9d098ssiskj8dhd39js0sjd0cn2ng4is9n40sj12d",
6+
"messageHeadline": "doc: add api description README",
7+
"author": {
8+
"login": "foo"
9+
},
10+
"status": {
11+
"state": "SUCCESS"
12+
},
13+
"checkSuites": {
14+
"nodes": [
15+
{
16+
"status": "COMPLETED",
17+
"conclusion": "SUCCESS"
18+
}
19+
]
20+
}
21+
}
22+
}
23+
]
24+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"commit": {
4+
"committedDate": "2017-10-26T12:10:20Z",
5+
"oid": "9d098ssiskj8dhd39js0sjd0cn2ng4is9n40sj12d",
6+
"messageHeadline": "doc: add api description README",
7+
"author": {
8+
"login": "foo"
9+
},
10+
"checkSuites": {
11+
"nodes": [
12+
{
13+
"status": "COMPLETED",
14+
"conclusion": "FAILURE"
15+
}
16+
]
17+
}
18+
}
19+
}
20+
]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"commit": {
4+
"committedDate": "2017-10-26T12:10:20Z",
5+
"oid": "9d098ssiskj8dhd39js0sjd0cn2ng4is9n40sj12d",
6+
"messageHeadline": "doc: add api description README",
7+
"author": {
8+
"login": "foo"
9+
},
10+
"checkSuites": {
11+
"nodes": [
12+
{
13+
"status": "IN_PROGRESS"
14+
}
15+
]
16+
}
17+
}
18+
}
19+
]
20+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"commit": {
4+
"committedDate": "2017-10-26T12:10:20Z",
5+
"oid": "9d098ssiskj8dhd39js0sjd0cn2ng4is9n40sj12d",
6+
"messageHeadline": "doc: add api description README",
7+
"author": {
8+
"login": "foo"
9+
},
10+
"checkSuites": {
11+
"nodes": [
12+
{
13+
"status": "COMPLETED",
14+
"conclusion": "SUCCESS"
15+
}
16+
]
17+
}
18+
}
19+
}
20+
]

0 commit comments

Comments
 (0)