├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── hook-cli.js ├── index.js ├── package.json └── test └── test-01-jthooks.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.js] 8 | indent_style = tab 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | curly_bracket_next_line = true 12 | indent_brace_style = Allman 13 | quote_type = single 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "node": true 5 | }, 6 | 7 | "globals": { 8 | "crypto": true, 9 | "escape": false, 10 | "unescape": false 11 | }, 12 | 13 | "ecmaFeatures": { 14 | "arrowFunctions": true, 15 | "binaryLiterals": true, 16 | "blockBindings": true, 17 | "defaultParams": true, 18 | "forOf": true, 19 | "generators": true, 20 | "objectLiteralComputedProperties": true, 21 | "objectLiteralDuplicateProperties": false, 22 | "objectLiteralShorthandMethods": true, 23 | "objectLiteralShorthandProperties": true, 24 | "octalLiterals": false, 25 | "regexUFlag": true, 26 | "regexYFlag": true, 27 | "superInFunctions": true, 28 | "templateStrings": true, 29 | "unicodeCodePointEscapes": true, 30 | "globalReturn": true 31 | }, 32 | 33 | "rules": { 34 | "block-scoped-var": 0, 35 | "brace-style": [2, "allman", { "allowSingleLine": true }], 36 | "camelcase": 0, 37 | "comma-spacing": [2, {"before": false, "after": true}], 38 | "comma-style": [2, "last"], 39 | "complexity": 0, 40 | "consistent-return": 0, 41 | "consistent-this": 0, 42 | "curly": 0, 43 | "default-case": 0, 44 | "dot-notation": 0, 45 | "eol-last": 2, 46 | "eqeqeq": [2, "allow-null"], 47 | "func-names": 0, 48 | "func-style": [0, "declaration"], 49 | "generator-star": 0, 50 | "global-strict": 0, 51 | "guard-for-in": 0, 52 | "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], 53 | "indent": [2, "tab"], 54 | "max-depth": 0, 55 | "max-len": 0, 56 | "max-nested-callbacks": 0, 57 | "max-params": 0, 58 | "max-statements": 0, 59 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 60 | "new-parens": 2, 61 | "no-alert": 2, 62 | "no-array-constructor": 2, 63 | "no-bitwise": 0, 64 | "no-caller": 2, 65 | "no-catch-shadow": 0, 66 | "no-cond-assign": 2, 67 | "no-console": 0, 68 | "no-constant-condition": 0, 69 | "no-control-regex": 2, 70 | "no-debugger": 2, 71 | "no-delete-var": 2, 72 | "no-div-regex": 0, 73 | "no-dupe-keys": 2, 74 | "no-else-return": 0, 75 | "no-empty": 0, 76 | "no-empty-character-class": 2, 77 | "no-eq-null": 0, 78 | "no-eval": 2, 79 | "no-ex-assign": 2, 80 | "no-extend-native": 2, 81 | "no-extra-bind": 2, 82 | "no-extra-boolean-cast": 2, 83 | "no-extra-parens": 0, 84 | "no-extra-semi": 2, 85 | "no-fallthrough": 2, 86 | "no-floating-decimal": 2, 87 | "no-func-assign": 2, 88 | "no-implied-eval": 2, 89 | "no-inline-comments": 0, 90 | "no-inner-declarations": [2, "functions"], 91 | "no-invalid-regexp": 2, 92 | "no-irregular-whitespace": 2, 93 | "no-iterator": 2, 94 | "no-label-var": 2, 95 | "no-labels": 0, 96 | "no-lone-blocks": 2, 97 | "no-lonely-if": 0, 98 | "no-loop-func": 0, 99 | "no-mixed-requires": [0, false], 100 | "no-mixed-spaces-and-tabs": [2, false], 101 | "no-multi-str": 2, 102 | "no-multiple-empty-lines": [2, {"max": 1}], 103 | "no-native-reassign": 2, 104 | "no-negated-in-lhs": 2, 105 | "no-nested-ternary": 0, 106 | "no-new": 2, 107 | "no-new-func": 2, 108 | "no-new-object": 2, 109 | "no-new-require": 2, 110 | "no-new-wrappers": 2, 111 | "no-obj-calls": 2, 112 | "no-octal": 0, 113 | "no-octal-escape": 2, 114 | "no-path-concat": 0, 115 | "no-plusplus": 0, 116 | "no-process-env": 0, 117 | "no-process-exit": 0, 118 | "no-proto": 2, 119 | "no-redeclare": 2, 120 | "no-regex-spaces": 2, 121 | "no-reserved-keys": 0, 122 | "no-restricted-modules": 0, 123 | "no-return-assign": 2, 124 | "no-script-url": 2, 125 | "no-self-compare": 2, 126 | "no-sequences": 2, 127 | "no-shadow": 0, 128 | "no-shadow-restricted-names": 2, 129 | "no-space-before-semi": 0, 130 | "no-spaced-func": 2, 131 | "no-sparse-arrays": 2, 132 | "no-sync": 0, 133 | "no-ternary": 0, 134 | "no-trailing-spaces": 2, 135 | "no-undef": 2, 136 | "no-undef-init": 2, 137 | "no-undefined": 0, 138 | "no-underscore-dangle": 0, 139 | "no-unreachable": 2, 140 | "no-unused-expressions": 0, 141 | "no-unused-vars": [2, {"vars": "local", "args": "none", "varsIgnorePattern": "demand"}], 142 | "no-use-before-define": 0, 143 | "no-var": 0, 144 | "no-void": 0, 145 | "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], 146 | "no-with": 2, 147 | "one-var": 0, 148 | "operator-assignment": [0, "always"], 149 | "padded-blocks": 0, 150 | "quote-props": 0, 151 | "quotes": [2, "single", "avoid-escape"], 152 | "radix": 2, 153 | "semi": [2, "always"], 154 | "sort-vars": 0, 155 | "space-before-function-paren": [2, "never"], 156 | "keyword-spacing": 2, 157 | "space-before-blocks": [2, "always"], 158 | "space-in-brackets": 0, 159 | "space-in-parens": [2, "never"], 160 | "space-infix-ops": 2, 161 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 162 | "strict": 0, 163 | "use-isnan": 2, 164 | "valid-jsdoc": 0, 165 | "valid-typeof": 2, 166 | "vars-on-top": 0, 167 | "wrap-iife": [2, "any"], 168 | "wrap-regex": 0, 169 | "yoda": [2, "never"] 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | .nyc_output 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | - "6" 5 | - "4" 6 | sudo: false 7 | script: npm run travis 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, C J Silverio 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jthooks 2 | 3 | Create a github webhook from the command line. Pronounced "ji-thooks", as one would say if pronouncing "githooks" with a soft G, instead of the more common hard-G "gih-thooks". 4 | 5 | [![on npm](http://img.shields.io/npm/v/jthooks.svg?style=flat)](https://www.npmjs.org/package/jthooks) [![Tests](http://img.shields.io/travis/ceejbot/jthooks.svg?style=flat)](http://travis-ci.org/ceejbot/jthooks) ![Coverage](http://img.shields.io/badge/coverage-71%25-red.svg?style=flat) [![Dependencies](http://img.shields.io/david/ceejbot/jthooks.svg?style=flat)](https://david-dm.org/ceejbot/jthooks) 6 | 7 | ## Usage 8 | 9 | First, create a Github oauth token that has permission to read & write webhooks. Full admin permission is not required. Keep a record of the token somewhere secure. 10 | 11 | ```shell 12 | jthooks [add|remove] user/repo https://example.com/hook shared-sekrit 13 | 14 | Commands: 15 | add add a hook to the given repo with the shared 16 | secret 17 | remove delete the given webhook; can pass id instead of 18 | url 19 | 20 | Options: 21 | --auth, -a auth token (can also be set in GITHUB_AUTH_TOKEN or 22 | GITHUB_API_TOKEN) 23 | --url, -u full URL of github API to use (optional) 24 | --quiet, -q only log errors 25 | --id id of existing hook to update (optional) 26 | --help show this help [boolean] 27 | 28 | Examples: 29 | jthooks add foo/bar https://example.com/hook sooper-sekrit -a auth-token add a webhook 30 | jthooks remove foo/bar https://example.com/hook remove a hook by url 31 | jthooks remove foo/bar 123456 remove a hook by id 32 | ``` 33 | 34 | If you want to update an existing webhook, run the script with `--id`. Otherwise the script will attempt to find an existing hook with the same url & update that in place. If no match is found, a hook is created. 35 | 36 | Set the `--url` option if you're not running against github.com but instead wish to change a repo on your Github Enterprise installation. 37 | 38 | ## TODO 39 | 40 | Delete a hook. 41 | 42 | More than merely cursory tests. 43 | 44 | ## License 45 | 46 | ISC; see included LICENSE file. 47 | -------------------------------------------------------------------------------- /hook-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /*eslint no-unused-vars: ["error", { "varsIgnorePattern": "argv" }]*/ 4 | 5 | var chalk = require('chalk'), 6 | Jthooks = require('./index'), 7 | url = require('url'), 8 | yargs = require('yargs') 9 | .command('add ', 'add a hook to the given repo with the shared secret', noop, addHook) 10 | .command('remove ', 'delete the given webhook; can pass id instead of url', noop, removeHook) 11 | .option('auth', { 12 | alias: 'a', 13 | description: 'auth token (can also be set in GITHUB_AUTH_TOKEN or GITHUB_API_TOKEN)', 14 | global: true, 15 | }) 16 | .option('url', { 17 | alias: 'u', 18 | description: 'full URL of github API to use (optional)', 19 | global: true 20 | }) 21 | .option('quiet', { 22 | alias: 'q', 23 | description: 'only log errors', 24 | global: true, 25 | }) 26 | .describe('id', 'id of existing hook to update (optional)') 27 | .usage('jthooks [add|remove] owner/repo https://example.com/hook shared-sekrit') 28 | .example('jthooks add foo/bar https://example.com/hook sooper-sekrit -a auth-token', 'add a webhook') 29 | .example('jthooks remove foo/bar https://example.com/hook', 'remove a hook by url') 30 | .example('jthooks remove foo/bar 123456', 'remove a hook by id') 31 | .help('help', 'show this help'); 32 | 33 | var argv = yargs.argv; 34 | 35 | function noop() { } 36 | 37 | function makeLogger(argv) 38 | { 39 | var logger = function(msg) { if (!argv.quiet) console.log(msg); }; 40 | return logger; 41 | } 42 | 43 | function create(argv) 44 | { 45 | var authtoken = process.env.GITHUB_AUTH_TOKEN || process.env.GITHUB_API_TOKEN || argv.auth; 46 | if (!authtoken) 47 | { 48 | yargs.showHelp(); 49 | process.exit(1); 50 | } 51 | 52 | var options = { 53 | auth: { 54 | type: 'oauth', 55 | token: authtoken 56 | } 57 | }; 58 | 59 | if (argv.url) 60 | { 61 | var api = url.parse(); 62 | options.host = api.host; 63 | options.prototol = api.protocol; 64 | options.pathPrefix = api.path; 65 | } 66 | 67 | var jthooks = new Jthooks(options); 68 | return jthooks; 69 | } 70 | 71 | function addHook(argv) 72 | { 73 | var jthooks = create(argv); 74 | var logger = makeLogger(argv); 75 | logger('Adding webhook to repo ' + chalk.blue(argv.repo) + '...'); 76 | 77 | var pieces = argv.repo.split('/'); 78 | var hookOpts = { 79 | owner: pieces[0], 80 | repo: pieces[1], 81 | secret: argv.secret, 82 | url: argv.hook, 83 | id: argv.id 84 | }; 85 | 86 | jthooks.setWebhook(hookOpts, function(err, isNew, result) 87 | { 88 | if (err) 89 | { 90 | console.log(chalk.red('Error!'), err.message); 91 | console.log(JSON.stringify(err)); 92 | process.exit(1); 93 | } 94 | 95 | logger('Hook id ' + chalk.green(result.id) + (isNew ? ' added' : ' updated') + '; test url is'); 96 | logger(chalk.green(result.test_url)); 97 | }); 98 | } 99 | 100 | function removeHook(argv) 101 | { 102 | var jthooks = create(argv); 103 | var logger = makeLogger(argv); 104 | logger('Removing webhook from repo ' + chalk.blue(argv.repo) + '...'); 105 | 106 | var pieces = argv.repo.split('/'); 107 | var opts = { 108 | owner: pieces[0], 109 | repo: pieces[1], 110 | }; 111 | 112 | if (typeof argv.hook === 'number' || !argv.hook.match(/^https?:\/\//)) 113 | opts.id = argv.hook; 114 | else 115 | opts.hook = argv.hook; 116 | 117 | jthooks.removeWebhook(opts, function(err, result) 118 | { 119 | if (err) 120 | { 121 | console.log(chalk.red('Error!'), err.message); 122 | console.log(JSON.stringify(err)); 123 | process.exit(1); 124 | } 125 | 126 | logger('Hook id ' + chalk.red(result.id) + ' removed'); 127 | }); 128 | } 129 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var 3 | isObject = require('lodash.isobject'), 4 | assert = require('assert'), 5 | Github = require('github') 6 | ; 7 | 8 | var Jthooks = module.exports = function Jthooks(opts) 9 | { 10 | assert(opts && isObject(opts), 'you must pass an options object to the constructor'); 11 | assert(opts.auth && isObject(opts.auth), 'you must pass an `auth` object option'); 12 | this.options = opts; 13 | 14 | this.client = new Github({ 15 | version: '3.0.0', 16 | timeout: 5000 17 | }); 18 | }; 19 | 20 | Jthooks.prototype.options = null; 21 | Jthooks.prototype.client = null; 22 | 23 | Jthooks.prototype.authenticate = function() 24 | { 25 | this.client.authenticate(this.options.auth); 26 | }; 27 | 28 | Jthooks.prototype.setWebhook = function(opts, callback) 29 | { 30 | var self = this; 31 | var func = (opts.id ? 'hook' : 'exists'); 32 | 33 | self[func](opts, function(err, hook) 34 | { 35 | if (err) return callback(err); 36 | if (hook) 37 | return self.update(hook, opts, callback); 38 | self.create(opts, callback); 39 | }); 40 | }; 41 | 42 | Jthooks.prototype.removeWebhook = function(opts, callback) 43 | { 44 | var self = this; 45 | 46 | if (opts.id) 47 | return self.remove(opts, callback); 48 | 49 | self.exists(opts, function(err, hook) 50 | { 51 | if (err) return callback(err); 52 | if (!hook) return callback(); 53 | var opts = { 54 | id: hook.id, 55 | repo: opts.repo, 56 | owner: opts.owner 57 | }; 58 | self.remove(opts, callback); 59 | }); 60 | }; 61 | 62 | Jthooks.prototype.update = function update(hook, opts, callback) 63 | { 64 | var newHook = { 65 | id: hook.id, 66 | repo: opts.repo, 67 | owner: opts.owner, 68 | name: hook.name, 69 | active: ('active' in opts ? opts.active : true), 70 | events: ('events' in opts ? opts.events : ['push']), 71 | config: { 72 | content_type: 'json', 73 | secret: opts.secret, 74 | url: opts.url 75 | }, 76 | }; 77 | 78 | this.authenticate(); 79 | this.client.repos.editHook(newHook, function(err, result) 80 | { 81 | callback(err, false, result); 82 | }); 83 | }; 84 | 85 | Jthooks.prototype.create = function create(opts, callback) 86 | { 87 | var hookOpts = { 88 | repo: opts.repo, 89 | owner: opts.owner, 90 | name: 'web', 91 | active: true, 92 | events: ['push'], 93 | config: { 94 | content_type: 'json', 95 | secret: opts.secret, 96 | url: opts.url 97 | }, 98 | }; 99 | 100 | this.authenticate(); 101 | this.client.repos.createHook(hookOpts, function(err, result) 102 | { 103 | callback(err, true, result); 104 | }); 105 | }; 106 | 107 | Jthooks.prototype.exists = function exists(opts, callback) 108 | { 109 | var self = this; 110 | 111 | self.hooks(opts, function(err, hooks) 112 | { 113 | if (err) return callback(err); 114 | for (var i = 0; i < hooks.length; i++) 115 | { 116 | var hook = hooks[i]; 117 | if (hook.config.url === opts.url) 118 | return callback(null, hook); 119 | // that check might need to be looser 120 | } 121 | 122 | callback(null, false); // not found 123 | }); 124 | }; 125 | 126 | Jthooks.prototype.hook = function hook(opts, callback) 127 | { 128 | this.authenticate(); 129 | this.client.repos.getHook(opts, callback); 130 | }; 131 | 132 | Jthooks.prototype.hooks = function hooks(opts, callback) 133 | { 134 | this.authenticate(); 135 | this.client.repos.getHooks(opts, callback); 136 | }; 137 | 138 | Jthooks.prototype.remove = function remove(opts, callback) 139 | { 140 | this.authenticate(); 141 | var hook = { 142 | id: opts.id, 143 | repo: opts.repo, 144 | owner: opts.owner, 145 | }; 146 | 147 | this.authenticate(); 148 | this.client.repos.deleteHook(hook, function(err, result) 149 | { 150 | callback(err, { id: opts.id }); 151 | }); 152 | }; 153 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jthooks", 3 | "description": "create a github webhook from the command-line", 4 | "version": "1.1.0", 5 | "author": "C J Silverio ", 6 | "bin": { 7 | "jthooks": "hook-cli.js" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/ceejbot/jthooks/issues" 11 | }, 12 | "dependencies": { 13 | "chalk": "~1.1.1", 14 | "github": "~6.0.1", 15 | "lodash.isobject": "~3.0.2", 16 | "yargs": "~6.3.0" 17 | }, 18 | "devDependencies": { 19 | "eslint-config-ceejbot": "~1.0.3", 20 | "mocha": "~3.1.2", 21 | "must": "~0.13.1", 22 | "nyc": "~8.3.2", 23 | "sinon": "~1.17.6", 24 | "xo": "~0.17.0" 25 | }, 26 | "homepage": "https://github.com/ceejbot/jthooks", 27 | "keywords": [ 28 | "cli", 29 | "github", 30 | "webhooks" 31 | ], 32 | "license": "ISC", 33 | "main": "index.js", 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/ceejbot/jthooks.git" 37 | }, 38 | "scripts": { 39 | "lint": "xo", 40 | "test": "nyc mocha -t 5000 --check-leaks --ui exports -R spec test", 41 | "travis": "nyc mocha -R spec -t 5000 test/test-*.js" 42 | }, 43 | "xo": { 44 | "extends": "eslint-config-ceejbot" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/test-01-jthooks.js: -------------------------------------------------------------------------------- 1 | /*global describe:true, it:true, before:true, after:true */ 2 | 'use strict'; 3 | 4 | var 5 | demand = require('must'), 6 | sinon = require('sinon'), 7 | Jthooks = require('../index') 8 | ; 9 | 10 | describe('jthooks', function() 11 | { 12 | var goodOpts = { auth: { type: 'oauth', token: 'deadbeef' } }; 13 | 14 | it('exports a single function', function(done) 15 | { 16 | Jthooks.must.be.a.function(); 17 | done(); 18 | }); 19 | 20 | it('requires an options object', function(done) 21 | { 22 | function shouldThrow() { return new Jthooks(); } 23 | shouldThrow.must.throw(/options/); 24 | done(); 25 | }); 26 | 27 | it('requires an auth option', function(done) 28 | { 29 | function shouldThrow() { return new Jthooks({}); } 30 | shouldThrow.must.throw(/auth/); 31 | done(); 32 | }); 33 | 34 | it('can be constructed', function(done) 35 | { 36 | var j = new Jthooks(goodOpts); 37 | j.must.be.instanceof(Jthooks); 38 | j.options.must.eql(goodOpts); 39 | done(); 40 | }); 41 | 42 | it('authenticate() calls authenticate on the client', function(done) 43 | { 44 | var j = new Jthooks(goodOpts); 45 | j.client.authenticate = sinon.spy(); 46 | j.authenticate(); 47 | j.client.authenticate.called.must.be.true(); 48 | j.client.authenticate.calledWith(goodOpts.auth).must.be.true(); 49 | done(); 50 | }); 51 | }); 52 | --------------------------------------------------------------------------------