issue 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29925e2f5c0b58136fe7097671b9577766034701c97aeb78fc9999a8fb4e23ee
4
- data.tar.gz: 54d441c401a125e0e0ebea5baa48ead70dc3dcd5ad4a49c660673a4c626e3e07
3
+ metadata.gz: 9a0d24902085e9ef25c6765748362b06f5015bbd88848db03513c0cd7f33ce9c
4
+ data.tar.gz: '099f1cce4e61d95ab0ca87a6c057f59c5b4bae50e2b59cca25df9e334d3b1c39'
5
5
  SHA512:
6
- metadata.gz: 495c6477690e7bc7c688876ac242bb7884f87c9746dcc3ba6e0e2b8cbd8a012a758afcf07a2ef9f09b1a9280afac97a255a317fdcbfabb4668860a442dd04f37
7
- data.tar.gz: 688bb57f456b5831a0840d8ad847bcd24a11baf42e43c09b1fe66fd2ae5292088da86aef45075c3c5025e39e792f51dff5d73594f7fea7f9a1d4605a2c05a0f2
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
- # issue
2
- Ruby gem to parse issue events coming from webhook payloads
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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Issue
2
- VERSION = "0.0.1"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -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
@@ -1,5 +1,6 @@
1
- require 'issue/version'
2
-
3
1
  module Issue
4
-
2
+ require_relative "issue/version"
3
+ require_relative "issue/payload"
4
+ require_relative "issue/webhook"
5
+ require_relative "issue/error"
5
6
  end
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.1
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-06-29 00:00:00.000000000 Z
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/master/CHANGELOG.md
57
- documentation_uri: https://www.rubydoc.info/gems/emoticon
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.15
111
+ rubygems_version: 3.2.22
79
112
  signing_key:
80
113
  specification_version: 4
81
114
  summary: Manage webhook payload for issue events