puma 6.1.1 → 6.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a987a441328b2f8be7f7a147af233c0c41b393b98714ed621e2508dc28819d8
4
- data.tar.gz: bc30afa48b1647abf34f6cf362e3374b76a7e25c605f3ed7e5fd1e7c96dad8fb
3
+ metadata.gz: 677c15b54efd114ff1e95b31d37efc806c3fdbc955988a2d33a37c4b4b10007f
4
+ data.tar.gz: fb375bd8be23a5945b5fa497b124444905a5364815e725636eaad86c7d42c5e2
5
5
  SHA512:
6
- metadata.gz: 4a1a05276e82ffd6d012bc75e6f38c2bf30932cb9cd9a6b73a14d783e012f4b942128a72bf0bb8f9bbf8a0062c03d5df234a6baccbc490a2bb64150f480b531f
7
- data.tar.gz: 6127f2e9b3fd82795696ecd8e0f2016b7252f71d86433087c42c826c7d0d1cc8d2ceda8e4243bf99969d96ba261172e39d59e977ba24d5d75225d7707c64f2a9
6
+ metadata.gz: 81fbd7fc8da45a1cf3a7dd7c233ae97a1cf41fb788a263ed14f4ce12c0ceac9c024d209f0a99ce61ed9a29e409e7c1442a266b43a89d87f7fb9537e3e6c3526a
7
+ data.tar.gz: 21651a742ad2ec25804b435b44c2cf3d7bca6f691a36d405f413a2b0f325216f45df5f983ada1b3b5367cf35306fae4d8de46613b657ea5c27a2097d74ba72fd
data/History.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 6.2.0 / 2023-03-29
2
+
3
+ * Features
4
+ * Ability to supply a custom logger ([#2770], [#2511])
5
+ * Warn when clustered-only hooks are defined in single mode ([#3089])
6
+ * Adds the on_booted event ([#2709])
7
+
8
+ * Bugfixes
9
+ * Loggers - internal_write - catch Errno::EINVAL ([#3091])
10
+ * commonlogger.rb - fix HIJACK time format, use constants, not strings ([#3074])
11
+ * Fixed some edge cases regarding request hijacking ([#3072])
12
+
13
+
1
14
  ## 6.1.1 / 2023-02-28
2
15
 
3
16
  * Bugfixes
@@ -1954,6 +1967,13 @@ be added back in a future date when a java Puma::MiniSSL is added.
1954
1967
  * Bugfixes
1955
1968
  * Your bugfix goes here <Most recent on the top, like GitHub> (#Github Number)
1956
1969
 
1970
+ [#2770]:https://github.com/puma/puma/pull/2770 "PR by @vzajkov, merged 2023-03-29"
1971
+ [#2511]:https://github.com/puma/puma/issues/2511 "Issue by @jchristie55332, closed 2021-12-12"
1972
+ [#3089]:https://github.com/puma/puma/pull/3089 "PR by @Vuta, merged 2023-03-06"
1973
+ [#2709]:https://github.com/puma/puma/pull/2709 "PR by @rodzyn, merged 2023-02-20"
1974
+ [#3091]:https://github.com/puma/puma/pull/3091 "PR by @MSP-Greg, merged 2023-03-28"
1975
+ [#3074]:https://github.com/puma/puma/pull/3074 "PR by @MSP-Greg, merged 2023-03-14"
1976
+ [#3072]:https://github.com/puma/puma/pull/3072 "PR by @MSP-Greg, merged 2023-02-17"
1957
1977
  [#3079]:https://github.com/puma/puma/pull/3079 "PR by @mohamedhafez, merged 2023-02-24"
1958
1978
  [#3080]:https://github.com/puma/puma/pull/3080 "PR by @MSP-Greg, merged 2023-02-16"
1959
1979
  [#3058]:https://github.com/puma/puma/pull/3058 "PR by @dentarg, merged 2023-01-29"
data/README.md CHANGED
@@ -157,6 +157,15 @@ before_fork do
157
157
  end
158
158
  ```
159
159
 
160
+ You can also specify a block to be run after puma is booted using `on_booted`:
161
+
162
+ ```ruby
163
+ # config/puma.rb
164
+ on_booted do
165
+ # configuration here
166
+ end
167
+ ```
168
+
160
169
  ### Error handling
161
170
 
162
171
  If puma encounters an error outside of the context of your application, it will respond with a 500 and a simple
@@ -345,11 +354,13 @@ end
345
354
 
346
355
  ## Deployment
347
356
 
348
- Puma has support for Capistrano with an [external gem](https://github.com/seuros/capistrano-puma).
357
+ * Puma has support for Capistrano with an [external gem](https://github.com/seuros/capistrano-puma).
358
+
359
+ * Additionally, Puma has support for built-in daemonization via the [puma-daemon](https://github.com/kigster/puma-daemon) ruby gem. The gem restores the `daemonize` option that was removed from Puma starting version 5, but only for MRI Ruby.
360
+
349
361
 
350
362
  It is common to use process monitors with Puma. Modern process monitors like systemd or rc.d
351
- provide continuous monitoring and restarts for increased
352
- reliability in production environments:
363
+ provide continuous monitoring and restarts for increased reliability in production environments:
353
364
 
354
365
  * [rc.d](docs/jungle/rc.d/README.md)
355
366
  * [systemd](docs/systemd.md)
data/lib/puma/cli.rb CHANGED
@@ -93,7 +93,7 @@ module Puma
93
93
  #
94
94
 
95
95
  def setup_options
96
- @conf = Configuration.new do |user_config, file_config|
96
+ @conf = Configuration.new({}, {events: @events}) do |user_config, file_config|
97
97
  @parser = OptionParser.new do |o|
98
98
  o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg|
99
99
  user_config.bind arg
@@ -3,7 +3,7 @@
3
3
  module Puma
4
4
  # Rack::CommonLogger forwards every request to the given +app+, and
5
5
  # logs a line in the
6
- # {Apache common log format}[https://httpd.apache.org/docs/1.3/logs.html#common]
6
+ # {Apache common log format}[https://httpd.apache.org/docs/2.4/logs.html#common]
7
7
  # to the +logger+.
8
8
  #
9
9
  # If +logger+ is nil, CommonLogger will fall back +rack.errors+, which is
@@ -16,7 +16,7 @@ module Puma
16
16
  # (which is called without arguments in order to make the error appear for
17
17
  # sure)
18
18
  class CommonLogger
19
- # Common Log Format: https://httpd.apache.org/docs/1.3/logs.html#common
19
+ # Common Log Format: https://httpd.apache.org/docs/2.4/logs.html#common
20
20
  #
21
21
  # lilith.local - - [07/Aug/2006 23:58:02 -0400] "GET / HTTP/1.1" 500 -
22
22
  #
@@ -25,10 +25,17 @@ module Puma
25
25
 
26
26
  HIJACK_FORMAT = %{%s - %s [%s] "%s %s%s %s" HIJACKED -1 %0.4f\n}
27
27
 
28
- CONTENT_LENGTH = 'Content-Length'.freeze
29
- PATH_INFO = 'PATH_INFO'.freeze
30
- QUERY_STRING = 'QUERY_STRING'.freeze
31
- REQUEST_METHOD = 'REQUEST_METHOD'.freeze
28
+ LOG_TIME_FORMAT = '%d/%b/%Y:%H:%M:%S %z'
29
+
30
+ CONTENT_LENGTH = 'Content-Length' # should be lower case from app,
31
+ # Util::HeaderHash allows mixed
32
+ HTTP_VERSION = Const::HTTP_VERSION
33
+ HTTP_X_FORWARDED_FOR = Const::HTTP_X_FORWARDED_FOR
34
+ PATH_INFO = Const::PATH_INFO
35
+ QUERY_STRING = Const::QUERY_STRING
36
+ REMOTE_ADDR = Const::REMOTE_ADDR
37
+ REMOTE_USER = 'REMOTE_USER'
38
+ REQUEST_METHOD = Const::REQUEST_METHOD
32
39
 
33
40
  def initialize(app, logger=nil)
34
41
  @app = app
@@ -57,13 +64,13 @@ module Puma
57
64
  now = Time.now
58
65
 
59
66
  msg = HIJACK_FORMAT % [
60
- env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
61
- env["REMOTE_USER"] || "-",
62
- now.strftime("%d/%b/%Y %H:%M:%S"),
67
+ env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR] || "-",
68
+ env[REMOTE_USER] || "-",
69
+ now.strftime(LOG_TIME_FORMAT),
63
70
  env[REQUEST_METHOD],
64
71
  env[PATH_INFO],
65
72
  env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
66
- env["HTTP_VERSION"],
73
+ env[HTTP_VERSION],
67
74
  now - began_at ]
68
75
 
69
76
  write(msg)
@@ -74,13 +81,13 @@ module Puma
74
81
  length = extract_content_length(header)
75
82
 
76
83
  msg = FORMAT % [
77
- env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
78
- env["REMOTE_USER"] || "-",
79
- now.strftime("%d/%b/%Y:%H:%M:%S %z"),
84
+ env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR] || "-",
85
+ env[REMOTE_USER] || "-",
86
+ now.strftime(LOG_TIME_FORMAT),
80
87
  env[REQUEST_METHOD],
81
88
  env[PATH_INFO],
82
89
  env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
83
- env["HTTP_VERSION"],
90
+ env[HTTP_VERSION],
84
91
  status.to_s[0..3],
85
92
  length,
86
93
  now - began_at ]
@@ -157,6 +157,7 @@ module Puma
157
157
  reaping_time: 1,
158
158
  remote_address: :socket,
159
159
  silence_single_worker_warning: false,
160
+ silence_fork_callback_warning: false,
160
161
  tag: File.basename(Dir.getwd),
161
162
  tcp_host: '0.0.0.0'.freeze,
162
163
  tcp_port: 9292,
data/lib/puma/const.rb CHANGED
@@ -99,8 +99,8 @@ module Puma
99
99
  # too taxing on performance.
100
100
  module Const
101
101
 
102
- PUMA_VERSION = VERSION = "6.1.1"
103
- CODE_NAME = "The Way Up"
102
+ PUMA_VERSION = VERSION = "6.2.0"
103
+ CODE_NAME = "Speaking of Now"
104
104
 
105
105
  PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
106
106
 
data/lib/puma/dsl.rb CHANGED
@@ -419,6 +419,11 @@ module Puma
419
419
  @options[:log_requests] = which
420
420
  end
421
421
 
422
+ # Pass in a custom logging class instance
423
+ def custom_logger(custom_logger)
424
+ @options[:custom_logger] = custom_logger
425
+ end
426
+
422
427
  # Show debugging info
423
428
  #
424
429
  def debug
@@ -585,6 +590,11 @@ module Puma
585
590
  @options[:silence_single_worker_warning] = true
586
591
  end
587
592
 
593
+ # Disable warning message when running single mode with callback hook defined.
594
+ def silence_fork_callback_warning
595
+ @options[:silence_fork_callback_warning] = true
596
+ end
597
+
588
598
  # Code to run immediately before master process
589
599
  # forks workers (once on boot). These hooks can block if necessary
590
600
  # to wait for background operations unknown to Puma to finish before
@@ -600,6 +610,8 @@ module Puma
600
610
  # puts "Starting workers..."
601
611
  # end
602
612
  def before_fork(&block)
613
+ warn_if_in_single_mode('before_fork')
614
+
603
615
  @options[:before_fork] ||= []
604
616
  @options[:before_fork] << block
605
617
  end
@@ -615,6 +627,8 @@ module Puma
615
627
  # puts 'Before worker boot...'
616
628
  # end
617
629
  def on_worker_boot(key = nil, &block)
630
+ warn_if_in_single_mode('on_worker_boot')
631
+
618
632
  process_hook :before_worker_boot, key, block, 'on_worker_boot'
619
633
  end
620
634
 
@@ -631,6 +645,8 @@ module Puma
631
645
  # puts 'On worker shutdown...'
632
646
  # end
633
647
  def on_worker_shutdown(key = nil, &block)
648
+ warn_if_in_single_mode('on_worker_shutdown')
649
+
634
650
  process_hook :before_worker_shutdown, key, block, 'on_worker_shutdown'
635
651
  end
636
652
 
@@ -645,6 +661,8 @@ module Puma
645
661
  # puts 'Before worker fork...'
646
662
  # end
647
663
  def on_worker_fork(&block)
664
+ warn_if_in_single_mode('on_worker_fork')
665
+
648
666
  process_hook :before_worker_fork, nil, block, 'on_worker_fork'
649
667
  end
650
668
 
@@ -659,11 +677,23 @@ module Puma
659
677
  # puts 'After worker fork...'
660
678
  # end
661
679
  def after_worker_fork(&block)
680
+ warn_if_in_single_mode('after_worker_fork')
681
+
662
682
  process_hook :after_worker_fork, nil, block, 'after_worker_fork'
663
683
  end
664
684
 
665
685
  alias_method :after_worker_boot, :after_worker_fork
666
686
 
687
+ # Code to run after puma is booted (works for both: single and clustered)
688
+ #
689
+ # @example
690
+ # on_booted do
691
+ # puts 'After booting...'
692
+ # end
693
+ def on_booted(&block)
694
+ @config.options[:events].on_booted(&block)
695
+ end
696
+
667
697
  # When `fork_worker` is enabled, code to run in Worker 0
668
698
  # before all other workers are re-forked from this process,
669
699
  # after the server has temporarily stopped serving requests
@@ -1063,7 +1093,20 @@ module Puma
1063
1093
  elsif key.nil?
1064
1094
  @options[options_key] << block
1065
1095
  else
1066
- raise "'#{method}' key must be String or Symbol"
1096
+ raise "'#{meth}' key must be String or Symbol"
1097
+ end
1098
+ end
1099
+
1100
+ def warn_if_in_single_mode(hook_name)
1101
+ return if @options[:silence_fork_callback_warning]
1102
+
1103
+ if (@options[:workers] || 0) == 0
1104
+ log_string =
1105
+ "Warning: You specified code to run in a `#{hook_name}` block, " \
1106
+ "but Puma is configured to run in cluster mode, " \
1107
+ "so your `#{hook_name}` block did not run"
1108
+
1109
+ LogWriter.stdio.log(log_string)
1067
1110
  end
1068
1111
  end
1069
1112
  end
@@ -102,7 +102,8 @@ module Puma
102
102
  @ioerr.is_a?(IO) and @ioerr.wait_writable(1)
103
103
  @ioerr.write "#{w_str}\n"
104
104
  @ioerr.flush unless @ioerr.sync
105
- rescue Errno::EPIPE, Errno::EBADF, IOError
105
+ rescue Errno::EPIPE, Errno::EBADF, IOError, Errno::EINVAL
106
+ # 'Invalid argument' (Errno::EINVAL) may be raised by flush
106
107
  end
107
108
  end
108
109
  rescue ThreadError
data/lib/puma/launcher.rb CHANGED
@@ -79,6 +79,8 @@ module Puma
79
79
  @log_writer.formatter = LogWriter::PidFormatter.new if clustered?
80
80
  @log_writer.formatter = options[:log_formatter] if @options[:log_formatter]
81
81
 
82
+ @log_writer.custom_logger = options[:custom_logger] if @options[:custom_logger]
83
+
82
84
  generate_restart_data
83
85
 
84
86
  if clustered? && !Puma.forkable?
@@ -28,11 +28,12 @@ module Puma
28
28
  attr_reader :stdout,
29
29
  :stderr
30
30
 
31
- attr_accessor :formatter
31
+ attr_accessor :formatter, :custom_logger
32
32
 
33
33
  # Create a LogWriter that prints to +stdout+ and +stderr+.
34
34
  def initialize(stdout, stderr)
35
35
  @formatter = DefaultFormatter.new
36
+ @custom_logger = nil
36
37
  @stdout = stdout
37
38
  @stderr = stderr
38
39
 
@@ -59,7 +60,11 @@ module Puma
59
60
 
60
61
  # Write +str+ to +@stdout+
61
62
  def log(str)
62
- internal_write "#{@formatter.call str}\n"
63
+ if @custom_logger&.respond_to?(:write)
64
+ @custom_logger.write(format(str))
65
+ else
66
+ internal_write "#{@formatter.call str}\n"
67
+ end
63
68
  end
64
69
 
65
70
  def write(str)
@@ -73,7 +78,8 @@ module Puma
73
78
  @stdout.is_a?(IO) and @stdout.wait_writable(1)
74
79
  @stdout.write w_str
75
80
  @stdout.flush unless @stdout.sync
76
- rescue Errno::EPIPE, Errno::EBADF, IOError
81
+ rescue Errno::EPIPE, Errno::EBADF, IOError, Errno::EINVAL
82
+ # 'Invalid argument' (Errno::EINVAL) may be raised by flush
77
83
  end
78
84
  end
79
85
  rescue ThreadError
data/lib/puma/request.rb CHANGED
@@ -106,6 +106,7 @@ module Puma
106
106
  # is called
107
107
  res_body = app_body
108
108
 
109
+ # full hijack, app called env['rack.hijack']
109
110
  return :async if client.hijacked
110
111
 
111
112
  status = status.to_i
@@ -169,54 +170,55 @@ module Puma
169
170
  resp_info = str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
170
171
 
171
172
  close_body = false
173
+ response_hijack = nil
174
+ content_length = resp_info[:content_length]
175
+ keep_alive = resp_info[:keep_alive]
172
176
 
173
- # below converts app_body into body, dependent on app_body's characteristics, and
174
- # resp_info[:content_length] will be set if it can be determined
175
- if !resp_info[:content_length] && !resp_info[:transfer_encoding] && status != 204
176
- if res_body.respond_to?(:to_ary) && (array_body = res_body.to_ary) && array_body.is_a?(Array)
177
- body = array_body
178
- resp_info[:content_length] = body.sum(&:bytesize)
179
- elsif res_body.is_a?(File) && res_body.respond_to?(:size)
180
- body = res_body
181
- resp_info[:content_length] = body.size
182
- elsif res_body.respond_to?(:to_path) && res_body.respond_to?(:each) &&
177
+ if res_body.respond_to?(:each) && !resp_info[:response_hijack]
178
+ # below converts app_body into body, dependent on app_body's characteristics, and
179
+ # content_length will be set if it can be determined
180
+ if !content_length && !resp_info[:transfer_encoding] && status != 204
181
+ if res_body.respond_to?(:to_ary) && (array_body = res_body.to_ary) &&
182
+ array_body.is_a?(Array)
183
+ body = array_body
184
+ content_length = body.sum(&:bytesize)
185
+ elsif res_body.is_a?(File) && res_body.respond_to?(:size)
186
+ body = res_body
187
+ content_length = body.size
188
+ elsif res_body.respond_to?(:to_path) && File.readable?(fn = res_body.to_path)
189
+ body = File.open fn, 'rb'
190
+ content_length = body.size
191
+ close_body = true
192
+ else
193
+ body = res_body
194
+ end
195
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:to_path) &&
183
196
  File.readable?(fn = res_body.to_path)
184
197
  body = File.open fn, 'rb'
185
- resp_info[:content_length] = body.size
198
+ content_length = body.size
186
199
  close_body = true
200
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:filename) &&
201
+ res_body.respond_to?(:bytesize) && File.readable?(fn = res_body.filename)
202
+ # Sprockets::Asset
203
+ content_length = res_body.bytesize unless content_length
204
+ if (body_str = res_body.to_hash[:source])
205
+ body = [body_str]
206
+ else # avoid each and use a File object
207
+ body = File.open fn, 'rb'
208
+ close_body = true
209
+ end
187
210
  else
188
211
  body = res_body
189
212
  end
190
- elsif !res_body.is_a?(::File) && res_body.respond_to?(:to_path) && res_body.respond_to?(:each) &&
191
- File.readable?(fn = res_body.to_path)
192
- body = File.open fn, 'rb'
193
- resp_info[:content_length] = body.size
194
- close_body = true
195
- elsif !res_body.is_a?(::File) && res_body.respond_to?(:filename) && res_body.respond_to?(:each) &&
196
- res_body.respond_to?(:bytesize) && File.readable?(fn = res_body.filename)
197
- # Sprockets::Asset
198
- resp_info[:content_length] = res_body.bytesize unless resp_info[:content_length]
199
- if res_body.to_hash[:source] # use each to return @source
200
- body = res_body
201
- else # avoid each and use a File object
202
- body = File.open fn, 'rb'
203
- close_body = true
204
- end
205
213
  else
206
- body = res_body
214
+ # partial hijack, from Rack spec:
215
+ # Servers must ignore the body part of the response tuple when the
216
+ # rack.hijack response header is present.
217
+ response_hijack = resp_info[:response_hijack] || res_body
207
218
  end
208
219
 
209
220
  line_ending = LINE_END
210
221
 
211
- content_length = resp_info[:content_length]
212
- keep_alive = resp_info[:keep_alive]
213
-
214
- if res_body && !res_body.respond_to?(:each)
215
- response_hijack = res_body
216
- else
217
- response_hijack = resp_info[:response_hijack]
218
- end
219
-
220
222
  cork_socket socket
221
223
 
222
224
  if resp_info[:no_body]
@@ -244,6 +246,8 @@ module Puma
244
246
 
245
247
  io_buffer << line_ending
246
248
 
249
+ # partial hijack, we write headers, then hand the socket to the app via
250
+ # response_hijack.call
247
251
  if response_hijack
248
252
  fast_write_str socket, io_buffer.read_and_reset
249
253
  uncork_socket socket
@@ -358,16 +362,15 @@ module Puma
358
362
  fast_write_str(socket, io_buffer.read_and_reset) unless io_buffer.length.zero?
359
363
  else
360
364
  # for enum bodies
361
- fast_write_str socket, io_buffer.read_and_reset
362
365
  if chunked
363
366
  body.each do |part|
364
367
  next if (byte_size = part.bytesize).zero?
365
- fast_write_str socket, (byte_size.to_s(16) << LINE_END)
366
- fast_write_str socket, part
367
- fast_write_str socket, LINE_END
368
+ io_buffer.append byte_size.to_s(16), LINE_END, part, LINE_END
369
+ fast_write_str socket, io_buffer.read_and_reset
368
370
  end
369
371
  fast_write_str socket, CLOSE_CHUNKED
370
372
  else
373
+ fast_write_str socket, io_buffer.read_and_reset
371
374
  body.each do |part|
372
375
  next if part.bytesize.zero?
373
376
  fast_write_str socket, part
@@ -27,7 +27,9 @@ module Puma
27
27
  end
28
28
  end
29
29
 
30
- conf = ::Puma::Configuration.new(options, default_options) do |user_config, file_config, default_config|
30
+ @events = options[:events] || ::Puma::Events.new
31
+
32
+ conf = ::Puma::Configuration.new(options, default_options.merge({events: @events})) do |user_config, file_config, default_config|
31
33
  if options.delete(:Verbose)
32
34
  require 'rack/common_logger'
33
35
  app = Rack::CommonLogger.new(app, STDOUT)
@@ -59,11 +61,11 @@ module Puma
59
61
  end
60
62
 
61
63
  def run(app, **options)
62
- conf = self.config(app, options)
64
+ conf = self.config(app, options)
63
65
 
64
66
  log_writer = options.delete(:Silent) ? ::Puma::LogWriter.strings : ::Puma::LogWriter.stdio
65
67
 
66
- launcher = ::Puma::Launcher.new(conf, :log_writer => log_writer)
68
+ launcher = ::Puma::Launcher.new(conf, :log_writer => log_writer, events: @events)
67
69
 
68
70
  yield launcher if block_given?
69
71
  begin
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.1.1
4
+ version: 6.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Phoenix