Skip to content

Commit

Permalink
[GitHub] Issue and pull request detail and check state (#1114)
Browse files Browse the repository at this point in the history
This adds badges for Github issues and pull requests. You can display the state, title, username, number of comments, age, time since last update, and state of checks.

Provides an endpoint the Shields CI can use to fetch PR titles for #979 and resolves #1011.
  • Loading branch information
paulmelnikow committed Oct 2, 2017
1 parent 8e08b37 commit bb66a99
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 10 deletions.
7 changes: 6 additions & 1 deletion lib/badge-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ function makeColor(color) {
}
}

function makeColorB(defaultColor, overrides) {
return makeColor(overrides.colorB || defaultColor);
}

function makeLabel(defaultLabel, overrides) {
return overrides.label || defaultLabel;
}
Expand Down Expand Up @@ -98,5 +102,6 @@ module.exports = {
makeLabel,
makeLogo,
makeBadgeData,
makeColor
makeColor,
makeColorB
};
19 changes: 19 additions & 0 deletions lib/github-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';

const { colorScale } = require('./color-formatters');

function stateColor(s) {
return { open: '2cbe4e', closed: 'cb2431', merged: '6f42c1' }[s];
}

function checkStateColor(s) {
return { pending: 'dbab09', success: '2cbe4e', failure: 'cb2431', error: 'cb2431' }[s];
}

const commentsColor = colorScale([1, 3, 10, 25], undefined, true);

module.exports = {
stateColor,
checkStateColor,
commentsColor
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@
"camp": "~16.2.3",
"chrome-web-store-item-property": "~1.1.2",
"dot": "~1.0.3",
"pretty-bytes": "^3.0.1",
"gm": "^1.23.0",
"json-autosave": "~1.1.2",
"lodash.countby": "^4.6.0",
"moment": "^2.18.1",
"pdfkit": "~0.8.0",
"pretty-bytes": "^3.0.1",
"redis": "~2.6.2",
"request": "~2.81.0",
"semver": "~5.3.0",
Expand Down
133 changes: 129 additions & 4 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,14 @@ const {
getAnalytics
} = require('./lib/analytics');
const {
makeColor,
makeColorB,
isValidStyle,
isSixHex: sixHex,
makeLabel: getLabel,
makeLogo: getLogo,
makeBadgeData: getBadgeData,
} = require('./lib/badge-data');
const countBy = require('lodash.countby');
const {
handleRequest: cache,
clearRequestCache
Expand Down Expand Up @@ -107,6 +108,11 @@ const {
getVscodeApiReqOptions,
getVscodeStatistic
} = require('./lib/vscode-badge-helpers');
const {
stateColor: githubStateColor,
checkStateColor: githubCheckStateColor,
commentsColor: githubCommentsColor
} = require('./lib/github-helpers');

var semver = require('semver');
var serverStartTime = new Date((new Date()).toGMTString());
Expand Down Expand Up @@ -3461,6 +3467,125 @@ cache(function(data, match, sendBadge, request) {
});
}));

// GitHub issue detail integration.
camp.route(/^\/github\/(?:issues|pulls)\/detail\/(s|title|u|label|comments|age|last-update)\/([^\/]+)\/([^\/]+)\/(\d+)\.(svg|png|gif|jpg|json)$/,
cache((queryParams, match, sendBadge, request) => {
const [, which, owner, repo, number, format] = match;
const uri = `${githubApiUrl}/repos/${owner}/${repo}/issues/${number}`;
const badgeData = getBadgeData('', queryParams);
if (badgeData.template === 'social') {
badgeData.logo = getLogo('github', queryParams);
}
githubAuth.request(request, uri, {}, (err, res, buffer) => {
if (err != null) {
badgeData.text[1] = 'inaccessible';
sendBadge(format, badgeData);
return;
}
try {
const parsedData = JSON.parse(buffer);
const isPR = 'pull_request' in parsedData;
const noun = isPR ? 'pull request' : 'issue';
badgeData.text[0] = getLabel(`${noun} ${parsedData.number}`, queryParams);
switch (which) {
case 's': {
const state = badgeData.text[1] = parsedData.state;
badgeData.colorscheme = null;
badgeData.colorB = makeColorB(githubStateColor(state), queryParams);
break;
}
case 'title':
badgeData.text[1] = parsedData.title;
break;
case 'u':
badgeData.text[0] = getLabel('author', queryParams);
badgeData.text[1] = parsedData.user.login;
break;
case 'label':
badgeData.text[0] = getLabel('label', queryParams);
badgeData.text[1] = parsedData.labels.map(i => i.name).join(' | ');
if (parsedData.labels.length === 1) {
badgeData.colorscheme = null;
badgeData.colorB = makeColorB(parsedData.labels[0].color, queryParams);
}
break;
case 'comments': {
badgeData.text[0] = getLabel('comments', queryParams);
const comments = badgeData.text[1] = parsedData.comments;
badgeData.colorscheme = null;
badgeData.colorB = makeColorB(githubCommentsColor(comments), queryParams);
break;
}
case 'age':
case 'last-update': {
const label = which === 'age' ? 'created' : 'updated';
const date = which === 'age' ? parsedData.created_at : parsedData.updated_at;
badgeData.text[0] = getLabel(label, queryParams);
badgeData.text[1] = formatDate(date);
badgeData.colorscheme = ageColor(Date.parse(date));
break;
}
default:
throw Error('Unreachable due to regex');
}
sendBadge(format, badgeData);
} catch(e) {
badgeData.text[1] = 'invalid';
sendBadge(format, badgeData);
}
});
}));

// GitHub pull request build status integration.
camp.route(/^\/github\/status\/(s|contexts)\/pulls\/([^\/]+)\/([^\/]+)\/(\d+)\.(svg|png|gif|jpg|json)$/,
cache((queryParams, match, sendBadge, request) => {
const [, which, owner, repo, number, format] = match;
const issueUri = `${githubApiUrl}/repos/${owner}/${repo}/pulls/${number}`;
const badgeData = getBadgeData('checks', queryParams);
if (badgeData.template === 'social') {
badgeData.logo = getLogo('github', queryParams);
}
githubAuth.request(request, issueUri, {}, (err, res, buffer) => {
if (err != null) {
badgeData.text[1] = 'inaccessible';
sendBadge(format, badgeData);
return;
}
try {
const parsedData = JSON.parse(buffer);
const ref = parsedData.head.sha;
const statusUri = `${githubApiUrl}/repos/${owner}/${repo}/commits/${ref}/status`;
githubAuth.request(request, statusUri, {}, (err, res, buffer) => {
try {
const parsedData = JSON.parse(buffer);
const state = badgeData.text[1] = parsedData.state;
badgeData.colorscheme = null;
badgeData.colorB = makeColorB(githubCheckStateColor(state), queryParams);
switch(which) {
case 's':
badgeData.text[1] = state;
break;
case 'contexts': {
const counts = countBy(parsedData.statuses, 'state');
badgeData.text[1] = Object.keys(counts).map(k => `${counts[k]} ${k}`).join(', ');
break;
}
default:
throw Error('Unreachable due to regex');
}
sendBadge(format, badgeData);
} catch(e) {
badgeData.text[1] = 'invalid';
sendBadge(format, badgeData);
}
});
} catch(e) {
badgeData.text[1] = 'invalid';
sendBadge(format, badgeData);
}
});
}));

// GitHub forks integration.
camp.route(/^\/github\/forks\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/,
cache(function(data, match, sendBadge, request) {
Expand Down Expand Up @@ -5837,7 +5962,7 @@ cache(function(data, match, sendBadge, request) {
return;
}
var count = 0;
var color;
var color = '78bdf2';
for (var i = 0; i < cards.length; i++) {
var cardMetadata = cards[i].githubMetadata;
if (cardMetadata.labels && cardMetadata.labels.length > 0) {
Expand All @@ -5850,10 +5975,10 @@ cache(function(data, match, sendBadge, request) {
}
}
}
badgeData.text[0] = data.label || ghLabel;
badgeData.text[0] = getLabel(ghLabel, data);
badgeData.text[1] = '' + count;
badgeData.colorscheme = null;
badgeData.colorB = makeColor(data.colorB || color || '78bdf2');
badgeData.colorB = makeColorB(color, data);
sendBadge(format, badgeData);
} catch(e) {
badgeData.text[1] = 'invalid';
Expand Down
58 changes: 54 additions & 4 deletions service-tests/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ const ServiceTester = require('./runner/service-tester');
const t = new ServiceTester({ id: 'github', title: 'Github' });
module.exports = t;

const validDateString = Joi.alternatives().try(
Joi.equal('today', 'yesterday'),
Joi.string().regex(/^last (sun|mon|tues|wednes|thurs|fri|satur)day$/),
Joi.string().regex(/^(january|february|march|april|may|june|july|august|september|october|november|december)( \d{4})?$/));

t.create('License')
.get('/license/badges/shields.json')
.expectJSONTypes(Joi.object().keys({
Expand Down Expand Up @@ -335,10 +340,7 @@ t.create('commit activity (1 week)')

t.create('last commit (recent)')
.get('/last-commit/eslint/eslint.json')
.expectJSONTypes(Joi.object().keys({
name: Joi.equal('last commit'),
value: Joi.string().regex(/^today|yesterday|last (?:sun|mon|tues|wednes|thurs|fri|satur)day/),
}));
.expectJSONTypes(Joi.object().keys({ name: 'last commit', value: validDateString }));

t.create('last commit (ancient)')
.get('/last-commit/badges/badgr.co.json')
Expand All @@ -353,3 +355,51 @@ t.create('last commit (on branch)')
name: Joi.equal('last commit'),
value: Joi.equal('july 2013'),
}));

t.create('github issue state')
.get('/issues/detail/s/badges/shields/979.json')
.expectJSONTypes(Joi.object().keys({
name: 'issue 979',
value: Joi.equal('open', 'closed'),
}));

t.create('github issue title')
.get('/issues/detail/title/badges/shields/979.json')
.expectJSONTypes(Joi.object().keys({
name: 'issue 979',
value: 'Github rate limits cause transient service test failures in CI',
}));

t.create('github issue author')
.get('/issues/detail/u/badges/shields/979.json')
.expectJSONTypes(Joi.object().keys({ name: 'author', value: 'paulmelnikow' }));

t.create('github issue label')
.get('/issues/detail/label/badges/shields/979.json')
.expectJSONTypes(Joi.object().keys({
name: 'label',
value: Joi.equal('bug | developer-experience', 'developer-experience | bug'),
}));

t.create('github issue comments')
.get('/issues/detail/comments/badges/shields/979.json')
.expectJSONTypes(Joi.object().keys({
name: 'comments',
value: Joi.number().greater(15),
}));

t.create('github issue age')
.get('/issues/detail/age/badges/shields/979.json')
.expectJSONTypes(Joi.object().keys({ name: 'created', value: validDateString }));

t.create('github issue update')
.get('/issues/detail/last-update/badges/shields/979.json')
.expectJSONTypes(Joi.object().keys({ name: 'updated', value: validDateString }));

t.create('github pull request check state')
.get('/status/s/pulls/badges/shields/1110.json')
.expectJSONTypes(Joi.object().keys({ name: 'checks', value: 'failure' }));

t.create('github pull request check contexts')
.get('/status/contexts/pulls/badges/shields/1110.json')
.expectJSONTypes(Joi.object().keys({ name: 'checks', value: '1 failure' }));
36 changes: 36 additions & 0 deletions try.html
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,42 @@ <h3 id="miscellaneous"> Miscellaneous </h3>
<td><img src='/github/issues-pr-raw/badges/shields/vendor-badge.svg' alt=''/></td>
<td><code>https://img.shields.io/github/issues-pr-raw/badges/shields/vendor-badge.svg</code></td>
</tr>
<tr><th data-keywords='GitHub issue pullrequest detail' data-doc='githubDoc'> GitHub issue state: </th>
<td><img src='/github/issues/detail/s/badges/shields/979.svg' alt=''/></td>
<td><code>https://img.shields.io/github/issues/detail/badges/shields/979.svg</code></td>
</tr>
<tr><th data-keywords='GitHub issue pullrequest detail' data-doc='githubDoc'> GitHub issue title: </th>
<td><img src='/github/issues/detail/s/badges/shields/979.svg' alt=''/></td>
<td><code>https://img.shields.io/github/issues/detail/s/badges/shields/979.svg</code></td>
</tr>
<tr><th data-keywords='GitHub issue pullrequest detail' data-doc='githubDoc'> GitHub issue author: </th>
<td><img src='/github/issues/detail/u/badges/shields/979.svg' alt=''/></td>
<td><code>https://img.shields.io/github/issues/detail/u/badges/shields/979.svg</code></td>
</tr>
<tr><th data-keywords='GitHub issue pullrqeuest detail' data-doc='githubDoc'> GitHub issue label: </th>
<td><img src='/github/issues/detail/label/badges/shields/979.svg' alt=''/></td>
<td><code>https://img.shields.io/github/issues/detail/label/badges/shields/979.svg</code></td>
</tr>
<tr><th data-keywords='GitHub issue pullrequest detail' data-doc='githubDoc'> GitHub issue comments: </th>
<td><img src='/github/issues/detail/comments/badges/shields/979.svg' alt=''/></td>
<td><code>https://img.shields.io/github/issues/detail/comments/badges/shields/979.svg</code></td>
</tr>
<tr><th data-keywords='GitHub issue pullrequest detail' data-doc='githubDoc'> GitHub issue age: </th>
<td><img src='/github/issues/detail/age/badges/shields/979.svg' alt=''/></td>
<td><code>https://img.shields.io/github/issues/detail/age/badges/shields/979.svg</code></td>
</tr>
<tr><th data-keywords='GitHub issue pullrequest detail' data-doc='githubDoc'> GitHub issue last update: </th>
<td><img src='/github/issues/detail/last-update/badges/shields/979.svg' alt=''/></td>
<td><code>https://img.shields.io/github/issues/detail/last-update/badges/shields/979.svg</code></td>
</tr>
<tr><th data-keywords='GitHub pullrequest detail check' data-doc='githubDoc'> GitHub pull request check state: </th>
<td><img src='/github/status/s/pulls/badges/shields/1110.svg' alt=''/></td>
<td><code>https://img.shields.io/github/status/s/pulls/badges/shields/1110.svg</code></td>
</tr>
<tr><th data-keywords='GitHub pullrequest detail check' data-doc='githubDoc'> GitHub pull request check contexts: </th>
<td><img src='/github/status/contexts/pulls/badges/shields/1110.svg' alt=''/></td>
<td><code>https://img.shields.io/github/status/contexts/pulls/badges/shields/1110.svg</code></td>
</tr>
<tr><th data-keywords='GitHub contributor' data-doc='githubDoc'> GitHub contributors: </th>
<td><img src='/github/contributors/cdnjs/cdnjs.svg' alt=''/></td>
<td><code>https://img.shields.io/github/contributors/cdnjs/cdnjs.svg</code></td>
Expand Down

0 comments on commit bb66a99

Please sign in to comment.