kettle-dev 3.0.34 → 3.1.0

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: cf4388ae02268bbe6676ece5cee25b5548277a6e60432554c460f189ca02d2f0
4
- data.tar.gz: 80841fd013dbd2868b22e36b185c18c0a7f90ed3698f5cb3a2d666c46a80b41b
3
+ metadata.gz: b7fdd37cea8287569cfe883f3b0830022afe7e90be2582adf2046cadcdd687c7
4
+ data.tar.gz: 327f6277c15a6e780001e321192b72e5a10afc56718171e3324e24445c0466b5
5
5
  SHA512:
6
- metadata.gz: 713c1d308268ce8cde18124db06ad5efed08d53fc945af0c723843de08d840b6db23f946941de187b2adcf61e482e206e307fda8bbd52b562c3aaed1fd61ee91
7
- data.tar.gz: 8e94e9a97e3ae52b955fd098db642ab23639fb6e0d67b2abed14fe8684248430c50618b8f8d6f3d0a57bb1934954e1f41fe7b17711fac151bdca9928b98a0795
6
+ metadata.gz: f7cb591d2c7e73f063f11362091eb7efc51ff3d9b6a5cd4242260e3dd45fe1ffce867a77e218f606880f7cfd7bf5fd1f071391714b8164721c89860d4b63c737
7
+ data.tar.gz: 99531967a52f754cbda4a1d868e216de15d1dac6e6d27d1b4028dad9dae87681b1a88761f91d0931425d050f9ae0a6a18344c19f48621dfccac346f9a539c9ec
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,19 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [3.1.0] - 2026-09-10
34
+
35
+ - TAG: [v3.1.0][3.1.0t]
36
+ - COVERAGE: 91.86% -- 6141/6685 lines in 48 files
37
+ - BRANCH COVERAGE: 76.10% -- 2337/3071 branches in 48 files
38
+ - 51.02% documented
39
+
40
+ ### Fixed
41
+
42
+ - [kc] release-graph-contracts: Honor the serialized release graph for lockfile normalization and child commands, retaining only declared CI-resident monorepo paths.
43
+
44
+ - Validate serialized family release graph contracts in release lockfile normalization and child commands instead of reconstructing local-path policy from ambient environment.
45
+
33
46
  ## [3.0.34] - 2026-09-09
34
47
 
35
48
  - TAG: [v3.0.34][3.0.34t]
@@ -3847,7 +3860,9 @@ Please file a bug if you notice a violation of semantic versioning.
3847
3860
  - Selecting will run the selected workflow via `act`
3848
3861
  - This may move to its own gem in the future.
3849
3862
 
3850
- [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.34...HEAD
3863
+ [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.0...HEAD
3864
+ [3.1.0]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.34...v3.1.0
3865
+ [3.1.0t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.1.0
3851
3866
  [3.0.34]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.33...v3.0.34
3852
3867
  [3.0.34t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.0.34
3853
3868
  [3.0.33]: https://github.com/kettle-dev/kettle-dev/compare/v3.0.32...v3.0.33
data/README.md CHANGED
@@ -1066,7 +1066,7 @@ Thanks for RTFM. ☺️
1066
1066
  [📌gitmoji]: https://gitmoji.dev
1067
1067
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1068
1068
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1069
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.635-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1069
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.685-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1070
1070
  [🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
1071
1071
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1072
1072
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -9,6 +9,7 @@ require "bundler"
9
9
  require "ripper"
10
10
 
11
11
  require_relative "bundler_env_guard"
12
+ require_relative "release_graph_contract"
12
13
 
13
14
  module Kettle
14
15
  module Dev
@@ -22,8 +23,6 @@ module Kettle
22
23
  "GALTZO_FLOSS_DEV" => "false",
23
24
  "UR_BRAIN_DEV" => "false"
24
25
  }.freeze
25
- ALLOWED_LOCAL_PATH_ROOTS_ENV = "KETTLE_RELEASE_ALLOWED_LOCAL_PATH_ROOTS"
26
- ALLOWED_LOCAL_PATH_ENVS_ENV = "KETTLE_RELEASE_ALLOWED_LOCAL_PATH_ENVS"
27
26
  RELEASE_LOCKFILES_TARGET = "release-lockfiles"
28
27
  SUPPORTED_TARGETS = ["Gemfile.lock", "Appraisal.root.gemfile.lock", RELEASE_LOCKFILES_TARGET].freeze
29
28
  UNBUNDLED_ENV_KEYS = (BundlerEnvGuard::RESET_ENV_KEYS + %w[
@@ -113,28 +112,30 @@ module Kettle
113
112
  @root = root
114
113
  @command_runner = command_runner
115
114
  @release_platforms = {}
116
- @allowed_local_path_roots = configured_allowed_local_path_roots
117
- @allowed_local_path_env_names = configured_allowed_local_path_env_names
115
+ @release_graph_contract = ReleaseGraphContract.from_environment(root: root)
118
116
  end
119
117
 
120
118
  def reset(target, skip_changelog_dependency: false)
121
119
  BundlerEnvGuard.warn_unexpected_env!
122
120
  paths = lockfile_paths_for(target)
123
- # A monorepo lockfile may deliberately use paths rooted in the
124
- # repository's members directory. Keep that valid development graph;
125
- # only rebuild when an unapproved path or other diagnostic remains.
126
- force_full_update = release_lockfiles_target?(target) &&
127
- (allowed_local_path_roots.empty? || paths.any? { |path| normalization_needed?(path) })
121
+ release_lockfiles = release_lockfiles_target?(target)
122
+ # The selected graph governs both tracked release locks and disposable
123
+ # release-task locks. Registry contracts reject every PATH source;
124
+ # CI-resident monorepo contracts retain only declared contained paths.
125
+ force_full_update = release_lockfiles && (
126
+ release_graph_contract.registry_only? || paths.any? { |path| normalization_needed?(path, strict: true) }
127
+ )
128
128
  platforms = release_platforms_for(paths) if force_full_update
129
129
  uninstall_unreleased_local_gems(paths) if force_full_update
130
130
  paths.each do |path|
131
- if force_full_update || normalization_needed?(path)
131
+ if force_full_update || normalization_needed?(path, strict: release_lockfiles)
132
132
  reset_lockfile!(
133
133
  path,
134
134
  full_update: force_full_update,
135
135
  platforms: platforms&.fetch(path),
136
136
  skip_changelog_dependency: skip_changelog_dependency,
137
- update_bundler: force_full_update
137
+ update_bundler: force_full_update,
138
+ strict: release_lockfiles
138
139
  )
139
140
  end
140
141
  end
@@ -145,17 +146,18 @@ module Kettle
145
146
  full_update: true,
146
147
  platforms: platforms.fetch(path),
147
148
  skip_changelog_dependency: skip_changelog_dependency,
148
- update_bundler: true
149
+ update_bundler: true,
150
+ strict: release_lockfiles
149
151
  )
150
152
  end
151
153
  end
152
- diagnostics = paths.flat_map { |path| diagnostics(path) }
154
+ diagnostics = paths.flat_map { |path| diagnostics(path, strict: release_lockfiles) }
153
155
  raise Error, validation_message(target, diagnostics) unless diagnostics.empty?
154
156
 
155
157
  paths
156
158
  end
157
159
 
158
- def reset_lockfile!(path, full_update: false, platforms: nil, skip_changelog_dependency: false, update_bundler: false)
160
+ def reset_lockfile!(path, full_update: false, platforms: nil, skip_changelog_dependency: false, update_bundler: false, strict: false)
159
161
  gemfile = gemfile_for_lockfile(path)
160
162
  unless gemfile && File.file?(gemfile)
161
163
  warn("Cannot reset #{display_path(path)} because its Gemfile was not found.")
@@ -171,9 +173,10 @@ module Kettle
171
173
  platforms: platforms,
172
174
  skip_changelog_dependency: skip_changelog_dependency,
173
175
  update_bundler: update_bundler,
174
- isolated_gem_home: gem_home
176
+ isolated_gem_home: gem_home,
177
+ strict: strict
175
178
  )
176
- if full_update || has_local_path_remote?(path)
179
+ if full_update || has_local_path_remote?(path, strict: strict)
177
180
  rebuild_lockfile(path) { command_runner.call(command) }
178
181
  else
179
182
  command_runner.call(command)
@@ -181,11 +184,11 @@ module Kettle
181
184
  end
182
185
  end
183
186
 
184
- def reset_command(path:, gemfile:, full_update: false, platforms: nil, skip_changelog_dependency: false, update_bundler: false, isolated_gem_home: nil, isolated_gem_path: nil)
185
- update_gems = (full_update || has_local_path_remote?(path)) ? [] : reset_update_gems(path)
187
+ def reset_command(path:, gemfile:, full_update: false, platforms: nil, skip_changelog_dependency: false, update_bundler: false, isolated_gem_home: nil, isolated_gem_path: nil, strict: false)
188
+ update_gems = (full_update || has_local_path_remote?(path, strict: strict)) ? [] : reset_update_gems(path)
186
189
  platforms ||= reset_platforms(path)
187
190
  removed_platforms = full_update ? [] : lockfile_platforms(path) - platforms
188
- env = normalization_env.merge(
191
+ env = (strict ? release_normalization_env : normalization_env).merge(
189
192
  "BUNDLE_GEMFILE" => gemfile,
190
193
  "BUNDLE_LOCKFILE" => path
191
194
  )
@@ -263,14 +266,14 @@ module Kettle
263
266
  ).uniq.sort
264
267
  end
265
268
 
266
- def normalization_needed?(path)
267
- has_local_path_remote?(path) ||
269
+ def normalization_needed?(path, strict: false)
270
+ has_local_path_remote?(path, strict: strict) ||
268
271
  !empty_registry_checksums(path).empty? ||
269
272
  !unreleased_workspace_registry_specs(path).empty? ||
270
273
  bundled_with_checksum_mismatch?(path)
271
274
  end
272
275
 
273
- def diagnostics(path)
276
+ def diagnostics(path, strict: false)
274
277
  diagnostics = []
275
278
  disallowed_local_path_remote_lines(path).each do |line_number|
276
279
  diagnostics << "#{display_path(path)} has local path remote at line #{line_number}"
@@ -332,9 +335,17 @@ module Kettle
332
335
  end
333
336
 
334
337
  def normalization_env
335
- DEFAULT_DISABLED_ENV
336
- .reject { |name, _| allowed_local_path_env_names.include?(name) }
337
- .merge(dynamic_local_path_env)
338
+ disabled_local_path_env
339
+ end
340
+
341
+ # The release graph, not ambient development ENV, decides which local
342
+ # selectors remain active while Bundler normalizes a release lockfile.
343
+ def release_normalization_env
344
+ disabled_local_path_env.merge(release_graph_contract.normalization_environment)
345
+ end
346
+
347
+ def disabled_local_path_env
348
+ DEFAULT_DISABLED_ENV.merge(dynamic_local_path_env)
338
349
  end
339
350
 
340
351
  # Appraisal generation normally uses the release-normalized environment.
@@ -354,7 +365,7 @@ module Kettle
354
365
  ENV.each_with_object({}) do |(key, value), env|
355
366
  next unless key.end_with?("_DEV", "_LOCAL")
356
367
  next unless local_path_env_value?(value)
357
- next if allowed_local_path_env_names.include?(key)
368
+ next if release_graph_contract.selector_env.key?(key)
358
369
 
359
370
  env[key] = "false"
360
371
  end
@@ -376,15 +387,16 @@ module Kettle
376
387
  local_path_env_value?(ENV[name])
377
388
  end
378
389
 
379
- def has_local_path_remote?(path)
380
- !disallowed_local_path_remote_lines(path).empty?
390
+ def has_local_path_remote?(path, strict: false)
391
+ paths = disallowed_local_path_remote_lines(path)
392
+ !paths.empty?
381
393
  end
382
394
 
383
395
  def local_path_remote_lines(path)
384
396
  self.class.local_path_remote_lines_from_source(File.read(path))
385
397
  end
386
398
 
387
- attr_reader :allowed_local_path_roots, :allowed_local_path_env_names
399
+ attr_reader :release_graph_contract
388
400
 
389
401
  def disallowed_local_path_remote_lines(path)
390
402
  local_path_remote_lines(path).reject do |line_number|
@@ -392,25 +404,12 @@ module Kettle
392
404
  end
393
405
  end
394
406
 
395
- def configured_allowed_local_path_roots
396
- ENV.fetch(ALLOWED_LOCAL_PATH_ROOTS_ENV, "").split(File::PATH_SEPARATOR).filter_map do |value|
397
- next if value.strip.empty?
398
-
399
- canonical_path(value)
400
- end.uniq
401
- end
402
-
403
- def configured_allowed_local_path_env_names
404
- ENV.fetch(ALLOWED_LOCAL_PATH_ENVS_ENV, "").split(",").map(&:strip).reject(&:empty?).uniq
405
- end
406
-
407
407
  def local_path_remote_at(path, line_number)
408
408
  File.readlines(path)[line_number - 1].to_s.split("remote:", 2).last.to_s.strip
409
409
  end
410
410
 
411
411
  def allowed_local_path?(path)
412
- remote = canonical_path(path)
413
- allowed_local_path_roots.any? { |root| remote == root || remote.start_with?("#{root}/") }
412
+ release_graph_contract.allowed_path?(path)
414
413
  end
415
414
 
416
415
  def canonical_path(path)
@@ -888,11 +888,11 @@ module Kettle
888
888
  end
889
889
 
890
890
  def release_lockfile_normalization_needed?(path)
891
- lockfile_reset.normalization_needed?(path)
891
+ lockfile_reset.normalization_needed?(path, strict: true)
892
892
  end
893
893
 
894
894
  def normalize_release_lockfile!(path)
895
- lockfile_reset.reset_lockfile!(path)
895
+ lockfile_reset.reset_lockfile!(path, strict: true)
896
896
  end
897
897
 
898
898
  def release_gemfile_for_lockfile(path)
@@ -900,29 +900,32 @@ module Kettle
900
900
  end
901
901
 
902
902
  def release_lockfile_normalization_env
903
- lockfile_reset.normalization_env
903
+ lockfile_reset.release_normalization_env
904
904
  end
905
905
 
906
906
  # The release process has three distinct lockfile roles:
907
907
  #
908
- # - canonical: tracked project lockfiles, always registry-backed;
908
+ # - canonical: tracked project lockfiles, using the declared release
909
+ # graph (normally registry-only, or contained CI-resident monorepo
910
+ # paths when kettle-family supplied that contract);
909
911
  # - task: a disposable Gemfile.lock copy for build and publication; and
910
912
  # - tool: a tool-owned Gemfile, such as kettle-changelog's release bundle.
911
913
  #
912
- # A family may use local sibling gems to launch this process, but no
913
- # release child may inherit that graph. Git hooks are release children
914
- # too: they commonly load project tooling and otherwise can silently
915
- # rewrite the canonical lockfile with local PATH sources.
914
+ # A family may use local sibling gems to launch this process. Release
915
+ # children receive only the serialized release graph, never the ambient
916
+ # launcher graph. Git hooks are release children too: they commonly load
917
+ # project tooling and otherwise can silently rewrite a lockfile with an
918
+ # undeclared local PATH source.
916
919
  def release_git_hook_environment
917
920
  release_child_environment
918
921
  end
919
922
 
920
923
  def release_lockfile_diagnostics(path)
921
- lockfile_reset.diagnostics(path)
924
+ lockfile_reset.diagnostics(path, strict: true)
922
925
  end
923
926
 
924
927
  def release_lockfile_has_local_path_remote?(path)
925
- lockfile_reset.has_local_path_remote?(path)
928
+ lockfile_reset.has_local_path_remote?(path, strict: true)
926
929
  end
927
930
 
928
931
  def release_lockfile_local_path_remote_lines(path)
@@ -1123,9 +1126,9 @@ module Kettle
1123
1126
  end
1124
1127
 
1125
1128
  # Every release child runs outside the release tool's Bundler context and
1126
- # with local sibling switches disabled. The optional lockfile identifies
1127
- # a disposable task lockfile; omitting it deliberately selects the
1128
- # canonical tracked lockfile for setup, checks, docs, and checksums.
1129
+ # with only the serialized release graph enabled. The optional lockfile
1130
+ # identifies a disposable task lockfile; omitting it deliberately selects
1131
+ # the canonical tracked lockfile for setup, checks, docs, and checksums.
1129
1132
  def release_child_command(command, environment: {}, lockfile: nil)
1130
1133
  command_body = command
1131
1134
  command = +"env"
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Kettle
6
+ module Dev
7
+ # Parsed contract supplied by kettle-family for a single release target.
8
+ # Direct kettle-release calls receive the safe registry-only default.
9
+ class ReleaseGraphContract
10
+ ENV_KEY = "KETTLE_RELEASE_GRAPH_CONTRACT_JSON"
11
+ NAMES = %w[registry_only wave_transition monorepo_ci_local branch_terminal].freeze
12
+
13
+ attr_reader :name, :ci_root, :local_path_roots, :selector_env
14
+
15
+ def self.from_environment(root:)
16
+ raw = (ENV[ENV_KEY] || "").strip
17
+ return new(name: "registry_only", root: root, ci_root: root) if raw.empty?
18
+
19
+ data = JSON.parse(raw)
20
+ new(
21
+ name: data["name"],
22
+ ci_root: data["ci_root"] || root,
23
+ local_path_roots: data["local_path_roots"] || [],
24
+ selector_env: data["selector_env"] || {},
25
+ root: root
26
+ )
27
+ rescue JSON::ParserError, KeyError, TypeError => error
28
+ raise Error, "invalid #{ENV_KEY}: #{error.message}"
29
+ end
30
+
31
+ def initialize(name:, root:, ci_root:, local_path_roots: [], selector_env: {})
32
+ @name = String(name)
33
+ @root = File.realpath(root)
34
+ @ci_root = canonical_path(ci_root)
35
+ @local_path_roots = Array(local_path_roots).map { |path| canonical_path(path) }.uniq.freeze
36
+ @selector_env = selector_env.to_h.map { |key, value| [String(key), String(value)] }.to_h.freeze
37
+ validate!
38
+ freeze
39
+ end
40
+
41
+ def local_paths?
42
+ !local_path_roots.empty?
43
+ end
44
+
45
+ def registry_only?
46
+ name == "registry_only" || name == "branch_terminal"
47
+ end
48
+
49
+ def allowed_path?(path)
50
+ candidate = canonical_path(path)
51
+ local_path_roots.any? { |root| path_within_root?(candidate, root) }
52
+ end
53
+
54
+ def normalization_environment
55
+ selector_env
56
+ end
57
+
58
+ private
59
+
60
+ def validate!
61
+ raise Error, "unknown release graph contract #{name.inspect}" unless NAMES.include?(name)
62
+
63
+ if registry_only? && (local_paths? || !selector_env.empty?)
64
+ raise Error, "#{name} release graph cannot declare local paths or selectors"
65
+ end
66
+ return unless %w[wave_transition monorepo_ci_local].include?(name)
67
+
68
+ raise Error, "#{name} release graph requires local path roots" unless local_paths?
69
+ raise Error, "#{name} release graph requires local path selectors" if selector_env.empty?
70
+
71
+ selector_env.each_value do |value|
72
+ next if allowed_path?(value)
73
+
74
+ raise Error, "#{name} selector #{value.inspect} is outside its declared local path roots"
75
+ end
76
+ return unless name == "monorepo_ci_local"
77
+
78
+ unless path_within_root?(@root, ci_root)
79
+ raise Error, "monorepo_ci_local release root #{@root.inspect} is outside CI root #{ci_root.inspect}"
80
+ end
81
+ local_path_roots.each do |path|
82
+ next if path_within_root?(path, ci_root)
83
+
84
+ raise Error, "monorepo_ci_local path #{path.inspect} is outside CI root #{ci_root.inspect}"
85
+ end
86
+ end
87
+
88
+ def canonical_path(path)
89
+ expanded = File.expand_path(path, @root)
90
+ File.realpath(expanded)
91
+ rescue Errno::ENOENT
92
+ expanded
93
+ end
94
+
95
+ def path_within_root?(path, root)
96
+ path == root || (path[0, root.length] == root && path[root.length] == "/")
97
+ end
98
+ end
99
+ end
100
+ end
@@ -5,7 +5,7 @@ module Kettle
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = "3.0.34"
8
+ VERSION = "3.1.0"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
data/lib/kettle/dev.rb CHANGED
@@ -49,6 +49,7 @@ module Kettle
49
49
  autoload :OpenCollectiveConfig, "kettle/dev/open_collective_config"
50
50
  autoload :ReleaseNotifier, "kettle/dev/release_notifier"
51
51
  autoload :ReleaseCLI, "kettle/dev/release_cli"
52
+ autoload :ReleaseGraphContract, "kettle/dev/release_graph_contract"
52
53
  autoload :ReleaseSecrets, "kettle/dev/release_secrets"
53
54
  autoload :ReleaseSecretsDoctor, "kettle/dev/release_secrets_doctor"
54
55
  autoload :ResetCLI, "kettle/dev/reset_cli"
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: 3.0.34
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -382,6 +382,7 @@ files:
382
382
  - lib/kettle/dev/rakelib/yard.rake
383
383
  - lib/kettle/dev/readme_backers.rb
384
384
  - lib/kettle/dev/release_cli.rb
385
+ - lib/kettle/dev/release_graph_contract.rb
385
386
  - lib/kettle/dev/release_notifier.rb
386
387
  - lib/kettle/dev/release_secrets.rb
387
388
  - lib/kettle/dev/release_secrets_doctor.rb
@@ -400,10 +401,10 @@ licenses:
400
401
  - AGPL-3.0-only
401
402
  metadata:
402
403
  homepage_uri: https://kettle-dev.galtzo.com
403
- source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v3.0.34
404
- changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.0.34/CHANGELOG.md
404
+ source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v3.1.0
405
+ changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.1.0/CHANGELOG.md
405
406
  bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
406
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/3.0.34
407
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/3.1.0
407
408
  funding_uri: https://github.com/sponsors/pboling
408
409
  wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
409
410
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file