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