right_hook 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in right_hook.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Mark Rushakoff
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # Right Hook
2
+
3
+ [![Build Status](https://travis-ci.org/mark-rushakoff/right_hook.png?branch=master)](https://travis-ci.org/mark-rushakoff/right_hook)
4
+ [![Code Climate](https://codeclimate.com/github/mark-rushakoff/right_hook.png)](https://codeclimate.com/github/mark-rushakoff/right_hook)
5
+ [![Coverage Status](https://coveralls.io/repos/mark-rushakoff/right_hook/badge.png)](https://coveralls.io/r/mark-rushakoff/right_hook)
6
+
7
+ Right Hook is a collection of tools to aid in setting up a web app to handle GitHub repo hooks.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'right_hook'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install right_hook
22
+
23
+ ## Usage
24
+
25
+ ### Your App
26
+
27
+ Create an application by subclassing `RightHook::App`:
28
+
29
+ ```ruby
30
+ # app.rb
31
+ require 'right_hook/app'
32
+
33
+ class MyApp < RightHook::App
34
+ # You must supply a secret for each repository and hook.
35
+ # The secret should only be known by GitHub; that's how we know the request is coming from GitHub's servers.
36
+ # (You'll specify that secret when you go through subscription.)
37
+ def secret(owner, repo_name, event_type)
38
+ if owner == 'octocat' && repo_name == 'Spoon-Fork' && event_type == 'pull_request'
39
+ 'qwertyuiop'
40
+ else
41
+ raise 'unrecognized!'
42
+ end
43
+ end
44
+
45
+ # Code to execute for GitHub's pull request hook.
46
+ # The secret has already been verified if your code is being called.
47
+ # See app.rb and spec/app/*_spec.rb for signatures and examples of the valid handlers.
48
+ def on_pull_request(owner, repo_name, action, number, pull_request_json)
49
+ message = <<-MSG
50
+ GitHub user #{pull_request_json['user']['login']} has opened pull request ##{number}
51
+ on repository #{owner}/#{repo_name}!
52
+ MSG
53
+ send_text_message(MY_PHONE_NUMBER, message) # or whatever you want
54
+ end
55
+ end
56
+ ```
57
+
58
+ You'll need to host your app online and hold on to the base URL so you can subscribe to hooks.
59
+
60
+ ### GitHub Authentication
61
+
62
+ To create hooks on GitHub repositories, you need to be authenticated as a collaborator on that repository.
63
+ GitHub's UI currently only supports configuring push hooks, so you'll want to authenticate through Right Hook to set up custom hooks.
64
+
65
+ Right Hook never stores your password.
66
+ He always uses OAuth tokens.
67
+ The only time he asks for your password is when he is creating a new token or listing existing tokens.
68
+
69
+ Right Hook doesn't store your tokens, either.
70
+ It's your duty to manage storage of tokens.
71
+
72
+ Here's one way you can generate and list tokens:
73
+
74
+ ```ruby
75
+ require 'right_hook/authenticator'
76
+
77
+ puts "Please enter your username:"
78
+ username = $stdin.gets
79
+
80
+ # Prompt the user for their password, without displaying it in the terminal
81
+ authenticator = RightHook::Client.interactive_build(username)
82
+
83
+ # Note for the token (this will be displayed in the user's settings on GitHub)
84
+ note = "Created in my awesome script"
85
+ authenticator.create_authorization(note)
86
+
87
+ authenticator.authorizations.each do |token|
88
+ puts "Token: #{auth.token}\nNote: #{auth.note}\n\n"
89
+ end
90
+ ```
91
+
92
+ ### Subscribing to Hooks
93
+
94
+ Right Hook provides a way to subscribe to hooks.
95
+ It's easy!
96
+
97
+ ```ruby
98
+ require 'right_hook/subscriber'
99
+
100
+ default_opts = {
101
+ base_url: "http://right-hook.example.com",
102
+ oauth_token: ENV['RIGHT_HOOK_TOKEN']
103
+ }
104
+
105
+ subscriber = RightHook::Subscriber.new(default_opts)
106
+
107
+ subscriber.subscribe(
108
+ owner: 'octocat',
109
+ repo_name: 'Hello-World',
110
+ event_type: 'pull_request',
111
+ secret: 'secret_for_hello_world'
112
+ )
113
+ ```
114
+
115
+ (For more details, consult the RDoc documentation.)
116
+
117
+ ## Contributing
118
+
119
+ 1. Fork it
120
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
121
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
122
+ 4. Push to the branch (`git push origin my-new-feature`)
123
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,43 @@
1
+ require 'sinatra/base'
2
+ require 'json'
3
+
4
+ module RightHook
5
+ class App < Sinatra::Base
6
+ KNOWN_EVENT_TYPES = %w(
7
+ pull_request
8
+ issue
9
+ )
10
+
11
+ post '/hook/:owner/:repo_name/:event_type' do
12
+ owner = params[:owner]
13
+ repo_name = params[:repo_name]
14
+ event_type = params[:event_type]
15
+ content = request.body.read
16
+
17
+ halt 404 unless KNOWN_EVENT_TYPES.include?(event_type)
18
+ halt 501 unless respond_to?("on_#{event_type}")
19
+
20
+ require_valid_signature(content, owner, repo_name, event_type)
21
+
22
+ json = JSON.parse(content)
23
+ case event_type
24
+ when 'pull_request'
25
+ on_pull_request(owner, repo_name, json['number'], json['action'], json['pull_request'])
26
+ when 'issue'
27
+ on_issue(owner, repo_name, json['action'], json['issue'])
28
+ else
29
+ halt 500
30
+ end
31
+ end
32
+
33
+ private
34
+ def require_valid_signature(content, owner, repo_name, event_type)
35
+ s = secret(owner, repo_name, event_type)
36
+ expected_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha1'), s, content)
37
+
38
+ # http://pubsubhubbub.googlecode.com/git/pubsubhubbub-core-0.4.html#authednotify
39
+ # "If the signature does not match, subscribers MUST still return a 2xx success response to acknowledge receipt, but locally ignore the message as invalid."
40
+ halt 202 unless request.env['X-Hub-Signature'] == "sha1=#{expected_signature}"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ require 'octokit'
2
+
3
+ module RightHook
4
+ # A base class for RightHook actors that interact with the GitHub API.
5
+ class AuthenticatedClient
6
+ # Create a new client, authenticating with the given OAuth token.
7
+ def initialize(token)
8
+ @client = Octokit::Client.new(access_token: token)
9
+ end
10
+
11
+ private
12
+ attr_reader :client
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ module RightHook
2
+ # The authenticator provides an interface to retrieving or creating GitHub authorizations.
3
+ class Authenticator
4
+ class << self
5
+ # Build a client with a username and an explicit password.
6
+ def build(username, password)
7
+ new(Octokit::Client.new(login: username, password: password))
8
+ end
9
+
10
+ # Prompt the user for their password (without displaying the entered keys).
11
+ # This approach is offered for convenience to make it easier to not store passwords on disk.
12
+ def interactive_build(username)
13
+ new(Octokit::Client.new(login: username, password: $stdin.noecho(&:gets).chomp))
14
+ end
15
+ end
16
+
17
+ # :nodoc:
18
+ def initialize(_client)
19
+ @_client = _client
20
+ end
21
+
22
+ # Create a new GitHub authorization with the given note.
23
+ # If one already exists with that note, it will not create a duplicate.
24
+ def create_authorization(note)
25
+ _client.create_authorization(scopes: %w(repo), note: note, idempotent: true).token
26
+ end
27
+
28
+ # Returns an array of all of the authorizations for the authenticated account.
29
+ def list_authorizations
30
+ _client.list_authorizations
31
+ end
32
+
33
+ private
34
+ attr_reader :_client
35
+ # Enforce use of build methods
36
+ private_class_method :new
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ require 'right_hook/authenticated_client'
2
+ require 'octokit'
3
+
4
+ module RightHook
5
+ # Provides an interface for adding comments on GitHub
6
+ class Commenter < AuthenticatedClient
7
+ def comment_on_issue(owner, repo_name, issue_number, comment)
8
+ client.add_comment("#{owner}/#{repo_name}", issue_number, comment)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,67 @@
1
+ require 'httparty'
2
+
3
+ module RightHook
4
+ # Subscriber can subscribe and unsubscribe GitHub hooks to a hosted instance of a specified {RightHook::App}.
5
+ # See the README for sample usage.
6
+ class Subscriber
7
+ # The base URL for the binding (where your {RightHook::App} is hosted).
8
+ attr_accessor :base_url
9
+
10
+ # The OAuth token to use for authenticating with GitHub.
11
+ # The token must belong to an account that has the +repo+ scope and collaborator privilege on the given repository.
12
+ attr_accessor :oauth_token
13
+
14
+ # The owner of the named repository.
15
+ attr_accessor :owner
16
+
17
+ # The event type of the hook.
18
+ # See http://developer.github.com/v3/repos/hooks/ for a complete list of valid types.
19
+ attr_accessor :event_type
20
+
21
+ # Initialize takes options which will be used as default values in other methods.
22
+ # The valid keys in the options are [+base_url+, +oauth_token+, +owner+, and +event_type+].
23
+ def initialize(default_opts = {})
24
+ @base_url = default_opts[:base_url]
25
+ @oauth_token = default_opts[:oauth_token]
26
+ @owner = default_opts[:owner]
27
+ @event_type = default_opts[:event_type]
28
+ end
29
+
30
+ # Subscribe an instance of {RightHook::App} hosted at +base_url+ to a hook for +owner+/+repo_name+, authenticating with +oauth_token+.
31
+ # +repo_name+ and +secret+ are required options and they are intentionally not stored as defaults on the +Subscriber+ instance.
32
+ # @return [bool success] Whether the request was successful.
33
+ def subscribe(opts)
34
+ hub_request_with_mode('subscribe', opts)
35
+ end
36
+
37
+ # Unsubscribe an instance of {RightHook::App} hosted at +base_url+ to a hook for +owner+/+repo_name+, authenticating with +oauth_token+.
38
+ # +repo_name+ and +secret+ are required options and they are intentionally not stored as defaults on the +Subscriber+ instance.
39
+ # (NB: It's possible that GitHub's API *doesn't* require secret; I haven't checked.)
40
+ # @return [bool success] Whether the request was successful.
41
+ def unsubscribe(opts)
42
+ hub_request_with_mode('unsubscribe', opts)
43
+ end
44
+
45
+ private
46
+ def hub_request_with_mode(mode, opts)
47
+ repo_name = opts.fetch(:repo_name) # explicitly not defaulted
48
+ secret = opts.fetch(:secret) # explicitly not defaulted
49
+ oauth_token = opts.fetch(:oauth_token) { self.oauth_token }
50
+ owner = opts.fetch(:owner) { self.owner }
51
+ base_url = opts.fetch(:base_url) { self.base_url }
52
+
53
+ HTTParty.post('https://api.github.com/hub',
54
+ headers: {
55
+ # http://developer.github.com/v3/#authentication
56
+ 'Authorization' => "token #{oauth_token}"
57
+ },
58
+ body: {
59
+ 'hub.mode' => mode,
60
+ 'hub.topic' => "https://github.com/#{owner}/#{repo_name}/events/#{event_type}",
61
+ 'hub.callback' => "#{base_url}/hook/#{owner}/#{repo_name}/#{event_type}",
62
+ 'hub.secret' => secret
63
+ }
64
+ ).success?
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,3 @@
1
+ module RightHook
2
+ VERSION = "0.2.0"
3
+ end
data/lib/right_hook.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "right_hook/version"
2
+
3
+ module RightHook
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'right_hook/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "right_hook"
8
+ spec.version = RightHook::VERSION
9
+ spec.authors = ["Mark Rushakoff"]
10
+ spec.email = ["mark.rushakoff@gmail.com"]
11
+ spec.description = %q{A simple sinatra app ready to go for GitHub service hooks.}
12
+ spec.summary = %q{Right Hook is a foundation to use when you just want to write a GitHub service hook.}
13
+ spec.homepage = "https://github.com/mark-rushakoff/right_hook"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec", "~> 2.14"
24
+ spec.add_development_dependency "rack-test"
25
+ spec.add_development_dependency "webmock", "~> 1.13"
26
+ spec.add_development_dependency "coveralls"
27
+
28
+ spec.add_runtime_dependency "sinatra", "~> 1.4"
29
+ spec.add_runtime_dependency "httparty", "~> 0.11.0"
30
+ spec.add_runtime_dependency "octokit", "~> 2.2"
31
+ end
@@ -0,0 +1,113 @@
1
+ require 'spec_helper'
2
+ require 'rack/test'
3
+
4
+ describe RightHook::App do
5
+ describe 'Issues' do
6
+ include Rack::Test::Methods
7
+
8
+ class IssueApp < RightHook::App
9
+ class << self
10
+ attr_accessor :owner, :repo_name, :action, :issue_json
11
+ end
12
+
13
+ def on_issue(owner, repo_name, action, issue_json)
14
+ self.class.owner = owner
15
+ self.class.repo_name = repo_name
16
+ self.class.action = action
17
+ self.class.issue_json = issue_json
18
+ end
19
+
20
+ def secret(owner, repo_name, event_type)
21
+ 'issue' if owner == 'mark-rushakoff' && repo_name == 'right_hook' && event_type == 'issue'
22
+ end
23
+ end
24
+
25
+ def app
26
+ IssueApp
27
+ end
28
+
29
+ before do
30
+ app.owner = app.repo_name = app.action = app.issue_json = nil
31
+ end
32
+
33
+ it 'captures the interesting data' do
34
+ post '/hook/mark-rushakoff/right_hook/issue', ISSUE_JSON, generate_secret_header('issue', ISSUE_JSON)
35
+ expect(last_response.status).to eq(200)
36
+ expect(app.owner).to eq('mark-rushakoff')
37
+ expect(app.repo_name).to eq('right_hook')
38
+ expect(app.action).to eq('opened')
39
+
40
+ # if it has one key it probably has them all
41
+ expect(app.issue_json['title']).to eq('Found a bug')
42
+ end
43
+
44
+ it 'fails when the secret is wrong' do
45
+ post '/hook/mark-rushakoff/right_hook/issue', ISSUE_JSON, generate_secret_header('wrong', ISSUE_JSON)
46
+ expect(last_response.status).to eq(202)
47
+ expect(app.owner).to be_nil
48
+ end
49
+ end
50
+ end
51
+
52
+ # from http://developer.github.com/v3/issues/#get-a-single-issue
53
+ ISSUE_JSON = <<-JSON
54
+ {
55
+ "action": "opened",
56
+ "issue": {
57
+ "url": "https://api.github.com/repos/octocat/Hello-World/issues/1347",
58
+ "html_url": "https://github.com/octocat/Hello-World/issues/1347",
59
+ "number": 1347,
60
+ "state": "open",
61
+ "title": "Found a bug",
62
+ "body": "I'm having a problem with this.",
63
+ "user": {
64
+ "login": "octocat",
65
+ "id": 1,
66
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
67
+ "gravatar_id": "somehexcode",
68
+ "url": "https://api.github.com/users/octocat"
69
+ },
70
+ "labels": [
71
+ {
72
+ "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug",
73
+ "name": "bug",
74
+ "color": "f29513"
75
+ }
76
+ ],
77
+ "assignee": {
78
+ "login": "octocat",
79
+ "id": 1,
80
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
81
+ "gravatar_id": "somehexcode",
82
+ "url": "https://api.github.com/users/octocat"
83
+ },
84
+ "milestone": {
85
+ "url": "https://api.github.com/repos/octocat/Hello-World/milestones/1",
86
+ "number": 1,
87
+ "state": "open",
88
+ "title": "v1.0",
89
+ "description": "",
90
+ "creator": {
91
+ "login": "octocat",
92
+ "id": 1,
93
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
94
+ "gravatar_id": "somehexcode",
95
+ "url": "https://api.github.com/users/octocat"
96
+ },
97
+ "open_issues": 4,
98
+ "closed_issues": 8,
99
+ "created_at": "2011-04-10T20:09:31Z",
100
+ "due_on": null
101
+ },
102
+ "comments": 0,
103
+ "pull_request": {
104
+ "html_url": "https://github.com/octocat/Hello-World/pull/1347",
105
+ "diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff",
106
+ "patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch"
107
+ },
108
+ "closed_at": null,
109
+ "created_at": "2011-04-22T13:33:48Z",
110
+ "updated_at": "2011-04-22T13:33:48Z"
111
+ }
112
+ }
113
+ JSON
@@ -0,0 +1,205 @@
1
+ require 'spec_helper'
2
+ require 'rack/test'
3
+
4
+ describe RightHook::App do
5
+ describe 'Pull requests' do
6
+ include Rack::Test::Methods
7
+
8
+ class PullRequestApp < RightHook::App
9
+ class << self
10
+ attr_accessor :owner, :repo_name, :action, :number, :pull_request_json
11
+ end
12
+
13
+ def on_pull_request(owner, repo_name, number, action, pull_request_json)
14
+ self.class.owner = owner
15
+ self.class.repo_name = repo_name
16
+ self.class.action = action
17
+ self.class.number = number
18
+ self.class.pull_request_json = pull_request_json
19
+ end
20
+
21
+ def secret(owner, repo_name, event_type)
22
+ 'pull_request' if owner == 'mark-rushakoff' && repo_name == 'right_hook' && event_type == 'pull_request'
23
+ end
24
+ end
25
+
26
+ def app
27
+ PullRequestApp
28
+ end
29
+
30
+ before do
31
+ app.owner = app.repo_name = app.action = app.number = app.pull_request_json = nil
32
+ end
33
+
34
+ it 'captures the interesting data' do
35
+ post '/hook/mark-rushakoff/right_hook/pull_request', PULL_REQUEST_JSON, generate_secret_header('pull_request', PULL_REQUEST_JSON)
36
+ expect(last_response.status).to eq(200)
37
+ expect(app.owner).to eq('mark-rushakoff')
38
+ expect(app.repo_name).to eq('right_hook')
39
+ expect(app.action).to eq('opened')
40
+ expect(app.number).to eq(1)
41
+
42
+ # if it has one key it probably has them all
43
+ expect(app.pull_request_json['body']).to eq('Please pull these awesome changes')
44
+ end
45
+
46
+ it 'fails when the secret is wrong' do
47
+ post '/hook/mark-rushakoff/right_hook/pull_request', PULL_REQUEST_JSON, generate_secret_header('wrong', PULL_REQUEST_JSON)
48
+ expect(last_response.status).to eq(202)
49
+ expect(app.owner).to be_nil
50
+ end
51
+ end
52
+ end
53
+
54
+ # from http://developer.github.com/v3/pulls/#get-a-single-pull-request
55
+ PULL_REQUEST_JSON = <<-JSON
56
+ {
57
+ "action": "opened",
58
+ "number": 1,
59
+ "pull_request": {
60
+ "url": "https://api.github.com/octocat/Hello-World/pulls/1",
61
+ "html_url": "https://github.com/octocat/Hello-World/pull/1",
62
+ "diff_url": "https://github.com/octocat/Hello-World/pulls/1.diff",
63
+ "patch_url": "https://github.com/octocat/Hello-World/pulls/1.patch",
64
+ "issue_url": "https://github.com/octocat/Hello-World/issue/1",
65
+ "number": 1,
66
+ "state": "open",
67
+ "title": "new-feature",
68
+ "body": "Please pull these awesome changes",
69
+ "created_at": "2011-01-26T19:01:12Z",
70
+ "updated_at": "2011-01-26T19:01:12Z",
71
+ "closed_at": "2011-01-26T19:01:12Z",
72
+ "merged_at": "2011-01-26T19:01:12Z",
73
+ "head": {
74
+ "label": "new-topic",
75
+ "ref": "new-topic",
76
+ "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
77
+ "user": {
78
+ "login": "octocat",
79
+ "id": 1,
80
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
81
+ "gravatar_id": "somehexcode",
82
+ "url": "https://api.github.com/users/octocat"
83
+ },
84
+ "repo": {
85
+ "id": 1296269,
86
+ "owner": {
87
+ "login": "octocat",
88
+ "id": 1,
89
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
90
+ "gravatar_id": "somehexcode",
91
+ "url": "https://api.github.com/users/octocat"
92
+ },
93
+ "name": "Hello-World",
94
+ "full_name": "octocat/Hello-World",
95
+ "description": "This your first repo!",
96
+ "private": false,
97
+ "fork": false,
98
+ "url": "https://api.github.com/repos/octocat/Hello-World",
99
+ "html_url": "https://github.com/octocat/Hello-World",
100
+ "clone_url": "https://github.com/octocat/Hello-World.git",
101
+ "git_url": "git://github.com/octocat/Hello-World.git",
102
+ "ssh_url": "git@github.com:octocat/Hello-World.git",
103
+ "svn_url": "https://svn.github.com/octocat/Hello-World",
104
+ "mirror_url": "git://git.example.com/octocat/Hello-World",
105
+ "homepage": "https://github.com",
106
+ "language": null,
107
+ "forks": 9,
108
+ "forks_count": 9,
109
+ "watchers": 80,
110
+ "watchers_count": 80,
111
+ "size": 108,
112
+ "master_branch": "master",
113
+ "open_issues": 0,
114
+ "open_issues_count": 0,
115
+ "pushed_at": "2011-01-26T19:06:43Z",
116
+ "created_at": "2011-01-26T19:01:12Z",
117
+ "updated_at": "2011-01-26T19:14:43Z"
118
+ }
119
+ },
120
+ "base": {
121
+ "label": "master",
122
+ "ref": "master",
123
+ "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
124
+ "user": {
125
+ "login": "octocat",
126
+ "id": 1,
127
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
128
+ "gravatar_id": "somehexcode",
129
+ "url": "https://api.github.com/users/octocat"
130
+ },
131
+ "repo": {
132
+ "id": 1296269,
133
+ "owner": {
134
+ "login": "octocat",
135
+ "id": 1,
136
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
137
+ "gravatar_id": "somehexcode",
138
+ "url": "https://api.github.com/users/octocat"
139
+ },
140
+ "name": "Hello-World",
141
+ "full_name": "octocat/Hello-World",
142
+ "description": "This your first repo!",
143
+ "private": false,
144
+ "fork": false,
145
+ "url": "https://api.github.com/repos/octocat/Hello-World",
146
+ "html_url": "https://github.com/octocat/Hello-World",
147
+ "clone_url": "https://github.com/octocat/Hello-World.git",
148
+ "git_url": "git://github.com/octocat/Hello-World.git",
149
+ "ssh_url": "git@github.com:octocat/Hello-World.git",
150
+ "svn_url": "https://svn.github.com/octocat/Hello-World",
151
+ "mirror_url": "git://git.example.com/octocat/Hello-World",
152
+ "homepage": "https://github.com",
153
+ "language": null,
154
+ "forks": 9,
155
+ "forks_count": 9,
156
+ "watchers": 80,
157
+ "watchers_count": 80,
158
+ "size": 108,
159
+ "master_branch": "master",
160
+ "open_issues": 0,
161
+ "open_issues_count": 0,
162
+ "pushed_at": "2011-01-26T19:06:43Z",
163
+ "created_at": "2011-01-26T19:01:12Z",
164
+ "updated_at": "2011-01-26T19:14:43Z"
165
+ }
166
+ },
167
+ "_links": {
168
+ "self": {
169
+ "href": "https://api.github.com/octocat/Hello-World/pulls/1"
170
+ },
171
+ "html": {
172
+ "href": "https://github.com/octocat/Hello-World/pull/1"
173
+ },
174
+ "comments": {
175
+ "href": "https://api.github.com/octocat/Hello-World/issues/1/comments"
176
+ },
177
+ "review_comments": {
178
+ "href": "https://api.github.com/octocat/Hello-World/pulls/1/comments"
179
+ }
180
+ },
181
+ "user": {
182
+ "login": "octocat",
183
+ "id": 1,
184
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
185
+ "gravatar_id": "somehexcode",
186
+ "url": "https://api.github.com/users/octocat"
187
+ },
188
+ "merge_commit_sha": "e5bd3914e2e596debea16f433f57875b5b90bcd6",
189
+ "merged": false,
190
+ "mergeable": true,
191
+ "merged_by": {
192
+ "login": "octocat",
193
+ "id": 1,
194
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
195
+ "gravatar_id": "somehexcode",
196
+ "url": "https://api.github.com/users/octocat"
197
+ },
198
+ "comments": 10,
199
+ "commits": 3,
200
+ "additions": 100,
201
+ "deletions": 3,
202
+ "changed_files": 5
203
+ }
204
+ }
205
+ JSON
data/spec/app_spec.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+ require 'rack/test'
3
+
4
+ describe RightHook::App do
5
+ include Rack::Test::Methods
6
+
7
+ class BareApp < RightHook::App
8
+ def secret(owner, repo_name, event_type)
9
+ ''
10
+ end
11
+ end
12
+
13
+ def app
14
+ BareApp
15
+ end
16
+
17
+ it 'is status 501 for a non-implemented hook' do
18
+ post '/hook/mark-rushakoff/right_hook/issue', '{}', generate_secret_header('secret', '{}')
19
+ expect(last_response.status).to eq(501)
20
+ end
21
+
22
+ it 'is 404 for an unknown hook' do
23
+ post '/hook/mark-rushakoff/right_hook/foobar', '{}', generate_secret_header('secret', '{}')
24
+ expect(last_response.status).to eq(404)
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe RightHook::AuthenticatedClient do
4
+ describe '.new' do
5
+ it 'creates an Octokit client with the given token' do
6
+ Octokit::Client.should_receive(:new).with(access_token: 'the_token')
7
+
8
+ described_class.new('the_token')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require 'octokit'
3
+
4
+ describe RightHook::Authenticator do
5
+ describe '.build' do
6
+ it 'delegates to Octokit' do
7
+ Octokit::Client.should_receive(:new).with(login: 'octocat', password: 'pass')
8
+
9
+ described_class.build('octocat', 'pass')
10
+ end
11
+ end
12
+
13
+ describe '.interactive_build' do
14
+ it 'delegates to Octokit and stdin.noecho' do
15
+ $stdin.should_receive(:noecho).and_return("pass\n")
16
+ Octokit::Client.should_receive(:new).with(login: 'octocat', password: 'pass')
17
+
18
+ described_class.interactive_build('octocat')
19
+ end
20
+ end
21
+
22
+ describe '#create_authorization' do
23
+ it 'delegates to create_authorization' do
24
+ Octokit::Client.any_instance.should_receive(:create_authorization).with(scopes: %w(repo), note: 'test note', idempotent: true).and_return(OpenStruct.new(token: 'my_token'))
25
+
26
+ expect(described_class.build('octocat', 'pass').create_authorization('test note')).to eq('my_token')
27
+ end
28
+ end
29
+
30
+ describe '#list_authorizations' do
31
+ it 'delegates to list_authorizations' do
32
+ Octokit::Client.any_instance.should_receive(:list_authorizations).and_return(:the_authorizations)
33
+
34
+ expect(described_class.build('octocat', 'pass').list_authorizations).to eq(:the_authorizations)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe RightHook do
4
+ it 'should have a version number' do
5
+ RightHook::VERSION.should_not be_nil
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe RightHook::Commenter do
4
+ subject(:commenter) do
5
+ described_class.new('a_token')
6
+ end
7
+
8
+ describe '.comment_on_issue' do
9
+ it 'delegates to Octokit' do
10
+ Octokit::Client.any_instance.should_receive(:add_comment).with('octocat/Spoon-Fork', 100, "Wow, 100!")
11
+
12
+ commenter.comment_on_issue("octocat", "Spoon-Fork", 100, "Wow, 100!")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'right_hook'
3
+ require 'right_hook/app'
4
+ require 'right_hook/authenticated_client'
5
+ require 'right_hook/authenticator'
6
+ require 'right_hook/commenter'
7
+ require 'right_hook/subscriber'
8
+
9
+ require_relative './support/spec_helpers.rb'
10
+
11
+ require 'webmock/rspec'
12
+
13
+ require 'coveralls'
14
+ Coveralls.wear!
15
+
16
+ RSpec.configure do |c|
17
+ c.include RightHook::SpecHelpers
18
+ end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ describe RightHook::Subscriber do
4
+ subject(:subscriber) do
5
+ described_class.new(
6
+ oauth_token: 'my_token',
7
+ owner: 'mark-rushakoff',
8
+ base_url: 'http://example.com',
9
+ event_type: 'issue',
10
+ )
11
+ end
12
+
13
+ describe '.subscribe' do
14
+ let!(:stubbed_request) do
15
+ stub_request(:post, 'https://api.github.com/hub').
16
+ with(:body => 'hub.mode=subscribe&hub.topic=https%3A%2F%2Fgithub.com%2Fmark-rushakoff%2Fright_hook%2Fevents%2Fissue&hub.callback=http%3A%2F%2Fexample.com%2Fhook%2Fmark-rushakoff%2Fright_hook%2Fissue&hub.secret=the-secret',
17
+ :headers => {'Authorization' => 'token my_token'}
18
+ ).to_return(:status => status_code, :body => '', :headers => {})
19
+ end
20
+
21
+ context 'When the request succeeds' do
22
+ let(:status_code) { 200 }
23
+ it 'returns true' do
24
+ result = subscriber.subscribe(repo_name: 'right_hook', secret: 'the-secret')
25
+ expect(result).to eq(true)
26
+
27
+ expect(stubbed_request).to have_been_requested
28
+ end
29
+ end
30
+
31
+ context 'When the request fails' do
32
+ let(:status_code) { 404 }
33
+ it 'returns false' do
34
+ result = subscriber.subscribe(repo_name: 'right_hook', secret: 'the-secret')
35
+ expect(result).to eq(false)
36
+
37
+ expect(stubbed_request).to have_been_requested
38
+ end
39
+ end
40
+ end
41
+
42
+ describe '.unsubscribe' do
43
+ let!(:stubbed_request) do
44
+ stub_request(:post, 'https://api.github.com/hub').
45
+ with(:body => 'hub.mode=unsubscribe&hub.topic=https%3A%2F%2Fgithub.com%2Fmark-rushakoff%2Fright_hook%2Fevents%2Fissue&hub.callback=http%3A%2F%2Fexample.com%2Fhook%2Fmark-rushakoff%2Fright_hook%2Fissue&hub.secret=the-secret',
46
+ :headers => {'Authorization' => 'token my_token'}
47
+ ).to_return(:status => status_code, :body => '', :headers => {})
48
+ end
49
+
50
+
51
+ context 'When the request succeeds' do
52
+ let(:status_code) { 200 }
53
+ it 'returns true' do
54
+ result = subscriber.unsubscribe(repo_name: 'right_hook', secret: 'the-secret')
55
+ expect(result).to eq(true)
56
+
57
+ expect(stubbed_request).to have_been_requested
58
+ end
59
+ end
60
+
61
+ context 'When the request fails' do
62
+ let(:status_code) { 404 }
63
+ it 'returns false' do
64
+ result = subscriber.unsubscribe(repo_name: 'right_hook', secret: 'the-secret')
65
+ expect(result).to eq(false)
66
+
67
+ expect(stubbed_request).to have_been_requested
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,10 @@
1
+ require 'openssl'
2
+
3
+ module RightHook
4
+ module SpecHelpers
5
+ def generate_secret_header(secret, body)
6
+ sha = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha1'), secret, body)
7
+ {'X-Hub-Signature' => "sha1=#{sha}"}
8
+ end
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,227 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: right_hook
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mark Rushakoff
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-09-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '2.14'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2.14'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rack-test
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: webmock
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '1.13'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: '1.13'
94
+ - !ruby/object:Gem::Dependency
95
+ name: coveralls
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: sinatra
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: '1.4'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: '1.4'
126
+ - !ruby/object:Gem::Dependency
127
+ name: httparty
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ~>
132
+ - !ruby/object:Gem::Version
133
+ version: 0.11.0
134
+ type: :runtime
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ~>
140
+ - !ruby/object:Gem::Version
141
+ version: 0.11.0
142
+ - !ruby/object:Gem::Dependency
143
+ name: octokit
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ~>
148
+ - !ruby/object:Gem::Version
149
+ version: '2.2'
150
+ type: :runtime
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ~>
156
+ - !ruby/object:Gem::Version
157
+ version: '2.2'
158
+ description: A simple sinatra app ready to go for GitHub service hooks.
159
+ email:
160
+ - mark.rushakoff@gmail.com
161
+ executables: []
162
+ extensions: []
163
+ extra_rdoc_files: []
164
+ files:
165
+ - .gitignore
166
+ - .rspec
167
+ - .travis.yml
168
+ - Gemfile
169
+ - LICENSE.txt
170
+ - README.md
171
+ - Rakefile
172
+ - lib/right_hook.rb
173
+ - lib/right_hook/app.rb
174
+ - lib/right_hook/authenticated_client.rb
175
+ - lib/right_hook/authenticator.rb
176
+ - lib/right_hook/commenter.rb
177
+ - lib/right_hook/subscriber.rb
178
+ - lib/right_hook/version.rb
179
+ - right_hook.gemspec
180
+ - spec/app/issue_spec.rb
181
+ - spec/app/pull_request_spec.rb
182
+ - spec/app_spec.rb
183
+ - spec/authenticated_client_spec.rb
184
+ - spec/authenticator_spec.rb
185
+ - spec/captain_hook_spec.rb
186
+ - spec/commenter_spec.rb
187
+ - spec/spec_helper.rb
188
+ - spec/subscriber_spec.rb
189
+ - spec/support/spec_helpers.rb
190
+ homepage: https://github.com/mark-rushakoff/right_hook
191
+ licenses:
192
+ - MIT
193
+ post_install_message:
194
+ rdoc_options: []
195
+ require_paths:
196
+ - lib
197
+ required_ruby_version: !ruby/object:Gem::Requirement
198
+ none: false
199
+ requirements:
200
+ - - ! '>='
201
+ - !ruby/object:Gem::Version
202
+ version: '0'
203
+ required_rubygems_version: !ruby/object:Gem::Requirement
204
+ none: false
205
+ requirements:
206
+ - - ! '>='
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ requirements: []
210
+ rubyforge_project:
211
+ rubygems_version: 1.8.23
212
+ signing_key:
213
+ specification_version: 3
214
+ summary: Right Hook is a foundation to use when you just want to write a GitHub service
215
+ hook.
216
+ test_files:
217
+ - spec/app/issue_spec.rb
218
+ - spec/app/pull_request_spec.rb
219
+ - spec/app_spec.rb
220
+ - spec/authenticated_client_spec.rb
221
+ - spec/authenticator_spec.rb
222
+ - spec/captain_hook_spec.rb
223
+ - spec/commenter_spec.rb
224
+ - spec/spec_helper.rb
225
+ - spec/subscriber_spec.rb
226
+ - spec/support/spec_helpers.rb
227
+ has_rdoc: