├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── Procfile ├── README.md ├── config.ru ├── script ├── bootstrap └── server └── server.rb /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'sinatra' 4 | gem 'octokit' 5 | gem 'dotenv' 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.8) 5 | dotenv (2.0.2) 6 | faraday (0.9.2) 7 | multipart-post (>= 1.2, < 3) 8 | multipart-post (2.0.0) 9 | octokit (4.1.1) 10 | sawyer (~> 0.6.0, >= 0.5.3) 11 | rack (1.6.4) 12 | rack-protection (1.5.3) 13 | rack 14 | sawyer (0.6.0) 15 | addressable (~> 2.3.5) 16 | faraday (~> 0.8, < 0.10) 17 | sinatra (1.4.6) 18 | rack (~> 1.4) 19 | rack-protection (~> 1.4) 20 | tilt (>= 1.3, < 3) 21 | tilt (2.0.1) 22 | 23 | PLATFORMS 24 | ruby 25 | 26 | DEPENDENCIES 27 | dotenv 28 | octokit 29 | sinatra 30 | 31 | BUNDLED WITH 32 | 1.10.6 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ben Balter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec rackup -p$PORT 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Issue Shadower 2 | 3 | A webhook receiver to mirror GitHub.com issues to private GitHub Enterprise forks. 4 | 5 | ## How it works 6 | 7 | Lets say you have a repository on GitHub.com, called `benbalter/awesome-project`, and a GitHub Enterprise instance at `github.benbalter.com`. 8 | 9 | Almost without exception, [you can, and should, work in the open, ensuring that all stakeholders, both those on the inside and those on the outside of your firewall are on equal footing](http://ben.balter.com/2015/03/08/open-source-best-practices-internal-collaboration/#minimize-information-imbalance). Sometimes that's not possible. That's where Issue Shadower comes in. 10 | 11 | Issue Shadower will open an issue in the corresponding repository on your GitHub Enterprise instance, each time an issue is opened on GitHub.com. Let's say someone opens an issue on `benbalter/awesome-project`. Issue Shadower will open an issue on `github.benbalter.com/benbalter/awesome-project`, and link back to the original issue. That way you can discuss the issue internally (such as weighing any security concerns), before replying publicly. 12 | 13 | ## Usage 14 | 15 | Issue Shadower is tiny Sinatra app designed to run on services like Heroku. You'll need to do two things, configure the server and configure the webhook on GitHub. 16 | 17 | ### Configure the server 18 | 19 | You need a Ruby server with the following environmental variables: 20 | 21 | * `GITHUB_TOKEN` - A personal access token of a bot account 22 | * `GITHUB_HOOK_SECRET` - Secret shared with webhook to authenticate payload 23 | * `GITHUB_HOST` - The URL to your GitHub Enterprise instance 24 | 25 | If not using a service like Heroku, you can start the server with the `script/server` command. 26 | 27 | ### Configure the webhook 28 | Navigate to the repository's settings, and create a new webhook with the following settings: 29 | 30 | - URL: `[SERVER URL]/payload` 31 | - Content Type: `application/json` 32 | - Secret: Your shared secret (`GITHUB_HOOK_SECRET`) 33 | - Select "let me select individual events" and check only the "issues" events 34 | 35 | ## Running locally 36 | 1. `script/bootstrap` 37 | 2. `script/server` 38 | 39 | You'll also probably want to [install ngrok](https://developer.github.com/webhooks/configuring/#using-ngrok) to test the hooks locally. 40 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require './server' 2 | run IssueShadower 3 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | bundle install 2 | -------------------------------------------------------------------------------- /script/server: -------------------------------------------------------------------------------- 1 | bundle exec rackup 2 | -------------------------------------------------------------------------------- /server.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'octokit' 3 | require 'json' 4 | 5 | class IssueShadower < Sinatra::Base 6 | def client 7 | @client ||= Octokit::Client.new({ 8 | access_token: ENV["GITHUB_TOKEN"], 9 | api_endpoint: "#{ENV["GITHUB_HOST"]}/api/v3/" 10 | }) 11 | end 12 | 13 | def payload 14 | request.body.rewind 15 | JSON.parse(request.body.read) 16 | end 17 | 18 | def signature_valid? 19 | return false unless request.body && request.env['HTTP_X_HUB_SIGNATURE'] 20 | digest = OpenSSL::Digest.new('sha1') 21 | signature = 'sha1=' + OpenSSL::HMAC.hexdigest(digest, ENV['GITHUB_HOOK_SECRET'], request.body.read) 22 | Rack::Utils.secure_compare signature, request.env['HTTP_X_HUB_SIGNATURE'] 23 | end 24 | 25 | def repo 26 | payload["repository"]["full_name"] 27 | end 28 | 29 | def issue 30 | # client.issue repo, payload["issue"]["number"] 31 | payload["issue"] 32 | end 33 | 34 | post "/payload" do 35 | halt 409 unless signature_valid? 36 | halt 200 unless issue && payload["action"] == "opened" 37 | client.create_issue repo, issue["title"], "Shaddow issue for #{issue['html_url']}\n---\n" + issue["body"] 38 | halt 201 39 | end 40 | end 41 | --------------------------------------------------------------------------------