good_job 4.14.1 → 4.14.2

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: af515dffcbbdb5c6f3e02ec7a1426b7d59aea355b33791c81cc20d043bcab7f4
4
- data.tar.gz: e44f93e479b483af4c3848c7957df934198fb780260c3008c427ff864c3d67a8
3
+ metadata.gz: 0f0f62c12c0c19a4afd8f71d3dae19479ac4a7df6342ac399bf8e447fbbe8f5b
4
+ data.tar.gz: 75a2a8a282b0c28e370230397dfc5f1de470681b1d53cbb0dd6ee46384e87fe4
5
5
  SHA512:
6
- metadata.gz: 812281e391af58a897dd119e875be51c7569eaefa021f7fc05d42d69b185819c4077c04e8f40d23c3cf498862e04f8f9285fe9a29931a1d9bba4f1a61650ca37
7
- data.tar.gz: 1941b6294d3339c590e2c37188a2f1568a3c32bc5e1f9cb2a76f0c58b94cce6781797d3d86853111a842f435adb8262611eca426dc16001bb6bc1b0a8437bde4
6
+ metadata.gz: ba1a959622762c21e723f6d406d30d8fda81d736560f3c710a7c363d8206697d0eb856dbe9ee865a340ad4f3d8db5354a707f78d695d0221fe1779ba07709c7b
7
+ data.tar.gz: 0bb8d09ca31aefe0e6da18096da53f5fdfc7e7be820afb54791b051f7ea29464c730aefb9eaeffaeaaa5fa3a6f5e029d158651fde41bd8876fd280e3fcd4269a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.14.2](https://github.com/bensheldon/good_job/tree/v4.14.2) (2026-04-06)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.14.1...v4.14.2)
6
+
7
+ **Closed issues:**
8
+
9
+ - Incompatible with permanent\_connection\_checkout = :disallowed [\#1729](https://github.com/bensheldon/good_job/issues/1729)
10
+
11
+ **Merged pull requests:**
12
+
13
+ - Replace Base.connection with lease\_connection and with\_connection throughout [\#1730](https://github.com/bensheldon/good_job/pull/1730) ([bensheldon](https://github.com/bensheldon))
14
+
3
15
  ## [v4.14.1](https://github.com/bensheldon/good_job/tree/v4.14.1) (2026-04-03)
4
16
 
5
17
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.14.0...v4.14.1)
@@ -23,7 +23,7 @@ module GoodJob
23
23
  ORDER BY timestamp ASC
24
24
  SQL
25
25
 
26
- executions_data = GoodJob::Job.connection.exec_query(GoodJob::Job.pg_or_jdbc_query(sum_query), "GoodJob Performance Chart", start_end_binds)
26
+ executions_data = GoodJob::Job.connection_pool.with_connection { |conn| conn.exec_query(GoodJob::Job.pg_or_jdbc_query(sum_query), "GoodJob Performance Chart", start_end_binds) }
27
27
 
28
28
  job_names = executions_data.reject { |d| d['sum'].nil? }.map { |d| d['job_class'] || BaseFilter::EMPTY }.uniq
29
29
  labels = []
@@ -40,7 +40,7 @@ module GoodJob
40
40
  ]
41
41
  labels = BUCKET_INTERVALS.map { |interval| GoodJob::ApplicationController.helpers.format_duration(interval) }
42
42
  labels[-1] = I18n.t("good_job.performance.show.slow")
43
- executions_data = GoodJob::Job.connection.exec_query(GoodJob::Job.pg_or_jdbc_query(sum_query), "GoodJob Performance Job Chart", binds)
43
+ executions_data = GoodJob::Job.connection_pool.with_connection { |conn| conn.exec_query(GoodJob::Job.pg_or_jdbc_query(sum_query), "GoodJob Performance Job Chart", binds) }
44
44
  executions_data = executions_data.to_a.index_by { |data| data["bucket_index"] }
45
45
 
46
46
  bucket_data = 0.upto(BUCKET_INTERVALS.count).map do |bucket_index|
@@ -28,7 +28,7 @@ module GoodJob
28
28
  ORDER BY timestamp ASC
29
29
  SQL
30
30
 
31
- executions_data = GoodJob::Job.connection.exec_query(GoodJob::Job.pg_or_jdbc_query(count_query), "GoodJob Dashboard Chart", start_end_binds)
31
+ executions_data = GoodJob::Job.connection_pool.with_connection { |conn| conn.exec_query(GoodJob::Job.pg_or_jdbc_query(count_query), "GoodJob Dashboard Chart", start_end_binds) }
32
32
 
33
33
  queue_names = executions_data.reject { |d| d['count'].nil? }.map { |d| d['queue_name'] || BaseFilter::EMPTY }.uniq
34
34
  labels = []
@@ -30,6 +30,24 @@ module GoodJob
30
30
  # Default Postgres function to be used for Advisory Locks
31
31
  class_attribute :advisory_lockable_function, default: "pg_try_advisory_lock"
32
32
 
33
+ # Rails < 7.2 does not have lease_connection as a class method.
34
+ define_singleton_method(:lease_connection) { connection } unless respond_to?(:lease_connection)
35
+
36
+ # Rails < 7.2 does not have adapter_class as a class method, and adapter
37
+ # quoting methods (quote_table_name, quote_column_name) are instance-only.
38
+ # Provide a proxy that responds to those methods by delegating to a connection.
39
+ unless respond_to?(:adapter_class)
40
+ define_singleton_method(:adapter_class) do
41
+ @_adapter_class ||= begin
42
+ pool = connection_pool
43
+ proxy = Object.new
44
+ proxy.define_singleton_method(:quote_table_name) { |name| pool.with_connection { |c| c.quote_table_name(name) } }
45
+ proxy.define_singleton_method(:quote_column_name) { |name| pool.with_connection { |c| c.quote_column_name(name) } }
46
+ proxy
47
+ end
48
+ end
49
+ end
50
+
33
51
  # Attempt to acquire an advisory lock on the selected records and
34
52
  # return only those records for which a lock could be acquired.
35
53
  # @!method advisory_lock(column: _advisory_lockable_column, function: advisory_lockable_function)
@@ -39,6 +57,7 @@ module GoodJob
39
57
  # @return [ActiveRecord::Relation]
40
58
  # A relation selecting only the records that were locked.
41
59
  scope :advisory_lock, (lambda do |column: _advisory_lockable_column, function: advisory_lockable_function, select_limit: nil|
60
+ lease_connection # ensure a sticky connection; advisory locks are session-scoped and must outlive this query
42
61
  original_query = self
43
62
 
44
63
  primary_key_for_select = primary_key.to_sym
@@ -55,7 +74,7 @@ module GoodJob
55
74
  cte_type = supports_cte_materialization_specifiers? ? :MATERIALIZED : :""
56
75
  composed_cte = Arel::Nodes::As.new(cte_table, Arel::Nodes::UnaryOperation.new(cte_type, cte_query.arel))
57
76
 
58
- lock_condition = "#{function}(('x' || substr(md5(#{connection.quote(table_name)} || '-' || #{connection.quote_table_name(cte_table.name)}.#{connection.quote_column_name(column)}::text), 1, 16))::bit(64)::bigint)"
77
+ lock_condition = "#{function}(('x' || substr(md5(#{_quoted_table_name_string} || '-' || #{adapter_class.quote_table_name(cte_table.name)}.#{adapter_class.quote_column_name(column)}::text), 1, 16))::bit(64)::bigint)"
59
78
  query = cte_table.project(cte_table[:id])
60
79
  .with(composed_cte)
61
80
  .where(defined?(Arel::Nodes::BoundSqlLiteral) ? Arel::Nodes::BoundSqlLiteral.new(lock_condition, [], {}) : Arel::Nodes::SqlLiteral.new(lock_condition))
@@ -82,11 +101,12 @@ module GoodJob
82
101
  # @example Get the records that have a session awaiting a lock:
83
102
  # MyLockableRecord.joins_advisory_locks.where("pg_locks.granted = ?", false)
84
103
  scope :joins_advisory_locks, (lambda do |column: _advisory_lockable_column|
104
+ quoted_column = adapter_class.quote_column_name(column)
85
105
  joins(<<~SQL.squish)
86
106
  LEFT JOIN pg_locks ON pg_locks.locktype = 'advisory'
87
107
  AND pg_locks.objsubid = 1
88
- AND pg_locks.classid = ('x' || substr(md5(#{connection.quote(table_name)} || '-' || #{quoted_table_name}.#{connection.quote_column_name(column)}::text), 1, 16))::bit(32)::int
89
- AND pg_locks.objid = (('x' || substr(md5(#{connection.quote(table_name)} || '-' || #{quoted_table_name}.#{connection.quote_column_name(column)}::text), 1, 16))::bit(64) << 32)::bit(32)::int
108
+ AND pg_locks.classid = ('x' || substr(md5(#{_quoted_table_name_string} || '-' || #{quoted_table_name}.#{quoted_column}::text), 1, 16))::bit(32)::int
109
+ AND pg_locks.objid = (('x' || substr(md5(#{_quoted_table_name_string} || '-' || #{quoted_table_name}.#{quoted_column}::text), 1, 16))::bit(64) << 32)::bit(32)::int
90
110
  SQL
91
111
  end)
92
112
 
@@ -96,8 +116,8 @@ module GoodJob
96
116
  # @param column [String, Symbol] column values to Advisory Lock against
97
117
  # @return [ActiveRecord::Relation]
98
118
  scope :includes_advisory_locks, (lambda do |column: _advisory_lockable_column|
99
- owns_advisory_lock_sql = "#{connection.quote_table_name('pg_locks')}.#{connection.quote_column_name('pid')} = pg_backend_pid() AS owns_advisory_lock"
100
- joins_advisory_locks(column: column).select("#{quoted_table_name}.*, #{connection.quote_table_name('pg_locks')}.locktype, #{owns_advisory_lock_sql}")
119
+ owns_advisory_lock_sql = "#{adapter_class.quote_table_name('pg_locks')}.#{adapter_class.quote_column_name('pid')} = pg_backend_pid() AS owns_advisory_lock"
120
+ joins_advisory_locks(column: column).select("#{quoted_table_name}.*, #{adapter_class.quote_table_name('pg_locks')}.locktype, #{owns_advisory_lock_sql}")
101
121
  end)
102
122
 
103
123
  # Find records that do not have an advisory lock on them.
@@ -174,18 +194,20 @@ module GoodJob
174
194
  def with_advisory_lock(column: _advisory_lockable_column, function: advisory_lockable_function, unlock_session: false, select_limit: nil)
175
195
  raise ArgumentError, "Must provide a block" unless block_given?
176
196
 
177
- records = advisory_lock(column: column, function: function, select_limit: select_limit).to_a
178
-
179
- begin
180
- unscoped { yield(records) }
181
- ensure
182
- if unlock_session
183
- advisory_unlock_session
184
- else
185
- unlock_function = advisory_unlockable_function(function)
186
- if unlock_function
187
- records.each do |record|
188
- record.advisory_unlock(key: record.lockable_column_key(column: column), function: unlock_function)
197
+ connection_pool.with_connection do
198
+ records = advisory_lock(column: column, function: function, select_limit: select_limit).to_a
199
+
200
+ begin
201
+ unscoped { yield(records) }
202
+ ensure
203
+ if unlock_session
204
+ advisory_unlock_session
205
+ else
206
+ unlock_function = advisory_unlockable_function(function)
207
+ if unlock_function
208
+ records.each do |record|
209
+ record.advisory_unlock(key: record.lockable_column_key(column: column), function: unlock_function)
210
+ end
189
211
  end
190
212
  end
191
213
  end
@@ -213,15 +235,20 @@ module GoodJob
213
235
  ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
214
236
  ]
215
237
 
216
- locked = connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).first['locked']
217
- return locked unless block_given?
218
- return nil unless locked
238
+ if block_given?
239
+ connection_pool.with_connection do |conn|
240
+ locked = conn.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).first['locked']
241
+ return nil unless locked
219
242
 
220
- begin
221
- yield
222
- ensure
223
- unlock_function = advisory_unlockable_function(function)
224
- advisory_unlock_key(key, function: unlock_function) if unlock_function
243
+ begin
244
+ yield
245
+ ensure
246
+ unlock_function = advisory_unlockable_function(function)
247
+ advisory_unlock_key(key, function: unlock_function) if unlock_function
248
+ end
249
+ end
250
+ else
251
+ lease_connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).first['locked']
225
252
  end
226
253
  end
227
254
 
@@ -241,7 +268,7 @@ module GoodJob
241
268
  binds = [
242
269
  ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
243
270
  ]
244
- connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Unlock', binds).first['unlocked']
271
+ lease_connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Unlock', binds).first['unlocked']
245
272
  end
246
273
 
247
274
  # Tests whether the provided key has an advisory lock on it.
@@ -261,7 +288,7 @@ module GoodJob
261
288
  ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
262
289
  ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
263
290
  ]
264
- connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Locked?', binds).any?
291
+ connection_pool.with_connection { |conn| conn.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Locked?', binds).any? }
265
292
  end
266
293
 
267
294
  # Tests whether this record is locked by the current database session.
@@ -282,17 +309,21 @@ module GoodJob
282
309
  ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
283
310
  ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
284
311
  ]
285
- connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Owns Advisory Lock?', binds).any?
312
+ lease_connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Owns Advisory Lock?', binds).any?
286
313
  end
287
314
 
288
315
  def _advisory_lockable_column
289
316
  advisory_lockable_column || primary_key
290
317
  end
291
318
 
319
+ def _quoted_table_name_string
320
+ @_quoted_table_name_string ||= "'#{table_name.gsub("'", "''")}'"
321
+ end
322
+
292
323
  def supports_cte_materialization_specifiers?
293
324
  return @_supports_cte_materialization_specifiers if defined?(@_supports_cte_materialization_specifiers)
294
325
 
295
- @_supports_cte_materialization_specifiers = connection.postgresql_version >= 120000
326
+ @_supports_cte_materialization_specifiers = connection_pool.with_connection { |conn| conn.postgresql_version >= 120000 }
296
327
  end
297
328
 
298
329
  # Postgres advisory unlocking function for the class
@@ -307,7 +338,7 @@ module GoodJob
307
338
  # Unlocks all advisory locks active in the current database session/connection
308
339
  # @return [void]
309
340
  def advisory_unlock_session
310
- connection.exec_query("SELECT pg_advisory_unlock_all()::text AS unlocked", 'GoodJob::Lockable Unlock Session').first[:unlocked]
341
+ lease_connection.exec_query("SELECT pg_advisory_unlock_all()::text AS unlocked", 'GoodJob::Lockable Unlock Session').first[:unlocked]
311
342
  end
312
343
 
313
344
  # Converts SQL query strings between PG-compatible and JDBC-compatible syntax
@@ -371,12 +402,14 @@ module GoodJob
371
402
  def with_advisory_lock(key: lockable_key, function: advisory_lockable_function)
372
403
  raise ArgumentError, "Must provide a block" unless block_given?
373
404
 
374
- advisory_lock!(key: key, function: function)
375
- begin
376
- yield
377
- ensure
378
- unlock_function = self.class.advisory_unlockable_function(function)
379
- advisory_unlock(key: key, function: unlock_function) if unlock_function
405
+ self.class.connection_pool.with_connection do
406
+ advisory_lock!(key: key, function: function)
407
+ begin
408
+ yield
409
+ ensure
410
+ unlock_function = self.class.advisory_unlockable_function(function)
411
+ advisory_unlock(key: key, function: unlock_function) if unlock_function
412
+ end
380
413
  end
381
414
  end
382
415
 
@@ -399,21 +432,6 @@ module GoodJob
399
432
  # @return [Boolean]
400
433
  def owns_advisory_lock?(key: lockable_key)
401
434
  self.class.owns_advisory_lock_key?(key)
402
- query = <<~SQL.squish
403
- SELECT 1 AS one
404
- FROM pg_locks
405
- WHERE pg_locks.locktype = 'advisory'
406
- AND pg_locks.objsubid = 1
407
- AND pg_locks.classid = ('x' || substr(md5($1::text), 1, 16))::bit(32)::int
408
- AND pg_locks.objid = (('x' || substr(md5($2::text), 1, 16))::bit(64) << 32)::bit(32)::int
409
- AND pg_locks.pid = pg_backend_pid()
410
- LIMIT 1
411
- SQL
412
- binds = [
413
- ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
414
- ActiveRecord::Relation::QueryAttribute.new('key', key, ActiveRecord::Type::String.new),
415
- ]
416
- self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Owns Advisory Lock?', binds).any?
417
435
  end
418
436
 
419
437
  # Releases all advisory locks on the record that are held by the current
@@ -52,7 +52,7 @@ module GoodJob
52
52
  def database_supports_websearch_to_tsquery?
53
53
  return @_database_supports_websearch_to_tsquery if defined?(@_database_supports_websearch_to_tsquery)
54
54
 
55
- @_database_supports_websearch_to_tsquery = connection.postgresql_version >= 110000
55
+ @_database_supports_websearch_to_tsquery = connection_pool.with_connection { |conn| conn.postgresql_version >= 110000 }
56
56
  end
57
57
  end
58
58
  end
@@ -44,6 +44,13 @@ module GoodJob
44
44
  def self.bind_value(name, value, type_class)
45
45
  Arel::Nodes::BindParam.new(ActiveRecord::Relation::QueryAttribute.new(name, value, type_class.new))
46
46
  end
47
+
48
+ # Rails < 7.2 does not have lease_connection; connection is behaviorally equivalent.
49
+ unless respond_to?(:lease_connection)
50
+ def self.lease_connection
51
+ connection
52
+ end
53
+ end
47
54
  end
48
55
  end
49
56
 
@@ -1,5 +1,47 @@
1
1
  {
2
2
  "ignored_warnings": [
3
+ {
4
+ "warning_type": "SQL Injection",
5
+ "warning_code": 0,
6
+ "fingerprint": "e3b77f1540f622502491e00b2825b0d5cb6cb3962017fa3ef18d4d463799f3fd",
7
+ "check_name": "SQL",
8
+ "message": "Possible SQL injection",
9
+ "file": "app/models/concerns/good_job/advisory_lockable.rb",
10
+ "line": 89,
11
+ "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
12
+ "code": "joins(\"LEFT JOIN pg_locks ON pg_locks.locktype = 'advisory'\\n AND pg_locks.objsubid = 1\\n AND pg_locks.classid = ...#{_quoted_table_name_string}...\\n\".squish)",
13
+ "render_path": null,
14
+ "location": {
15
+ "type": "method",
16
+ "class": "GoodJob",
17
+ "method": null
18
+ },
19
+ "user_input": "_quoted_table_name_string",
20
+ "confidence": "Medium",
21
+ "cwe_id": [89],
22
+ "note": "Values are quoted identifiers and an escaped table name string; not user input."
23
+ },
24
+ {
25
+ "warning_type": "SQL Injection",
26
+ "warning_code": 0,
27
+ "fingerprint": "ffcbcbd3d170cb7e40cc3b7054e6157983b6ccdf7eab02bc1b0f525158b35248",
28
+ "check_name": "SQL",
29
+ "message": "Possible SQL injection",
30
+ "file": "app/models/concerns/good_job/advisory_lockable.rb",
31
+ "line": 89,
32
+ "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
33
+ "code": "joins(\"LEFT JOIN pg_locks ON pg_locks.locktype = 'advisory'\\n AND pg_locks.objsubid = 1\\n AND pg_locks.classid = ...#{_quoted_table_name_string}...\\n\".squish)",
34
+ "render_path": null,
35
+ "location": {
36
+ "type": "method",
37
+ "class": "GoodJob::AdvisoryLockable",
38
+ "method": null
39
+ },
40
+ "user_input": "_quoted_table_name_string",
41
+ "confidence": "Medium",
42
+ "cwe_id": [89],
43
+ "note": "Values are quoted identifiers and an escaped table name string; not user input."
44
+ },
3
45
  {
4
46
  "warning_type": "Dynamic Render Path",
5
47
  "warning_code": 15,
@@ -77,11 +77,11 @@ module GoodJob # :nodoc:
77
77
  @record.advisory_lock!
78
78
  @record.update(lock_type: :advisory)
79
79
  end
80
- @advisory_locked_connection = WeakRef.new(@record.class.connection)
80
+ @advisory_locked_connection = WeakRef.new(@record.class.lease_connection)
81
81
  end
82
82
  else
83
83
  @record = GoodJob::Process.find_or_create_record(id: @record_id, with_advisory_lock: true)
84
- @advisory_locked_connection = WeakRef.new(@record.class.connection)
84
+ @advisory_locked_connection = WeakRef.new(@record.class.lease_connection)
85
85
  create_refresh_task
86
86
  end
87
87
  end
@@ -155,7 +155,8 @@ module GoodJob # :nodoc:
155
155
  private
156
156
 
157
157
  def advisory_locked_connection?
158
- @record&.class&.connection && @advisory_locked_connection&.weakref_alive? && @advisory_locked_connection.eql?(@record.class.connection)
158
+ conn = @record&.class&.lease_connection
159
+ conn && @advisory_locked_connection&.weakref_alive? && @advisory_locked_connection.eql?(conn)
159
160
  end
160
161
 
161
162
  def task_interval
@@ -54,10 +54,11 @@ module GoodJob # :nodoc:
54
54
  # Send a message via Postgres NOTIFY
55
55
  # @param message [#to_json]
56
56
  def self.notify(message)
57
- connection = ::GoodJob::Job.connection
58
- connection.exec_query <<~SQL.squish
59
- NOTIFY #{CHANNEL}, #{connection.quote(message.to_json)}
60
- SQL
57
+ ::GoodJob::Job.connection_pool.with_connection do |connection|
58
+ connection.exec_query <<~SQL.squish
59
+ NOTIFY #{CHANNEL}, #{connection.quote(message.to_json)}
60
+ SQL
61
+ end
61
62
  end
62
63
 
63
64
  # List of recipients that will receive notifications.
@@ -17,6 +17,12 @@ module GoodJob # :nodoc:
17
17
  _overridden_connection || super
18
18
  end
19
19
 
20
+ # Overrides lease_connection to use the assigned connection when set.
21
+ # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter]
22
+ def lease_connection
23
+ _overridden_connection || super
24
+ end
25
+
20
26
  # Block interface to assign the connection, yield, then unassign the connection.
21
27
  # @param conn [ActiveRecord::ConnectionAdapters::AbstractAdapter]
22
28
  # @return [void]
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GoodJob
4
4
  # GoodJob gem version.
5
- VERSION = '4.14.1'
5
+ VERSION = '4.14.2'
6
6
 
7
7
  # GoodJob version as Gem::Version object
8
8
  GEM_VERSION = Gem::Version.new(VERSION)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: good_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.14.1
4
+ version: 4.14.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon