├── .gitignore ├── History.md ├── package.json ├── bin └── hookshot ├── LICENSE ├── lib └── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.1.0 / 2014-09-16 3 | ================== 4 | 5 | * Bump dependency versions. 6 | * Correctly handle both json and x-www-form-urlencoded payloads. 7 | 8 | 0.0.2 / 2013-05-16 9 | ================== 10 | 11 | * Implement initial Windows and Cygwin support 12 | 13 | 0.0.1 / 2013-04-22 14 | ================== 15 | 16 | * Initial Release 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hookshot", 3 | "author": "Marco Aurelio ", 4 | "version": "0.1.0", 5 | "dependencies": { 6 | "express": "~4.9.0", 7 | "commander": "~2.3.0", 8 | "lockfile": "~1.0.0", 9 | "body-parser": "~1.8.2" 10 | }, 11 | "main": "lib", 12 | "bin": { 13 | "hookshot": "./bin/hookshot" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bin/hookshot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var program = require('commander'); 6 | 7 | program 8 | .version(JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8')).version) 9 | .usage('') 10 | .option('-p, --port ', 'Port number to listen to (defaults to 3000)', parseInt) 11 | .option('-r, --ref ', 'Ref to look for (defaults to all refs)') 12 | .parse(process.argv); 13 | 14 | var hookshot = require('../lib'); 15 | 16 | if (program.args.length == 0) { 17 | program.help(); 18 | } 19 | 20 | var action = program.args.join(' '); 21 | 22 | hookshot(program.ref || 'hook', action).listen(program.port || 3000); 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Marco Aurélio 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var bodyParser = require('body-parser'); 2 | var normalize = require('path').normalize; 3 | var spawn = require('child_process').spawn; 4 | var express = require('express'); 5 | 6 | module.exports = function (ref, action) { 7 | 8 | // create express instance 9 | var hookshot = express(); 10 | 11 | // middleware 12 | hookshot.use(bodyParser.urlencoded({ extended: false })); 13 | hookshot.use(bodyParser.json()); 14 | 15 | // main POST handler 16 | hookshot.post('/', function (req, res, next) { 17 | var payload = req.body; 18 | if (typeof payload.payload != 'undefined') { 19 | payload = JSON.parse(payload.payload); 20 | } 21 | if (typeof payload.ref != 'string') { 22 | throw new Error('Invalid ref'); 23 | } 24 | if (payload.created) { 25 | hookshot.emit('create', payload); 26 | } else if (payload.deleted) { 27 | hookshot.emit('delete', payload); 28 | } else { 29 | hookshot.emit('push', payload); 30 | } 31 | hookshot.emit('hook', payload); 32 | hookshot.emit(payload.ref, payload); 33 | res.send(202, 'Accepted\n'); 34 | }); 35 | 36 | if (arguments.length == 1) { 37 | action = ref; 38 | ref = 'hook'; 39 | } 40 | 41 | if (typeof action == 'string') { 42 | var shell = process.env.SHELL; 43 | var args = ['-c', action]; 44 | var opts = { stdio: 'inherit' }; 45 | if (shell && isCygwin()) { 46 | shell = cygpath(shell); 47 | } else if (isWin()) { 48 | shell = process.env.ComSpec; 49 | args = ['/s', '/c', '"' + action + '"']; 50 | opts.windowsVerbatimArguments = true; 51 | } 52 | 53 | hookshot.on(ref, function(payload) { 54 | // shell command 55 | spawn(shell, args, opts); 56 | }); 57 | } else if (typeof action == 'function') { 58 | hookshot.on(ref, action); 59 | } 60 | 61 | return hookshot; 62 | }; 63 | 64 | /** 65 | * Returns `true` if node is currently running on Windows, `false` otherwise. 66 | * 67 | * @return {Boolean} 68 | * @api private 69 | */ 70 | 71 | function isWin () { 72 | return 'win32' == process.platform; 73 | } 74 | 75 | /** 76 | * Returns `true` if node is currently running from within a "cygwin" environment. 77 | * Returns `false` otherwise. 78 | * 79 | * @return {Boolean} 80 | * @api private 81 | */ 82 | 83 | function isCygwin () { 84 | // TODO: implement a more reliable check here... 85 | return isWin() && /cygwin/i.test(process.env.HOME); 86 | } 87 | 88 | /** 89 | * Convert a Unix-style Cygwin path (i.e. "/bin/bash") to a Windows-style path 90 | * (i.e. "C:\cygwin\bin\bash"). 91 | * 92 | * @param {String} path 93 | * @return {String} 94 | * @api private 95 | */ 96 | 97 | function cygpath (path) { 98 | path = normalize(path); 99 | if (path[0] == '\\') { 100 | // TODO: implement better cygwin root detection... 101 | path = 'C:\\cygwin' + path; 102 | } 103 | return path; 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hookshot 2 | 3 | ![](http://i.cloudup.com/i_vGKjtQcY2.png) 4 | 5 | "You found the *hookshot*! It's a spring-loaded chain that you can cast out to hook things." 6 | 7 | ## Intro 8 | 9 | **hookshot** is a tiny library and companion CLI tool for handling [GitHub post-receive hooks](https://help.github.com/articles/post-receive-hooks). 10 | 11 | ## Examples 12 | 13 | ### Library 14 | 15 | ```javascript 16 | var hookshot = require('hookshot'); 17 | hookshot('refs/heads/master', 'git pull && make').listen(3000) 18 | ``` 19 | 20 | ### CLI Tool 21 | 22 | ```bash 23 | hookshot -r refs/heads/master 'git pull && make' 24 | ``` 25 | 26 | ## Usage 27 | 28 | The library exposes a single function, `hookshot()`. When called, this functions returns an express instance configured to handle post-receive hooks from GitHub. You can react to pushes to specific branches by listening to specific events on the returned instance, or by providing optional arguments to the `hookshot()` function. 29 | 30 | ```javascript 31 | hookshot() 32 | .on('refs/heads/master', 'git pull && make') 33 | .listen(3000) 34 | ``` 35 | 36 | ```javascript 37 | hookshot('refs/heads/master', 'git pull && make').listen(3000) 38 | ``` 39 | 40 | ### Actions 41 | 42 | Actions can either be shell commands or JavaScript functions. 43 | 44 | ```javascript 45 | hookshot('refs/heads/master', 'git pull && make').listen(3000) 46 | ``` 47 | 48 | ```javascript 49 | hookshot('refs/heads/master', function(info) { 50 | // do something with push info ... 51 | }).listen(3000) 52 | ``` 53 | 54 | ### Mounting to existing express servers 55 | 56 | **hookshot** can be mounted to a custom route on your existing express server: 57 | 58 | ```javascript 59 | // ... 60 | app.use('/my-github-hook', hookshot('refs/heads/master', 'git pull && make')); 61 | // ... 62 | ``` 63 | 64 | ### Special Events 65 | 66 | Special events are fired when branches/tags are created, deleted: 67 | 68 | ```javascript 69 | hookshot() 70 | .on('create', function(info) { 71 | console.log('ref ' + info.ref + ' was created.') 72 | }) 73 | .on('delete', function(info) { 74 | console.log('ref ' + info.ref + ' was deleted.') 75 | }) 76 | ``` 77 | 78 | The `push` event is fired when a push is made to any ref: 79 | 80 | ```javascript 81 | hookshot() 82 | .on('push', function(info) { 83 | console.log('ref ' + info.ref + ' was pushed.') 84 | }) 85 | ``` 86 | 87 | Finally, the `hook` event is fired for every post-receive hook that is send by GitHub. 88 | 89 | ```javascript 90 | hookshot() 91 | .on('push', function(info) { 92 | console.log('ref ' + info.ref + ' was pushed.') 93 | }) 94 | ``` 95 | 96 | ### CLI Tool 97 | 98 | A companion CLI tool is provided for convenience. To use it, install **hookshot** via npm using the `-g` flag: 99 | 100 | ```bash 101 | npm install -g hookshot 102 | ``` 103 | 104 | The CLI tool takes as argument a command to execute upon GitHub post-receive hook: 105 | 106 | ```bash 107 | hookshot 'echo "PUSHED!"' 108 | ``` 109 | 110 | You can optionally specify an HTTP port via the `-p` flag (defaults to 3000) and a ref via the `-r` flag (defaults to all refs): 111 | 112 | ```bash 113 | hookshot -r refs/heads/master -p 9001 'echo "pushed to master!"' 114 | ``` 115 | --------------------------------------------------------------------------------