issue 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/LICENSE +21 -0
- data/README.md +88 -2
- data/lib/issue/error.rb +15 -0
- data/lib/issue/payload.rb +123 -0
- data/lib/issue/version.rb +1 -1
- data/lib/issue/webhook.rb +102 -0
- data/lib/issue.rb +4 -3
- metadata +39 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a0d24902085e9ef25c6765748362b06f5015bbd88848db03513c0cd7f33ce9c
|
4
|
+
data.tar.gz: '099f1cce4e61d95ab0ca87a6c057f59c5b4bae50e2b59cca25df9e334d3b1c39'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d5a4bcfa1cb46b5b6a78184e235743cc846f213448c208e0f0da5345c8635e2c635437d57ad66d186f07feb63b5bd0bbcddfae35b42e9e2873d65b4525e8be7f
|
7
|
+
data.tar.gz: 81f10062dc2f189ea2ca8e891139718048a24072ec7d9168391a9ce5eaee5baaf5f6d0359c36cc0cb323fd717821989bb5941a194a6b8c4c3f08159522aeb661
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
|
4
|
+
## 1.0.0 (2021-12-17)
|
5
|
+
|
6
|
+
- Webhook can discard requests by multiple sender/events pairs
|
7
|
+
|
8
|
+
## 0.3.0 (2021-10-04)
|
9
|
+
|
10
|
+
- Add comment_id to Payload's context
|
11
|
+
|
12
|
+
## 0.2.0 (2021-07-06)
|
13
|
+
|
14
|
+
- Added support for `pull_request` type events
|
15
|
+
|
16
|
+
## 0.1.0 (2021-07-01)
|
17
|
+
|
18
|
+
- Added initial functionality and classes
|
19
|
+
|
20
|
+
- Issue::WebHook
|
21
|
+
- Issue::Payload
|
22
|
+
- Issue::Error
|
23
|
+
|
24
|
+
## 0.0.1 (2021-06-29)
|
25
|
+
|
26
|
+
- Gem created
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Juanjo Bazán
|
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.
|
data/README.md
CHANGED
@@ -1,2 +1,88 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# Issue
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/issue.svg)](https://badge.fury.io/rb/issue)
|
3
|
+
[![Tests](https://github.com/xuanxu/issue/actions/workflows/tests.yml/badge.svg)](https://github.com/xuanxu/issue/actions/workflows/tests.yml)
|
4
|
+
|
5
|
+
Issue is a small library dedicated to parse requests coming from GitHub webhooks triggered by `issues`, `issue_comment` and `pull_request` events.
|
6
|
+
|
7
|
+
## Getting started
|
8
|
+
|
9
|
+
Depending on your project you may:
|
10
|
+
|
11
|
+
Install the gem:
|
12
|
+
```bash
|
13
|
+
$ gem install issue
|
14
|
+
```
|
15
|
+
or add it to your Gemfile:
|
16
|
+
```ruby
|
17
|
+
gem 'issue'
|
18
|
+
```
|
19
|
+
|
20
|
+
Require the gem with:
|
21
|
+
```ruby
|
22
|
+
require 'issue'
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
The `Issue::Webhook` is used to declare and parse a GitHub webhook. At initialization it accepts a hash of configuration settings:
|
28
|
+
|
29
|
+
- **secret_token**: The GitHub secret access token for authorization
|
30
|
+
- **origin**: The repository to accept payloads from. If nil any origin will be accepted. If not nil any request from a different repository will be ignored
|
31
|
+
- **discard_sender**: The GitHub handle of a user whose events will be ignored. Usually the organization bot. If nil no user will be ignored. To ignore only specific events use a Hash where keys are usernames and values are arrays of events to ignore for that username.
|
32
|
+
- **accept_events**: An Array of GitHub event types to accept. If nil all events will be accepted.
|
33
|
+
|
34
|
+
Once it is initialized a request can be parsed passing it to the **`parse_request`** method. After verifying the request signature and checking for the configurated conditions the `parse_request` method returns a [Payload, Error] pair, where the error is nil if nothing failed, and the payload is nil if an error ocurred.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
webhook = Issue::Webhook.new(secret_token: ENV["GH_SECRET"],
|
38
|
+
origin: "myorg/reponame",
|
39
|
+
discard_sender: "myorg_bot"
|
40
|
+
accept_events: ["issues", "issue_comment"])
|
41
|
+
|
42
|
+
payload, error = webhook.parse_request(request)
|
43
|
+
|
44
|
+
if webhook.errored?
|
45
|
+
head error.status, msg: error.message
|
46
|
+
else
|
47
|
+
# do_something_based_on_the(payload)
|
48
|
+
head 200
|
49
|
+
end
|
50
|
+
|
51
|
+
```
|
52
|
+
|
53
|
+
### The Payload object
|
54
|
+
|
55
|
+
The `Issue::Payload` object includes all the parsed information coming from the webhook request. It has the following instance methods:
|
56
|
+
|
57
|
+
- **context**: This method returns a OpenStruct with the following structure:
|
58
|
+
```ruby
|
59
|
+
action: # the webhook action,
|
60
|
+
event: # the GitHub event coming in the HTTP_X_GITHUB_EVENT request header
|
61
|
+
issue_id: # the issue number
|
62
|
+
issue_title: # issue title,
|
63
|
+
issue_body: # body of the issue
|
64
|
+
issue_author: # author of the issue
|
65
|
+
issue_labels: # labels of the issue
|
66
|
+
repo: # the full name of the origin repository
|
67
|
+
sender: # the login of the user triggering the webhook action
|
68
|
+
event_action: # a string: "event.action"
|
69
|
+
comment_id: # id of the comment
|
70
|
+
comment_body: # body of the comment
|
71
|
+
comment_created_at: # created_at value of the comment
|
72
|
+
comment_url: # the html url for the comment
|
73
|
+
raw_payload: # a hash with the complete parsed JSON request
|
74
|
+
```
|
75
|
+
- **accesor methods** for every key in the context
|
76
|
+
- **opened?**: `true` if the action is `opened` or `reopened`
|
77
|
+
- **closed?**: `true` if the action is `closed`
|
78
|
+
- **commented?**: `true` if the action is `created`
|
79
|
+
- **edited?**: `true` if the action is `edited`
|
80
|
+
- **locked?**: `true` if the action is `locked`
|
81
|
+
- **unlocked?**: `true` if the action is `unlocked`
|
82
|
+
- **pinned?**: `true` if the action is `pinned` or `unpinned`
|
83
|
+
- **assigned?**: `true` if the action is `assigned` or `unassigned`
|
84
|
+
- **labeled?**: `true` if the action is `labeled` or `unlabeled`
|
85
|
+
|
86
|
+
## License
|
87
|
+
|
88
|
+
Released under the MIT license.
|
data/lib/issue/error.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Issue
|
2
|
+
class Error
|
3
|
+
attr :status
|
4
|
+
attr :message
|
5
|
+
|
6
|
+
# Initialize Issue::Error object with:
|
7
|
+
# status: html status code
|
8
|
+
# msg: message to send back in response
|
9
|
+
def initialize(status, msg)
|
10
|
+
@status = status
|
11
|
+
@message = msg
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Issue
|
5
|
+
class Payload
|
6
|
+
attr_accessor :context
|
7
|
+
|
8
|
+
# Initialize Issue::Payload object with:
|
9
|
+
#
|
10
|
+
# json_data: the parsed json sent from a GitHub webhook
|
11
|
+
# event: the value of the HTTP_X_GITHUB_EVENT header
|
12
|
+
#
|
13
|
+
# Initializing a new Issue::Payload instance makes all this info
|
14
|
+
# from the json webhook available via accessor methods:
|
15
|
+
#
|
16
|
+
# action
|
17
|
+
# event
|
18
|
+
# issue_id
|
19
|
+
# issue_title
|
20
|
+
# issue_body
|
21
|
+
# issue_author
|
22
|
+
# repo
|
23
|
+
# sender
|
24
|
+
# event_action
|
25
|
+
# raw_payload
|
26
|
+
#
|
27
|
+
# And when the event is 'issue_comment' also:
|
28
|
+
#
|
29
|
+
# comment_body
|
30
|
+
# comment_created_at
|
31
|
+
# comment_url
|
32
|
+
#
|
33
|
+
def initialize(json_data, event)
|
34
|
+
action = json_data.dig("action")
|
35
|
+
sender = json_data.dig("sender", "login")
|
36
|
+
repo = json_data.dig("repository", "full_name")
|
37
|
+
|
38
|
+
if event == "pull_request"
|
39
|
+
issue_id = json_data.dig("pull_request", "number")
|
40
|
+
issue_title = json_data.dig("pull_request", "title")
|
41
|
+
issue_body = json_data.dig("pull_request", "body")
|
42
|
+
issue_labels = json_data.dig("pull_request", "labels")
|
43
|
+
issue_author = json_data.dig("pull_request", "user", "login")
|
44
|
+
else
|
45
|
+
issue_id = json_data.dig("issue", "number")
|
46
|
+
issue_title = json_data.dig("issue", "title")
|
47
|
+
issue_body = json_data.dig("issue", "body")
|
48
|
+
issue_labels = json_data.dig("issue", "labels")
|
49
|
+
issue_author = json_data.dig("issue", "user", "login")
|
50
|
+
end
|
51
|
+
|
52
|
+
@context = OpenStruct.new(
|
53
|
+
action: action,
|
54
|
+
event: event,
|
55
|
+
issue_id: issue_id,
|
56
|
+
issue_title: issue_title,
|
57
|
+
issue_body: issue_body,
|
58
|
+
issue_author: issue_author,
|
59
|
+
issue_labels: issue_labels,
|
60
|
+
repo: repo,
|
61
|
+
sender: sender,
|
62
|
+
event_action: "#{event}.#{action}",
|
63
|
+
raw_payload: json_data
|
64
|
+
)
|
65
|
+
|
66
|
+
if event == "issue_comment"
|
67
|
+
@context[:comment_id] = json_data.dig("comment", "id")
|
68
|
+
@context[:comment_body] = json_data.dig("comment", "body")
|
69
|
+
@context[:comment_created_at] = json_data.dig("comment", "created_at")
|
70
|
+
@context[:comment_url] = json_data.dig("comment", "html_url")
|
71
|
+
end
|
72
|
+
|
73
|
+
@context.each_pair do |method_name, value|
|
74
|
+
define_singleton_method(method_name) {value}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# True if the payload is coming from an issue that has just been opened
|
79
|
+
def opened?
|
80
|
+
action == "opened" || action == "reopened"
|
81
|
+
end
|
82
|
+
|
83
|
+
# True if the payload is coming from an issue that has just been closed
|
84
|
+
def closed?
|
85
|
+
action == "closed"
|
86
|
+
end
|
87
|
+
|
88
|
+
# True if the payload is coming from a new comment
|
89
|
+
def commented?
|
90
|
+
action == "created"
|
91
|
+
end
|
92
|
+
|
93
|
+
# True if the payload is coming from an edition of a comment or issue
|
94
|
+
def edited?
|
95
|
+
action == "edited"
|
96
|
+
end
|
97
|
+
|
98
|
+
# True if the payload is coming from locking an issue
|
99
|
+
def locked?
|
100
|
+
action == "locked"
|
101
|
+
end
|
102
|
+
|
103
|
+
# True if the payload is coming from unlocking an issue
|
104
|
+
def unlocked?
|
105
|
+
action == "unlocked"
|
106
|
+
end
|
107
|
+
|
108
|
+
# True if the payload is coming from pinning or unpinning an issue
|
109
|
+
def pinned?
|
110
|
+
action == "pinned" || action == "unpinned"
|
111
|
+
end
|
112
|
+
|
113
|
+
# True if the payload is coming from un/assigning an issue
|
114
|
+
def assigned?
|
115
|
+
action == "assigned" || action == "unassigned"
|
116
|
+
end
|
117
|
+
|
118
|
+
# True if the payload is coming from un/labeling an issue
|
119
|
+
def labeled?
|
120
|
+
action == "labeled" || action == "unlabeled"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/lib/issue/version.rb
CHANGED
@@ -0,0 +1,102 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
require "json"
|
3
|
+
require "openssl"
|
4
|
+
require "rack"
|
5
|
+
|
6
|
+
module Issue
|
7
|
+
class Webhook
|
8
|
+
attr_accessor :secret_token
|
9
|
+
attr_accessor :request
|
10
|
+
attr_accessor :accept_origin
|
11
|
+
attr_accessor :discard_sender
|
12
|
+
attr_accessor :accept_events
|
13
|
+
attr_accessor :error
|
14
|
+
attr_accessor :payload
|
15
|
+
|
16
|
+
# Initialize the Issue::Webhook object
|
17
|
+
# This method should receive a Hash with the following settings:
|
18
|
+
# secret_token: the GitHub secret token needed to verify the request signature.
|
19
|
+
# accept_events: an Array of valid values for the HTTP_X_GITHUB_EVENT header. If empty any event will be processed.
|
20
|
+
# origin: the respository where the webhook should be sent to be accepted. If empty any request will be processed.
|
21
|
+
# discard_sender: an optional GitHub user handle to discard all events triggered by it.
|
22
|
+
def initialize(settings={})
|
23
|
+
@secret_token = settings[:secret_token]
|
24
|
+
@accept_origin = settings[:origin]
|
25
|
+
@accept_events = [settings[:accept_events]].flatten.compact.uniq.map(&:to_s)
|
26
|
+
@discard_sender = parse_discard_senders(settings[:discard_sender])
|
27
|
+
end
|
28
|
+
|
29
|
+
# This method will parse the passed request.
|
30
|
+
# If the request signature is incorrect or any of the conditions set
|
31
|
+
# via the initialization settings are not met an error will be created
|
32
|
+
# with the appropiate html status and message. Otherwise a Issue::Payload
|
33
|
+
# object will be created with the information contained in the request payload.
|
34
|
+
#
|
35
|
+
# This method returns a pair [payload, error] where only one of them will be nil
|
36
|
+
def parse_request(request)
|
37
|
+
@payload = nil
|
38
|
+
@error = nil
|
39
|
+
@request = request
|
40
|
+
|
41
|
+
if verify_signature
|
42
|
+
parse_payload
|
43
|
+
end
|
44
|
+
|
45
|
+
return [payload, error]
|
46
|
+
end
|
47
|
+
|
48
|
+
# This method returns True if parsing a request has generated an Issue::Error object
|
49
|
+
# That object will be available at the #error accessor method.
|
50
|
+
def errored?
|
51
|
+
!error.nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def parse_discard_senders(discard_sender_settings)
|
57
|
+
if discard_sender_settings.is_a?(String)
|
58
|
+
return { discard_sender_settings => [] }
|
59
|
+
elsif discard_sender_settings.is_a?(Hash)
|
60
|
+
return discard_sender_settings.transform_keys {|k| k.to_s }.transform_values {|v| [v].flatten}
|
61
|
+
else
|
62
|
+
return {}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def verify_signature
|
67
|
+
gh_signature = request.get_header "HTTP_X_HUB_SIGNATURE"
|
68
|
+
return error!(500, "Can't compute signature") if secret_token.nil? || secret_token.empty?
|
69
|
+
return error!(403, "Request missing signature") if gh_signature.nil? || gh_signature.empty?
|
70
|
+
request.body.rewind
|
71
|
+
payload_body = request.body.read
|
72
|
+
signature = "sha1=" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha1"), secret_token, payload_body)
|
73
|
+
return error!(403, "Signatures didn't match!") unless Rack::Utils.secure_compare(signature, gh_signature)
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
def parse_payload
|
78
|
+
begin
|
79
|
+
request.body.rewind
|
80
|
+
json_payload = JSON.parse(request.body.read)
|
81
|
+
event = request.get_header "HTTP_X_GITHUB_EVENT"
|
82
|
+
sender = json_payload.dig("sender", "login")
|
83
|
+
origin = json_payload.dig("repository", "full_name")
|
84
|
+
rescue JSON::ParserError
|
85
|
+
return error!(400, "Malformed request")
|
86
|
+
end
|
87
|
+
|
88
|
+
return error!(422, "No payload") if json_payload.nil? || json_payload.empty?
|
89
|
+
return error!(422, "No event") if event.nil?
|
90
|
+
return error!(200, "Event discarded") unless (accept_events.empty? || accept_events.include?(event))
|
91
|
+
return error!(200, "Event origin discarded") if (discard_sender[sender] == [] || discard_sender[sender].to_a.include?(event))
|
92
|
+
return error!(403, "Event origin not allowed") if (accept_origin && origin != accept_origin)
|
93
|
+
|
94
|
+
@payload = Issue::Payload.new(json_payload, event)
|
95
|
+
end
|
96
|
+
|
97
|
+
def error!(status, msg)
|
98
|
+
@error = Issue::Error.new(status, msg)
|
99
|
+
false
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/issue.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: issue
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Juanjo Bazán
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-12-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: openssl
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rack
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
13
41
|
- !ruby/object:Gem::Dependency
|
14
42
|
name: rake
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,23 +66,28 @@ dependencies:
|
|
38
66
|
- - "~>"
|
39
67
|
- !ruby/object:Gem::Version
|
40
68
|
version: '3.10'
|
41
|
-
description: Receive, parse and manage GitHub webhook events for issues and issue's
|
69
|
+
description: Receive, parse and manage GitHub webhook events for issues, PRs and issue's
|
42
70
|
comments
|
43
71
|
email:
|
44
72
|
executables: []
|
45
73
|
extensions: []
|
46
74
|
extra_rdoc_files: []
|
47
75
|
files:
|
76
|
+
- CHANGELOG.md
|
77
|
+
- LICENSE
|
48
78
|
- README.md
|
49
79
|
- lib/issue.rb
|
80
|
+
- lib/issue/error.rb
|
81
|
+
- lib/issue/payload.rb
|
50
82
|
- lib/issue/version.rb
|
83
|
+
- lib/issue/webhook.rb
|
51
84
|
homepage: http://github.com/xuanxu/issue
|
52
85
|
licenses:
|
53
86
|
- MIT
|
54
87
|
metadata:
|
55
88
|
bug_tracker_uri: https://github.com/xuanxu/issue/issues
|
56
|
-
changelog_uri: https://github.com/xuanxu/issue/blob/
|
57
|
-
documentation_uri: https://www.rubydoc.info/gems/
|
89
|
+
changelog_uri: https://github.com/xuanxu/issue/blob/main/CHANGELOG.md
|
90
|
+
documentation_uri: https://www.rubydoc.info/gems/issue
|
58
91
|
homepage_uri: http://github.com/xuanxu/issue
|
59
92
|
source_code_uri: http://github.com/xuanxu/issue
|
60
93
|
post_install_message:
|
@@ -75,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
108
|
- !ruby/object:Gem::Version
|
76
109
|
version: '0'
|
77
110
|
requirements: []
|
78
|
-
rubygems_version: 3.2.
|
111
|
+
rubygems_version: 3.2.22
|
79
112
|
signing_key:
|
80
113
|
specification_version: 4
|
81
114
|
summary: Manage webhook payload for issue events
|