[GitHub] Issue and pull request detail and check state (#1114)

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.
This commit is contained in:
Paul Melnikow
2017-10-02 13:26:42 -04:00
committed by GitHub
parent 8e08b374a4
commit bb66a99a66
6 changed files with 246 additions and 10 deletions

View File

@@ -47,6 +47,10 @@ function makeColor(color) {
}
}
function makeColorB(defaultColor, overrides) {
return makeColor(overrides.colorB || defaultColor);
}
function makeLabel(defaultLabel, overrides) {
return overrides.label || defaultLabel;
}
@@ -98,5 +102,6 @@ module.exports = {
makeLabel,
makeLogo,
makeBadgeData,
makeColor
makeColor,
makeColorB
};

19
lib/github-helpers.js Normal file
View File

@@ -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
};

View File

@@ -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",

133
server.js
View File

@@ -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
@@ -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());
@@ -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) {
@@ -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) {
@@ -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';

View File

@@ -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({
@@ -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')
@@ -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' }));

View File

@@ -880,6 +880,42 @@ Pixel-perfect &nbsp; Retina-ready &nbsp; Fast &nbsp; Consistent &nbsp; Hackable
<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>