[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:
@@ -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
19
lib/github-helpers.js
Normal 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
|
||||
};
|
||||
@@ -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
133
server.js
@@ -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';
|
||||
|
||||
@@ -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' }));
|
||||
|
||||
36
try.html
36
try.html
@@ -880,6 +880,42 @@ Pixel-perfect Retina-ready Fast Consistent 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>
|
||||
|
||||
Reference in New Issue
Block a user