This Sunday, I worked on an API that creates a GitHub badge based off a project's mood. Mood meaning the average time of day that the repository is worked on. I find that I work on different projects at different times of the day. In the morning, I skew towards back-end focused repositories. Maybe it's the coffee? ā
GitHub badges can either be generated dynamically by a library or via third-party services like shields.io. For Project Mood, we use gh-badges ā an npm package. Here's how a badge is built.
const bf = new BadgeFactory();// ...// Customize badgeconst format = {text: ['project mood', timeOfDay],colorA: '#555',colorB: color,template: 'flat',}// Create SVG/add titleconst title = `<title>average commit: ${parseInt(average)}:${parseInt((average % 1) * 60)}</title>`;const svg = bf.create(format).replace(/>/, `>${title}`);
It's a prototype, and the library doesn't allow custom attributes, so we inject titles in with a RegEx replace. The sole route we make avaliable is /:owner/:repo.svg
for example: /healeycodes/project-mood.svg
. With Express, SVGs can be sent back much like a string would be.
return res.status(200).send(svg).end();
The color of these badges is decided by scanning recent commits and finding the average time of day. The GitHub API requires a user agent and a personal access token. We process the commits with a map/reduce. JavaScript's Date
responds well to time zone correction.
// Options for the request callconst options = {url: `${api}repos/${req.params.owner}/${req.params.repo}/commits?${token}`,headers: {'User-Agent': process.env.USERAGENT}};// ...// As part of the request callback, commits are scannedconst times = json.map(item => item.commit.author.date);const average = times.reduce((sum, time) => {const d = new Date(time);const hours = d.getHours() + (d.getMinutes() / 60) + (d.getSeconds() / 60 / 60);return hours + sum;}, 0) / times.length;
It takes ~0.75ms to generate a badge on a modern PC ā this includes our title insertion method. Since there is no state being managed, this project would respond well to horizontal scaling. However, the roadmap describes some ways that scale can be managed without throwing money at the problem.
ššš- Caching:- Repositories should be scanned infrequently rather than per request.- We can store the most recently requested SVGs in memory.- Basically, don't generate the SVG for every request (which is used for the prototype).- Blended colors depending on average time rather than fixed colors.
No project is complete without tests! A simple test plan, executed by a cloud-build, is one of my favorite markers to pass during development. For Project Mood, Mocha and SuperTest are paired with Travis CI. The Express app is exported when the NODE_ENV
is set to 'test'.
if (process.env.NODE_ENV === 'test') {module.exports = app;} else {const PORT = process.env.PORT || 8080;app.listen(PORT, () => {console.log(`App listening on port ${PORT}`);console.log('Press Ctrl+C to quit.');});}
This allows us to import it into app.test.js
which will be called by npm test
. Other enviromental values in use are USERAGENT
which is required by the GitHub API, as well as GHTOKEN
. The latter is set to be hidden by Travis CI so that public builds don't leak secrets.
One of the lovely tests.
// Entry - "mocha test/app.test.js --exit"const request = require('supertest');const app = require('../app');const assert = require('assert');/*** Test SVG request*/describe('GET /healeycodes/project-mood', () => {it('responds with an SVG', (done) => {request(app).get('/healeycodes/project-mood.svg').expect((res) => {// SVG XML Namespaceassert(res.text.match(/http:\/\/www.w3.org\/2000\/svg/gmi) !== null);// Error message not presentassert(res.text.match(/unknown/gmi) === null);}).expect(200, done);});});
š¬ twitter/healeycodes for complaints.