kettle-dev 1.2.2 → 1.2.3

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: cc18821059e69cb633ba8dbde14b8edf85b999738d86044c5df905f82a203bab
4
- data.tar.gz: b534a2bf911e1a6ff8618209caebb93fbad634e647a454344ccc4221a5a58b1f
3
+ metadata.gz: cad2d78d297d0e69cd016624ff87dba0c5fa4bd3af5399d7c97c4e8fcc2af82a
4
+ data.tar.gz: '08f58025922178025f79d3ef3004b35f9207e23764c490587ec3ed5dfc240ca5'
5
5
  SHA512:
6
- metadata.gz: 3978928d9ef6fda478251deda2c79818bc645bff3b6150e44e4b2a8ebbf13c49de8a676d76eb8ff0795d3c93c7a326c5fa153562f8ac9a93f95e083e2d077723
7
- data.tar.gz: 3686018133d386691fefdab49fa10e7dea1c4a4ae65e2486cd161091a0d98e7e2b76721b9f45edc536763dfa8d2f1dbc97bcc83f90a39d1149f8c414873ed02a
6
+ metadata.gz: 12563697c8b719fc13ded6b096fcd92266dbb98f9f8c87a21aad756bebbe2065f7dc17e2be982d1a3765a15c8c3b8cbd0eec88642fc7fb90600e1be88c4db149
7
+ data.tar.gz: e3ce95406792e64d08da4db003a5771c45c975155c417176e10972e13ad0e012ed006aa9193b81b1fbecc96d59edb752cba80a26a538c60f824c52228e6110e9
checksums.yaml.gz.sig CHANGED
Binary file
data/.rubocop_rspec.yml CHANGED
@@ -25,6 +25,9 @@ RSpec/ExpectInHook:
25
25
  RSpec/DescribeClass:
26
26
  Exclude:
27
27
  - 'spec/examples/*'
28
+ - 'spec/integration/*'
29
+ - 'spec/system/*'
30
+ - 'spec/e2e/*'
28
31
 
29
32
  RSpec/MultipleMemoizedHelpers:
30
33
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -30,6 +30,22 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.2.3] - 2025-11-28
34
+
35
+ - TAG: [v1.2.3][1.2.3t]
36
+ - COVERAGE: 93.43% -- 4681/5010 lines in 31 files
37
+ - BRANCH COVERAGE: 76.63% -- 1912/2495 branches in 31 files
38
+ - 70.55% documented
39
+
40
+ ### Fixed
41
+
42
+ - Fixed Gemfile parsing to properly deduplicate comments across multiple template runs
43
+ - Implemented two-pass comment deduplication: sequences first, then individual lines
44
+ - Magic comments (frozen_string_literal, encoding, etc.) are now properly deduplicated by content, not line position
45
+ - File-level comments are deduplicated while preserving leading comments attached to statements
46
+ - Ensures idempotent behavior when running templating multiple times on the same file
47
+ - Prevents accumulation of duplicate frozen_string_literal comments and comment blocks
48
+
33
49
  ## [1.2.2] - 2025-11-27
34
50
 
35
51
  - TAG: [v1.2.2][1.2.2t]
@@ -1491,7 +1507,9 @@ Please file a bug if you notice a violation of semantic versioning.
1491
1507
  - Selecting will run the selected workflow via `act`
1492
1508
  - This may move to its own gem in the future.
1493
1509
 
1494
- [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.2.2...HEAD
1510
+ [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.2.3...HEAD
1511
+ [1.2.3]: https://github.com/kettle-rb/kettle-dev/compare/v1.2.2...v1.2.3
1512
+ [1.2.3t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.2.3
1495
1513
  [1.2.2]: https://github.com/kettle-rb/kettle-dev/compare/v1.2.1...v1.2.2
1496
1514
  [1.2.2t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.2.2
1497
1515
  [1.2.0]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.60...v1.2.0
data/README.md CHANGED
@@ -1026,7 +1026,7 @@ Thanks for RTFM. ☺️
1026
1026
  [📌gitmoji]: https://gitmoji.dev
1027
1027
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1028
1028
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1029
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.927-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1029
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-5.010-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1030
1030
  [🔐security]: SECURITY.md
1031
1031
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1032
1032
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/README.md.example CHANGED
@@ -548,7 +548,7 @@ Thanks for RTFM. ☺️
548
548
  [📌gitmoji]: https://gitmoji.dev
549
549
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
550
550
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
551
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.927-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
551
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-5.010-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
552
552
  [🔐security]: SECURITY.md
553
553
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
554
554
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/Rakefile.example CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # kettle-dev Rakefile v1.2.2 - 2025-11-27
3
+ # kettle-dev Rakefile v1.2.3 - 2025-11-28
4
4
  # Ruby 2.3 (Safe Navigation) or higher required
5
5
  #
6
6
  # MIT License (see License.txt)
@@ -114,10 +114,10 @@ Gem::Specification.new do |spec|
114
114
  # and preferably a modular one (see gemfiles/modular/*.gemfile).
115
115
 
116
116
  # Dev, Test, & Release Tasks
117
- spec.add_development_dependency("{KETTLE|DEV|GEM}", "~> 1.1") # ruby >= 2.3.0
117
+ spec.add_development_dependency("{KETTLE|DEV|GEM}", "~> 1.2") # ruby >= 2.3.0
118
118
 
119
119
  # Security
120
- spec.add_development_dependency("bundler-audit", "~> 0.9.2") # ruby >= 2.0.0
120
+ spec.add_development_dependency("bundler-audit", "~> 0.9.3") # ruby >= 2.0.0
121
121
 
122
122
  # Tasks
123
123
  spec.add_development_dependency("rake", "~> 13.0") # ruby >= 2.2.0
@@ -131,7 +131,7 @@ Gem::Specification.new do |spec|
131
131
 
132
132
  # Releasing
133
133
  spec.add_development_dependency("ruby-progressbar", "~> 1.13") # ruby >= 0
134
- spec.add_development_dependency("stone_checksums", "~> 1.0", ">= 1.0.2") # ruby >= 2.2.0
134
+ spec.add_development_dependency("stone_checksums", "~> 1.0", ">= 1.0.3") # ruby >= 2.2.0
135
135
 
136
136
  # Git integration (optional)
137
137
  # The 'git' gem is optional; kettle-dev falls back to shelling out to `git` if it is not present.
@@ -48,7 +48,7 @@ module Kettle
48
48
  content =
49
49
  case strategy
50
50
  when :skip
51
- src_with_reminder
51
+ normalize_source(src_with_reminder)
52
52
  when :replace
53
53
  normalize_source(src_with_reminder)
54
54
  when :append
@@ -81,17 +81,22 @@ module Kettle
81
81
  [before, snippet, after].join
82
82
  end
83
83
 
84
- # Normalize source code while preserving formatting
84
+ # Normalize source code by parsing and rebuilding to deduplicate comments
85
85
  #
86
86
  # @param source [String] Ruby source code
87
- # @return [String] Normalized source with trailing newline
87
+ # @return [String] Normalized source with trailing newline and deduplicated comments
88
88
  # @api private
89
89
  def normalize_source(source)
90
90
  parse_result = PrismUtils.parse_with_comments(source)
91
91
  return ensure_trailing_newline(source) unless parse_result.success?
92
92
 
93
- # Use Prism's slice to preserve original formatting
94
- ensure_trailing_newline(source)
93
+ # Extract and deduplicate comments
94
+ magic_comments = extract_magic_comments(parse_result)
95
+ file_leading_comments = extract_file_leading_comments(parse_result)
96
+ node_infos = extract_nodes_with_comments(parse_result)
97
+
98
+ # Rebuild source with deduplicated comments
99
+ build_source_from_nodes(node_infos, magic_comments: magic_comments, file_leading_comments: file_leading_comments)
95
100
  end
96
101
 
97
102
  def reminder_present?(content)
@@ -231,8 +236,8 @@ module Kettle
231
236
  end
232
237
 
233
238
  def prism_merge(src_content, dest_content)
234
- src_result = PrismUtils.parse_with_comments(src_content)
235
- dest_result = PrismUtils.parse_with_comments(dest_content)
239
+ src_result = Kettle::Dev::PrismUtils.parse_with_comments(src_content)
240
+ dest_result = Kettle::Dev::PrismUtils.parse_with_comments(dest_content)
236
241
 
237
242
  # If src parsing failed, return src unchanged to avoid losing content
238
243
  unless src_result.success?
@@ -245,11 +250,48 @@ module Kettle
245
250
 
246
251
  merged_nodes = yield(src_nodes, dest_nodes, src_result, dest_result)
247
252
 
248
- # Extract magic comments from source (frozen_string_literal, etc.)
249
- magic_comments = extract_magic_comments(src_result)
253
+ # Extract and deduplicate comments from src and dest SEPARATELY
254
+ # This allows sequence detection to work within each source
255
+ src_tuples = create_comment_tuples(src_result)
256
+ src_deduplicated = deduplicate_comment_sequences(src_tuples)
257
+
258
+ dest_tuples = dest_result.success? ? create_comment_tuples(dest_result) : []
259
+ dest_deduplicated = deduplicate_comment_sequences(dest_tuples)
260
+
261
+ # Now merge the deduplicated tuples by hash+type only (ignore line numbers)
262
+ seen_hash_type = Set.new
263
+ final_tuples = []
264
+
265
+ # Add all deduplicated src tuples
266
+ src_deduplicated.each do |tuple|
267
+ hash_val = tuple[0]
268
+ type = tuple[1]
269
+ key = [hash_val, type]
270
+ unless seen_hash_type.include?(key)
271
+ final_tuples << tuple
272
+ seen_hash_type << key
273
+ end
274
+ end
275
+
276
+ # Add deduplicated dest tuples that don't duplicate src (by hash+type)
277
+ dest_deduplicated.each do |tuple|
278
+ hash_val = tuple[0]
279
+ type = tuple[1]
280
+ key = [hash_val, type]
281
+ unless seen_hash_type.include?(key)
282
+ final_tuples << tuple
283
+ seen_hash_type << key
284
+ end
285
+ end
286
+
287
+ # Extract magic and file-level comments from final merged tuples
288
+ magic_comments = final_tuples
289
+ .select { |tuple| tuple[1] == :magic }
290
+ .map { |tuple| tuple[2] }
250
291
 
251
- # Extract file-level leading comments (comments before first statement)
252
- file_leading_comments = extract_file_leading_comments(src_result)
292
+ file_leading_comments = final_tuples
293
+ .select { |tuple| tuple[1] == :file_level }
294
+ .map { |tuple| tuple[2] }
253
295
 
254
296
  build_source_from_nodes(merged_nodes, magic_comments: magic_comments, file_leading_comments: file_leading_comments)
255
297
  end
@@ -257,45 +299,155 @@ module Kettle
257
299
  def extract_magic_comments(parse_result)
258
300
  return [] unless parse_result.success?
259
301
 
260
- magic_comments = []
261
- source_lines = parse_result.source.lines
302
+ tuples = create_comment_tuples(parse_result)
303
+ deduplicated = deduplicate_comment_sequences(tuples)
262
304
 
263
- # Magic comments appear at the very top of the file (possibly after shebang)
264
- # They must be on the first or second line
265
- source_lines.first(2).each do |line|
266
- stripped = line.strip
267
- # Check for shebang
268
- if stripped.start_with?("#!")
269
- magic_comments << line.rstrip
270
- # Check for magic comments like frozen_string_literal, encoding, etc.
271
- elsif stripped.start_with?("#") &&
272
- (stripped.include?("frozen_string_literal:") ||
273
- stripped.include?("encoding:") ||
274
- stripped.include?("warn_indent:") ||
275
- stripped.include?("shareable_constant_value:"))
276
- magic_comments << line.rstrip
305
+ # Filter to only magic comments and return their text
306
+ deduplicated
307
+ .select { |tuple| tuple[1] == :magic }
308
+ .map { |tuple| tuple[2] }
309
+ end
310
+
311
+ # Create a tuple for each comment: [hash, type, text, line_number]
312
+ # where type is one of: :magic, :file_level, :leading
313
+ # (inline comments are handled with their associated statements)
314
+ def create_comment_tuples(parse_result)
315
+ return [] unless parse_result.success?
316
+
317
+ statements = PrismUtils.extract_statements(parse_result.value.statements)
318
+ first_stmt_line = statements.any? ? statements.first.location.start_line : Float::INFINITY
319
+
320
+ tuples = []
321
+
322
+ parse_result.comments.each do |comment|
323
+ comment_line = comment.location.start_line
324
+ comment_text = comment.slice.strip
325
+
326
+ # Determine comment type - magic comments are identified by content, not line number
327
+ type = if is_magic_comment?(comment_text)
328
+ :magic
329
+ elsif comment_line < first_stmt_line
330
+ :file_level
331
+ else
332
+ # This will be handled as a leading or inline comment for a statement
333
+ :leading
334
+ end
335
+
336
+ # Create hash from normalized comment text (ignoring trailing whitespace)
337
+ comment_hash = comment_text.hash
338
+
339
+ tuples << [comment_hash, type, comment.slice.rstrip, comment_line]
340
+ end
341
+
342
+ tuples
343
+ end
344
+
345
+ def is_magic_comment?(text)
346
+ text.include?("frozen_string_literal:") ||
347
+ text.include?("encoding:") ||
348
+ text.include?("warn_indent:") ||
349
+ text.include?("shareable_constant_value:")
350
+ end
351
+
352
+ # Two-pass deduplication:
353
+ # Pass 1: Deduplicate multi-line sequences
354
+ # Pass 2: Deduplicate single-line duplicates
355
+ def deduplicate_comment_sequences(tuples)
356
+ return [] if tuples.empty?
357
+
358
+ # Group tuples by type
359
+ by_type = tuples.group_by { |tuple| tuple[1] }
360
+
361
+ result = []
362
+
363
+ [:magic, :file_level, :leading].each do |type|
364
+ type_tuples = by_type[type] || []
365
+ next if type_tuples.empty?
366
+
367
+ # Pass 1: Remove duplicate sequences
368
+ after_pass1 = deduplicate_sequences_pass1(type_tuples)
369
+
370
+ # Pass 2: Remove single-line duplicates
371
+ after_pass2 = deduplicate_singles_pass2(after_pass1)
372
+
373
+ result.concat(after_pass2)
374
+ end
375
+
376
+ result
377
+ end
378
+
379
+ # Pass 1: Find and remove duplicate multi-line comment sequences
380
+ # A sequence is defined by consecutive comments (ignoring blank lines in between)
381
+ def deduplicate_sequences_pass1(tuples)
382
+ return tuples if tuples.length <= 1
383
+
384
+ # Group tuples into sequences (consecutive comments, allowing gaps for blank lines)
385
+ sequences = []
386
+ current_seq = []
387
+ prev_line = nil
388
+
389
+ tuples.each do |tuple|
390
+ line_num = tuple[3]
391
+
392
+ # If this is consecutive with previous (allowing reasonable gaps for blank lines)
393
+ if prev_line.nil? || (line_num - prev_line) <= 3
394
+ current_seq << tuple
395
+ else
396
+ # Start new sequence
397
+ sequences << current_seq if current_seq.any?
398
+ current_seq = [tuple]
399
+ end
400
+
401
+ prev_line = line_num
402
+ end
403
+ sequences << current_seq if current_seq.any?
404
+
405
+ # Find duplicate sequences by comparing hash signatures
406
+ seen_seq_signatures = Set.new
407
+ unique_tuples = []
408
+
409
+ sequences.each do |seq|
410
+ # Create signature from hashes and sequence length
411
+ seq_signature = seq.map { |t| t[0] }.join(",")
412
+
413
+ unless seen_seq_signatures.include?(seq_signature)
414
+ seen_seq_signatures << seq_signature
415
+ unique_tuples.concat(seq)
277
416
  end
278
417
  end
279
418
 
280
- magic_comments
419
+ unique_tuples
420
+ end
421
+
422
+ # Pass 2: Remove single-line duplicates from already sequence-deduplicated tuples
423
+ def deduplicate_singles_pass2(tuples)
424
+ return tuples if tuples.length <= 1
425
+
426
+ seen_hashes = Set.new
427
+ unique_tuples = []
428
+
429
+ tuples.each do |tuple|
430
+ comment_hash = tuple[0]
431
+
432
+ unless seen_hashes.include?(comment_hash)
433
+ seen_hashes << comment_hash
434
+ unique_tuples << tuple
435
+ end
436
+ end
437
+
438
+ unique_tuples
281
439
  end
282
440
 
283
441
  def extract_file_leading_comments(parse_result)
284
442
  return [] unless parse_result.success?
285
443
 
286
- statements = PrismUtils.extract_statements(parse_result.value.statements)
287
- return [] if statements.empty?
444
+ tuples = create_comment_tuples(parse_result)
445
+ deduplicated = deduplicate_comment_sequences(tuples)
288
446
 
289
- first_stmt = statements.first
290
- first_stmt_line = first_stmt.location.start_line
291
-
292
- # Extract file-level comments that appear after magic comments (line 1-2)
293
- # but before the first executable statement. These are typically documentation
294
- # comments describing the file's purpose.
295
- parse_result.comments.select do |comment|
296
- comment.location.start_line > 2 &&
297
- comment.location.start_line < first_stmt_line
298
- end.map { |comment| comment.slice.rstrip }
447
+ # Filter to only file-level comments and return their text
448
+ deduplicated
449
+ .select { |tuple| tuple[1] == :file_level }
450
+ .map { |tuple| tuple[2] }
299
451
  end
300
452
 
301
453
  def extract_nodes_with_comments(parse_result)
@@ -358,8 +510,6 @@ module Kettle
358
510
  end
359
511
 
360
512
  def build_source_from_nodes(node_infos, magic_comments: [], file_leading_comments: [])
361
- return "" if node_infos.empty?
362
-
363
513
  lines = []
364
514
 
365
515
  # Add magic comments at the top (frozen_string_literal, etc.)
@@ -371,9 +521,16 @@ module Kettle
371
521
  # Add file-level leading comments (comments before first statement)
372
522
  if file_leading_comments.any?
373
523
  lines.concat(file_leading_comments)
374
- lines << "" # Add blank line after file-level comments
524
+ # Only add blank line if there are statements following
525
+ lines << "" if node_infos.any?
375
526
  end
376
527
 
528
+ # If there are no statements and no comments, return empty string
529
+ return "" if node_infos.empty? && lines.empty?
530
+
531
+ # If there are only comments and no statements, return the comments
532
+ return lines.join("\n") if node_infos.empty?
533
+
377
534
  node_infos.each do |node_info|
378
535
  # Add blank lines before this statement (for visual grouping)
379
536
  blank_lines = node_info[:blank_lines_before] || 0
@@ -435,7 +592,14 @@ module Kettle
435
592
  def restore_custom_leading_comments(dest_content, merged_content)
436
593
  block = leading_comment_block(dest_content)
437
594
  return merged_content if block.strip.empty?
438
- return merged_content if merged_content.start_with?(block)
595
+
596
+ # Check if the merged content already starts with this block
597
+ # Use normalized comparison to handle whitespace differences
598
+ merged_leading = leading_comment_block(merged_content)
599
+
600
+ # If merged already has the same or more comprehensive leading comments, don't add
601
+ return merged_content if merged_leading.strip == block.strip
602
+ return merged_content if merged_content.include?(block.strip)
439
603
 
440
604
  # Insert after shebang / frozen string literal comments (same place reminder goes)
441
605
  insertion_index = reminder_insertion_index(merged_content)
@@ -6,7 +6,7 @@ module Kettle
6
6
  module Version
7
7
  # The gem version.
8
8
  # @return [String]
9
- VERSION = "1.2.2"
9
+ VERSION = "1.2.3"
10
10
 
11
11
  module_function
12
12
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-dev
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -57,14 +57,14 @@ dependencies:
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: 0.9.2
60
+ version: 0.9.3
61
61
  type: :development
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: 0.9.2
67
+ version: 0.9.3
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: require_bench
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -142,7 +142,7 @@ dependencies:
142
142
  version: '1.0'
143
143
  - - ">="
144
144
  - !ruby/object:Gem::Version
145
- version: 1.0.2
145
+ version: 1.0.3
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
@@ -152,7 +152,7 @@ dependencies:
152
152
  version: '1.0'
153
153
  - - ">="
154
154
  - !ruby/object:Gem::Version
155
- version: 1.0.2
155
+ version: 1.0.3
156
156
  - !ruby/object:Gem::Dependency
157
157
  name: gitmoji-regex
158
158
  requirement: !ruby/object:Gem::Requirement
@@ -414,10 +414,10 @@ licenses:
414
414
  - MIT
415
415
  metadata:
416
416
  homepage_uri: https://kettle-dev.galtzo.com/
417
- source_code_uri: https://github.com/kettle-rb/kettle-dev/tree/v1.2.2
418
- changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.2.2/CHANGELOG.md
417
+ source_code_uri: https://github.com/kettle-rb/kettle-dev/tree/v1.2.3
418
+ changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.2.3/CHANGELOG.md
419
419
  bug_tracker_uri: https://github.com/kettle-rb/kettle-dev/issues
420
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.2.2
420
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.2.3
421
421
  funding_uri: https://github.com/sponsors/pboling
422
422
  wiki_uri: https://github.com/kettle-rb/kettle-dev/wiki
423
423
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file