kettle-dev 3.1.14 → 3.1.15

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: f91c0c4ef8cfb88983835109c778d084a739a78ed4cfc83318be404cd2ba9eee
4
- data.tar.gz: c16964dc845379bc31fa3106af5567d950d00a2a2741bb79e50f816235420f41
3
+ metadata.gz: a92c25418ab7487a902fd03260cf2c457e2d156c09f2ec9ec426998fd7a31729
4
+ data.tar.gz: 437b8e39726ba9a2ed4990b38217dcb1336f48eeb504e5edff13009601febc8e
5
5
  SHA512:
6
- metadata.gz: 7b9d783784e66864cd733aa63abdfb15161b93224b81bd8d5db9a2a2dc71cd3ca45ef1bd87f389895b9ef3e78d2d7c3432b4ea78ef176e70c3e6936eac9e5ce7
7
- data.tar.gz: a7fb4dedb0aeb325ffca2a717e0700ae2d37a73ef38bde1006bfd5b8926bf6b6d9ebedefbe45400ba885c1aaa523db229854b6a0f0174a42ca0229ce5b47d925
6
+ metadata.gz: ce6778cb2d3676ef46727327a08f464a6c664cec8e6d1c472ed6ef283c4e8e8d132a6b1a36012597efaf392d22785c2c559b07c107c13eb2fba142a4f21255ed
7
+ data.tar.gz: 8ff9fc510de127537b010179d6f67548e2df8a0b72c76e7ab1a680d9417aeb16c2f98eda05ef532b51c9604a85b98dbe03d1678220e89ea65982dd2e0e538297
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,21 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [3.1.15] - 2026-10-16
34
+
35
+ - TAG: [v3.1.15][3.1.15t]
36
+ - COVERAGE: 91.74% -- 5733/6249 lines in 48 files
37
+ - BRANCH COVERAGE: 75.88% -- 2209/2911 branches in 48 files
38
+ - 53.60% documented
39
+
40
+ ### Added
41
+
42
+ - RubyGemsVersions confirm reads: fetch and published_version_numbers accept confirm: true for answers feeding destructive or commit-producing decisions — the registry is always contacted, revalidation rides the cached entrys ETag/Last-Modified (a 304 re-arms the TTL without rewriting the payload, even past expiry), and an unconfirmable answer returns nil instead of the stale entry
43
+
44
+ - kettle-release step 1 now pins release lockfiles to the newest STABLE Bundler the registry serves instead of the executing Bundler: when the registry is newer it installs that version (rubygems.org only, prereleases excluded) and runs the bundler update under it, so BUNDLED WITH stops drifting behind CI images; a registry outage fails open to the executing Bundler
45
+
46
+ - New Kettle::Dev::BundlerPinning module: shared stable-Bundler resolution (installed_stable, published_stable, install_stable, ensure_latest_stable, update_argv) so kettle-release step 1 and kettle-family bupb/update-bundler can share one implementation instead of divergent copies; kettle-release now delegates to it
47
+
33
48
  ## [3.1.14] - 2026-10-16
34
49
 
35
50
  - TAG: [v3.1.14][3.1.14t]
@@ -4130,7 +4145,9 @@ Please file a bug if you notice a violation of semantic versioning.
4130
4145
  - Selecting will run the selected workflow via `act`
4131
4146
  - This may move to its own gem in the future.
4132
4147
 
4133
- [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.14...HEAD
4148
+ [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.15...HEAD
4149
+ [3.1.15]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.14...v3.1.15
4150
+ [3.1.15t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.1.15
4134
4151
  [3.1.14]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.13...v3.1.14
4135
4152
  [3.1.14t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v3.1.14
4136
4153
  [3.1.13]: https://github.com/kettle-dev/kettle-dev/compare/v3.1.12...v3.1.13
data/README.md CHANGED
@@ -1084,7 +1084,7 @@ Thanks for RTFM. ☺️
1084
1084
  [📌gitmoji]: https://gitmoji.dev
1085
1085
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1086
1086
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1087
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.177-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1087
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.249-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1088
1088
  [🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
1089
1089
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1090
1090
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/dependency_installer"
4
+
5
+ module Kettle
6
+ module Dev
7
+ # Shared Bundler version resolution and maintenance for the release
8
+ # toolchain. Both kettle-release (release prep pins BUNDLED WITH) and
9
+ # kettle-family (bupb / update-bundler pin every member lockfile) need the
10
+ # same three answers — what stable Bundler is installed, what stable
11
+ # Bundler the registry serves, and how to install/pin one — and divergent
12
+ # copies are how lockfiles drifted behind CI images.
13
+ #
14
+ # Every lookup rejects prereleases: bundler betas ship as "latest" and a
15
+ # beta in BUNDLED WITH reinfects every child that boots the bundle.
16
+ module BundlerPinning
17
+ RUBYGEMS_SOURCE = "https://rubygems.org"
18
+
19
+ module_function
20
+
21
+ # Newest stable Bundler installed locally. Consults BOTH sources —
22
+ # the activated spec and every installed spec — because bundler/setup
23
+ # scoping can hide one from the other depending on how the process booted.
24
+ def installed_stable
25
+ active = Gem.loaded_specs["bundler"]&.version
26
+ versions = Gem::Specification.find_all_by_name("bundler").map(&:version)
27
+ versions.push(active) if active
28
+ versions.reject(&:prerelease?).max
29
+ end
30
+
31
+ # Newest stable Bundler the registry serves; nil when the registry
32
+ # cannot be consulted, so callers can fail open to local state.
33
+ def published_stable
34
+ numbers = Kettle::Dev::RubyGemsVersions.published_version_numbers("bundler")
35
+ return nil unless numbers
36
+
37
+ numbers.filter_map { |number| published_candidate(number) }.max
38
+ end
39
+
40
+ # Install a stable Bundler — the exact version given, or the latest
41
+ # stable when nil. rubygems.org only, never prereleases, and a spec
42
+ # reset so the just-installed version is visible to later gem lookups
43
+ # in this process. Returns the installed specs.
44
+ def install_stable(version = nil)
45
+ original_sources = Gem.sources.to_a
46
+ Gem.sources = [RUBYGEMS_SOURCE]
47
+ requirements = version ? ["= #{version}"] : []
48
+ specs = Gem::DependencyInstaller.new(domain: :remote, prerelease: false, document: []).install("bundler", *requirements)
49
+ Gem::Specification.reset
50
+ specs
51
+ ensure
52
+ Gem.sources = original_sources if original_sources
53
+ end
54
+
55
+ # The version a release should pin BUNDLED WITH to: the registry's
56
+ # newest stable, installing it first when the executing Bundler lags.
57
+ # A registry outage fails open to the executing Bundler rather than
58
+ # blocking the release.
59
+ def ensure_latest_stable(executing: Bundler::VERSION)
60
+ current = Gem::Version.new(executing)
61
+ latest = published_stable
62
+ return current.to_s if latest.nil? || latest <= current
63
+
64
+ install_stable(latest)
65
+ latest.to_s
66
+ end
67
+
68
+ # Pinned argv for a bundler-update spawn. A bare `--bundler` lets
69
+ # RubyGems resolve the newest bundler INCLUDING prereleases (the
70
+ # recurring 4.1.0.betaN infection vector), so the version is pinned on
71
+ # both the executable selector and the flag.
72
+ def update_argv(version, extra_gems: [])
73
+ ["bundle", "_#{version}_", "update", *extra_gems, "--bundler=#{version}"]
74
+ end
75
+
76
+ def published_candidate(number)
77
+ candidate = Gem::Version.new(number)
78
+ candidate unless candidate.prerelease?
79
+ rescue ArgumentError
80
+ nil
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1891,23 +1891,30 @@ module Kettle
1891
1891
  end
1892
1892
  end
1893
1893
 
1894
+ # The release must pin BUNDLED WITH to the newest stable Bundler the
1895
+ # registry serves; BundlerPinning installs it first when the executing
1896
+ # Bundler lags and fails open to the executing Bundler on a registry
1897
+ # outage. kettle-family's bupb/update-bundler delegate to the same
1898
+ # module once the floor reaches the version shipping it.
1899
+ def ensure_latest_stable_bundler!
1900
+ Kettle::Dev::BundlerPinning.ensure_latest_stable
1901
+ end
1902
+
1894
1903
  # Keep Bundler maintenance separate from the release metadata commit.
1895
1904
  # The project lockfiles are shared development state, so the update must
1896
1905
  # be real and committed rather than hidden behind a temporary lockfile.
1897
1906
  #
1898
- # The version is pinned to the Bundler actually executing rather than left
1899
- # bare. `bundle update --bundler` resolves to whatever RubyGems reports as
1900
- # bundler's "latest", which includes prereleases: bundler published
1901
- # 4.1.0.beta1 as latest, so a bare update installed the beta and rewrote
1902
- # BUNDLED WITH to it. That beta vendors its own copy of rubygems' URI
1903
- # constants, and loading it against rubygems' copy emits a wall of
1904
- # "already initialized constant Gem::URI::..." warnings on stderr, which
1905
- # broke any spec shelling out to the executable and asserting clean stderr
1906
- # — failing a later release at its coverage step. Pinning matches
1907
- # LockfileReset#lockfile_command, which already documents this hazard.
1907
+ # The version is pinned to the newest STABLE Bundler the registry
1908
+ # serves, not whatever is executing: a bare `bundle update --bundler`
1909
+ # resolves "latest" including prereleases (bundler 4.1.0.beta1 shipped
1910
+ # as latest and its vendored rubygems URI constants broke specs
1911
+ # asserting clean stderr), while pinning to the executing Bundler
1912
+ # quietly re-records a stale BUNDLED WITH whenever the releasing
1913
+ # machine lags the registry — and CI images carrying a newer default
1914
+ # Bundler then drift from the committed lockfile.
1908
1915
  def update_bundler_and_commit!
1909
- bundler_version = Shellwords.escape(Bundler::VERSION)
1910
- run_cmd!(release_project_command("bundle update --bundler=#{bundler_version}"))
1916
+ bundler_version = Shellwords.escape(ensure_latest_stable_bundler!)
1917
+ run_cmd!(release_project_command("bundle _#{bundler_version}_ update --bundler=#{bundler_version}"))
1911
1918
 
1912
1919
  appraisal_gemfile = File.join(@root, "Appraisal.root.gemfile")
1913
1920
  if File.file?(appraisal_gemfile)
@@ -1923,7 +1930,7 @@ module Kettle
1923
1930
  unless File.file?(appraisal_lockfile)
1924
1931
  run_cmd!(release_project_command("#{appraisal_env} bundle install"))
1925
1932
  end
1926
- appraisal_command = "#{appraisal_env} bundle update --bundler=#{bundler_version}"
1933
+ appraisal_command = "#{appraisal_env} bundle _#{bundler_version}_ update --bundler=#{bundler_version}"
1927
1934
  run_cmd!(release_project_command(appraisal_command))
1928
1935
  run_cmd!(release_project_command("bundle exec rake appraisal:reset"))
1929
1936
  end
@@ -70,11 +70,21 @@ module Kettle
70
70
  # such as gem.coop can lag rubygems.org: a version present on one may
71
71
  # be absent from the other. Cache entries are keyed by source for the
72
72
  # same reason, so the two registries cannot poison each other.
73
- def fetch(gem_name, version_hint: nil, refresh: false, source: DEFAULT_SOURCE)
73
+ #
74
+ # +confirm: true+ is the consequence-based read: the answer feeds a
75
+ # destructive or commit-producing decision (uninstalling a gem,
76
+ # resetting a lockfile), so the TTL'd cache is never trusted on its
77
+ # own. The registry is always contacted, but revalidation is cheap:
78
+ # the cached entry's ETag/Last-Modified go out as conditional headers
79
+ # and a 304 re-arms the cached answer instead of replacing it. When
80
+ # the network cannot confirm freshness, +nil+ is returned — never the
81
+ # stale entry — because a destructive caller must read that as
82
+ # "unknown", and absence is the dangerous answer to get wrong.
83
+ def fetch(gem_name, version_hint: nil, refresh: false, source: DEFAULT_SOURCE, confirm: false)
74
84
  name = gem_name.to_s
75
85
  key = cache_key(name, source)
76
86
  cached = cached_versions(key)
77
- cache_bust = refresh || env_refresh? || fresh_release_marker?(name, version_hint) || cached_behind_version?(cached, version_hint)
87
+ cache_bust = refresh || confirm || env_refresh? || fresh_release_marker?(name, version_hint) || cached_behind_version?(cached, version_hint)
78
88
  return cached if cached && !cache_bust
79
89
 
80
90
  uri = versions_uri(gem_name, source: source, cache_bust: cache_bust)
@@ -82,6 +92,9 @@ module Kettle
82
92
  if cache_bust
83
93
  request["Cache-Control"] = "no-cache"
84
94
  request["Pragma"] = "no-cache"
95
+ validators = cache_validators(key)
96
+ request["If-None-Match"] = validators["etag"] if validators["etag"]
97
+ request["If-Modified-Since"] = validators["last_modified"] if validators["last_modified"]
85
98
  end
86
99
  response = Net::HTTP.start(
87
100
  uri.host,
@@ -92,16 +105,28 @@ module Kettle
92
105
  ) do |http|
93
106
  http.request(request)
94
107
  end
108
+ if response.is_a?(Net::HTTPNotModified)
109
+ stored = stored_entry(key)
110
+ if stored
111
+ rearm_versions(key)
112
+ return Array(stored["entries"])
113
+ end
114
+ end
95
115
  if response.code.to_i == 404
96
116
  write_versions(key, [])
97
117
  return []
98
118
  end
99
- return cached unless response.is_a?(Net::HTTPSuccess)
119
+ unless response.is_a?(Net::HTTPSuccess)
120
+ return nil if confirm
121
+
122
+ return cached
123
+ end
100
124
 
101
125
  data = JSON.parse(response.body)
102
- write_versions(key, data) if data.is_a?(Array)
126
+ write_versions(key, data, etag: response["etag"], last_modified: response["last-modified"]) if data.is_a?(Array)
103
127
  data
104
128
  rescue => error
129
+ return nil if confirm
105
130
  return cached if cached
106
131
 
107
132
  raise error
@@ -121,8 +146,12 @@ module Kettle
121
146
  # within the marker TTL. Without it a caller cannot tell "this version
122
147
  # was published moments ago in this very run" from "this gem was
123
148
  # released at some point in the last month".
124
- def published_version_numbers(gem_name, source: DEFAULT_SOURCE, version: nil)
125
- versions = fetch(gem_name, version_hint: version, source: normalize_source(source))
149
+ # +confirm: true+ marks the read as feeding a destructive or
150
+ # commit-producing decision; it always revalidates against the
151
+ # registry (cheaply, via the cached entry's validators) and returns
152
+ # nil when freshness cannot be confirmed.
153
+ def published_version_numbers(gem_name, source: DEFAULT_SOURCE, version: nil, confirm: false)
154
+ versions = fetch(gem_name, version_hint: version, source: normalize_source(source), confirm: confirm)
126
155
  return nil if versions.nil?
127
156
 
128
157
  versions.filter_map { |entry| entry["number"] if entry.is_a?(Hash) }
@@ -246,26 +275,69 @@ module Kettle
246
275
  end
247
276
 
248
277
  def cached_versions(cache_key)
278
+ entry = cached_entry(cache_key)
279
+ return nil unless entry
280
+
281
+ Array(entry["entries"])
282
+ end
283
+
284
+ # Validators from the last successful fetch, sent as conditional
285
+ # headers on revalidation so a 304 can confirm freshness cheaply.
286
+ # Deliberately TTL-agnostic: an expired entry's validators still prove
287
+ # payload currency when the server answers 304, which is the entire
288
+ # point of carrying them.
289
+ def cache_validators(cache_key)
290
+ entry = stored_entry(cache_key)
291
+ return {} unless entry
292
+
293
+ {"etag" => entry["etag"], "last_modified" => entry["last_modified"]}.compact
294
+ end
295
+
296
+ def cached_entry(cache_key)
297
+ entry = stored_entry(cache_key)
298
+ return nil unless entry
299
+ return nil unless fresh_version_cache_entry?(entry)
300
+
301
+ entry
302
+ end
303
+
304
+ def stored_entry(cache_key)
249
305
  path = version_cache_path
250
306
  return nil if path.empty?
251
307
 
252
308
  entry = read_version_cache(path).fetch("versions", {})[cache_key]
253
- return nil unless entry.is_a?(Hash)
254
- return nil unless fresh_version_cache_entry?(entry)
309
+ entry.is_a?(Hash) ? entry : nil
310
+ end
255
311
 
256
- Array(entry["entries"])
312
+ # A 304 means the cached payload is still current; re-arming the TTL
313
+ # without rewriting the payload keeps the entry valid for TTL reads.
314
+ def rearm_versions(cache_key)
315
+ path = version_cache_path
316
+ return if path.empty?
317
+
318
+ data = read_version_cache(path)
319
+ entry = data.fetch("versions", {})[cache_key]
320
+ return unless entry.is_a?(Hash)
321
+
322
+ entry["cached_at"] = Time.now.utc.iso8601
323
+ File.write(path, JSON.pretty_generate(data) << "\n")
324
+ rescue => error
325
+ warn("[kettle-dev] could not re-arm RubyGems version cache: #{error.class}: #{error.message}") if Kettle::Dev::DEBUGGING
257
326
  end
258
327
 
259
- def write_versions(cache_key, entries)
328
+ def write_versions(cache_key, entries, etag: nil, last_modified: nil)
260
329
  path = version_cache_path
261
330
  return if path.empty?
262
331
 
263
332
  data = read_version_cache(path)
264
333
  data["versions"] ||= {}
265
- data["versions"][cache_key] = {
334
+ entry = {
266
335
  "cached_at" => Time.now.utc.iso8601,
267
336
  "entries" => entries
268
337
  }
338
+ entry["etag"] = etag if etag
339
+ entry["last_modified"] = last_modified if last_modified
340
+ data["versions"][cache_key] = entry
269
341
  FileUtils.mkdir_p(File.dirname(path))
270
342
  File.write(path, JSON.pretty_generate(data) << "\n")
271
343
  rescue => error
@@ -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.1.14"
8
+ VERSION = "3.1.15"
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
@@ -30,6 +30,7 @@ module Kettle
30
30
  autoload :ChangelogEntryAdder, "kettle/dev/changelog_entry_adder"
31
31
  autoload :BumpCLI, "kettle/dev/bump_cli"
32
32
  autoload :BundlerEnvGuard, "kettle/dev/bundler_env_guard"
33
+ autoload :BundlerPinning, "kettle/dev/bundler_pinning"
33
34
  autoload :CIHelpers, "kettle/dev/ci_helpers"
34
35
  autoload :CIMonitor, "kettle/dev/ci_monitor"
35
36
  autoload :CommitMsg, "kettle/dev/commit_msg"
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.1.14
4
+ version: 3.1.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -371,6 +371,7 @@ files:
371
371
  - lib/kettle/dev.rb
372
372
  - lib/kettle/dev/bump_cli.rb
373
373
  - lib/kettle/dev/bundler_env_guard.rb
374
+ - lib/kettle/dev/bundler_pinning.rb
374
375
  - lib/kettle/dev/cache_progress.rb
375
376
  - lib/kettle/dev/changelog_cli.rb
376
377
  - lib/kettle/dev/changelog_entry_adder.rb
@@ -422,10 +423,10 @@ licenses:
422
423
  - AGPL-3.0-only
423
424
  metadata:
424
425
  homepage_uri: https://kettle-dev.galtzo.com
425
- source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v3.1.14
426
- changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.1.14/CHANGELOG.md
426
+ source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v3.1.15
427
+ changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v3.1.15/CHANGELOG.md
427
428
  bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
428
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/3.1.14
429
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/3.1.15
429
430
  funding_uri: https://github.com/sponsors/pboling
430
431
  wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
431
432
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file