kettle-dev 2.3.7 → 2.3.9

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: 4e582c50d05608c5e338caeac74f7c339fb2d66010b58c8cce836a78d8e4ded5
4
- data.tar.gz: e9ce2e0857dbf506017c8ddab46c5049ffc5003af5778a2e2954dd759cee8183
3
+ metadata.gz: 846ffb7540c0c7d69659591baf978274184f722f726aacc6e4915c0f376dbeb4
4
+ data.tar.gz: '08818034dfa875f33cdc89579cf455a011ecebf25be759b31af8702458e8eb6b'
5
5
  SHA512:
6
- metadata.gz: cea8e2be14321cc1e70534197cd2b01dea382a62f6de651ebfb35ccf1025acdfb0a11ee74c31e7c5c78dfe216ec51fdbe21a02f4f1358b6e8b526365b9a5f5af
7
- data.tar.gz: 47dada022728fd8414d252df3d01ecb992f18ccea69ed20ed8670dbd87ab3cf7f205bb488a48ab95039a4548a0b278c5e8aaf7099b7d94248539db5e7630c26b
6
+ metadata.gz: 0014c68a3e93d78a01c8b561956cbf474eaf25c6c8491b8ff1d229e7070eca06d3a3ed0282f739b1a3e4c38040f9bd8e5155e030a6baf1e7694a731a4bce5e2d
7
+ data.tar.gz: eeb028f65feddaac27a4f268a627d77fa686836bf368ca62c8b4b169acff3b43abf8aa432d96dc24341b4d27a61d74cd1197a529db1f2287c05661257f67880c
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,56 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [2.3.9] - 2026-07-21
34
+
35
+ - TAG: [v2.3.9][2.3.9t]
36
+ - COVERAGE: 92.06% -- 4443/4826 lines in 37 files
37
+ - BRANCH COVERAGE: 74.09% -- 1733/2339 branches in 37 files
38
+ - 54.64% documented
39
+
40
+ ### Fixed
41
+
42
+ - `kettle-release` now records a short-lived gem.coop release-state cache-bust
43
+ marker after publishing, and `kettle-changelog --release-state` uses it to
44
+ refresh just-published gem version lookups.
45
+
46
+ - `dep-heads.yml` now runs each engine directly from
47
+ `gemfiles/dep_heads.gemfile`, avoiding `Appraisal.root.gemfile` bootstrap
48
+ failures with RubyGems/Bundler and gem.coop before the dep-heads appraisal is
49
+ selected.
50
+
51
+ ## [2.3.8] - 2026-07-21
52
+
53
+ - TAG: [v2.3.8][2.3.8t]
54
+ - COVERAGE: 91.98% -- 4379/4761 lines in 36 files
55
+ - BRANCH COVERAGE: 73.91% -- 1711/2315 branches in 36 files
56
+ - 56.04% documented
57
+
58
+ ### Added
59
+
60
+ - `kettle-changelog --release-state` now reports an `ahead` value counting
61
+ commits from the latest release tag to the local default branch when
62
+ available.
63
+ - `kettle-release` now accepts `--skip-remotes` and
64
+ `K_RELEASE_SKIP_REMOTES` to exclude temporarily unavailable git remotes from
65
+ release fetch/push parity checks.
66
+ - `kettle-release` now creates or reuses an open GitHub pull request for
67
+ non-trunk release branches before waiting for GitHub Actions, so
68
+ pull-request-only workflows start after release branch pushes.
69
+
70
+ ### Changed
71
+
72
+ - kettle-jem-template-20260720-001 - Generated READMEs can now render
73
+ template-managed corporate sponsor logos from project or family config.
74
+ - kettle-jem-template-20260720-002 - Generated development Gemfiles now use the
75
+ released `tree_sitter_language_pack` gem 1.13.3 or newer by default.
76
+ - kettle-jem-template-20260720-003 - Generated StructuredMerge Git diff driver
77
+ config now uses the installed `smorg-rb` Ruby driver name.
78
+ - kettle-jem-template-20260720-004 - Generated multi-engine workflow files now
79
+ omit JRuby and TruffleRuby jobs when project config declares MRI-only engines.
80
+ - kettle-jem-template-20260720-005 - Generated README Support & Community rows
81
+ now include a RubyForum help badge.
82
+
33
83
  ## [2.3.7] - 2026-07-18
34
84
 
35
85
  - TAG: [v2.3.7][2.3.7t]
@@ -2536,7 +2586,11 @@ Please file a bug if you notice a violation of semantic versioning.
2536
2586
  - Selecting will run the selected workflow via `act`
2537
2587
  - This may move to its own gem in the future.
2538
2588
 
2539
- [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.3.7...HEAD
2589
+ [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.3.9...HEAD
2590
+ [2.3.9]: https://github.com/kettle-dev/kettle-dev/compare/v2.3.8...v2.3.9
2591
+ [2.3.9t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.3.9
2592
+ [2.3.8]: https://github.com/kettle-dev/kettle-dev/compare/v2.3.7...v2.3.8
2593
+ [2.3.8t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.3.8
2540
2594
  [2.3.7]: https://github.com/kettle-dev/kettle-dev/compare/v2.3.6...v2.3.7
2541
2595
  [2.3.7t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.3.7
2542
2596
  [2.3.6]: https://github.com/kettle-dev/kettle-dev/compare/v2.3.5...v2.3.6
data/README.md CHANGED
@@ -93,7 +93,7 @@ bin/kettle-release
93
93
  | Works with MRI Ruby 4 | [![Ruby current Compat][💎ruby-c-i]][🚎11-c-wf] [![Ruby HEAD Compat][💎ruby-headi]][🚎3-hd-wf]|
94
94
  | Works with MRI Ruby 3 | [![Ruby 3.0 Compat][💎ruby-3.0i]][🚎ruby-3.0-wf] [![Ruby 3.1 Compat][💎ruby-3.1i]][🚎ruby-3.1-wf] [![Ruby 3.2 Compat][💎ruby-3.2i]][🚎ruby-3.2-wf] [![Ruby 3.3 Compat][💎ruby-3.3i]][🚎ruby-3.3-wf] [![Ruby 3.4 Compat][💎ruby-3.4i]][🚎ruby-3.4-wf]|
95
95
  | Works with MRI Ruby 2 | [![Ruby 2.4 Compat][💎ruby-2.4i]][🚎ruby-2.4-wf] [![Ruby 2.5 Compat][💎ruby-2.5i]][🚎ruby-2.5-wf] [![Ruby 2.6 Compat][💎ruby-2.6i]][🚎ruby-2.6-wf] [![Ruby 2.7 Compat][💎ruby-2.7i]][🚎ruby-2.7-wf]|
96
- | Support & Community | [![Join Me on Daily.dev's RubyFriends][✉️ruby-friends-img]][✉️ruby-friends] [![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork] [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor] |
96
+ | Support & Community | [![Join Me on Daily.dev's RubyFriends][✉️ruby-friends-img]][✉️ruby-friends] [![Get help from RubyForum][✉️ruby-forum-img]][✉️ruby-forum] [![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork] [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor] |
97
97
  | Source | [![Source on GitLab.com][📜src-gl-img]][📜src-gl] [![Source on CodeBerg.org][📜src-cb-img]][📜src-cb] [![Source on Github.com][📜src-gh-img]][📜src-gh] [![The best SHA: dQw4w9WgXcQ!][🧮kloc-img]][🧮kloc] |
98
98
  | Documentation | [![Current release on RubyDoc.info][📜docs-cr-rd-img]][🚎yard-current] [![YARD on Galtzo.com][📜docs-head-rd-img]][🚎yard-head] [![Maintainer Blog][🚂maint-blog-img]][🚂maint-blog] [![GitLab Wiki][📜gl-wiki-img]][📜gl-wiki] [![GitHub Wiki][📜gh-wiki-img]][📜gh-wiki] |
99
99
  | Compliance | [![License: AGPL-3.0-only][📄license-img]][📄license] [![Apache license compatibility: Category X][📄license-compat-img]][📄license-compat] [![📄ilo-declaration-img]][📄ilo-declaration] [![Security Policy][🔐security-img]][🔐security] [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct] [![SemVer 2.0.0][📌semver-img]][📌semver] |
@@ -734,6 +734,8 @@ Thanks for RTFM. ☺️
734
734
  [✉️discord-invite-img-ftb]: https://img.shields.io/discord/1373797679469170758?style=for-the-badge&logo=discord
735
735
  [✉️ruby-friends-img]: https://img.shields.io/badge/daily.dev-%F0%9F%92%8E_Ruby_Friends-0A0A0A?style=for-the-badge&logo=dailydotdev&logoColor=white
736
736
  [✉️ruby-friends]: https://app.daily.dev/squads/rubyfriends
737
+ [✉️ruby-forum-img]: https://img.shields.io/badge/RubyForum-Help-CC342D?style=for-the-badge&logo=ruby&logoColor=white
738
+ [✉️ruby-forum]: https://www.rubyforum.org/c/help/8
737
739
 
738
740
  [✇bundle-group-pattern]: https://gist.github.com/pboling/4564780
739
741
  [⛳️gem-namespace]: https://github.com/kettle-dev/kettle-dev
@@ -887,7 +889,7 @@ Thanks for RTFM. ☺️
887
889
  [📌gitmoji]: https://gitmoji.dev
888
890
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
889
891
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
890
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.694-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
892
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.826-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
891
893
  [🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
892
894
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
893
895
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/exe/kettle-changelog CHANGED
@@ -55,7 +55,7 @@ end
55
55
 
56
56
  if ARGV.include?("-h") || ARGV.include?("--help")
57
57
  puts <<~USAGE
58
- Usage: kettle-changelog [--version VERSION] [--update-prep] [--pending-release] [--release-state] [--add-unreleased-entry --section SECTION --entry TEXT] [--json] [--no-strict] [--no-coverage-threshold]
58
+ Usage: kettle-changelog [--version VERSION] [--update-prep] [--pending-release] [--release-state] [--refresh-cache] [--add-unreleased-entry --section SECTION --entry TEXT] [--json] [--no-strict] [--no-coverage-threshold]
59
59
 
60
60
  Detects the current version from lib/**/version.rb, the latest live release, and
61
61
  the most recent CHANGELOG.md release section, then prompts to confirm the selected plan:
@@ -70,6 +70,7 @@ if ARGV.include?("-h") || ARGV.include?("--help")
70
70
  --pending-release Query whether the changelog has release work pending; exits 0 for yes, 1 for no
71
71
  --release-state,
72
72
  --release-status Print changelog release state, including latest published release and pending sources
73
+ --refresh-cache Bypass cached gem.coop version data for this invocation
73
74
  --add-unreleased-entry Add one entry to an existing section under ## [Unreleased]
74
75
  --section SECTION Unreleased section to receive the entry (Added, Changed, Deprecated, Removed, Fixed, Security)
75
76
  --entry TEXT Changelog entry text; "- " is added when omitted
@@ -82,6 +83,7 @@ if ARGV.include?("-h") || ARGV.include?("--help")
82
83
  K_CHANGELOG_COVERAGE_HARD=false Disable coverage threshold hard-failure
83
84
  K_CHANGELOG_VERSION_FILE=path Read VERSION from this file instead of lib/**/version.rb
84
85
  K_CHANGELOG_GEM_NAME=name Read live release data as this gem instead of root .gemspec
86
+ KETTLE_GEM_COOP_REFRESH=true Bypass cached gem.coop version data
85
87
 
86
88
  Data generation:
87
89
  Strict mode is the default. Release plans remove stale coverage output,
@@ -131,6 +133,7 @@ begin
131
133
  update_prep = ARGV.delete("--update-prep")
132
134
  pending_release_query = ARGV.delete("--pending-release")
133
135
  release_state_query = ARGV.delete("--release-state") || ARGV.delete("--release-status")
136
+ refresh_cache = ARGV.delete("--refresh-cache")
134
137
  add_unreleased_entry = ARGV.delete("--add-unreleased-entry") || ARGV.delete("--add-changelog-entry")
135
138
  json_output = ARGV.delete("--json")
136
139
  coverage_threshold_disabled = ARGV.include?("--no-coverage-threshold") || ARGV.include?("--no-coverage-thresholds")
@@ -167,7 +170,7 @@ begin
167
170
  exit(0)
168
171
  end
169
172
 
170
- cli = Kettle::Dev::ChangelogCLI.new(strict: strict_mode, enforce_coverage_thresholds: coverage_hard, update_prep: update_prep, version: version_override)
173
+ cli = Kettle::Dev::ChangelogCLI.new(strict: strict_mode, enforce_coverage_thresholds: coverage_hard, update_prep: update_prep, version: version_override, refresh_cache: refresh_cache)
171
174
  if release_state_query
172
175
  state = cli.release_state
173
176
  puts(json_output ? JSON.pretty_generate(state) : cli.release_state_table(state))
data/exe/kettle-release CHANGED
@@ -43,7 +43,7 @@ end
43
43
  # Do not guard with __FILE__ == $PROGRAM_NAME because binstubs use Kernel.load.
44
44
  if ARGV.include?("-h") || ARGV.include?("--help")
45
45
  puts <<~USAGE
46
- Usage: kettle-release [--version VERSION] [--local-ci] [--appraisal-update] [--skip-bundle-audit] [--skip-steps STEPS] [--ci-workflows WORKFLOWS] [start_step=<0-19>]
46
+ Usage: kettle-release [--version VERSION] [--local-ci] [--appraisal-update] [--skip-bundle-audit] [--skip-steps STEPS] [--ci-workflows WORKFLOWS] [--skip-remotes REMOTES] [start_step=<0-19>]
47
47
 
48
48
  Automates the release flow for a Ruby gem in the host project.
49
49
 
@@ -82,6 +82,7 @@ if ARGV.include?("-h") || ARGV.include?("--help")
82
82
  --appraisal-update # Use slower appraisal:update instead of default appraisal:generate
83
83
  --skip-bundle-audit # Skip bundle:audit/update during release rake checks
84
84
  --ci-workflows WORKFLOWS # Comma-separated workflow files or stems to monitor at CI step 10
85
+ --skip-remotes REMOTES # Comma-separated git remote names to exclude from release fetch/push parity
85
86
  --local-ci # Sensitive release mode: run act locally, publish before any git push,
86
87
  # create the git tag locally, then push commits and tags after publish
87
88
 
@@ -91,6 +92,7 @@ if ARGV.include?("-h") || ARGV.include?("--help")
91
92
  K_RELEASE_LOCAL_CI=ask|1|0 # Use 'act' locally before push; 'ask' prompts, '1' forces, default off
92
93
  K_RELEASE_LOCAL_CI_WORKFLOW # Name of workflow (without .yml) for local CI; defaults to locked_deps or first
93
94
  K_RELEASE_CI_WORKFLOWS # Comma-separated workflow files or stems to monitor at CI step 10
95
+ K_RELEASE_SKIP_REMOTES # Comma-separated git remote names to exclude from release fetch/push parity
94
96
  KETTLE_RELEASE_APPRAISAL_TASK=appraisal:update
95
97
  # Use slower appraisal:update instead of default appraisal:generate
96
98
  KETTLE_DEV_SKIP_BUNDLE_AUDIT=true
@@ -168,6 +170,27 @@ def extract_ci_workflows_arg!(argv)
168
170
  workflows
169
171
  end
170
172
 
173
+ def extract_skip_remotes_arg!(argv)
174
+ remotes = nil
175
+ if (idx = argv.index("--skip-remotes"))
176
+ remotes = argv[idx + 1]
177
+ if remotes.to_s.empty? || remotes.to_s.start_with?("-")
178
+ Kettle::Dev::ExitAdapter.abort("--skip-remotes requires a comma-separated remote list")
179
+ end
180
+ argv.slice!(idx, 2)
181
+ end
182
+ argv.delete_if do |arg|
183
+ if arg.start_with?("--skip-remotes=", "skip_remotes=")
184
+ remotes = arg.split("=", 2)[1]
185
+ Kettle::Dev::ExitAdapter.abort("--skip-remotes requires a comma-separated remote list") if remotes.to_s.empty?
186
+ true
187
+ else
188
+ false
189
+ end
190
+ end
191
+ remotes
192
+ end
193
+
171
194
  local_ci = ARGV.include?("--local-ci")
172
195
  appraisal_task = ARGV.delete("--appraisal-update") ? "appraisal:update" : nil
173
196
  skip_bundle_audit = !!ARGV.delete("--skip-bundle-audit")
@@ -176,9 +199,10 @@ start_step = start_step_arg ? start_step_arg.split("=", 2)[1].to_i : 0
176
199
  version_override = extract_version_arg!(ARGV)
177
200
  skip_steps = extract_skip_steps_arg!(ARGV)
178
201
  ci_workflows = extract_ci_workflows_arg!(ARGV)
202
+ skip_remotes = extract_skip_remotes_arg!(ARGV)
179
203
 
180
204
  begin
181
- Kettle::Dev::ReleaseCLI.new(start_step: start_step, local_ci: local_ci, version: version_override, appraisal_task: appraisal_task, skip_steps: skip_steps, skip_bundle_audit: skip_bundle_audit, ci_workflows: ci_workflows).run
205
+ Kettle::Dev::ReleaseCLI.new(start_step: start_step, local_ci: local_ci, version: version_override, appraisal_task: appraisal_task, skip_steps: skip_steps, skip_bundle_audit: skip_bundle_audit, ci_workflows: ci_workflows, skip_remotes: skip_remotes).run
182
206
  rescue LoadError => e
183
207
  warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
184
208
  warn(Array(e.backtrace).join("\n")) if ENV["DEBUG"]
@@ -27,7 +27,7 @@ module Kettle
27
27
  # @param enforce_coverage_thresholds [Boolean] when true, fail strict coverage generation below project thresholds
28
28
  # @param update_prep [Boolean] when true, update the most recent prepared release section in place
29
29
  # @param version [String, nil] explicit version override for gems without a literal VERSION constant
30
- def initialize(strict: true, enforce_coverage_thresholds: true, update_prep: false, version: nil, root: Kettle::Dev::CIHelpers.project_root)
30
+ def initialize(strict: true, enforce_coverage_thresholds: true, update_prep: false, version: nil, root: Kettle::Dev::CIHelpers.project_root, refresh_cache: false)
31
31
  @root = root
32
32
  @changelog_path = File.join(@root, "CHANGELOG.md")
33
33
  @coverage_path = File.join(@root, "coverage", "coverage.json")
@@ -35,6 +35,7 @@ module Kettle
35
35
  @enforce_coverage_thresholds = enforce_coverage_thresholds
36
36
  @update_prep = update_prep
37
37
  @version_override = Kettle::Dev::Versioning.normalize_explicit_version(version)
38
+ @refresh_cache = refresh_cache
38
39
  end
39
40
 
40
41
  # Main entry point to update CHANGELOG.md
@@ -150,6 +151,7 @@ module Kettle
150
151
  unless changelog_present
151
152
  latest_overall, latest_for_series, latest_for_major = latest_released_versions(gem_name, version)
152
153
  latest_target = latest_release_target(version, latest_overall, latest_for_series, latest_for_major)
154
+ ahead = commits_ahead_of_release(latest_target || latest_overall)
153
155
  return {
154
156
  root: @root,
155
157
  gem_name: gem_name,
@@ -164,7 +166,8 @@ module Kettle
164
166
  latest_released_overall: latest_overall,
165
167
  latest_released_for_current_major: latest_for_major,
166
168
  latest_released_for_current_series: latest_for_series,
167
- latest_release_target: latest_target
169
+ latest_release_target: latest_target,
170
+ ahead: ahead
168
171
  }
169
172
  end
170
173
 
@@ -176,6 +179,7 @@ module Kettle
176
179
  latest_overall, latest_for_series, latest_for_major = latest_released_versions(gem_name, release_lookup_version)
177
180
  latest_target = latest_release_target(release_lookup_version, latest_overall, latest_for_series, latest_for_major)
178
181
  prepared_release_pending = !!latest_changelog_version && latest_target != latest_changelog_version
182
+ ahead = commits_ahead_of_release(latest_target || latest_overall)
179
183
 
180
184
  {
181
185
  root: @root,
@@ -191,18 +195,20 @@ module Kettle
191
195
  latest_released_overall: latest_overall,
192
196
  latest_released_for_current_major: latest_for_major,
193
197
  latest_released_for_current_series: latest_for_series,
194
- latest_release_target: latest_target
198
+ latest_release_target: latest_target,
199
+ ahead: ahead
195
200
  }
196
201
  end
197
202
 
198
203
  def release_state_table(state = release_state)
199
204
  rows = [
200
- ["gem", "version.rb", "latest released", "latest changelog", "unreleased", "prepared", "pending"],
205
+ ["gem", "version.rb", "latest released", "latest changelog", "ahead", "unreleased", "prepared", "pending"],
201
206
  [
202
207
  state.fetch(:gem_name),
203
208
  state.fetch(:version),
204
209
  state.fetch(:latest_released) || "unknown",
205
210
  state.fetch(:latest_changelog_version) || "none",
211
+ state.fetch(:ahead, nil).nil? ? "unknown" : state.fetch(:ahead).to_s,
206
212
  yes_no(state.fetch(:unreleased_entries)),
207
213
  yes_no(state.fetch(:prepared_release_pending)),
208
214
  yes_no(state.fetch(:pending_release))
@@ -239,6 +245,33 @@ module Kettle
239
245
  value ? "yes" : "no"
240
246
  end
241
247
 
248
+ def commits_ahead_of_release(version)
249
+ tag = release_tag_for_version(version)
250
+ branch = default_branch_ref
251
+ return nil unless tag && branch
252
+
253
+ stdout, _stderr, status = Open3.capture3("git", "rev-list", "--count", "#{tag}..#{branch}", chdir: @root)
254
+ status.success? ? stdout.to_i : nil
255
+ end
256
+
257
+ def release_tag_for_version(version)
258
+ return nil if version.to_s.empty?
259
+
260
+ ["v#{version}", version.to_s].find { |tag| git_ref_exists?("refs/tags/#{tag}^{commit}") }
261
+ end
262
+
263
+ def default_branch_ref
264
+ stdout, _stderr, status = Open3.capture3("git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD", chdir: @root)
265
+ return stdout.strip if status.success? && !stdout.strip.empty?
266
+
267
+ %w[main master HEAD].find { |ref| git_ref_exists?(ref) }
268
+ end
269
+
270
+ def git_ref_exists?(ref)
271
+ _stdout, _stderr, status = Open3.capture3("git", "rev-parse", "--verify", "--quiet", ref, chdir: @root)
272
+ status.success?
273
+ end
274
+
242
275
  def detect_plan(changelog, version)
243
276
  latest_overall = nil
244
277
  latest_for_series = nil
@@ -367,11 +400,9 @@ module Kettle
367
400
  end
368
401
 
369
402
  def latest_released_versions(gem_name, current_version)
370
- uri = URI("https://gem.coop/api/v1/versions/#{gem_name}.json")
371
- res = Net::HTTP.get_response(uri)
372
- return [nil, nil] unless res.is_a?(Net::HTTPSuccess)
403
+ data = Kettle::Dev::GemCoopVersions.fetch(gem_name, version_hint: current_version, refresh: @refresh_cache)
404
+ return [nil, nil, nil] unless data.is_a?(Array)
373
405
 
374
- data = JSON.parse(res.body)
375
406
  versions = data.map { |h| h["number"] }.compact
376
407
  versions.reject! { |v| v.to_s.include?("-pre") || v.to_s.include?(".pre") || v.to_s.match?(/[a-zA-Z]/) }
377
408
  gversions = versions.map { |s| Gem::Version.new(s) }.sort
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "net/http"
6
+ require "time"
7
+ require "uri"
8
+
9
+ module Kettle
10
+ module Dev
11
+ module GemCoopVersions
12
+ CACHE_BUST_TTL_SECONDS = 15 * 60
13
+ ENV_REFRESH = "KETTLE_GEM_COOP_REFRESH"
14
+ ENV_MARKER_PATH = "KETTLE_GEM_COOP_CACHE_BUST_PATH"
15
+
16
+ class << self
17
+ def fetch(gem_name, version_hint: nil, refresh: false)
18
+ cache_bust = refresh || env_refresh? || fresh_release_marker?(gem_name, version_hint)
19
+ uri = versions_uri(gem_name, cache_bust: cache_bust)
20
+ request = Net::HTTP::Get.new(uri)
21
+ if cache_bust
22
+ request["Cache-Control"] = "no-cache"
23
+ request["Pragma"] = "no-cache"
24
+ end
25
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
26
+ http.request(request)
27
+ end
28
+ return nil unless response.is_a?(Net::HTTPSuccess)
29
+
30
+ JSON.parse(response.body)
31
+ end
32
+
33
+ def mark_released(gem_name, version)
34
+ return if gem_name.to_s.empty? || version.to_s.empty?
35
+
36
+ path = marker_path
37
+ data = read_marker(path)
38
+ data["releases"] ||= {}
39
+ data["releases"][gem_name.to_s] = {
40
+ "version" => version.to_s,
41
+ "released_at" => Time.now.utc.iso8601
42
+ }
43
+ FileUtils.mkdir_p(File.dirname(path))
44
+ File.write(path, JSON.pretty_generate(data) << "\n")
45
+ rescue => error
46
+ warn("[kettle-dev] could not update gem.coop cache-bust marker: #{error.class}: #{error.message}") if Kettle::Dev::DEBUGGING
47
+ end
48
+
49
+ def marker_path
50
+ configured = ENV.fetch(ENV_MARKER_PATH, "").to_s
51
+ return configured unless configured.empty?
52
+
53
+ state_home = ENV["XDG_STATE_HOME"]
54
+ state_home = File.join(Dir.home, ".local", "state") if state_home.to_s.empty?
55
+ File.join(state_home, "kettle-dev", "gem-coop-cache-bust.json")
56
+ end
57
+
58
+ private
59
+
60
+ def versions_uri(gem_name, cache_bust:)
61
+ uri = URI("https://gem.coop/api/v1/versions/#{gem_name}.json")
62
+ uri.query = "_kettle_cache_bust=#{Time.now.to_i}" if cache_bust
63
+ uri
64
+ end
65
+
66
+ def fresh_release_marker?(gem_name, version_hint)
67
+ entry = read_marker(marker_path).fetch("releases", {})[gem_name.to_s]
68
+ return false unless entry
69
+ return false if version_hint && entry["version"].to_s != version_hint.to_s
70
+
71
+ released_at = Time.iso8601(entry["released_at"].to_s)
72
+ released_at >= Time.now.utc - CACHE_BUST_TTL_SECONDS
73
+ rescue ArgumentError
74
+ false
75
+ end
76
+
77
+ def read_marker(path)
78
+ return {} unless File.file?(path)
79
+
80
+ parsed = JSON.parse(File.read(path))
81
+ parsed.is_a?(Hash) ? parsed : {}
82
+ rescue JSON::ParserError
83
+ {}
84
+ end
85
+
86
+ def env_refresh?
87
+ ENV.fetch(ENV_REFRESH, "").match?(Kettle::Dev::ENV_TRUE_RE)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -9,7 +9,7 @@ begin
9
9
  "KETTLE_JEM_QUIET" => "true",
10
10
  "KETTLE_JEM_DEBUG" => "false",
11
11
  "KETTLE_DEV_DEBUG" => "false",
12
- "SMORG_RB_DEBUG" => "false",
12
+ "STRUCTUREDMERGE_DEBUG" => "false",
13
13
  "DEBUG" => nil,
14
14
  "BUNDLE_QUIET" => "true",
15
15
  "BUNDLE_DEBUG" => "false",
@@ -22,7 +22,7 @@ module Kettle
22
22
  "KETTLE_JEM_QUIET" => "true",
23
23
  "KETTLE_JEM_DEBUG" => "false",
24
24
  "KETTLE_DEV_DEBUG" => "false",
25
- "SMORG_RB_DEBUG" => "false",
25
+ "STRUCTUREDMERGE_DEBUG" => "false",
26
26
  "DEBUG" => nil,
27
27
  "BUNDLE_QUIET" => "true",
28
28
  "BUNDLE_DEBUG" => "false",
@@ -108,13 +108,14 @@ module Kettle
108
108
 
109
109
  public
110
110
 
111
- def initialize(start_step: 0, local_ci: false, version: nil, appraisal_task: nil, skip_steps: nil, skip_bundle_audit: nil, ci_workflows: nil, **options)
111
+ def initialize(start_step: 0, local_ci: false, version: nil, appraisal_task: nil, skip_steps: nil, skip_bundle_audit: nil, ci_workflows: nil, skip_remotes: nil, **options)
112
112
  @root = Kettle::Dev::CIHelpers.project_root
113
113
  @git = Kettle::Dev::GitAdapter.new
114
114
  @start_step = (start_step || 0).to_i
115
115
  @start_step = 0 if @start_step < 0
116
116
  @skip_steps = normalize_skip_steps(skip_steps)
117
117
  @ci_workflows = normalize_ci_workflows(ci_workflows || ENV["K_RELEASE_CI_WORKFLOWS"])
118
+ @skip_remotes = normalize_skip_remotes(skip_remotes || ENV["K_RELEASE_SKIP_REMOTES"])
118
119
  @local_ci = !!local_ci
119
120
  @skip_bundle_audit = truthy_value?(skip_bundle_audit) || truthy_value?(ENV["KETTLE_DEV_SKIP_BUNDLE_AUDIT"])
120
121
  @version_override = Kettle::Dev::Versioning.normalize_explicit_version(version)
@@ -343,6 +344,7 @@ module Kettle
343
344
  else
344
345
  puts "Running release (you may be prompted for signing key password and RubyGems MFA OTP)..."
345
346
  run_cmd!("bundle exec rake release")
347
+ mark_gem_coop_release_cache_bust(version || detect_version)
346
348
  end
347
349
  end
348
350
 
@@ -411,6 +413,14 @@ module Kettle
411
413
  workflows.map { |workflow| workflow.match?(/\.ya?ml\z/) ? workflow : "#{workflow}.yml" }.uniq
412
414
  end
413
415
 
416
+ def normalize_skip_remotes(value)
417
+ remotes = Array(value).flat_map { |part| part.to_s.split(",") }.map(&:strip).reject(&:empty?)
418
+ invalid = remotes.find { |remote| !remote.match?(/\A[A-Za-z0-9_.-]+\z/) }
419
+ abort("Invalid skip remotes value #{invalid.inspect}; use comma-separated git remote names.") if invalid
420
+
421
+ remotes.uniq
422
+ end
423
+
414
424
  private
415
425
 
416
426
  def local_ci?
@@ -719,10 +729,85 @@ module Kettle
719
729
  end
720
730
 
721
731
  def monitor_workflows_after_push!
732
+ ensure_github_pull_request_for_ci!
722
733
  # Use abort-on-failure CI monitor to match historical behavior and specs
723
734
  Kettle::Dev::CIMonitor.monitor_all!(restart_hint: "bundle exec kettle-release start_step=10", workflows: @ci_workflows)
724
735
  end
725
736
 
737
+ def ensure_github_pull_request_for_ci!
738
+ branch = current_branch
739
+ return if branch.to_s.empty?
740
+
741
+ trunk = detect_trunk_branch
742
+ return if branch == trunk
743
+
744
+ gh_remote = preferred_github_remote
745
+ return unless gh_remote
746
+
747
+ owner, repo = parse_github_owner_repo(remote_url(gh_remote))
748
+ return unless owner && repo
749
+
750
+ pull_request = github_pull_request_for_branch(owner: owner, repo: repo, branch: branch, base: trunk)
751
+ if pull_request
752
+ puts "GitHub pull request ##{pull_request.fetch("number")} already open for #{branch} -> #{trunk}: #{pull_request.fetch("url")}"
753
+ return
754
+ end
755
+
756
+ create_github_pull_request!(owner: owner, repo: repo, branch: branch, base: trunk)
757
+ end
758
+
759
+ def github_pull_request_for_branch(owner:, repo:, branch:, base:)
760
+ output = gh_output!(
761
+ "pr",
762
+ "list",
763
+ "--repo",
764
+ "#{owner}/#{repo}",
765
+ "--head",
766
+ branch,
767
+ "--base",
768
+ base,
769
+ "--state",
770
+ "open",
771
+ "--json",
772
+ "number,url",
773
+ "--limit",
774
+ "1"
775
+ )
776
+ Array(JSON.parse(output)).first
777
+ rescue JSON::ParserError => e
778
+ Kettle::Dev.debug_error(e, __method__)
779
+ abort("Could not parse GitHub pull request list from gh CLI.")
780
+ end
781
+
782
+ def create_github_pull_request!(owner:, repo:, branch:, base:)
783
+ output = gh_output!(
784
+ "pr",
785
+ "create",
786
+ "--repo",
787
+ "#{owner}/#{repo}",
788
+ "--head",
789
+ branch,
790
+ "--base",
791
+ base,
792
+ "--title",
793
+ "Release #{branch}",
794
+ "--body",
795
+ "Automated release validation PR for `#{branch}`.\n\nThis PR lets GitHub Actions run before kettle-release merges the branch into `#{base}`."
796
+ )
797
+ puts "Created GitHub pull request for #{branch} -> #{base}: #{output.strip}"
798
+ end
799
+
800
+ def gh_output!(*args)
801
+ stdout_str, stderr_str, status = Open3.capture3(self.class.send(:command_env), "gh", *args)
802
+ return stdout_str if status.success?
803
+
804
+ exit_code = status.respond_to?(:exitstatus) ? status.exitstatus : 1
805
+ diag = stderr_str.to_s.empty? ? "" : "\n--- STDERR ---\n#{stderr_str}".rstrip
806
+ abort("GitHub pull request setup failed: gh #{args.join(" ")} (exit #{exit_code})#{diag}")
807
+ rescue Errno::ENOENT
808
+ abort("GitHub pull request setup failed: gh CLI is required to create or find the release PR.")
809
+ end
810
+
726
811
  def run_cmd!(cmd)
727
812
  cmd = bundle_audit_skip_command(cmd)
728
813
  with_bundle_audit_skip_env do
@@ -870,6 +955,7 @@ module Kettle
870
955
 
871
956
  puts "Publishing #{File.basename(gem_path)} to RubyGems without pushing git refs..."
872
957
  run_cmd!("gem push #{Shellwords.escape(gem_path)}")
958
+ mark_gem_coop_release_cache_bust(version, gem_name: gem_name_from_gem_path(gem_path, version))
873
959
  end
874
960
 
875
961
  def detect_version
@@ -887,11 +973,9 @@ module Kettle
887
973
  end
888
974
 
889
975
  def latest_released_versions(gem_name, current_version)
890
- uri = URI("https://gem.coop/api/v1/versions/#{gem_name}.json")
891
- res = Net::HTTP.get_response(uri)
892
- return [nil, nil] unless res.is_a?(Net::HTTPSuccess)
976
+ data = Kettle::Dev::GemCoopVersions.fetch(gem_name, version_hint: current_version)
977
+ return [nil, nil] unless data.is_a?(Array)
893
978
 
894
- data = JSON.parse(res.body)
895
979
  versions = data.map { |h| h["number"] }.compact
896
980
  versions.reject! { |v| v.to_s.include?("-pre") || v.to_s.include?(".pre") || /[a-zA-Z]/.match?(v.to_s) }
897
981
  gversions = versions.map { |s| Gem::Version.new(s) }.sort
@@ -907,6 +991,18 @@ module Kettle
907
991
  [nil, nil]
908
992
  end
909
993
 
994
+ def mark_gem_coop_release_cache_bust(version, gem_name: nil)
995
+ gem_name ||= detect_gem_name
996
+ Kettle::Dev::GemCoopVersions.mark_released(gem_name, version)
997
+ rescue => error
998
+ warn("[kettle-release] could not mark gem.coop cache-bust for release-state: #{error.class}: #{error.message}") if Kettle::Dev::DEBUGGING
999
+ end
1000
+
1001
+ def gem_name_from_gem_path(gem_path, version)
1002
+ basename = File.basename(gem_path, ".gem")
1003
+ basename.delete_suffix("-#{version}")
1004
+ end
1005
+
910
1006
  def commit_release_prep!(version)
911
1007
  msg = "🔖 Prepare release v#{version}"
912
1008
  # Stage all changes (including new/untracked files) prior to committing
@@ -925,7 +1021,7 @@ module Kettle
925
1021
  branch = current_branch
926
1022
  abort("Could not determine current branch to push.") unless branch
927
1023
 
928
- if has_remote?("all")
1024
+ if use_all_remote?
929
1025
  puts "$ git push all #{branch}"
930
1026
  success = @git.push("all", branch)
931
1027
  unless success
@@ -936,7 +1032,7 @@ module Kettle
936
1032
  end
937
1033
 
938
1034
  remotes = []
939
- remotes << "origin" if has_remote?("origin")
1035
+ remotes << "origin" if has_remote?("origin") && !skipped_remote?("origin")
940
1036
  remotes |= github_remote_candidates
941
1037
  remotes |= gitlab_remote_candidates
942
1038
  remotes |= codeberg_remote_candidates
@@ -967,13 +1063,13 @@ module Kettle
967
1063
  # 1) If a remote named "all" exists, push tags only to it.
968
1064
  # 2) Otherwise, if other remotes exist, push tags to each of them.
969
1065
  # 3) If no remotes are configured, push tags using default remote.
970
- if has_remote?("all")
1066
+ if use_all_remote?
971
1067
  ok = @git.push_tags("all")
972
1068
  warn("Push tags to 'all' reported failure.") unless ok
973
1069
  return
974
1070
  end
975
1071
 
976
- remotes = list_remotes
1072
+ remotes = active_remotes
977
1073
  remotes -= ["all"] if remotes
978
1074
  if remotes.nil? || remotes.empty?
979
1075
  ok = @git.push_tags(nil)
@@ -1021,15 +1117,15 @@ module Kettle
1021
1117
  end
1022
1118
 
1023
1119
  def github_remote_candidates
1024
- remotes_with_urls.select { |n, u| u.include?("github.com") }.keys
1120
+ active_remote_candidates(remotes_with_urls.select { |n, u| u.include?("github.com") }.keys)
1025
1121
  end
1026
1122
 
1027
1123
  def gitlab_remote_candidates
1028
- remotes_with_urls.select { |n, u| u.include?("gitlab.com") }.keys
1124
+ active_remote_candidates(remotes_with_urls.select { |n, u| u.include?("gitlab.com") }.keys)
1029
1125
  end
1030
1126
 
1031
1127
  def codeberg_remote_candidates
1032
- remotes_with_urls.select { |n, u| u.include?("codeberg.org") }.keys
1128
+ active_remote_candidates(remotes_with_urls.select { |n, u| u.include?("codeberg.org") }.keys)
1033
1129
  end
1034
1130
 
1035
1131
  def preferred_github_remote
@@ -1052,6 +1148,22 @@ module Kettle
1052
1148
  list_remotes.include?(name)
1053
1149
  end
1054
1150
 
1151
+ def skipped_remote?(name)
1152
+ @skip_remotes.include?(name)
1153
+ end
1154
+
1155
+ def active_remotes
1156
+ list_remotes.reject { |remote| skipped_remote?(remote) }
1157
+ end
1158
+
1159
+ def active_remote_candidates(candidates)
1160
+ candidates.reject { |remote| skipped_remote?(remote) }
1161
+ end
1162
+
1163
+ def use_all_remote?
1164
+ has_remote?("all") && @skip_remotes.empty?
1165
+ end
1166
+
1055
1167
  def remote_branch_exists?(remote, branch)
1056
1168
  _out, ok = git_output(["show-ref", "--verify", "--quiet", "refs/remotes/#{remote}/#{branch}"])
1057
1169
  ok
@@ -1076,9 +1188,15 @@ module Kettle
1076
1188
 
1077
1189
  def ensure_trunk_synced_before_push!(trunk, feature)
1078
1190
  if has_remote?("all")
1079
- puts "Remote 'all' detected. Fetching from all remotes and enforcing strict trunk parity..."
1080
- run_cmd!("git fetch --all")
1081
- remotes = list_remotes
1191
+ remotes = active_remotes
1192
+ skipped = list_remotes.select { |remote| skipped_remote?(remote) }
1193
+ puts "Remote 'all' detected. Fetching from active remotes and enforcing strict trunk parity..."
1194
+ puts "Skipping configured remotes: #{skipped.join(", ")}" unless skipped.empty?
1195
+ remotes.each do |remote|
1196
+ next if remote == "all"
1197
+
1198
+ run_cmd!("git fetch #{Shellwords.escape(remote)}")
1199
+ end
1082
1200
  missing_from = []
1083
1201
  remotes.each do |r|
1084
1202
  next if r == "all"
@@ -3,7 +3,7 @@
3
3
  module Kettle
4
4
  module Dev
5
5
  module Version
6
- VERSION = "2.3.7"
6
+ VERSION = "2.3.9"
7
7
  end
8
8
  VERSION = Version::VERSION # Traditional Constant Location
9
9
  end
data/lib/kettle/dev.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "dev/version"
5
5
  if Gem.ruby_version < Gem::Version.new("2.7")
6
6
  begin
7
7
  require "backports/2.7.0/enumerable/filter_map"
8
+ require "backports/2.5.0/string/delete_suffix"
8
9
  rescue LoadError => error
9
10
  message = "kettle-dev on Ruby <= 2.6 requires the backports gem; " \
10
11
  "add `gem \"backports\", \">= 3.25\"` to your Gemfile (#{error.message})"
@@ -33,6 +34,7 @@ module Kettle
33
34
  autoload :CommitMsg, "kettle/dev/commit_msg"
34
35
  autoload :DvcsCLI, "kettle/dev/dvcs_cli"
35
36
  autoload :ExitAdapter, "kettle/dev/exit_adapter"
37
+ autoload :GemCoopVersions, "kettle/dev/gem_coop_versions"
36
38
  autoload :GemSpecReader, "kettle/dev/gem_spec_reader"
37
39
  autoload :GitAdapter, "kettle/dev/git_adapter"
38
40
  autoload :GitCommitFooter, "kettle/dev/git_commit_footer"
data/sig/kettle/dev.rbs CHANGED
@@ -21,7 +21,6 @@ module Kettle
21
21
  def self.defaults: () -> Array[String]
22
22
  def self.register_default: (String | Symbol task_name) -> Array[String]
23
23
  VERSION: String
24
- # AST-driven merger for Appraisals files using Prism
25
24
  module AppraisalsAstMerger
26
25
  TRACKED_METHODS: Array[Symbol]
27
26
 
@@ -382,7 +381,7 @@ module Kettle
382
381
  end
383
382
 
384
383
  class ReleaseCLI
385
- def initialize: (?start_step: Integer, ?local_ci: bool, ?version: String?, ?appraisal_task: String?, ?skip_steps: untyped, ?skip_bundle_audit: bool?, ?ci_workflows: untyped, **untyped) -> void
384
+ def initialize: (?start_step: Integer, ?local_ci: bool, ?version: String?, ?appraisal_task: String?, ?skip_steps: untyped, ?skip_bundle_audit: bool?, ?ci_workflows: untyped, ?skip_remotes: untyped, **untyped) -> void
386
385
  def run: () -> void
387
386
 
388
387
  private
@@ -469,9 +468,6 @@ module Kettle
469
468
  end
470
469
 
471
470
  module Tasks
472
- # Namespace module for task-related classes and Rake task loaders.
473
- # Namespace module for task-related classes and Rake task loaders.
474
- # Namespace module for task-related classes and Rake task loaders.
475
471
  # Namespace module for task-related classes and Rake task loaders.
476
472
  module CITask
477
473
  def self.act: (?String opt) -> void
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: 2.3.7
4
+ version: 2.3.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -159,40 +159,40 @@ dependencies:
159
159
  requirements:
160
160
  - - "~>"
161
161
  - !ruby/object:Gem::Version
162
- version: '3.1'
162
+ version: '3.2'
163
163
  - - ">="
164
164
  - !ruby/object:Gem::Version
165
- version: 3.1.4
165
+ version: 3.2.0
166
166
  type: :development
167
167
  prerelease: false
168
168
  version_requirements: !ruby/object:Gem::Requirement
169
169
  requirements:
170
170
  - - "~>"
171
171
  - !ruby/object:Gem::Version
172
- version: '3.1'
172
+ version: '3.2'
173
173
  - - ">="
174
174
  - !ruby/object:Gem::Version
175
- version: 3.1.4
175
+ version: 3.2.0
176
176
  - !ruby/object:Gem::Dependency
177
177
  name: turbo_tests2
178
178
  requirement: !ruby/object:Gem::Requirement
179
179
  requirements:
180
180
  - - "~>"
181
181
  - !ruby/object:Gem::Version
182
- version: '3.1'
182
+ version: '3.2'
183
183
  - - ">="
184
184
  - !ruby/object:Gem::Version
185
- version: 3.1.14
185
+ version: 3.2.0
186
186
  type: :development
187
187
  prerelease: false
188
188
  version_requirements: !ruby/object:Gem::Requirement
189
189
  requirements:
190
190
  - - "~>"
191
191
  - !ruby/object:Gem::Version
192
- version: '3.1'
192
+ version: '3.2'
193
193
  - - ">="
194
194
  - !ruby/object:Gem::Version
195
- version: 3.1.14
195
+ version: 3.2.0
196
196
  - !ruby/object:Gem::Dependency
197
197
  name: stone_checksums
198
198
  requirement: !ruby/object:Gem::Requirement
@@ -282,6 +282,7 @@ files:
282
282
  - lib/kettle/dev/commit_msg.rb
283
283
  - lib/kettle/dev/dvcs_cli.rb
284
284
  - lib/kettle/dev/exit_adapter.rb
285
+ - lib/kettle/dev/gem_coop_versions.rb
285
286
  - lib/kettle/dev/gem_spec_reader.rb
286
287
  - lib/kettle/dev/gha_sha_pins_cli.rb
287
288
  - lib/kettle/dev/git_adapter.rb
@@ -313,10 +314,10 @@ licenses:
313
314
  - AGPL-3.0-only
314
315
  metadata:
315
316
  homepage_uri: https://kettle-dev.galtzo.com
316
- source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.3.7
317
- changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.3.7/CHANGELOG.md
317
+ source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.3.9
318
+ changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.3.9/CHANGELOG.md
318
319
  bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
319
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.3.7
320
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.3.9
320
321
  funding_uri: https://github.com/sponsors/pboling
321
322
  wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
322
323
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file