Skip to content

Commit

Permalink
feat: initial
Browse files Browse the repository at this point in the history
  • Loading branch information
christophwitzko committed May 12, 2017
1 parent 66f420b commit cdf2001
Show file tree
Hide file tree
Showing 17 changed files with 575 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]

end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.env
.nyc_output/
13 changes: 13 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
*.log
*.dump
*.swp
.DS_Store
.env
.nyc_output
.travis.yml
.editorconfig
Dockerfile
deploy
test
coverage
start-couchdb
13 changes: 13 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
language: node_js
services:
- docker
cache:
directories:
- node_modules
node_js:
- '7'
after_success:
- npm run deploy
branches:
except:
- /^v\d+\.\d+\.\d+$/
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM mhart/alpine-node:7

ARG PKG_VERSION
ADD greenkeeper-badges-${PKG_VERSION}.tgz ./
WORKDIR /package

ENV PORT 5000
EXPOSE 5000

CMD ["npm", "start"]
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Badges for Greenkeeper

This is the webservice that is responsible for the Greenkeeper badges.
Empty file added couchdb/payments/.gitkeep
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function (doc) {
if (doc.type !== 'repository') return

emit(doc.fullName.toLowerCase())
}
14 changes: 14 additions & 0 deletions deploy
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash

# immediately fail if an error occurs
set -e

export NAME=badges
export PUSH_TO_REPLICATED=true
export ZDT_DEPLOYMENT=true
export NO_VHOST=false
export GITHUB_TOKEN=${GITHUB_TOKEN:-$GH_TOKEN}

git clone https://${GITHUB_TOKEN}@github.com/neighbourhoodie/gk-deploy-scripts.git

./gk-deploy-scripts/deploy
58 changes: 58 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
global.Promise = require('bluebird')
Promise.config({
longStackTraces: true
})

const hapi = require('hapi')
const StatsD = require('hot-shots')

const env = require('./lib/env')
require('./lib/rollbar')

;(async () => {
const statsdClient = new StatsD({
host: env.STATSD_HOST,
prefix: 'badges.',
globalTags: [env.NODE_ENV]
})

const server = new hapi.Server()
server.connection({
port: env.PORT
})

server.route({
method: 'GET',
path: '/',
handler: (request, reply) => reply('OK').type('text/plain')
})

await server.register([{
register: require('./lib/badges')
}, {
register: require('good'),
options: {
reporters: {
myConsoleReporter: [{
module: 'good-squeeze',
name: 'Squeeze',
args: [{
log: '*',
response: '*'
}]
}, {
module: 'good-console'
},
'stdout']
}
}
}])

server.on('response', (request, reply) => {
statsdClient.increment(`status_code.${request.response.statusCode}`)
statsdClient.timing('response_time', Date.now() - request.info.received)
})

await server.start()
console.log('server running')
})()
142 changes: 142 additions & 0 deletions lib/badges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
const crypto = require('crypto')

const _ = require('lodash')

const dbs = require('./dbs')
const env = require('./env')

// error second API like a pro
const ghBadgesCb = require('gh-badges')
const ghBadges = (data) => new Promise((resolve, reject) => {
try {
ghBadgesCb(data, (result, err) => {
if (err) return reject(err)
resolve(result)
})
} catch (err) {
reject(err)
}
})

module.exports = async function badges (server, options, next) {
const enabled = await ghBadges({
format: 'svg',
template: 'flat',
colorscheme: 'brightgreen',
text: [
'Greenkeeper',
'enabled'
]
})

const disabled = await ghBadges({
format: 'svg',
template: 'flat',
colorscheme: 'lightgray',
text: [
'Greenkeeper',
'disabled'
]
})

const notfound = await ghBadges({
format: 'svg',
template: 'flat',
colorscheme: 'lightgray',
text: [
'Greenkeeper',
'not found'
]
})

const paymentRequired = await ghBadges({
format: 'svg',
template: 'flat',
colorscheme: 'yellow',
text: [
'Greenkeeper',
'payment required'
]
})

const paymentRequiredSoon = await ghBadges({
format: 'svg',
template: 'flat',
colorscheme: 'yellowgreen',
text: [
'Greenkeeper',
'payment required soon'
]
})

function createHash (bytes) {
const hash = crypto.createHash('sha256')
hash.update(bytes)
return hash.digest('hex')
}

const etags = {
notfound: createHash(notfound),
enabled: createHash(enabled),
disabled: createHash(disabled),
paymentRequired: createHash(paymentRequired),
paymentRequiredSoon: createHash(paymentRequiredSoon)
}

function getBadge (repoDoc, payDoc) {
if (!repoDoc.enabled) return [disabled, etags['disabled']]
if (!repoDoc.private) return [enabled, etags['enabled']]
if (!payDoc) return [paymentRequired, etags['paymentRequired']]
if (payDoc.plan === 'beta') return [paymentRequiredSoon, etags['paymentRequiredSoon']]
if (payDoc.plan === 'org' || payDoc.plan === 'personal') return [enabled, etags['enabled']]
return [paymentRequired, etags['paymentRequired']]
}

function getToken (data) {
return crypto.createHmac('sha256', env.BADGES_SECRET).update(data).digest('hex')
}

function replyNotFound (reply) {
reply(notfound)
.type('image/svg+xml')
.etag(etags.notfound)
}

server.route({
method: 'GET',
path: '/{owner}/{repo}.svg',
handler: handleBadges
})

async function handleBadges (request, reply) {
const {repositories, payments} = await dbs()

const key = request.paramsArray.join('/').toLowerCase()
try {
var repoDoc = _.get((await repositories.query('by_full_name', {
key,
include_docs: true
})), 'rows[0].doc')
} catch (err) {}

if (!repoDoc) {
return replyNotFound(reply)
}

if (repoDoc.private && request.query.token !== getToken(key) && request.query.token !== getToken(repoDoc._id)) return replyNotFound(reply)

try {
var payDoc = await payments.get(repoDoc.accountId)
} catch (err) {}

const bt = getBadge(repoDoc, payDoc)
reply(bt[0])
.type('image/svg+xml')
.etag(bt[1])
}
next()
}

module.exports.attributes = {
name: 'badges'
}
20 changes: 20 additions & 0 deletions lib/dbs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const {resolve} = require('url')

global.Promise = require('bluebird')

const _ = require('lodash')
const bootstrap = require('couchdb-bootstrap')
const PouchDB = require('pouchdb-http').plugin(require('pouchdb-mapreduce'))
const {promisify} = require('bluebird')

const env = require('./env')

module.exports = _.memoize(async function () {
const result = await promisify(bootstrap)(env.COUCH_URL, 'couchdb', {
mapDbName: dbname => dbname + (env.isProduction ? '' : '-staging')
})
return _(result.push)
.mapValues((v, name) => new PouchDB(resolve(env.COUCH_URL, name)))
.mapKeys((v, name) => name.replace('-staging', ''))
.value()
})
11 changes: 11 additions & 0 deletions lib/env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const envalid = require('envalid')
const {str, num, url} = envalid

module.exports = envalid.cleanEnv(process.env, {
PORT: num({default: 8000}),
NODE_ENV: str({choices: ['development', 'staging', 'production'], devDefault: 'development'}),
ROLLBAR_TOKEN_BADGES: str({devDefault: ''}),
STATSD_HOST: str({default: '172.17.0.1'}),
COUCH_URL: url({devDefault: 'http://localhost:5984/'}),
BADGES_SECRET: str({devDefault: 'badges-secret'})
})
26 changes: 26 additions & 0 deletions lib/rollbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const {resolve} = require('path')

const rollbar = require('rollbar')

const env = require('./env')
const pkg = require('../package.json')

const enabled = env.NODE_ENV !== 'development'

module.exports = rollbar

rollbar.init(env.ROLLBAR_TOKEN_BADGES, {
branch: 'master',
codeVersion: `v${pkg.version}`,
environment: env.NODE_ENV,
root: resolve(__dirname, '../'),
enabled
})

if (enabled) {
rollbar.handleUncaughtExceptions(env.ROLLBAR_TOKEN_BADGES, {
exitOnUncaughtException: true
})

rollbar.handleUnhandledRejections(env.ROLLBAR_TOKEN_BADGES)
}
48 changes: 48 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@greenkeeper/badges",
"version": "0.0.0-placeholder",
"dependencies": {
"bluebird": "^3.4.6",
"couchdb-bootstrap": "^1.14.0",
"envalid": "^2.3.0",
"gh-badges": "^1.3.0",
"good": "^7.0.2",
"good-console": "^6.3.1",
"good-squeeze": "^5.0.0",
"hapi": "^16.1.0",
"hot-shots": "^4.3.0",
"lodash": "^4.16.2",
"pouchdb-http": "^6.0.2",
"pouchdb-mapreduce": "^6.0.7",
"rollbar": "^0.6.2"
},
"devDependencies": {
"bundle-dependencies": "^1.0.2",
"nyc": "^10.1.2",
"standard": "^9.0.0",
"tap": "^10.0.1"
},
"engines": {
"node": "7"
},
"license": "UNLICENSED",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/greenkeeperio/badges.git"
},
"scripts": {
"db:start": "./start-couchdb",
"deploy": "./deploy",
"pretest": "standard && npm run db:start",
"start": "node --harmony_async_await index.js",
"test": "nyc tap --node-arg=--harmony_async_await --no-cov ./test"
},
"standard": {
"ignore": [
"couchdb"
]
}
}
7 changes: 7 additions & 0 deletions start-couchdb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash

# immediately fail if an error occurs
set -e

docker rm -fv gk-couchdb || true
docker run -d -p 5984:5984 --name gk-couchdb klaemo/couchdb:2.0.0
Loading

0 comments on commit cdf2001

Please sign in to comment.