hooks-ruby 0.0.2 → 0.0.4

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.
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Hooks
6
+ module Plugins
7
+ module Auth
8
+ # Validates and parses timestamps for webhook authentication
9
+ #
10
+ # This class provides secure timestamp validation supporting both
11
+ # ISO 8601 UTC format and Unix timestamp format. It includes
12
+ # strict validation to prevent various injection attacks.
13
+ #
14
+ # @example Basic usage
15
+ # validator = TimestampValidator.new
16
+ # validator.valid?("1609459200", 300) # => true/false
17
+ # validator.parse("2021-01-01T00:00:00Z") # => 1609459200
18
+ #
19
+ # @api private
20
+ class TimestampValidator
21
+ # Validate timestamp against current time with tolerance
22
+ #
23
+ # @param timestamp_value [String] The timestamp string to validate
24
+ # @param tolerance [Integer] Maximum age in seconds (default: 300)
25
+ # @return [Boolean] true if timestamp is valid and within tolerance
26
+ def valid?(timestamp_value, tolerance = 300)
27
+ return false if timestamp_value.nil? || timestamp_value.strip.empty?
28
+
29
+ parsed_timestamp = parse(timestamp_value.strip)
30
+ return false unless parsed_timestamp.is_a?(Integer)
31
+
32
+ now = Time.now.utc.to_i
33
+ (now - parsed_timestamp).abs <= tolerance
34
+ end
35
+
36
+ # Parse timestamp value supporting both ISO 8601 UTC and Unix formats
37
+ #
38
+ # @param timestamp_value [String] The timestamp string to parse
39
+ # @return [Integer, nil] Epoch seconds if parsing succeeds, nil otherwise
40
+ # @note Security: Strict validation prevents various injection attacks
41
+ def parse(timestamp_value)
42
+ return nil if invalid_characters?(timestamp_value)
43
+
44
+ parse_iso8601_timestamp(timestamp_value) || parse_unix_timestamp(timestamp_value)
45
+ end
46
+
47
+ private
48
+
49
+ # Check for control characters, whitespace, or null bytes
50
+ #
51
+ # @param timestamp_value [String] The timestamp to check
52
+ # @return [Boolean] true if contains invalid characters
53
+ def invalid_characters?(timestamp_value)
54
+ if timestamp_value =~ /[\u0000-\u001F\u007F-\u009F]/
55
+ log_warning("Timestamp contains invalid characters")
56
+ true
57
+ else
58
+ false
59
+ end
60
+ end
61
+
62
+ # Parse ISO 8601 UTC timestamp string (must have UTC indicator)
63
+ #
64
+ # @param timestamp_value [String] ISO 8601 timestamp string
65
+ # @return [Integer, nil] Epoch seconds if parsing succeeds, nil otherwise
66
+ def parse_iso8601_timestamp(timestamp_value)
67
+ # Handle space-separated format and convert to standard ISO format
68
+ if timestamp_value =~ /\A(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)(?: )\+0000\z/
69
+ timestamp_value = "#{$1}T#{$2}+00:00"
70
+ end
71
+
72
+ # Ensure the timestamp explicitly includes a UTC indicator
73
+ return nil unless timestamp_value =~ /(Z|\+00:00|\+0000)\z/
74
+ return nil unless iso8601_format?(timestamp_value)
75
+
76
+ parsed_time = parse_time_safely(timestamp_value)
77
+ return nil unless parsed_time&.utc_offset&.zero?
78
+
79
+ parsed_time.to_i
80
+ end
81
+
82
+ # Parse Unix timestamp string (must be positive integer, no leading zeros except for "0")
83
+ #
84
+ # @param timestamp_value [String] Unix timestamp string
85
+ # @return [Integer, nil] Epoch seconds if parsing succeeds, nil otherwise
86
+ def parse_unix_timestamp(timestamp_value)
87
+ return nil unless unix_format?(timestamp_value)
88
+
89
+ ts = timestamp_value.to_i
90
+ return nil if ts <= 0
91
+
92
+ ts
93
+ end
94
+
95
+ # Check if timestamp string looks like ISO 8601 format
96
+ #
97
+ # @param timestamp_value [String] The timestamp string to check
98
+ # @return [Boolean] true if it appears to be ISO 8601 format
99
+ def iso8601_format?(timestamp_value)
100
+ !!(timestamp_value =~ /\A\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(Z|\+00:00|\+0000)?\z/)
101
+ end
102
+
103
+ # Check if timestamp string looks like Unix timestamp format
104
+ #
105
+ # @param timestamp_value [String] The timestamp string to check
106
+ # @return [Boolean] true if it appears to be Unix timestamp format
107
+ def unix_format?(timestamp_value)
108
+ return true if timestamp_value == "0"
109
+ !!(timestamp_value =~ /\A[1-9]\d*\z/)
110
+ end
111
+
112
+ # Safely parse time string with error handling
113
+ #
114
+ # @param timestamp_value [String] The timestamp string to parse
115
+ # @return [Time, nil] Parsed time object or nil if parsing fails
116
+ def parse_time_safely(timestamp_value)
117
+ Time.parse(timestamp_value)
118
+ rescue ArgumentError
119
+ nil
120
+ end
121
+
122
+ # Log warning message
123
+ #
124
+ # @param message [String] Warning message to log
125
+ def log_warning(message)
126
+ return unless defined?(Hooks::Log) && Hooks::Log.instance
127
+
128
+ Hooks::Log.instance.warn("Auth::TimestampValidator validation failed: #{message}")
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../core/global_components"
4
+ require_relative "../../core/component_access"
5
+
3
6
  module Hooks
4
7
  module Plugins
5
8
  module Handlers
@@ -7,6 +10,8 @@ module Hooks
7
10
  #
8
11
  # All custom handlers must inherit from this class and implement the #call method
9
12
  class Base
13
+ include Hooks::Core::ComponentAccess
14
+
10
15
  # Process a webhook request
11
16
  #
12
17
  # @param payload [Hash, String] Parsed request body (JSON Hash) or raw string
@@ -17,18 +22,6 @@ module Hooks
17
22
  def call(payload:, headers:, config:)
18
23
  raise NotImplementedError, "Handler must implement #call method"
19
24
  end
20
-
21
- # Short logger accessor for all subclasses
22
- # @return [Hooks::Log] Logger instance
23
- #
24
- # Provides a convenient way for handlers to log messages without needing
25
- # to reference the full Hooks::Log namespace.
26
- #
27
- # @example Logging an error in an inherited class
28
- # log.error("oh no an error occured")
29
- def log
30
- Hooks::Log.instance
31
- end
32
25
  end
33
26
  end
34
27
  end
@@ -1,8 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Default handler when no custom handler is found
4
- # This handler simply acknowledges receipt of the webhook and shows a few of the built-in features
3
+ # Default webhook handler implementation
4
+ #
5
+ # This handler provides a basic webhook processing implementation that can be used
6
+ # as a fallback when no custom handler is configured for an endpoint. It demonstrates
7
+ # the standard handler interface and provides basic logging functionality.
8
+ #
9
+ # @example Usage in endpoint configuration
10
+ # handler:
11
+ # type: DefaultHandler
12
+ #
13
+ # @see Hooks::Plugins::Handlers::Base
5
14
  class DefaultHandler < Hooks::Plugins::Handlers::Base
15
+ # Process a webhook request with basic acknowledgment
16
+ #
17
+ # Provides a simple webhook processing implementation that logs the request
18
+ # and returns a standard acknowledgment response. This is useful for testing
19
+ # webhook endpoints or as a placeholder during development.
20
+ #
21
+ # @param payload [Hash, String] The webhook payload (parsed JSON or raw string)
22
+ # @param headers [Hash<String, String>] HTTP headers from the webhook request
23
+ # @param config [Hash] Endpoint configuration containing handler options
24
+ # @return [Hash] Response indicating successful processing
25
+ # @option config [Hash] :opts Additional handler-specific configuration options
26
+ #
27
+ # @example Basic usage
28
+ # handler = DefaultHandler.new
29
+ # response = handler.call(
30
+ # payload: { "event" => "push" },
31
+ # headers: { "Content-Type" => "application/json" },
32
+ # config: { opts: {} }
33
+ # )
34
+ # # => { message: "webhook processed successfully", handler: "DefaultHandler", timestamp: "..." }
6
35
  def call(payload:, headers:, config:)
7
36
 
8
37
  log.info("🔔 Default handler invoked for webhook 🔔")
@@ -15,7 +44,7 @@ class DefaultHandler < Hooks::Plugins::Handlers::Base
15
44
  {
16
45
  message: "webhook processed successfully",
17
46
  handler: "DefaultHandler",
18
- timestamp: Time.now.iso8601
47
+ timestamp: Time.now.utc.iso8601
19
48
  }
20
49
  end
21
50
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "failbot_base"
4
+
5
+ module Hooks
6
+ module Plugins
7
+ module Instruments
8
+ # Default failbot instrument implementation
9
+ #
10
+ # This is a no-op implementation that provides the error reporting interface
11
+ # without actually sending errors anywhere. It serves as a safe default when
12
+ # no custom error reporting implementation is configured.
13
+ #
14
+ # Users should replace this with their own implementation for services
15
+ # like Sentry, Rollbar, Honeybadger, etc.
16
+ #
17
+ # @example Replacing with a custom implementation
18
+ # # In your application initialization:
19
+ # custom_failbot = MySentryFailbotImplementation.new
20
+ # Hooks::Core::GlobalComponents.failbot = custom_failbot
21
+ #
22
+ # @see Hooks::Plugins::Instruments::FailbotBase
23
+ # @see Hooks::Core::GlobalComponents
24
+ class Failbot < FailbotBase
25
+ # Inherit from FailbotBase to provide a default no-op implementation
26
+ # of the error reporting instrument interface.
27
+ #
28
+ # All methods from FailbotBase are inherited and provide safe no-op behavior.
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../core/component_access"
4
+
5
+ module Hooks
6
+ module Plugins
7
+ module Instruments
8
+ # Base class for all failbot instrument plugins
9
+ #
10
+ # This class provides the foundation for implementing custom error reporting
11
+ # instruments. Subclasses should implement specific methods for their target
12
+ # error reporting service (Sentry, Rollbar, Honeybadger, etc.).
13
+ #
14
+ # @abstract Subclass and implement service-specific error reporting methods
15
+ # @example Implementing a custom failbot instrument
16
+ # class MySentryFailbot < Hooks::Plugins::Instruments::FailbotBase
17
+ # def report(error_or_message, context = {})
18
+ # case error_or_message
19
+ # when Exception
20
+ # Sentry.capture_exception(error_or_message, extra: context)
21
+ # else
22
+ # Sentry.capture_message(error_or_message.to_s, extra: context)
23
+ # end
24
+ # log.debug("Reported error to Sentry")
25
+ # end
26
+ # end
27
+ #
28
+ # @see Hooks::Plugins::Instruments::Failbot
29
+ class FailbotBase
30
+ include Hooks::Core::ComponentAccess
31
+
32
+ # Report an error or message to the error tracking service
33
+ #
34
+ # This is a no-op implementation that subclasses should override
35
+ # to provide actual error reporting functionality.
36
+ #
37
+ # @param error_or_message [Exception, String] The error to report or message string
38
+ # @param context [Hash] Additional context information about the error
39
+ # @return [void]
40
+ # @note Subclasses should implement this method for their specific service
41
+ # @example Override in subclass
42
+ # def report(error_or_message, context = {})
43
+ # if error_or_message.is_a?(Exception)
44
+ # ErrorService.report_exception(error_or_message, context)
45
+ # else
46
+ # ErrorService.report_message(error_or_message, context)
47
+ # end
48
+ # end
49
+ def report(error_or_message, context = {})
50
+ # No-op implementation for base class
51
+ end
52
+
53
+ # Report a warning-level message
54
+ #
55
+ # This is a no-op implementation that subclasses should override
56
+ # to provide actual warning reporting functionality.
57
+ #
58
+ # @param message [String] Warning message to report
59
+ # @param context [Hash] Additional context information
60
+ # @return [void]
61
+ # @note Subclasses should implement this method for their specific service
62
+ # @example Override in subclass
63
+ # def warn(message, context = {})
64
+ # ErrorService.report_warning(message, context)
65
+ # end
66
+ def warn(message, context = {})
67
+ # No-op implementation for base class
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stats_base"
4
+
5
+ module Hooks
6
+ module Plugins
7
+ module Instruments
8
+ # Default stats instrument implementation
9
+ #
10
+ # This is a no-op implementation that provides the stats interface without
11
+ # actually sending metrics anywhere. It serves as a safe default when no
12
+ # custom stats implementation is configured.
13
+ #
14
+ # Users should replace this with their own implementation for services
15
+ # like DataDog, New Relic, StatsD, etc.
16
+ #
17
+ # @example Replacing with a custom implementation
18
+ # # In your application initialization:
19
+ # custom_stats = MyCustomStatsImplementation.new
20
+ # Hooks::Core::GlobalComponents.stats = custom_stats
21
+ #
22
+ # @see Hooks::Plugins::Instruments::StatsBase
23
+ # @see Hooks::Core::GlobalComponents
24
+ class Stats < StatsBase
25
+ # Inherit from StatsBase to provide a default no-op implementation
26
+ # of the stats instrument interface.
27
+ #
28
+ # All methods from StatsBase are inherited and provide safe no-op behavior.
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../core/component_access"
4
+
5
+ module Hooks
6
+ module Plugins
7
+ module Instruments
8
+ # Base class for all stats instrument plugins
9
+ #
10
+ # This class provides the foundation for implementing custom metrics reporting
11
+ # instruments. Subclasses should implement specific methods for their target
12
+ # metrics service (DataDog, New Relic, StatsD, etc.).
13
+ #
14
+ # @abstract Subclass and implement service-specific metrics methods
15
+ # @example Implementing a custom stats instrument
16
+ # class MyStatsImplementation < Hooks::Plugins::Instruments::StatsBase
17
+ # def increment(metric_name, tags = {})
18
+ # # Send increment metric to your service
19
+ # MyMetricsService.increment(metric_name, tags)
20
+ # log.debug("Sent increment metric: #{metric_name}")
21
+ # end
22
+ #
23
+ # def timing(metric_name, duration, tags = {})
24
+ # # Send timing metric to your service
25
+ # MyMetricsService.timing(metric_name, duration, tags)
26
+ # end
27
+ # end
28
+ #
29
+ # @see Hooks::Plugins::Instruments::Stats
30
+ class StatsBase
31
+ include Hooks::Core::ComponentAccess
32
+
33
+ # Record an increment metric
34
+ #
35
+ # This is a no-op implementation that subclasses should override
36
+ # to provide actual metrics reporting functionality.
37
+ #
38
+ # @param metric_name [String] Name of the metric to increment
39
+ # @param tags [Hash] Optional tags/labels for the metric
40
+ # @return [void]
41
+ # @note Subclasses should implement this method for their specific service
42
+ # @example Override in subclass
43
+ # def increment(metric_name, tags = {})
44
+ # statsd.increment(metric_name, tags: tags)
45
+ # end
46
+ def increment(metric_name, tags = {})
47
+ # No-op implementation for base class
48
+ end
49
+
50
+ # Record a timing/duration metric
51
+ #
52
+ # This is a no-op implementation that subclasses should override
53
+ # to provide actual metrics reporting functionality.
54
+ #
55
+ # @param metric_name [String] Name of the timing metric
56
+ # @param duration [Numeric] Duration value (typically in milliseconds)
57
+ # @param tags [Hash] Optional tags/labels for the metric
58
+ # @return [void]
59
+ # @note Subclasses should implement this method for their specific service
60
+ # @example Override in subclass
61
+ # def timing(metric_name, duration, tags = {})
62
+ # statsd.timing(metric_name, duration, tags: tags)
63
+ # end
64
+ def timing(metric_name, duration, tags = {})
65
+ # No-op implementation for base class
66
+ end
67
+
68
+ # Record a gauge metric
69
+ #
70
+ # This is a no-op implementation that subclasses should override
71
+ # to provide actual metrics reporting functionality.
72
+ #
73
+ # @param metric_name [String] Name of the gauge metric
74
+ # @param value [Numeric] Current value for the gauge
75
+ # @param tags [Hash] Optional tags/labels for the metric
76
+ # @return [void]
77
+ # @note Subclasses should implement this method for their specific service
78
+ # @example Override in subclass
79
+ # def gauge(metric_name, value, tags = {})
80
+ # statsd.gauge(metric_name, value, tags: tags)
81
+ # end
82
+ def gauge(metric_name, value, tags = {})
83
+ # No-op implementation for base class
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../core/global_components"
4
+ require_relative "../core/component_access"
5
+
3
6
  module Hooks
4
7
  module Plugins
5
8
  # Base class for global lifecycle plugins
6
9
  #
7
10
  # Plugins can hook into request/response/error lifecycle events
8
11
  class Lifecycle
12
+ include Hooks::Core::ComponentAccess
13
+
9
14
  # Called before handler execution
10
15
  #
11
16
  # @param env [Hash] Rack environment
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "retryable"
4
+
5
+ # Utility module for retry functionality
6
+ module Retry
7
+ # This method should be called as early as possible in the startup of your application
8
+ # It sets up the Retryable gem with custom contexts and passes through a few options
9
+ # Should the number of retries be reached without success, the last exception will be raised
10
+ #
11
+ # @param log [Logger] The logger to use for retryable logging
12
+ # @raise [ArgumentError] If no logger is provided
13
+ # @return [void]
14
+ def self.setup!(log: nil, log_retries: ENV.fetch("RETRY_LOG_RETRIES", "true") == "true")
15
+ raise ArgumentError, "a logger must be provided" if log.nil?
16
+
17
+ log_method = lambda do |retries, exception|
18
+ # :nocov:
19
+ if log_retries
20
+ log.debug("[retry ##{retries}] #{exception.class}: #{exception.message} - #{exception.backtrace.join("\n")}")
21
+ end
22
+ # :nocov:
23
+ end
24
+
25
+ ######## Retryable Configuration ########
26
+ # All defaults available here:
27
+ # https://github.com/nfedyashev/retryable/blob/6a04027e61607de559e15e48f281f3ccaa9750e8/lib/retryable/configuration.rb#L22-L33
28
+ Retryable.configure do |config|
29
+ config.contexts[:default] = {
30
+ on: [StandardError],
31
+ sleep: ENV.fetch("DEFAULT_RETRY_SLEEP", "1").to_i,
32
+ tries: ENV.fetch("DEFAULT_RETRY_TRIES", "10").to_i,
33
+ log_method:
34
+ }
35
+ end
36
+ end
37
+ end
data/lib/hooks/version.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Main Hooks module containing version information
3
4
  module Hooks
4
- VERSION = "0.0.2"
5
+ # Current version of the Hooks webhook framework
6
+ # @return [String] The version string following semantic versioning
7
+ VERSION = "0.0.4".freeze
5
8
  end
data/lib/hooks.rb CHANGED
@@ -2,16 +2,29 @@
2
2
 
3
3
  require_relative "hooks/version"
4
4
  require_relative "hooks/core/builder"
5
-
6
- # Load all plugins (auth plugins, handler plugins, lifecycle hooks, etc.)
7
- Dir[File.join(__dir__, "hooks/plugins/**/*.rb")].sort.each do |file|
8
- require file
9
- end
10
-
11
- # Load all utils
12
- Dir[File.join(__dir__, "hooks/utils/**/*.rb")].sort.each do |file|
13
- require file
14
- end
5
+ require_relative "hooks/core/config_loader"
6
+ require_relative "hooks/core/config_validator"
7
+ require_relative "hooks/core/logger_factory"
8
+ require_relative "hooks/core/plugin_loader"
9
+ require_relative "hooks/core/global_components"
10
+ require_relative "hooks/core/component_access"
11
+ require_relative "hooks/core/log"
12
+ require_relative "hooks/core/failbot"
13
+ require_relative "hooks/core/stats"
14
+ require_relative "hooks/plugins/auth/base"
15
+ require_relative "hooks/plugins/auth/hmac"
16
+ require_relative "hooks/plugins/auth/shared_secret"
17
+ require_relative "hooks/plugins/handlers/base"
18
+ require_relative "hooks/plugins/handlers/default"
19
+ require_relative "hooks/plugins/lifecycle"
20
+ require_relative "hooks/plugins/instruments/stats_base"
21
+ require_relative "hooks/plugins/instruments/failbot_base"
22
+ require_relative "hooks/plugins/instruments/stats"
23
+ require_relative "hooks/plugins/instruments/failbot"
24
+ require_relative "hooks/utils/normalize"
25
+ require_relative "hooks/utils/retry"
26
+ require_relative "hooks/security"
27
+ require_relative "hooks/version"
15
28
 
16
29
  # Main module for the Hooks webhook server framework
17
30
  module Hooks
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hooks-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - github
@@ -78,26 +78,6 @@ dependencies:
78
78
  - - "~>"
79
79
  - !ruby/object:Gem::Version
80
80
  version: '2.3'
81
- - !ruby/object:Gem::Dependency
82
- name: grape-swagger
83
- requirement: !ruby/object:Gem::Requirement
84
- requirements:
85
- - - "~>"
86
- - !ruby/object:Gem::Version
87
- version: '2.1'
88
- - - ">="
89
- - !ruby/object:Gem::Version
90
- version: 2.1.2
91
- type: :runtime
92
- prerelease: false
93
- version_requirements: !ruby/object:Gem::Requirement
94
- requirements:
95
- - - "~>"
96
- - !ruby/object:Gem::Version
97
- version: '2.1'
98
- - - ">="
99
- - !ruby/object:Gem::Version
100
- version: 2.1.2
101
81
  - !ruby/object:Gem::Dependency
102
82
  name: puma
103
83
  requirement: !ruby/object:Gem::Requirement
@@ -150,19 +130,29 @@ files:
150
130
  - lib/hooks/app/endpoints/version.rb
151
131
  - lib/hooks/app/helpers.rb
152
132
  - lib/hooks/core/builder.rb
133
+ - lib/hooks/core/component_access.rb
153
134
  - lib/hooks/core/config_loader.rb
154
135
  - lib/hooks/core/config_validator.rb
136
+ - lib/hooks/core/failbot.rb
137
+ - lib/hooks/core/global_components.rb
155
138
  - lib/hooks/core/log.rb
156
139
  - lib/hooks/core/logger_factory.rb
157
140
  - lib/hooks/core/plugin_loader.rb
141
+ - lib/hooks/core/stats.rb
158
142
  - lib/hooks/plugins/auth/base.rb
159
143
  - lib/hooks/plugins/auth/hmac.rb
160
144
  - lib/hooks/plugins/auth/shared_secret.rb
145
+ - lib/hooks/plugins/auth/timestamp_validator.rb
161
146
  - lib/hooks/plugins/handlers/base.rb
162
147
  - lib/hooks/plugins/handlers/default.rb
148
+ - lib/hooks/plugins/instruments/failbot.rb
149
+ - lib/hooks/plugins/instruments/failbot_base.rb
150
+ - lib/hooks/plugins/instruments/stats.rb
151
+ - lib/hooks/plugins/instruments/stats_base.rb
163
152
  - lib/hooks/plugins/lifecycle.rb
164
153
  - lib/hooks/security.rb
165
154
  - lib/hooks/utils/normalize.rb
155
+ - lib/hooks/utils/retry.rb
166
156
  - lib/hooks/version.rb
167
157
  homepage: https://github.com/github/hooks
168
158
  licenses: