From 4bca52a87e0bd7b96facf74efafbd0a14342ea9f Mon Sep 17 00:00:00 2001 From: Tatsuyuki Ishi Date: Fri, 16 Dec 2016 18:38:17 +0900 Subject: [PATCH] Add GitLab CI badge support Address #541 --- lib/gitlab-auth.js | 77 ++++++++++++++++++++++++++++++++ server.js | 107 +++++++++++++++++++++++++++++++++++++++++++++ try.html | 16 +++++++ 3 files changed, 200 insertions(+) create mode 100644 lib/gitlab-auth.js diff --git a/lib/gitlab-auth.js b/lib/gitlab-auth.js new file mode 100644 index 0000000000000..974f08d1fe21e --- /dev/null +++ b/lib/gitlab-auth.js @@ -0,0 +1,77 @@ +var querystring = require('querystring'); +var request = require('request'); +var autosave = require('json-autosave'); +var serverSecrets; +try { + // Everything that cannot be checked in but is useful server-side + // is stored in this JSON data. + serverSecrets = require('../secret.json'); +} catch(e) {} +var gitlabUserTokens; +var gitlabUserTokensFile = '.gitlab-user-tokens.json'; +autosave(gitlabUserTokensFile, {data:[]}).then(function(f) { + gitlabUserTokens = f; + for (var i = 0; i < gitlabUserTokens.data.length; i++) { + addGitlabToken(gitlabUserTokens.data[i]); + } +}).catch(function(e) { console.error('Could not create ' + gitlabUserTokensFile); }); + +// Retrieve a user token if there is one for which we believe there are requests +// remaining. Return undefined if we could not find one. +function getReqRemainingToken() { + return gitlabUserTokens.data[0]; +} + +function addGitlabToken(token) { + // Insert it only if it is not registered yet. + if (gitlabUserTokens.data.indexOf(token) === -1) { + gitlabUserTokens.data.push(token); + } +} + +function rmGitlabToken(token) { + // Remove it only if it is in there. + var idx = gitlabUserTokens.data.indexOf(token); + if (idx >= 0) { + gitlabUserTokens.data.splice(idx, 1); + } +} + +// Personal tokens allow access to GitLab private repositories. +// You can manage your personal GitLab token at +// . +if (serverSecrets && serverSecrets.gl_token) { + addGitlabToken(serverSecrets.gl_token); +} + +// Act like request(), but tweak headers and query to avoid hitting a rate +// limit. +function gitlabRequest(request, url, query, cb) { + query = query || {}; + var headers = { + 'User-Agent': 'Shields.io', + 'Accept': 'application/json', + }; + var gitlabToken = getReqRemainingToken(); + + if (gitlabToken != null) { + // There's currently no rate limit on API as of 2016. + headers['PRIVATE-TOKEN'] = gitlabToken; + } + // Else, try without authentication (not implemented as of 2016) + + var qs = querystring.stringify(query); + if (qs) { url += '?' + qs; } + request(url, {headers: headers}, function(err, res, buffer) { + if (gitlabToken != null) { + if (res.statusCode === 401) { // Unauthorized. + rmGitlabToken(gitlabToken); + } else { + // This was originally for checking rate limits + } + } + cb(err, res, buffer); + }); +} + +exports.request = gitlabRequest; \ No newline at end of file diff --git a/server.js b/server.js index e0d416daf6a1d..0178f06e80e6e 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,7 @@ var serverPort = +process.env.PORT || +process.argv[2] || (secureServer? 443: 80 var bindAddress = process.env.BIND_ADDRESS || process.argv[3] || '::'; var infoSite = process.env.INFOSITE || "http://shields.io"; var githubApiUrl = process.env.GITHUB_URL || 'https://api.github.com'; +var gitlabApiUrl = process.env.GITLAB_URL || 'https://gitlab.com/api/v3'; var Camp = require('camp'); var camp = Camp.start({ documentRoot: __dirname, @@ -20,6 +21,7 @@ var badge = require('./badge.js'); var svg2img = require('./svg-to-img.js'); var loadLogos = require('./load-logos.js'); var githubAuth = require('./lib/github-auth.js'); +var gitlabAuth = require('./lib/gitlab-auth.js'); var querystring = require('querystring'); var xml2js = require('xml2js'); var serverSecrets; @@ -572,6 +574,111 @@ cache(function(data, match, sendBadge, request) { }); })); +camp.route(/^\/gitlab\/ci\/([sc])\/([^\/]+\/[^\/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var dataKind = match[1]; + var userRepo = match[2]; // namespace/project + var branch = match[3]; + var format = match[4]; + var apiUrl = gitlabApiUrl + '/projects/' + encodeURIComponent(userRepo) + '/pipelines'; + console.log(apiUrl); + var badgeData = getBadgeData({'s':'build', 'c': 'coverage'}[dataKind], data); + var callback = function(branch, page){ + var qs = { + page: page, + per_page: 100, + }; + gitlabAuth.request(request, apiUrl, qs, function(err, res, buffer) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + var data = JSON.parse(buffer); + var done = false; + // Assume sorted by time + if(page >= 10 || data.length == 0) throw 'No match'; + for(var i = 0; i < data.length; ++i) { + if(data[i].ref === branch && ['success', 'failed'].indexOf(data[i].status) != -1) { + switch(dataKind) { + case 's': + var state = data[i].status; + badgeData.text[1] = state; + if (state === 'success') { + badgeData.colorscheme = 'brightgreen'; + } else if (state === 'failed') { + badgeData.colorscheme = 'red'; + } + sendBadge(format, badgeData); + break; + case 'c': + var buildApiUrl = gitlabApiUrl + '/projects/' + encodeURIComponent(userRepo) + + '/repository/commits/' + data[i].sha + '/builds'; + gitlabAuth.request(request, buildApiUrl, {per_page: 100}, function(err, res, buffer) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + var data = JSON.parse(buffer); + var done = false; + for(var i = 0; i < data.length; ++i) { + if(data[i].coverage != null) { + console.log(data[i].coverage) + var percentage = parseFloat(data[i].coverage); + if(percentage === NaN) throw 'NaN'; + badgeData.text[1] = percentage.toFixed(0) + '%'; + badgeData.colorscheme = coveragePercentageColor(percentage); + done = true; + } + } + if(!done) throw 'No coverage match' + } catch(e) { + badgeData.text[1] = 'unknown'; + } + sendBadge(format, badgeData); + }); + break; + } + done = true; + break; + } + } + if(!done) { + callback(branch, page+1); + } + } catch(e) { + badgeData.text[1] = 'unknown'; + sendBadge(format, badgeData); + } + }); + } + if(branch == null) { + var projectApiUrl = gitlabApiUrl + '/projects/' + encodeURIComponent(userRepo); + gitlabAuth.request(request, projectApiUrl, {}, function(err, res, buffer) { + if(err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; + } + try { + var data = JSON.parse(buffer); + branch = data.default_branch; + } catch(e) {} + if(branch == null || branch === undefined) { + badgeData.text[1] = 'unknown'; + sendBadge(format, badgeData); + return; + } + callback(branch, 1); + }); + } else { + callback(branch, 1); + } +})); + // Rust download and version integration camp.route(/^\/crates\/(d|v|dv|l)\/([A-Za-z0-9_-]+)(?:\/([0-9.]+))?\.(svg|png|gif|jpg|json)$/, cache(function (data, match, sendBadge, request) { diff --git a/try.html b/try.html index 11b3eaed6d183..f37c6eb307174 100644 --- a/try.html +++ b/try.html @@ -156,6 +156,22 @@

Build

https://img.shields.io/jenkins/c/https/jenkins.qa.ubuntu.com/address-book-service-utopic-i386-ci.svg + GitLab CI: + + https://img.shields.io/gitlab/ci/USER/REPO.svg + + GitLab CI branch: + + https://img.shields.io/gitlab/ci/USER/REPO/BRANCH.svg + + GitLab CI coverage: + + https://img.shields.io/gitlab/ci/PROJECT_ID.svg + + GitLab CI branch coverage: + + https://img.shields.io/gitlab/ci/PROJECT_ID/BRANCH.svg + Coveralls: https://img.shields.io/coveralls/jekyll/jekyll.svg