diff --git a/lib/badge-data.js b/lib/badge-data.js index 9558aa01aa98f..864f9f707ae17 100644 --- a/lib/badge-data.js +++ b/lib/badge-data.js @@ -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 }; diff --git a/lib/github-helpers.js b/lib/github-helpers.js new file mode 100644 index 0000000000000..39aee7e4acc8d --- /dev/null +++ b/lib/github-helpers.js @@ -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 +}; diff --git a/package.json b/package.json index 176e3b4e53f56..2a17e1b554d1d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server.js b/server.js index 8b58e3304a5ff..c0a4cdc24f17c 100644 --- a/server.js +++ b/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'; diff --git a/service-tests/github.js b/service-tests/github.js index 7d5a08eb43f29..4a44fd0356a29 100644 --- a/service-tests/github.js +++ b/service-tests/github.js @@ -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' })); diff --git a/try.html b/try.html index c37d943bd91b4..da4aa04a63e01 100644 --- a/try.html +++ b/try.html @@ -880,6 +880,42 @@

Miscellaneous

https://img.shields.io/github/issues-pr-raw/badges/shields/vendor-badge.svg + GitHub issue state: + + https://img.shields.io/github/issues/detail/badges/shields/979.svg + + GitHub issue title: + + https://img.shields.io/github/issues/detail/s/badges/shields/979.svg + + GitHub issue author: + + https://img.shields.io/github/issues/detail/u/badges/shields/979.svg + + GitHub issue label: + + https://img.shields.io/github/issues/detail/label/badges/shields/979.svg + + GitHub issue comments: + + https://img.shields.io/github/issues/detail/comments/badges/shields/979.svg + + GitHub issue age: + + https://img.shields.io/github/issues/detail/age/badges/shields/979.svg + + GitHub issue last update: + + https://img.shields.io/github/issues/detail/last-update/badges/shields/979.svg + + GitHub pull request check state: + + https://img.shields.io/github/status/s/pulls/badges/shields/1110.svg + + GitHub pull request check contexts: + + https://img.shields.io/github/status/contexts/pulls/badges/shields/1110.svg + GitHub contributors: https://img.shields.io/github/contributors/cdnjs/cdnjs.svg