mirror of
https://github.com/samsonjs/ThePusher.git
synced 2026-03-25 09:15:57 +00:00
first commit
This commit is contained in:
commit
d4f78bd6e6
4 changed files with 395 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
85
Readme.md
Normal file
85
Readme.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
|
||||
# ThePusher
|
||||
|
||||
Github post-receive hook router.
|
||||
|
||||
## Installation
|
||||
|
||||
Make sure you've [got node and npm installed](https://gist.github.com/579814) and then
|
||||
run `npm i --global thepusher`.
|
||||
|
||||
## Use
|
||||
|
||||
Run `thepusher` from the command line. It does not daemonize itself, it only logs to
|
||||
stdout, and it doesn't run at startup. Yet. Pull requests accepted and encouraged.
|
||||
|
||||
ThePusher's config file resides at `~/.pusher` and looks like this:
|
||||
|
||||
host github.samhuri.net
|
||||
port 6177
|
||||
|
||||
# a unique identifier used in the receive hook url
|
||||
token e815fb07bb390b5e47e509fd1e31e0d82e5d9c24
|
||||
|
||||
# a branch named "feature" is created
|
||||
create branch feature notify-mailing-list.sh
|
||||
|
||||
# any branch is deleted in a repo named "my-project"
|
||||
delete branch my-project:* notify-mailing-list.sh
|
||||
|
||||
# commits are pushed to any branch on samsonjs/ThePusher, fast-forward merge
|
||||
# (e.g. https://github.com/samsonjs/ThePusher)
|
||||
merge branch samsonjs/ThePusher post-to-twitter.sh
|
||||
|
||||
# someone force pushed to master in any of my projects
|
||||
force branch samsonjs/*:master send-an-angry-email.sh
|
||||
|
||||
# any tag is created in "my-project"
|
||||
create tag my-project:* build-tag.sh
|
||||
|
||||
# any tag is deleted in "my-project"
|
||||
delete tag my-project:* delete-build-for-tag.sh
|
||||
|
||||
As you probably noticed triggers follow the form:
|
||||
|
||||
<action> <ref type> <owner/repo:ref> <command>
|
||||
|
||||
Actions are `create`, `delete`, `merge`, and `force`. `merge` and `force` are only
|
||||
valid when the ref type is `branch`.
|
||||
|
||||
The ref type is `branch` or `tag`.
|
||||
|
||||
The ref spec consists of 1 to 3 parts, any of which can be omitted as long as at least
|
||||
one of them is present. To reference the branch or tag named `staging` in the repo
|
||||
named `server` owned by `samsonjs` you write `samsonjs/server:staging`. If there is one
|
||||
name it's the branch or tag name, effectively `*/*:name`.
|
||||
|
||||
(It's not real globbing the value `*` is just special cased.)
|
||||
|
||||
Everything after the ref spec is the command. Commands are not quoted, just good old
|
||||
terrible space splitting.
|
||||
|
||||
## License
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2011 Sami Samhuri <sami@samhuri.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
24
package.json
Normal file
24
package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{ "name" : "thepusher"
|
||||
, "version" : "0.1.0"
|
||||
, "description" : "Github post-receive hook router"
|
||||
, "keywords" : [ "github", "git", "post-receive", "post", "receive", "hook", "router" ]
|
||||
, "author": "Sami Samhuri <sami@samhuri.net>"
|
||||
, "homepage" : "http://samhuri.net/proj/ThePusher"
|
||||
, "dependencies" : { "batteries" : "0.4.x" }
|
||||
, "main" : "pusher"
|
||||
, "bin" : { "thepusher" : "./pusher.js" }
|
||||
, "engines" : { "node" : "0.4.x" }
|
||||
, "repository" :
|
||||
{ "type" : "git"
|
||||
, "url" : "http://github.com/samsonjs/ThePusher.git"
|
||||
}
|
||||
, "bugs" :
|
||||
{ "mail" : "sami@samhuri.net"
|
||||
, "web" : "http://github.com/samsonjs/ThePusher/issues"
|
||||
}
|
||||
, "licenses" :
|
||||
[ { "type" : "MIT"
|
||||
, "url" : "http://github.com/samsonjs/ThePusher/raw/master/LICENSE"
|
||||
}
|
||||
]
|
||||
}
|
||||
285
pusher.js
Executable file
285
pusher.js
Executable file
|
|
@ -0,0 +1,285 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// ThePusher by Sami Samhuri 2011 MIT
|
||||
//
|
||||
// TODO:
|
||||
// - fork & run headless
|
||||
// - watch .pusher and automatically reload changes, rolling back on errors
|
||||
//
|
||||
// Possible TODOs, may be out of scope:
|
||||
// - mail output to MAILTO
|
||||
|
||||
var crypto = require('crypto')
|
||||
, fs = require('fs')
|
||||
, http = require('http')
|
||||
, querystring = require('querystring')
|
||||
, server = http.createServer(routeRequest)
|
||||
, serverOptions = { host: '127.0.0.1'
|
||||
, port: 6177
|
||||
, githubToken: sha1('pusher')
|
||||
}
|
||||
|
||||
// why not export the pieces if someone wants to play around
|
||||
module.exports =
|
||||
{ serverOptions: serverOptions
|
||||
, startServer: startServer
|
||||
, stopServer: stopServer
|
||||
, addTrigger: addTrigger
|
||||
, parseLine: parseLine
|
||||
, parseRefSpec: parseRefSpec
|
||||
, routeRequest: routeRequest
|
||||
, main: main
|
||||
}
|
||||
|
||||
function sha1(s) {
|
||||
return crypto.createHash('sha1').update(s).digest('hex')
|
||||
}
|
||||
|
||||
function main() {
|
||||
var path = require('path')
|
||||
, eachLine = require('batteries').fs.eachLine
|
||||
, rcFile = require('path').join(process.env.HOME, '.pusher')
|
||||
|
||||
fs.stat(rcFile, function(err, s) {
|
||||
if (err) {
|
||||
spiel()
|
||||
return
|
||||
}
|
||||
eachLine(rcFile, { line: parseLine, end: startServer })
|
||||
})
|
||||
}
|
||||
|
||||
var triggers = []
|
||||
|
||||
function addTrigger(t) { triggers.push(t) }
|
||||
|
||||
function parseLine(line) {
|
||||
// ignore comments and blank lines
|
||||
if (line.match(/^\s*(#.*|)?$/)) return
|
||||
|
||||
line = line.trim() // don't want to match \s* everywhere
|
||||
|
||||
var m
|
||||
|
||||
// variables, host and port
|
||||
if (m = line.match(/^\s*([a-z]+)\s+(\S+)\s*$/i)) {
|
||||
var name = m[1].trim().toLowerCase()
|
||||
, val = m[2].trim()
|
||||
if (name === 'host' || name === 'port') {
|
||||
serverOptions[name] = val
|
||||
}
|
||||
else if (name === 'token') {
|
||||
serverOptions.githubToken = val
|
||||
process.env.PUSHER_GITHUB_TOKEN = val
|
||||
}
|
||||
else {
|
||||
console.warn('>>> unrecognized variable: ' + name + ' = ' + val)
|
||||
}
|
||||
}
|
||||
|
||||
// <action> <ref type> <owner/repo:branch or tag> <command>
|
||||
else if (m = line.match(/^(create|delete|force|merge)\s+(branch|tag)\s+(\S+)\s+(.+)$/i)) {
|
||||
var action = m[1].toLowerCase()
|
||||
, refType = m[2].toLowerCase()
|
||||
, ref = parseRefSpec(m[3])
|
||||
if (refType === 'tag' && (action === 'force' || action === 'merge')) {
|
||||
throw new Error(action + ' is not supported with tags, try create or delete')
|
||||
}
|
||||
console.log('>>> ' + line)
|
||||
addTrigger({ action: action
|
||||
, refType: refType
|
||||
, refSpec: ref.spec
|
||||
, owner: ref.owner
|
||||
, repo: ref.repo
|
||||
, ref: ref.ref
|
||||
, command: m[4]
|
||||
})
|
||||
}
|
||||
|
||||
// if you're happy and you know it, syntax error!
|
||||
else {
|
||||
throw new Error('syntax error: ' + line)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO run these things through git check-ref-format or do similar checks
|
||||
function parseRefSpec(spec) {
|
||||
var result = { owner: '*', repo: '*', ref: '*' }
|
||||
, m
|
||||
// <owner/repo:ref>
|
||||
if (m = spec.match(/^([^\/]+)\/([^:]+):(\S+)$/)) {
|
||||
result.owner = m[1]
|
||||
result.repo = m[2]
|
||||
result.ref = m[3]
|
||||
}
|
||||
// <repo:ref>
|
||||
else if (m = spec.match(/^([^:]+):(\S+)$/)) {
|
||||
result.repo = m[1]
|
||||
result.ref = m[2]
|
||||
}
|
||||
// <owner/repo>
|
||||
else if (m = spec.match(/^([^\/]+)\/(\S+)$/)) {
|
||||
result.owner = m[1]
|
||||
result.repo = m[2]
|
||||
}
|
||||
// <ref>
|
||||
else {
|
||||
result.ref = spec
|
||||
}
|
||||
result.spec = result.owner + '/' + result.repo + ':' + result.ref
|
||||
return result
|
||||
}
|
||||
|
||||
function textResponder(status, text) {
|
||||
var headers = { 'content-type': 'text/plain', 'content-length': (text || ''.length) }
|
||||
return function(req, res) {
|
||||
res.writeHead(status, headers)
|
||||
res.end(text)
|
||||
}
|
||||
}
|
||||
|
||||
var notFound = textResponder(404, 'not found')
|
||||
var badRequest = textResponder(400, 'bad request')
|
||||
|
||||
function parseRequest(req, cb) {
|
||||
var parts = []
|
||||
req.on('data', function(b) { parts.push(b) })
|
||||
req.on('end', function() {
|
||||
var body = parts.join('')
|
||||
try {
|
||||
cb(null, JSON.parse(querystring.parse(body).payload))
|
||||
}
|
||||
catch (err) {
|
||||
err.body = body
|
||||
cb(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function routeRequest(req, res) {
|
||||
console.log([req.method, req.url, req.connection.remoteAddress, req.headers['content-length'], req.headers['content-type']].join(' '))
|
||||
if (req.url === '/' + serverOptions.githubToken && req.method === 'POST') {
|
||||
parseRequest(req, function(err, payload) {
|
||||
if (err) {
|
||||
console.error('!!! invalid json or missing payload: ' + err.body)
|
||||
console.error(err.message)
|
||||
console.error(err.stack)
|
||||
badRequest(req, res)
|
||||
return
|
||||
}
|
||||
|
||||
res.writeHead(204)
|
||||
res.end()
|
||||
|
||||
var action
|
||||
, owner = payload.repository.owner.name
|
||||
, repo = payload.repository.name
|
||||
, ref = payload.ref
|
||||
, branch = ref.match(/^refs\/heads\//) ? ref.split('/')[2] : null
|
||||
, tag = ref.match(/^refs\/tags\//) ? ref.split('/')[2] : null
|
||||
, refType = branch ? 'branch' : tag ? 'tag' : 'unknown'
|
||||
|
||||
if (payload.created) {
|
||||
action = 'create'
|
||||
}
|
||||
else if (payload.deleted) {
|
||||
action = 'delete'
|
||||
}
|
||||
else if (payload.forced && branch) {
|
||||
action = 'force'
|
||||
}
|
||||
else {
|
||||
action = 'merge'
|
||||
}
|
||||
|
||||
console.log('* ' + [action, refType, owner + '/' + repo + ':' + (branch || tag || ref)].join(' '))
|
||||
|
||||
triggers.forEach(function(t) {
|
||||
if (t.action === action &&
|
||||
t.refType === refType &&
|
||||
(t.owner === '*' || t.owner === owner) &&
|
||||
(t.repo === '*' || t.repo === repo) &&
|
||||
(t.ref === '*' || t.ref === (branch || tag)))
|
||||
{
|
||||
console.log('>>> match: ', [t.action, t.refType, t.owner + '/' + t.repo + ':' + t.ref].join(' '))
|
||||
runCommand({ command: t.command
|
||||
, action: action
|
||||
, owner: owner
|
||||
, repo: repo
|
||||
, branch: branch
|
||||
, tag: tag
|
||||
, refType: refType
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
else {
|
||||
notFound(req, res)
|
||||
}
|
||||
}
|
||||
|
||||
function runCommand(options) {
|
||||
console.log('>>> running command: ' + options.command)
|
||||
var spawn = require('child_process').spawn
|
||||
// TODO quoting
|
||||
, args = options.command.split(/\s+/)
|
||||
, cmd = args.shift()
|
||||
process.env.PUSHER_ACTION = options.action || ''
|
||||
process.env.PUSHER_OWNER = options.owner || ''
|
||||
process.env.PUSHER_REPO = options.repo || ''
|
||||
process.env.PUSHER_REFTYPE = options.refType || ''
|
||||
process.env.PUSHER_BRANCH = options.branch || ''
|
||||
process.env.PUSHER_TAG = options.tag || ''
|
||||
var child = spawn(cmd, args)
|
||||
if (options.verbose) {
|
||||
child.stdout.on('data', function(b) { console.log('out>>> ' + b) })
|
||||
child.stderr.on('data', function(b) { console.log('err>>> ' + b) })
|
||||
}
|
||||
}
|
||||
|
||||
function startServer() {
|
||||
server.listen(serverOptions.port, serverOptions.host)
|
||||
}
|
||||
|
||||
function stopServer() {
|
||||
server.stop()
|
||||
}
|
||||
|
||||
function spiel() {
|
||||
[ "ThePusher is a Github post-receive hook router."
|
||||
, ""
|
||||
, "Lines that begin with # are comments. No trailing comments."
|
||||
, ""
|
||||
, "ThePusher's config file resides at `~/.pusher` and looks like this:"
|
||||
, ""
|
||||
, "host github.samhuri.net"
|
||||
, "port 6177"
|
||||
, ""
|
||||
, "# a unique identifier used in the receive hook url"
|
||||
, "token e815fb07bb390b5e47e509fd1e31e0d82e5d9c24"
|
||||
, ""
|
||||
, "# a branch named \"feature\" is created"
|
||||
, "create branch feature notify-mailing-list.sh"
|
||||
, ""
|
||||
, "# any branch is deleted in a repo named \"my-project\""
|
||||
, "delete branch my-project:* notify-mailing-list.sh"
|
||||
, ""
|
||||
, "# commits are pushed to any branch on samsonjs/ThePusher, fast-forward merge"
|
||||
, "# (e.g. https://github.com/samsonjs/ThePusher)"
|
||||
, "merge branch samsonjs/ThePusher post-to-twitter.sh"
|
||||
, ""
|
||||
, "# someone force pushed to master in any of my projects"
|
||||
, "force branch samsonjs/*:master send-an-angry-email.sh"
|
||||
, ""
|
||||
, "# any tag is created in \"my-project\""
|
||||
, "create tag my-project:* build-tag.sh"
|
||||
, ""
|
||||
, "# any tag is deleted in \"my-project\""
|
||||
, "delete tag my-project:* delete-build-for-tag.sh"
|
||||
, ""
|
||||
, "Create ~/.pusher and run `thepusher` again."
|
||||
].forEach(function(s) { console.log(s) })
|
||||
}
|
||||
|
||||
if (require.main === module) main()
|
||||
Loading…
Reference in a new issue