kettle-family 1.0.2 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19486d91816150dae28ac5f1e8b31ef3f43b0f4f7004337681f08819fb41b27d
4
- data.tar.gz: e2bf61a97f739f211accb4fc5cbfe91acc4121077c5f620786d30d217b4cc803
3
+ metadata.gz: f81e03e49736eb1884f0272f7af5ee78b14239ace620cf5427da26cfbaf2df5c
4
+ data.tar.gz: cc974713f574848b1740c413c1db51913bd99f8fc91fa2683b312f7cdbcfcf91
5
5
  SHA512:
6
- metadata.gz: e8826e083336b3de83e313fc3207c60461e494fc8c1c4ad22b8148b861024ca78adcefa1b5c357d7ed90af79dddb3cda59dc4900434e862739d8d2f930e8e0c3
7
- data.tar.gz: 1d735e6c84877eab971381d22226c96871e2fa0198bab1d9895bf771abdbbe2d5bf2c69a0d16bd04c0a3cdfdc830c7e7354f526313a5f0b8dbeec2a96df7d908
6
+ metadata.gz: 7fb8fa2d38f8a84c537558ab48d90fd17f9933cd27e8017b60a9f6ecf89f54a928d0c21b058dcc0e41766d47372dffb4a2a125c18c0e7049d11ce31ddaac24cd
7
+ data.tar.gz: 2e546a76ba606eb8078a477b696109a30a0acdfe23bda7267905fc926cb65dec5237f938f5d42ab8e8e75fb131a316d0303a3ec637284c7c41331e98a02bcdd2
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,42 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.0.4] - 2026-07-21
34
+
35
+ - TAG: [v1.0.4][1.0.4t]
36
+ - COVERAGE: 95.68% -- 2726/2849 lines in 24 files
37
+ - BRANCH COVERAGE: 77.97% -- 952/1221 branches in 24 files
38
+ - 28.68% documented
39
+
40
+ ### Fixed
41
+
42
+ - `kettle-family release --publish` now skips already-published members with no
43
+ unreleased changes even when post-release commits leave HEAD ahead of the
44
+ release tag, allowing later pending members to continue.
45
+
46
+ ## [1.0.3] - 2026-07-21
47
+
48
+ - TAG: [v1.0.3][1.0.3t]
49
+ - COVERAGE: 95.61% -- 2724/2849 lines in 24 files
50
+ - BRANCH COVERAGE: 77.97% -- 952/1221 branches in 24 files
51
+ - 28.68% documented
52
+
53
+ ### Changed
54
+
55
+ - `kettle-family release-state` text tables now use compact column headings and
56
+ truncate checkout branch names to 10 characters.
57
+
58
+ ### Fixed
59
+
60
+ - `kettle-family release` now builds release waves from gemspec development
61
+ dependencies and literal Gemfile dependencies, including dependencies in
62
+ modular Gemfiles loaded with `eval_gemfile`, so development-tooling releases
63
+ unblock dependent member releases before the next wave starts.
64
+
65
+ - `kettle-family bump` now rewrites exact family dependency pins according to
66
+ release-wave order, so earlier members are not changed to require unreleased
67
+ later member versions.
68
+
33
69
  ## [1.0.2] - 2026-07-21
34
70
 
35
71
  - TAG: [v1.0.2][1.0.2t]
@@ -823,7 +859,11 @@ Please file a bug if you notice a violation of semantic versioning.
823
859
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
824
860
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
825
861
 
826
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.0.2...HEAD
862
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.0.4...HEAD
863
+ [1.0.4]: https://github.com/kettle-dev/kettle-family/compare/v1.0.3...v1.0.4
864
+ [1.0.4t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.0.4
865
+ [1.0.3]: https://github.com/kettle-dev/kettle-family/compare/v1.0.2...v1.0.3
866
+ [1.0.3t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.0.3
827
867
  [1.0.2]: https://github.com/kettle-dev/kettle-family/compare/v1.0.1...v1.0.2
828
868
  [1.0.2t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.0.2
829
869
  [1.0.1]: https://github.com/kettle-dev/kettle-family/compare/v1.0.0...v1.0.1
data/README.md CHANGED
@@ -669,7 +669,7 @@ Thanks for RTFM. ☺️
669
669
  [📌gitmoji]: https://gitmoji.dev
670
670
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
671
671
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
672
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.761-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
672
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.849-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
673
673
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
674
674
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
675
675
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -743,14 +743,25 @@ module Kettle
743
743
 
744
744
  def bump_version_results(members:, options:, phase:)
745
745
  require_relative "version_bump"
746
+ require_relative "release_waves"
747
+
748
+ results = []
749
+ completed_target_versions = {}
750
+ ReleaseWaves.new(members: members).waves.each do |wave|
751
+ bump = VersionBump.new(
752
+ members: wave,
753
+ target_version: options[:target_version],
754
+ from_version: options[:from_version],
755
+ mode: bump_version_mode(options),
756
+ phase: phase,
757
+ dependency_target_versions: completed_target_versions
758
+ )
759
+ wave_results = bump.results
760
+ results.concat(wave_results)
761
+ break unless wave_results.all?(&:ok?)
746
762
 
747
- results = VersionBump.new(
748
- members: members,
749
- target_version: options[:target_version],
750
- from_version: options[:from_version],
751
- mode: bump_version_mode(options),
752
- phase: phase
753
- ).results
763
+ completed_target_versions.merge!(bump.target_versions)
764
+ end
754
765
  return results if options[:check] || !options[:commit]
755
766
  return results unless results.all?(&:ok?)
756
767
 
@@ -5,7 +5,7 @@ require "kettle/dev"
5
5
  module Kettle
6
6
  module Family
7
7
  class DependencyFloor
8
- DEPENDENCY_METHODS = %i[add_dependency add_runtime_dependency].freeze
8
+ DEPENDENCY_METHODS = %i[add_dependency add_runtime_dependency add_development_dependency].freeze
9
9
 
10
10
  def initialize(released_members:, dependent_members:, mode: :dry_run)
11
11
  @released_members = released_members
@@ -57,19 +57,41 @@ module Kettle
57
57
 
58
58
  def member_from_gemspec(path)
59
59
  spec = load_gemspec(path)
60
+ root = File.dirname(path)
61
+ dependencies = spec.runtime_dependencies.map(&:name).sort
60
62
  Member.new(
61
63
  name: spec.name,
62
- root: File.dirname(path),
64
+ root: root,
63
65
  gemspec_path: path,
64
- version_file: version_file(File.dirname(path), spec.name),
66
+ version_file: version_file(root, spec.name),
65
67
  version: spec.version.to_s,
66
- dependencies: spec.runtime_dependencies.map(&:name).sort,
68
+ dependencies: dependencies,
69
+ release_dependencies: release_dependencies(root: root, spec: spec, dependencies: dependencies),
67
70
  required_ruby_version: required_ruby_version(spec),
68
71
  licenses: licenses(spec),
69
72
  authors: authors(spec)
70
73
  )
71
74
  end
72
75
 
76
+ def release_dependencies(root:, spec:, dependencies:)
77
+ (
78
+ dependencies +
79
+ spec.development_dependencies.map(&:name) +
80
+ gemfile_dependencies(root)
81
+ ).uniq.sort
82
+ end
83
+
84
+ def gemfile_dependencies(root)
85
+ gemfile = File.join(root, "Gemfile")
86
+ return [] unless File.file?(gemfile)
87
+
88
+ gemfile_dependency_collector(root: root).dependencies_for(gemfile).sort
89
+ end
90
+
91
+ def gemfile_dependency_collector(root:)
92
+ GemfileDependencyCollector.new(root: root)
93
+ end
94
+
73
95
  def required_ruby_version(spec)
74
96
  value = spec.required_ruby_version&.to_s&.strip
75
97
  value.empty? ? nil : value
@@ -153,6 +175,79 @@ module Kettle
153
175
 
154
176
  expanded_path.delete_prefix(prefix)
155
177
  end
178
+
179
+ class GemfileDependencyCollector
180
+ GEM_METHODS = %i[gem].freeze
181
+ EVAL_GEMFILE_METHODS = %i[eval_gemfile].freeze
182
+
183
+ def initialize(root:)
184
+ @root = root
185
+ @dependencies = Set.new
186
+ @visited = Set.new
187
+ end
188
+
189
+ def dependencies_for(path)
190
+ collect(File.expand_path(path))
191
+ dependencies.to_a
192
+ end
193
+
194
+ private
195
+
196
+ attr_reader :root, :dependencies, :visited
197
+
198
+ def collect(path)
199
+ return unless File.file?(path)
200
+ return if visited.include?(path)
201
+
202
+ visited << path
203
+ source = File.read(path)
204
+ parse_result = parse_source(source, path)
205
+ each_node(parse_result.value) do |node|
206
+ collect_call(path, node) if node.is_a?(Prism::CallNode)
207
+ end
208
+ end
209
+
210
+ def collect_call(path, node)
211
+ args = node.arguments&.arguments || []
212
+ first_arg = args.first
213
+ dependencies << first_arg.unescaped if GEM_METHODS.include?(node.name) && first_arg.is_a?(Prism::StringNode)
214
+ collect(eval_gemfile_path(path, first_arg)) if EVAL_GEMFILE_METHODS.include?(node.name) && first_arg.is_a?(Prism::StringNode)
215
+ end
216
+
217
+ def eval_gemfile_path(path, node)
218
+ gemfile_path = node.unescaped
219
+ return File.expand_path(gemfile_path, root) if gemfile_path.start_with?("/")
220
+
221
+ File.expand_path(gemfile_path, File.dirname(path))
222
+ end
223
+
224
+ def parse_source(source, path)
225
+ require_prism
226
+ parse_result = Prism.parse(source)
227
+ raise Error, "could not parse Gemfile dependency file #{path}" unless parse_result.success?
228
+
229
+ parse_result
230
+ end
231
+
232
+ def require_prism
233
+ return if defined?(Prism)
234
+
235
+ require "prism"
236
+ rescue LoadError => error
237
+ raise Error, "Gemfile dependency discovery requires Prism; install the prism gem or run on Ruby 3.3+ (#{error.message})"
238
+ end
239
+
240
+ def each_node(root)
241
+ return enum_for(__method__, root) unless block_given?
242
+
243
+ queue = [root]
244
+ until queue.empty?
245
+ node = queue.shift
246
+ yield node
247
+ queue.concat(node.child_nodes.compact) if node.respond_to?(:child_nodes)
248
+ end
249
+ end
250
+ end
156
251
  end
157
252
  end
158
253
  end
@@ -9,6 +9,7 @@ module Kettle
9
9
  :version_file,
10
10
  :version,
11
11
  :dependencies,
12
+ :release_dependencies,
12
13
  :required_ruby_version,
13
14
  :licenses,
14
15
  :authors
@@ -21,6 +22,7 @@ module Kettle
21
22
  "version_file" => version_file,
22
23
  "version" => version,
23
24
  "dependencies" => dependencies,
25
+ "release_dependencies" => release_dependencies || dependencies,
24
26
  "required_ruby_version" => required_ruby_version,
25
27
  "licenses" => Array(licenses),
26
28
  "authors" => Array(authors)
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kettle
4
+ module Family
5
+ class ReleaseWaves
6
+ def initialize(members:)
7
+ @members = members
8
+ end
9
+
10
+ def waves
11
+ by_name = members.to_h { |member| [member.name, member] }
12
+ pending = by_name.keys
13
+ completed = []
14
+ [].tap do |planned_waves|
15
+ until pending.empty?
16
+ hard_ready = pending.select do |name|
17
+ selected_hard_dependencies_for(by_name.fetch(name), by_name).all? { |dependency| completed.include?(dependency) }
18
+ end
19
+ raise Error, "cyclic release dependency order: #{pending.join(", ")}" if hard_ready.empty?
20
+
21
+ wave_names = hard_ready.select do |name|
22
+ selected_dependencies_for(by_name.fetch(name), by_name).all? { |dependency| completed.include?(dependency) }
23
+ end
24
+ wave_names = [hard_ready.first] if wave_names.empty?
25
+
26
+ planned_waves << wave_names.map { |name| by_name.fetch(name) }
27
+ completed.concat(wave_names)
28
+ pending -= wave_names
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :members
36
+
37
+ def selected_hard_dependencies_for(member, by_name)
38
+ Array(member.dependencies).map(&:to_s).select { |dependency| by_name.key?(dependency) }
39
+ end
40
+
41
+ def selected_dependencies_for(member, by_name)
42
+ release_dependency_names(member).select { |dependency| by_name.key?(dependency) }
43
+ end
44
+
45
+ def release_dependency_names(member)
46
+ Array(member.release_dependencies || member.dependencies).map(&:to_s)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -357,7 +357,7 @@ module Kettle
357
357
  state = result.state || {}
358
358
  row = [
359
359
  state.fetch("gem_name", result.member_name).to_s,
360
- state.fetch("current_branch", nil).to_s.empty? ? "unknown" : state.fetch("current_branch").to_s,
360
+ release_state_checkout(state),
361
361
  state.fetch("version", "unknown").to_s,
362
362
  state.fetch("latest_released", nil).to_s.empty? ? "unknown" : state.fetch("latest_released").to_s,
363
363
  state.fetch("latest_changelog_version", nil).to_s.empty? ? "none" : state.fetch("latest_changelog_version").to_s,
@@ -372,12 +372,17 @@ module Kettle
372
372
  end
373
373
 
374
374
  def release_state_header
375
- header = [["gem", "checkout", "version.rb", "latest released", "latest changelog", "ahead / behind", "unreleased", "prepared", "pending"]]
375
+ header = [["gem", "checkout", "V.rb", "V.rel", "V.ch.md", "🔼 / 🔽", "unrel", "prep", "pend"]]
376
376
  return header unless release_state_has_branches?
377
377
 
378
378
  [["branch", *header.first]]
379
379
  end
380
380
 
381
+ def release_state_checkout(state)
382
+ branch = state.fetch("current_branch", nil).to_s
383
+ branch.empty? ? "unknown" : branch[0, 10]
384
+ end
385
+
381
386
  def release_state_ahead_behind(state)
382
387
  local_ahead = state.fetch("ahead", nil)
383
388
  local_behind = state.fetch("behind", nil)
@@ -3,7 +3,7 @@
3
3
  module Kettle
4
4
  module Family
5
5
  module Version
6
- VERSION = "1.0.2"
6
+ VERSION = "1.0.4"
7
7
  end
8
8
  VERSION = Version::VERSION # Traditional Constant Location
9
9
  end
@@ -5,9 +5,9 @@ require "kettle/dev"
5
5
  module Kettle
6
6
  module Family
7
7
  class VersionBump
8
- DEPENDENCY_METHODS = %i[add_dependency add_runtime_dependency].freeze
8
+ DEPENDENCY_METHODS = %i[add_dependency add_runtime_dependency add_development_dependency].freeze
9
9
 
10
- def initialize(members:, target_version:, from_version: nil, mode: :dry_run, phase: "bump-version")
10
+ def initialize(members:, target_version:, from_version: nil, mode: :dry_run, phase: "bump-version", dependency_target_versions: nil)
11
11
  @members = members
12
12
  @target_version = target_version.to_s
13
13
  @explicit_target_version = validate_version(target_version) unless Kettle::Dev::VersionBump::BUMP_TYPES.include?(@target_version)
@@ -18,15 +18,21 @@ module Kettle
18
18
  @member_target_versions = members.each_with_object({}) do |member, memo|
19
19
  memo[member.name] = resolve_target_version(member)
20
20
  end
21
+ @dependency_target_versions = dependency_target_versions || member_target_versions
22
+ @dependency_member_names = @dependency_target_versions.keys
21
23
  end
22
24
 
23
25
  def results
24
26
  members.map { |member| result_for(member) }
25
27
  end
26
28
 
29
+ def target_versions
30
+ member_target_versions.dup
31
+ end
32
+
27
33
  private
28
34
 
29
- attr_reader :members, :target_version, :explicit_target_version, :from_version, :mode, :phase, :member_names, :member_target_versions
35
+ attr_reader :members, :target_version, :explicit_target_version, :from_version, :mode, :phase, :member_target_versions, :dependency_target_versions, :dependency_member_names
30
36
 
31
37
  def validate_version(version)
32
38
  with_dev_errors { Kettle::Dev::VersionBump.validate_version(version) }
@@ -90,9 +96,11 @@ module Kettle
90
96
 
91
97
  args = node.arguments&.arguments || []
92
98
  name_node, requirement_node = args
93
- return unless name_node.is_a?(Prism::StringNode) && member_names.include?(name_node.unescaped)
99
+ return unless name_node.is_a?(Prism::StringNode) && dependency_member_names.include?(name_node.unescaped)
94
100
  return unless requirement_node
95
- dependency_target_version = member_target_versions.fetch(name_node.unescaped)
101
+ dependency_target_version = dependency_target_versions[name_node.unescaped]
102
+ return unless dependency_target_version
103
+
96
104
  if same_version_dependency_requirement?(node, requirement_node)
97
105
  return if dependency_target_version == target_version_for(member)
98
106
 
@@ -520,25 +520,7 @@ module Kettle
520
520
  end
521
521
 
522
522
  def release_waves(release_members)
523
- by_name = release_members.to_h { |member| [member.name, member] }
524
- pending = by_name.keys
525
- completed = []
526
- waves = []
527
- until pending.empty?
528
- wave_names = pending.select do |name|
529
- selected_dependencies_for(by_name.fetch(name), by_name).all? { |dependency| completed.include?(dependency) }
530
- end
531
- raise Error, "cyclic release dependency order: #{pending.join(", ")}" if wave_names.empty?
532
-
533
- waves << wave_names.map { |name| by_name.fetch(name) }
534
- completed.concat(wave_names)
535
- pending -= wave_names
536
- end
537
- waves
538
- end
539
-
540
- def selected_dependencies_for(member, by_name)
541
- Array(member.dependencies).map(&:to_s).select { |dependency| by_name.key?(dependency) }
523
+ ReleaseWaves.new(members: release_members).waves
542
524
  end
543
525
 
544
526
  def append_dependency_floor_results(released_members:, dependent_members:, runner:, memo:)
@@ -565,10 +547,14 @@ module Kettle
565
547
  def dependent_members_depending_on(released_members:, dependent_members:)
566
548
  released_names = released_members.map(&:name)
567
549
  dependent_members.select do |member|
568
- Array(member.dependencies).map(&:to_s).any? { |dependency| released_names.include?(dependency) }
550
+ release_dependency_names(member).any? { |dependency| released_names.include?(dependency) }
569
551
  end
570
552
  end
571
553
 
554
+ def release_dependency_names(member)
555
+ Array(member.release_dependencies || member.dependencies).map(&:to_s)
556
+ end
557
+
572
558
  def append_registry_wait_result(released_members:, dependent_members:, memo:)
573
559
  return unless execute && publish
574
560
  return if dependent_members.empty?
@@ -807,14 +793,15 @@ module Kettle
807
793
  stdout = "#{member.name} #{member.version} is already published and current HEAD matches #{tag}; skipping release"
808
794
  reason = "already released"
809
795
  skipped = true
810
- elsif release_pending?(member)
811
- stdout = "#{member.name} #{member.version} is already published, but current HEAD is not #{tag}. " \
812
- "Bump the version with `kettle-family bump-version patch --execute --only #{member.name}` before releasing."
813
- reason = "published version does not match current HEAD"
796
+ elsif unreleased_changes_pending?(member)
797
+ stdout = "#{member.name} #{member.version} is already published, but release-state reports unreleased changes. " \
798
+ "Bump the version with `kettle-family bump patch --execute --only #{member.name}` before releasing."
799
+ reason = "published version has unreleased changes"
814
800
  skipped = false
815
801
  else
816
- stdout = "#{member.name} #{member.version} is already published and release-state reports no pending release; skipping release"
817
- reason = "already released; no pending release"
802
+ stdout = "#{member.name} #{member.version} is already published, current HEAD is newer than #{tag}, " \
803
+ "and release-state reports no unreleased changes; skipping release"
804
+ reason = "already released; no unreleased changes"
818
805
  skipped = true
819
806
  end
820
807
 
@@ -833,11 +820,11 @@ module Kettle
833
820
  )
834
821
  end
835
822
 
836
- def release_pending?(member)
823
+ def unreleased_changes_pending?(member)
837
824
  results = ReleaseStateCheck.new(members: [member], config: config).results
838
825
  return true unless results.all?(&:ok?)
839
826
 
840
- results.any? { |result| result.state.fetch("pending_release", true) }
827
+ results.any? { |result| result.state.fetch("unreleased_entries", true) }
841
828
  end
842
829
 
843
830
  def release_tag_name(version)
data/lib/kettle/family.rb CHANGED
@@ -15,6 +15,7 @@ require_relative "family/config"
15
15
  require_relative "family/branch_target_config"
16
16
  require_relative "family/release_state_check"
17
17
  require_relative "family/local_install"
18
+ require_relative "family/release_waves"
18
19
  require_relative "family/workflow"
19
20
  require_relative "family/discovery"
20
21
  require_relative "family/orderer"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-family
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -291,6 +291,7 @@ files:
291
291
  - lib/kettle/family/readiness_check.rb
292
292
  - lib/kettle/family/release_state_check.rb
293
293
  - lib/kettle/family/release_state_result.rb
294
+ - lib/kettle/family/release_waves.rb
294
295
  - lib/kettle/family/report.rb
295
296
  - lib/kettle/family/selection.rb
296
297
  - lib/kettle/family/version.rb
@@ -303,10 +304,10 @@ licenses:
303
304
  - AGPL-3.0-only
304
305
  metadata:
305
306
  homepage_uri: https://kettle-family.galtzo.com
306
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.0.2
307
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.0.2/CHANGELOG.md
307
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.0.4
308
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.0.4/CHANGELOG.md
308
309
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
309
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.0.2
310
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.0.4
310
311
  funding_uri: https://github.com/sponsors/pboling
311
312
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
312
313
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file