Permalink
Browse files

Mint Source!

  • Loading branch information...
0 parents commit 2eb070b44e7f472625589c6c1500a8a07bbbf818 Andrew Appleton committed Nov 23, 2011
@@ -0,0 +1,4 @@
+.DS_Store
+.monitor
+/node_modules
+settings.coffee
@@ -0,0 +1,2 @@
+web: node index.js
+
@@ -0,0 +1,63 @@
+Mint Source
+=========
+
+A simple Node.js status board showing:
+
+- Recent Github commits
+- Jenkins CI build status
+- Current song playing through Last.fm
+
+Dependencies
+------------
+
+- node.js 0.4
+- npm
+ - To install run: `curl http://npmjs.org/install.sh | sh`
+- Redis (local install for development)
+- Heroku account (for production deploy)
+
+Development
+------------
+
+1. Install dependencies `npm install`
+2. Copy `settings.coffee.example` to `settings.coffee`.
+3. Add relevant details to the settings.coffee
+4. Start redis `redis-server`
+5. Run the app `node index.js`
+6. Visit `localhost:1337` in a browser.
+
+Deploy to Heroku
+------------
+
+1. Create a new app on Heroku: `heroku create appname --stack cedar`
+2. Deploy to heroku `git push heroku master`
+3. Scale the web process on Heroku `heroku ps:scale web=1`
+4. Add Redis to go to your app `heroku addons:add redistogo`
+5. Add your app settings:
+ - `heroku config:add NODE_ENV=production`
+ - `heroku config:add AUTH_ENABLED=true` => enable/disable basic HTTP auth
+ - `heroku config:add AUTH_USER=username` => optional basic auth username
+ - `heroku config:add AUTH_PASS=password` => optional basic auth password
+ - `heroku config:add JENKINS_ENABLED=true` => enable build monitoring with Jenkins?
+ - `heroku config:add JENKINS_IP=10.20.30.40` => set if you'd like to IP filter the Jenkins receive action
+ - `heroku config:add LASTFM_ENABLED=true` => optional - show current Last.fm song
+ - `heroku config:add LASTFM_KEY=apikey` => your Last.fm api key
+ - `heroku config:add LASTFM_USER=username` => the Last.fm account you'd like to track
+
+Wait, I have NDA projects which I can't display on my status board!
+---------------
+
+We totally have your back, substitute sensitive names by adding them to Redis (there will be a page to administer this eventually):
+
+ LPUSH Discretions "{\"orig\":\"secret-name\",\"subs\":\"public-name\"}"
+
+Testing locally
+---------------
+
+You can manually add mock Github post receive hook data to Redis.
+
+ redis-cli
+
+ LPUSH Commits "{\"message\":\"update pricing a tad\",\"project\":\"github\",\"timestamp\":\"2008-02-15T14:36:34-08:00\",\"author\":\"Chris Wanstrath\"}"
+
+ LPUSH Commits "{\"message\":\"woo\! it works\",\"project\":\"github\",\"timestamp\":\"2011-02-15T14:36:34-08:00\",\"author\":\"Chris Wanstrath\"}"
@@ -0,0 +1,207 @@
+connect = require('connect')
+express = require('express')
+url = require('url')
+fs = require('fs')
+qs = require('qs')
+async = require('async')
+moment = require('moment')
+gravatar = require('gravatar')
+template = require('jqtpl')
+Jenkins = require('./jenkins')
+Lastfm = require('./lastfm')
+helpers = require('./helpers/helpers')
+settings = {}
+redis = {}
+app = module.exports = express.createServer()
+io = require('socket.io').listen(app)
+
+# Configuration
+
+app.configure( ()->
+ app.set('views', __dirname + '/views')
+ app.set('view engine', 'html')
+ app.register('.html', require('jqtpl').express)
+ app.use(app.router)
+ app.use(express.static(__dirname + '/public'))
+ return
+)
+
+app.configure('development', () ->
+ app.use(express.errorHandler({ dumpExceptions: true, showStack: true }))
+ settings = require('./settings')
+ redis = require('redis').createClient()
+ return
+)
+
+app.configure('production', () ->
+ app.use(express.errorHandler())
+ settings = require('./settings-heroku')
+ # Redis to go on Heroku, local Redis in development
+ rtg = url.parse(process.env.REDISTOGO_URL)
+ redis = require('redis').createClient(rtg.port, rtg.hostname)
+ redis.auth(rtg.auth.split(":")[1])
+ # No websockets for Heroku
+ io.configure () ->
+ io.set('transports', ['xhr-polling'])
+ io.set('polling duration', 10)
+ return
+)
+
+app.helpers({
+ gravatar:gravatar,
+ moment:moment
+})
+
+# get discretionList from Redis
+settings.discretionList = []
+getDiscretionList = (listName) ->
+ redis.llen(listName, (err, i) ->
+ redis.lrange(listName, 0, i, (err, replies) ->
+ for reply in replies
+ settings.discretionList.push(JSON.parse(reply))
+ )
+ )
+getDiscretionList('Discretions')
+
+
+# Middleware functions
+
+basicAuth = (req, res, next) ->
+ if settings.auth.enabled
+ connect.basicAuth(settings.auth.user, settings.auth.pass)(req, res, next)
+ else
+ next()
+
+ipWhitelist = (req, res, next) ->
+ remote = req.connection.remoteAddress
+ if settings.jenkins.ip and remote != settings.jenkins.ip
+ res.writeHead 403
+ res.end '403 Forbidden'
+ console.log "IP whitelist: blocked request to '#{req.url}' from '#{remote}'"
+ else
+ console.log "IP whitelist: allowed request to '#{req.url}' from '#{remote}'"
+ next()
+
+# Routes
+
+app.get('/javascripts/:resource.js', (req, res) ->
+ # Compile and serve client side CoffeeScript on the fly
+ filePath = "./public/javascripts/#{req.params.resource}.coffee"
+ fs.readFile(filePath, 'utf-8', (err, data) ->
+ res.writeHead(200, {'Content-Type': 'application/javascript'})
+ res.write(coffee.compile(data))
+ res.end()
+ )
+)
+
+app.get('/', basicAuth, (req, res) ->
+ commits = []
+ statuses = []
+ getStatuses = ->
+ redis.hgetall('Jenkins', (err, replies) ->
+ for project, status of replies
+ project = helpers.discretify(project, settings.discretionList)
+ if status != 'SUCCESS'
+ statuses.push({project: project, status: status})
+ res.header('Cache-Control', 'no-cache')
+ res.render('index', {
+ title: 'Mint Source',
+ commits: commits,
+ statuses: statuses,
+ songsEnabled: settings.lastfm.enabled
+ })
+ )
+ app.emit 'pageload'
+ redis.lrange('Commits', 0, 5, (err, replies) ->
+ for reply in replies
+ reply = JSON.parse(reply)
+ reply.project = helpers.discretify(reply.project, settings.discretionList)
+ commits.push(reply)
+
+ async.sortBy(commits, (commit, callback) ->
+ callback(null, 1 / Date.parse(commit.timestamp))
+ ,(err, results) ->
+ commits = results
+ getStatuses()
+ )
+ )
+)
+
+app.post('/github_prh', basicAuth, (req, res) ->
+ # GitHub post recieve hook
+ # Remember to post to http://user:password@your-app/github_prh
+ # if using basic auth - https://github.com/blog/237-basic-auth-post-receives
+ body = ''
+
+ req.on('data', (data) ->
+ body += data
+ )
+
+ req.on('end', () ->
+ payload = JSON.parse(qs.parse(body).payload)
+ commits = payload.commits
+ project = helpers.discretify(payload.repository.name, settings.discretionList)
+ out = []
+ for commit in commits
+ data =
+ message: commit.message
+ project: project
+ timestamp: commit.timestamp
+ author: commit.author.name
+ image: gravatar.url(commit.author.email, {s:120})
+ data.relTime = moment(data.timestamp).fromNow()
+ out.push(data)
+
+ async.sortBy(out, (commit, callback) ->
+ callback(null, 1 / Date.parse(commit.timestamp))
+ ,(err, results) ->
+ out = results
+ )
+
+ for commit in out
+ redis.lpush('Commits', JSON.stringify(commit))
+
+ redis.ltrim('Commits', 0, 5)
+
+ fs.readFile('./views/_commit.html', 'utf-8', (err, rawTemplate) ->
+ io.sockets.emit('message', template.tmpl(rawTemplate, out))
+ )
+
+ res.writeHead(200, {'Content-Type': 'text/html'})
+ res.end('OK')
+ )
+)
+
+app.post('/jenkins_pbh', ipWhitelist, (req, res) ->
+ # Jenkins post build hook
+ # Requires https://wiki.jenkins-ci.org/display/JENKINS/Notification+Plugin
+ if !settings.jenkins.enabled
+ console.log('Jenkins disabled')
+ res.writeHead(404)
+ res.end()
+ else
+ body = ''
+ req.on('data', (data) ->
+ body += data
+ )
+ req.on('end', () ->
+ post = JSON.parse(body)
+ return unless post.build.phase == 'FINISHED'
+ j = new Jenkins(post)
+ j.on('data', (data) -> io.sockets.emit('jenkins', data))
+ return
+ )
+ res.writeHead(200, {'Content-Type': 'text/html'})
+ res.end('OK')
+)
+
+if settings.lastfm.enabled
+ lfm = new Lastfm({
+ user: settings.lastfm.user
+ apiKey: settings.lastfm.apiKey
+ })
+ app.on 'pageload', () -> lfm.createRequest()
+ lfm.on('song', (data) -> io.sockets.emit('lastfm', data))
+
+app.listen(process.env.PORT || 1337)
+console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env)
@@ -0,0 +1,7 @@
+{exec} = require 'child_process'
+
+# Should never need this as everything is compiled on the fly...
+task 'compile', 'Compile CoffeeScript into JavaScipt', (options) ->
+ exec 'coffee --compile --output compiled/ .', (err, stdout, stderr) ->
+ throw err if err
+ console.log stdout + stderr
@@ -0,0 +1,9 @@
+exports.discretify = (name, substitutes) ->
+ discreteName = name
+ if substitutes
+ for sub of substitutes
+ if substitutes[sub].orig == name
+ discreteName = substitutes[sub].subs
+ return discreteName
+
+ return discreteName
@@ -0,0 +1,2 @@
+coffee = require('coffee-script')
+require('./app')
@@ -0,0 +1,47 @@
+util = require('util')
+events = require('events')
+https = require('https')
+url = require('url')
+if process.env.REDISTOGO_URL
+ rtg = url.parse(process.env.REDISTOGO_URL)
+ redis = require('redis').createClient(rtg.port, rtg.hostname)
+ redis.auth(rtg.auth.split(":")[1])
+else
+ redis = require('redis').createClient()
+
+class Jenkins extends events.EventEmitter
+ constructor: (data) ->
+ events.EventEmitter.call(this)
+ @project = data.name
+ @status = data.build.status
+
+ @parseStatus()
+
+ parseStatus: () ->
+ prevStatus = ''
+
+ statusLogic = =>
+ redis.hset('Jenkins', @project, @status)
+ @status = 'RECENT FAILURE' if prevStatus == 'FAILURE' and @status == 'SUCCESS'
+ @sendOutput()
+
+ if redis.hexists('Jenkins', @project)
+ redis.hget('Jenkins', @project, (err, result) ->
+ prevStatus = result
+ statusLogic()
+ )
+ else
+ statusLogic()
+
+ sendOutput: () ->
+ if @status is 'FAILURE' or 'RECENT FAILURE'
+ output = {
+ project: @project,
+ status: @status
+ }
+ console.log('Sending: ', output)
+ @emit('data', output);
+ return
+
+module.exports = Jenkins
+
Oops, something went wrong.

0 comments on commit 2eb070b

Please sign in to comment.