kettle-dev 1.2.1 → 1.2.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: cf48cdba2c3cbe1c95eab4c61a4073f7849fda6b7dbc41ec2dedaca59b414242
4
- data.tar.gz: 171598b7227ced01474c97a14a8167d3c6c64785b08daeb6996e64d1ed42973d
3
+ metadata.gz: cc18821059e69cb633ba8dbde14b8edf85b999738d86044c5df905f82a203bab
4
+ data.tar.gz: b534a2bf911e1a6ff8618209caebb93fbad634e647a454344ccc4221a5a58b1f
5
5
  SHA512:
6
- metadata.gz: 224a2b34db6d14760ae6105eb67407baebb25a67d7ac1ccad37eaa01823278d3d6c68e6822430f47d986c756f232ce637df248184ec61ca7a4394df3c4ced4c8
7
- data.tar.gz: f8d133803fdbcf798e05faf1749a3e03efc539d5513142bbe76da9b3954662cf043ae868b79151c0c1cb84129f15b4e3e3af7a9764e301d14134fa352ba38ff5
6
+ metadata.gz: 3978928d9ef6fda478251deda2c79818bc645bff3b6150e44e4b2a8ebbf13c49de8a676d76eb8ff0795d3c93c7a326c5fa153562f8ac9a93f95e083e2d077723
7
+ data.tar.gz: 3686018133d386691fefdab49fa10e7dea1c4a4ae65e2486cd161091a0d98e7e2b76721b9f45edc536763dfa8d2f1dbc97bcc83f90a39d1149f8c414873ed02a
checksums.yaml.gz.sig CHANGED
@@ -1,2 +1,2 @@
1
- YGZ�ܧ*٫e�.ڴ��C��G�7� ��QMhz�G��o����7v��<^�x��g�|2��������I@ -N�9pG}��D�^��ӓU���FnA�|3oރ���苊E�
2
- Zff�^v� ��hPxvD���ޓ���HV�~60-6���h��U M��/qo0}dy+���k��3����񿡤�tr:��t��$�u����%`���{�`z!XBG+�$[��z��_Y</l3���)�X�; c� ���U=��l$��a��<O0��q��۠�Unrf�1y����J޸ �V� ���T񾹈7�T���=�O��
1
+ -����g*�y��aʨ��n]}�
2
+ ���3�#���U4f�����ٖ�Q{m߇��E�v����J�������9&4m��n��Q>t���ȋ�������5��G�����C��3��9iS�ð��:;����!<�e)H��۲=˄4q2�P��E���+���^��wWZ�ų�$m�I[��%��25lZL!�o ߹�Z����v�{T�]�K����Mw�Č�۠j�r�%�'����hr���8��p��;޴��VG{Se��6�,p޲gx���Su݋�U�?�^�t3S��*PN[>��qb��y�<���$�����de�NLy��CQh_�������Dn2 !0�J����^�IL(Z
data/CHANGELOG.md CHANGED
@@ -20,13 +20,6 @@ Please file a bug if you notice a violation of semantic versioning.
20
20
 
21
21
  ### Added
22
22
 
23
- - Prism AST-based manipulation of ruby during templating
24
- - Gemfiles
25
- - gemspecs
26
- - .simplecov
27
- - Stop rescuing Exception in certain scenarios (just StandardError)
28
- - Refactored logging logic and documentation
29
-
30
23
  ### Changed
31
24
 
32
25
  ### Deprecated
@@ -37,6 +30,27 @@ Please file a bug if you notice a violation of semantic versioning.
37
30
 
38
31
  ### Security
39
32
 
33
+ ## [1.2.2] - 2025-11-27
34
+
35
+ - TAG: [v1.2.2][1.2.2t]
36
+ - COVERAGE: 93.28% -- 4596/4927 lines in 31 files
37
+ - BRANCH COVERAGE: 76.45% -- 1883/2463 branches in 31 files
38
+ - 70.00% documented
39
+
40
+ ### Added
41
+
42
+ - Prism AST-based manipulation of ruby during templating
43
+ - Gemfiles
44
+ - gemspecs
45
+ - .simplecov
46
+ - Stop rescuing Exception in certain scenarios (just StandardError)
47
+ - Refactored logging logic and documentation
48
+ - Prevent self-referential gemfile injection
49
+ - in Gemfiles, gemspecs, and Appraisals
50
+ - Improve reliability of coverage and documentation stats
51
+ - in the changelog version heading
52
+ - fails hard when unable to generate stats, unless `--no-strict` provided
53
+
40
54
  ## [1.2.1] - 2025-11-25
41
55
 
42
56
  - TAG: [v1.2.0][1.2.0t]
@@ -1477,7 +1491,9 @@ Please file a bug if you notice a violation of semantic versioning.
1477
1491
  - Selecting will run the selected workflow via `act`
1478
1492
  - This may move to its own gem in the future.
1479
1493
 
1480
- [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.2.0...HEAD
1494
+ [Unreleased]: https://github.com/kettle-rb/kettle-dev/compare/v1.2.2...HEAD
1495
+ [1.2.2]: https://github.com/kettle-rb/kettle-dev/compare/v1.2.1...v1.2.2
1496
+ [1.2.2t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.2.2
1481
1497
  [1.2.0]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.60...v1.2.0
1482
1498
  [1.2.0t]: https://github.com/kettle-rb/kettle-dev/releases/tag/v1.2.0
1483
1499
  [1.1.60]: https://github.com/kettle-rb/kettle-dev/compare/v1.1.59...v1.1.60
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.308-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1029
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.927-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.308-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
551
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.927-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.1 - 2025-11-26
3
+ # kettle-dev Rakefile v1.2.2 - 2025-11-27
4
4
  # Ruby 2.3 (Safe Navigation) or higher required
5
5
  #
6
6
  # MIT License (see License.txt)
data/exe/kettle-changelog CHANGED
@@ -50,22 +50,37 @@ end
50
50
  begin
51
51
  if ARGV.include?("-h") || ARGV.include?("--help")
52
52
  puts <<~USAGE
53
- Usage: kettle-changelog
53
+ Usage: kettle-changelog [--no-strict]
54
54
 
55
55
  Generates a new CHANGELOG.md entry for the current version detected from lib/**/version.rb.
56
56
  Moves entries from [Unreleased] into the new section, adds coverage and documentation stats,
57
57
  and updates bottom link references to GitHub style, adding new compare/tag links.
58
58
 
59
+ Options:
60
+ --no-strict Allow missing coverage and yard data (warnings only, no errors)
61
+
62
+ Environment:
63
+ K_CHANGELOG_STRICT=false Disable strict mode (equivalent to --no-strict flag)
64
+
59
65
  Prerequisites:
60
- - coverage/coverage.json present (run: K_SOUP_COV_FORMATTERS="json" bin/rspec)
66
+ - coverage/coverage.json present (run: bin/rake coverage to generate)
61
67
  - yard installed and available via bin/yard
68
+
69
+ By default (strict mode), if coverage.json or yard stats are missing, the script will:
70
+ 1. Attempt to generate them by running bin/rake coverage and bin/rake yard
71
+ 2. Fail with an error if generation fails or data is still unavailable
72
+
73
+ Use --no-strict or K_CHANGELOG_STRICT=false to allow missing data (backward compatible behavior).
62
74
  USAGE
63
75
  exit(0)
64
76
  end
65
77
  end
66
78
 
67
79
  begin
68
- Kettle::Dev::ChangelogCLI.new.run
80
+ # Determine if strict mode is enabled (default: true)
81
+ strict_mode = !ARGV.include?("--no-strict") && ENV.fetch("K_CHANGELOG_STRICT", "true").downcase != "false"
82
+
83
+ Kettle::Dev::ChangelogCLI.new(strict: strict_mode).run
69
84
  rescue LoadError => e
70
85
  warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
71
86
  warn(e.backtrace.join("\n")) if ENV["DEBUG"]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "open3"
4
+
3
5
  module Kettle
4
6
  module Dev
5
7
  # CLI for updating CHANGELOG.md with new version sections
@@ -11,10 +13,12 @@ module Kettle
11
13
 
12
14
  # Initialize the changelog CLI
13
15
  # Sets up paths for CHANGELOG.md and coverage.json
14
- def initialize
16
+ # @param strict [Boolean] when true (default), require coverage and yard data; raise errors if unavailable
17
+ def initialize(strict: true)
15
18
  @root = Kettle::Dev::CIHelpers.project_root
16
19
  @changelog_path = File.join(@root, "CHANGELOG.md")
17
20
  @coverage_path = File.join(@root, "coverage", "coverage.json")
21
+ @strict = strict
18
22
  end
19
23
 
20
24
  # Main entry point to update CHANGELOG.md
@@ -215,11 +219,49 @@ module Kettle
215
219
  end
216
220
 
217
221
  def coverage_lines
218
- unless File.file?(@coverage_path)
219
- warn("Coverage JSON not found at #{@coverage_path}.")
220
- warn("Run: K_SOUP_COV_FORMATTERS=\"json\" bin/rspec")
221
- return [nil, nil]
222
+ if @strict
223
+ # Always generate fresh coverage data in strict mode
224
+ # Delete old coverage files to ensure we get current data
225
+ coverage_dir = File.dirname(@coverage_path)
226
+ if Dir.exist?(coverage_dir)
227
+ puts "Cleaning old coverage data from #{coverage_dir}..."
228
+ Dir.glob(File.join(coverage_dir, "*")).each do |file|
229
+ File.delete(file) if File.file?(file)
230
+ end
231
+ end
232
+
233
+ puts "Generating fresh coverage data by running: bin/rake coverage"
234
+
235
+ # Run bin/rake coverage to generate coverage.json
236
+ rake_cmd = File.join(@root, "bin", "rake")
237
+ unless File.executable?(rake_cmd)
238
+ raise "bin/rake not found or not executable; cannot generate coverage data"
239
+ end
240
+
241
+ # Run the command exactly as the user would run it manually
242
+ # The coverage task knows how to configure itself properly
243
+ success = system(rake_cmd, "coverage", chdir: @root)
244
+
245
+ unless success
246
+ raise "bin/rake coverage failed with exit status #{$?.exitstatus || "unknown"}"
247
+ end
248
+
249
+ puts "Coverage generation complete."
250
+
251
+ # Check if coverage.json was generated
252
+ unless File.file?(@coverage_path)
253
+ raise "Coverage JSON not found at #{@coverage_path} after running bin/rake coverage"
254
+ end
255
+ else
256
+ # Non-strict mode: check if coverage.json exists, warn if not
257
+ unless File.file?(@coverage_path)
258
+ warn("Coverage JSON not found at #{@coverage_path}.")
259
+ warn("Run: bin/rake coverage to generate it")
260
+ return [nil, nil]
261
+ end
222
262
  end
263
+
264
+ # Parse the coverage data
223
265
  data = JSON.parse(File.read(@coverage_path))
224
266
  files = data["coverage"] || {}
225
267
  file_count = 0
@@ -252,31 +294,55 @@ module Kettle
252
294
  line_str = format("COVERAGE: %.2f%% -- %d/%d lines in %d files", line_pct, covered_lines, total_lines, file_count)
253
295
  branch_str = format("BRANCH COVERAGE: %.2f%% -- %d/%d branches in %d files", branch_pct, covered_branches, total_branches, file_count)
254
296
  [line_str, branch_str]
297
+ rescue JSON::ParserError => e
298
+ if @strict
299
+ raise "Failed to parse coverage JSON at #{@coverage_path}: #{e.class}: #{e.message}"
300
+ else
301
+ warn("Failed to parse coverage: #{e.class}: #{e.message}")
302
+ [nil, nil]
303
+ end
255
304
  rescue StandardError => e
256
- warn("Failed to parse coverage: #{e.class}: #{e.message}")
257
- [nil, nil]
305
+ if @strict
306
+ raise "Failed to get coverage data: #{e.class}: #{e.message}"
307
+ else
308
+ warn("Failed to get coverage data: #{e.class}: #{e.message}")
309
+ [nil, nil]
310
+ end
258
311
  end
259
312
 
260
313
  def yard_percent_documented
261
314
  cmd = File.join(@root, "bin", "yard")
262
315
  unless File.executable?(cmd)
263
- warn("bin/yard not found or not executable; ensure yard is installed via bundler")
264
- return
316
+ if @strict
317
+ raise "bin/yard not found or not executable; ensure yard is installed via bundler"
318
+ else
319
+ warn("bin/yard not found or not executable; ensure yard is installed via bundler")
320
+ return
321
+ end
265
322
  end
266
- out, _ = Open3.capture2(cmd)
267
- # Look for a line containing e.g., "95.35% documented"
268
- line = out.lines.find { |l| l =~ /\d+(?:\.\d+)?%\s+documented/ }
269
- if line
270
- line = line.strip
271
- # Return exactly as requested: e.g. "95.35% documented"
272
- line
273
- else
274
- warn("Could not find documented percentage in bin/yard output.")
275
- nil
323
+
324
+ begin
325
+ # Run bin/yard to get documentation percentage
326
+ out, _ = Open3.capture2(cmd, {chdir: @root})
327
+ # Look for a line containing e.g., "95.35% documented"
328
+ line = out.lines.find { |l| l =~ /\d+(?:\.\d+)?%\s+documented/ }
329
+
330
+ if line
331
+ line.strip
332
+ elsif @strict
333
+ raise "Could not find documented percentage in bin/yard output"
334
+ else
335
+ warn("Could not find documented percentage in bin/yard output.")
336
+ nil
337
+ end
338
+ rescue StandardError => e
339
+ if @strict
340
+ raise "Failed to run bin/yard: #{e.class}: #{e.message}"
341
+ else
342
+ warn("Failed to run bin/yard: #{e.class}: #{e.message}")
343
+ nil
344
+ end
276
345
  end
277
- rescue StandardError => e
278
- warn("Failed to run bin/yard: #{e.class}: #{e.message}")
279
- nil
280
346
  end
281
347
 
282
348
  # Transform legacy release headings that include a tag suffix, e.g.:
@@ -301,6 +301,51 @@ module Kettle
301
301
  statements = body.is_a?(Prism::StatementsNode) ? body.body : [body]
302
302
  statements.compact.map { |stmt| {node: stmt, inline_comments: [], leading_comments: []} }
303
303
  end
304
+
305
+ # Remove gem calls that reference the given gem name (to prevent self-dependency).
306
+ # Works by locating gem() call nodes within appraise blocks where the first argument matches gem_name.
307
+ # @param content [String] Appraisals content
308
+ # @param gem_name [String] the gem name to remove
309
+ # @return [String] modified content with self-referential gem calls removed
310
+ def remove_gem_dependency(content, gem_name)
311
+ return content if gem_name.to_s.strip.empty?
312
+
313
+ result = PrismUtils.parse_with_comments(content)
314
+ root = result.value
315
+ return content unless root&.statements&.body
316
+
317
+ out = content.dup
318
+
319
+ # Iterate through all appraise blocks
320
+ root.statements.body.each do |node|
321
+ next unless appraise_call?(node)
322
+ next unless node.block&.body
323
+
324
+ body_stmts = PrismUtils.extract_statements(node.block.body)
325
+
326
+ # Find gem call nodes within this appraise block where first argument matches gem_name
327
+ body_stmts.each do |stmt|
328
+ next unless stmt.is_a?(Prism::CallNode) && stmt.name == :gem
329
+
330
+ first_arg = stmt.arguments&.arguments&.first
331
+ arg_val = begin
332
+ PrismUtils.extract_literal_value(first_arg)
333
+ rescue StandardError
334
+ nil
335
+ end
336
+
337
+ if arg_val && arg_val.to_s == gem_name.to_s
338
+ # Remove this gem call from content
339
+ out = out.sub(stmt.slice, "")
340
+ end
341
+ end
342
+ end
343
+
344
+ out
345
+ rescue StandardError => e
346
+ Kettle::Dev.debug_error(e, __method__) if defined?(Kettle::Dev.debug_error)
347
+ content
348
+ end
304
349
  end
305
350
  end
306
351
  end
@@ -131,6 +131,47 @@ module Kettle
131
131
  end
132
132
  dest_content
133
133
  end
134
+
135
+ # Remove gem calls that reference the given gem name (to prevent self-dependency).
136
+ # Works by locating gem() call nodes where the first argument matches gem_name.
137
+ # @param content [String] Gemfile-like content
138
+ # @param gem_name [String] the gem name to remove
139
+ # @return [String] modified content with self-referential gem calls removed
140
+ def remove_gem_dependency(content, gem_name)
141
+ return content if gem_name.to_s.strip.empty?
142
+
143
+ result = PrismUtils.parse_with_comments(content)
144
+ stmts = PrismUtils.extract_statements(result.value.statements)
145
+
146
+ # Find gem call nodes where first argument matches gem_name
147
+ gem_nodes = stmts.select do |n|
148
+ next false unless n.is_a?(Prism::CallNode) && n.name == :gem
149
+
150
+ first_arg = n.arguments&.arguments&.first
151
+ arg_val = begin
152
+ PrismUtils.extract_literal_value(first_arg)
153
+ rescue StandardError
154
+ nil
155
+ end
156
+ arg_val && arg_val.to_s == gem_name.to_s
157
+ end
158
+
159
+ # Remove each matching gem call from content
160
+ out = content.dup
161
+ gem_nodes.each do |gn|
162
+ # Remove the entire line(s) containing this node
163
+ out = out.sub(gn.slice, "")
164
+ end
165
+
166
+ out
167
+ rescue StandardError => e
168
+ if defined?(Kettle::Dev) && Kettle::Dev.respond_to?(:debug_error)
169
+ Kettle::Dev.debug_error(e, __method__)
170
+ else
171
+ Kernel.warn("[#{__method__}] #{e.class}: #{e.message}")
172
+ end
173
+ content
174
+ end
134
175
  end
135
176
  end
136
177
  end
@@ -298,6 +298,19 @@ module Kettle
298
298
  end
299
299
  end
300
300
 
301
+ # Apply self-dependency removal for all gem-related files
302
+ # This ensures we don't introduce a self-dependency when templating
303
+ begin
304
+ meta = gemspec_metadata
305
+ gem_name = meta[:gem_name]
306
+ if gem_name && !gem_name.to_s.empty?
307
+ content = remove_self_dependency(content, gem_name, dest_path)
308
+ end
309
+ rescue StandardError => e
310
+ Kettle::Dev.debug_error(e, __method__)
311
+ # If metadata extraction or removal fails, proceed with content as-is
312
+ end
313
+
301
314
  write_file(dest_path, content)
302
315
  begin
303
316
  # Ensure executable bit for git hook scripts when writing under .git-hooks
@@ -342,6 +355,38 @@ module Kettle
342
355
  content
343
356
  end
344
357
 
358
+ # Remove self-referential gem dependencies from content based on file type.
359
+ # Applies to gemspec, Gemfile, modular gemfiles, Appraisal.root.gemfile, and Appraisals.
360
+ # @param content [String] file content
361
+ # @param gem_name [String] the gem name to remove
362
+ # @param file_path [String] path to the file (used to determine type)
363
+ # @return [String] content with self-dependencies removed
364
+ def remove_self_dependency(content, gem_name, file_path)
365
+ return content if gem_name.to_s.strip.empty?
366
+
367
+ basename = File.basename(file_path.to_s)
368
+
369
+ begin
370
+ case basename
371
+ when /\.gemspec$/
372
+ # Use PrismGemspec for gemspec files
373
+ Kettle::Dev::PrismGemspec.remove_spec_dependency(content, gem_name)
374
+ when "Gemfile", "Appraisal.root.gemfile", /\.gemfile$/
375
+ # Use PrismGemfile for Gemfile-like files
376
+ Kettle::Dev::PrismGemfile.remove_gem_dependency(content, gem_name)
377
+ when "Appraisals"
378
+ # Use PrismAppraisals for Appraisals files
379
+ Kettle::Dev::PrismAppraisals.remove_gem_dependency(content, gem_name)
380
+ else
381
+ # Return content unchanged for unknown file types
382
+ content
383
+ end
384
+ rescue StandardError => e
385
+ Kettle::Dev.debug_error(e, __method__)
386
+ content
387
+ end
388
+ end
389
+
345
390
  # Copy a directory tree, prompting before creating or overwriting.
346
391
  # @return [void]
347
392
  def copy_dir_with_prompt(src_dir, dest_dir)
@@ -6,7 +6,7 @@ module Kettle
6
6
  module Version
7
7
  # The gem version.
8
8
  # @return [String]
9
- VERSION = "1.2.1"
9
+ VERSION = "1.2.2"
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.1
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -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.1
418
- changelog_uri: https://github.com/kettle-rb/kettle-dev/blob/v1.2.1/CHANGELOG.md
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
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.1
420
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/1.2.2
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