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 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