kettle-jem 7.1.21 → 7.1.22

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: 683eb5054612a2b6b31db5e9edb84218e8f9af95a11683c873d1cc854412aa90
4
- data.tar.gz: 7533b06602be3246bde2f16435a26e67cc01ae88b05e48d24af88de67b14655a
3
+ metadata.gz: 8a252bd3cf77adb0181517db1810fd56da1d4a7a7636ca931da5d678a91e3dc2
4
+ data.tar.gz: ab1773b86d3487522ab3b69ead5c74083f69a675d988135f284344ff248c11ab
5
5
  SHA512:
6
- metadata.gz: 764654bb020c4b4cc10c38a18c27fc8fa59a15efcf90d51f0577fdb89bb94bb82a4bfd517162c32d4ea26d1c7f1a55a1f199123e2c793329e12c1e490e4c9cf6
7
- data.tar.gz: 44d4ffb1f84faffb6172178cbb741bc97367a4da9b9d96fd9cbff41722d4cc8326dc3702a3de5834401229c2fe0080b736bfdc7666ff28b073e911b571512fbd
6
+ metadata.gz: b8ed892bad61aa16ef3d61acc4a06e8434a38884ba24381f38ee3f2910643a36812e3eb2bc3aa26c6b5ce8769753968e6d1e7aba712e774514cc239ea021c30c
7
+ data.tar.gz: aaa6bcd692fcaa6a83a6a87501142cb0761a01b8435d698980fe2519ff59cb2ab7eace9b81d0ee02336b457c0c91518ce503de980680896f60f57772341b621a
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
+ ## [7.1.22] - 2026-09-07
34
+
35
+ - TAG: [v7.1.22][7.1.22t]
36
+ - COVERAGE: 93.66% -- 11247/12008 lines in 16 files
37
+ - BRANCH COVERAGE: 77.06% -- 4787/6212 branches in 16 files
38
+ - 18.04% documented
39
+
40
+ ### Fixed
41
+
42
+ - Reconcile managed template-tool dependency requirements in their existing declarations before template bootstrapping, and maintain their floors with kettle-jem-deps-floor.
43
+
44
+ - Derive injected kettle-jem template requirements from VersionGem major, minor, and current version so releases cannot leave a stale self-dependency floor.
45
+
33
46
  ## [7.1.21] - 2026-09-07
34
47
 
35
48
  - TAG: [v7.1.21][7.1.21t]
@@ -1915,7 +1928,9 @@ Please file a bug if you notice a violation of semantic versioning.
1915
1928
  - Released kettle-jem as part of the initial StructuredMerge Ruby 7.0.0 gem set.
1916
1929
  - Included packaged templates and parser-backed merge support for Ruby gem templating.
1917
1930
 
1918
- [Unreleased]: https://github.com/structuredmerge/structuredmerge-ruby/compare/v7.1.21...HEAD
1931
+ [Unreleased]: https://github.com/structuredmerge/structuredmerge-ruby/compare/v7.1.22...HEAD
1932
+ [7.1.22]: https://github.com/structuredmerge/structuredmerge-ruby/compare/v7.1.21...v7.1.22
1933
+ [7.1.22t]: https://github.com/structuredmerge/structuredmerge-ruby/releases/tag/v7.1.22
1919
1934
  [7.1.21]: https://github.com/structuredmerge/structuredmerge-ruby/compare/v7.1.20...v7.1.21
1920
1935
  [7.1.21t]: https://github.com/structuredmerge/structuredmerge-ruby/releases/tag/v7.1.21
1921
1936
  [7.1.20]: https://github.com/structuredmerge/structuredmerge-ruby/compare/v7.1.19...v7.1.20
data/README.md CHANGED
@@ -655,6 +655,26 @@ The `kettle:jem:*` rake tasks are internal targets used by the executable after
655
655
  it prepares the templating environment; call `kettle-jem` directly for normal
656
656
  templating work.
657
657
 
658
+ ### Template Dependency Transitions
659
+
660
+ Before Bundler prepares a destination, `kettle-jem` reconciles its managed
661
+ template-tool dependencies where they are already declared: the root Gemfile,
662
+ root `*.gemfile` files, gemspecs, and modular Gemfile fragments. It updates the
663
+ complete requirement set in place; it never moves a declaration, adds a missing
664
+ dependency, or uses a local path as a compatibility fallback. A managed
665
+ dependency with non-static version requirements is rejected before Bundler runs.
666
+
667
+ The managed dependency registry is owned by `kettle-jem`. Run
668
+ `kettle-jem-deps-floor --write` to raise its requirement floors together with
669
+ the template dependency floors. The registry may declare a Ruby-version
670
+ requirement, so a bootstrap only updates dependencies active for the current
671
+ Ruby.
672
+
673
+ `kettle-jem` itself is not a dependency floor. Generated templating Gemfiles
674
+ derive its pessimistic requirement from `Kettle::Jem::Version.major` and
675
+ `.minor`, and use the running full version as the minimum. This makes a major
676
+ release emit the matching major requirement without a second version to update.
677
+
658
678
  ## 🔐 Security
659
679
 
660
680
  See [SECURITY.md][🔐security].
@@ -879,7 +899,7 @@ Thanks for RTFM. ☺️
879
899
  [📌gitmoji]: https://gitmoji.dev
880
900
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
881
901
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
882
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-11.937-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
902
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-12.008-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
883
903
  [🔐security]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/SECURITY.md
884
904
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
885
905
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -12,6 +12,7 @@ module Kettle
12
12
  PREPARE_ONLY_PATHS = [
13
13
  Kettle::Jem::KETTLE_CONFIG_PATH,
14
14
  "Gemfile",
15
+ "*.gemspec",
15
16
  # Gemfile evaluates these fragments immediately. Preparing only the
16
17
  # templating pair leaves a newly migrated project with references to
17
18
  # files that do not exist yet, so Bundler cannot perform the bootstrap.
@@ -36,6 +37,7 @@ module Kettle
36
37
  Kettle::Jem.apply_project(project_root, env: env, run_options: prepare_run_options)
37
38
  end
38
39
  report = merge_supplemental_prepare_report(report, supplemental_report)
40
+ transition_step = reconcile_template_managed_dependencies_step(project_root, events: events)
39
41
  nomono_bootstrap_step = normalize_existing_local_gemfile_bootstraps_step(project_root, events: events)
40
42
  setup_env = Kettle::Jem::Tasks::InstallTask.setup_command_env(project_root, env)
41
43
  setup_env["BUNDLE_DISABLE_CHECKSUM_VALIDATION"] = "true"
@@ -81,9 +83,10 @@ module Kettle
81
83
  %w[skipped succeeded].include?(reset_step.fetch(:status)) &&
82
84
  %w[skipped succeeded].include?(bundle_step.fetch(:status)),
83
85
  prepare_only: PREPARE_ONLY_PATHS,
84
- prepare_steps: [profile_sync_step, nomono_bootstrap_step, reset_step, bootstrap_step, bundle_step].compact,
86
+ prepare_steps: [profile_sync_step, transition_step, nomono_bootstrap_step, reset_step, bootstrap_step, bundle_step].compact,
85
87
  changed_files: (
86
88
  report.fetch(:changed_files, []) +
89
+ transition_step.fetch(:changed_files, []) +
87
90
  nomono_bootstrap_step.fetch(:changed_files, []) +
88
91
  reset_step.fetch(:changed_files, []) +
89
92
  bootstrap_step.fetch(:changed_files, []) +
@@ -136,6 +139,30 @@ module Kettle
136
139
  step
137
140
  end
138
141
 
142
+ def reconcile_template_managed_dependencies_step(project_root, events:)
143
+ paths = [
144
+ File.join(project_root.to_s, "Gemfile"),
145
+ *Dir.glob(File.join(project_root.to_s, "*.gemfile")),
146
+ *Dir.glob(File.join(project_root.to_s, "*.gemspec")),
147
+ *Dir.glob(File.join(project_root.to_s, "gemfiles/modular/**/*.gemfile"))
148
+ ].select { |path| File.file?(path) }.sort
149
+ changed_files = paths.filter_map do |path|
150
+ before = File.read(path)
151
+ after = Kettle::Jem.reconcile_template_managed_dependencies(before)
152
+ next if after == before
153
+
154
+ File.write(path, after)
155
+ Pathname.new(path).relative_path_from(Pathname.new(project_root.to_s)).to_s
156
+ end
157
+ step = {
158
+ name: "reconcile_template_managed_dependencies",
159
+ status: changed_files.empty? ? "already_current" : "applied",
160
+ changed_files: changed_files
161
+ }
162
+ Kettle::Jem.emit_step_event(events, "command_step", step, phase: "prepare")
163
+ step
164
+ end
165
+
139
166
  def reset_release_lockfiles_step(project_root:, setup_env:, quiet:, command_runner:, events:)
140
167
  if local_path_development_env?(setup_env)
141
168
  return {
@@ -191,7 +218,7 @@ module Kettle
191
218
  end
192
219
 
193
220
  def bundle_update_templating_bootstrap_command(project_root = Dir.pwd)
194
- %w[bundle update] + CRITICAL_TEMPLATING_GEMS + locked_templating_gems(project_root)
221
+ %w[bundle update] + managed_bootstrap_gems(project_root) + locked_templating_gems(project_root)
195
222
  end
196
223
 
197
224
  def templating_bootstrap_command(project_root = Dir.pwd)
@@ -211,12 +238,33 @@ module Kettle
211
238
  return false unless File.file?(lock_path)
212
239
 
213
240
  locked_gem_names(project_root).then do |names|
214
- CRITICAL_TEMPLATING_GEMS.all? { |gem_name| names.include?(gem_name) }
241
+ managed_bootstrap_gems(project_root).all? { |gem_name| names.include?(gem_name) }
215
242
  end
216
243
  rescue Bundler::LockfileError
217
244
  false
218
245
  end
219
246
 
247
+ def managed_bootstrap_gems(project_root)
248
+ declared = declared_managed_dependency_names(project_root)
249
+ (CRITICAL_TEMPLATING_GEMS + Kettle::Jem.template_bootstrap_dependency_names.select do |name|
250
+ declared.include?(name)
251
+ end).uniq
252
+ end
253
+
254
+ def declared_managed_dependency_names(project_root)
255
+ paths = [
256
+ File.join(project_root.to_s, "Gemfile"),
257
+ *Dir.glob(File.join(project_root.to_s, "*.gemfile")),
258
+ *Dir.glob(File.join(project_root.to_s, "*.gemspec")),
259
+ *Dir.glob(File.join(project_root.to_s, "gemfiles/modular/**/*.gemfile"))
260
+ ].select { |path| File.file?(path) }
261
+ paths.flat_map do |path|
262
+ Kettle::Jem.ruby_call_records(File.read(path), nil).filter_map do |call|
263
+ Kettle::Jem.ruby_string_argument(call) if Kettle::Jem.template_managed_dependency_call?(call)
264
+ end
265
+ end.uniq
266
+ end
267
+
220
268
  def bundle_install_after_bootstrap_step(project_root:, setup_env:, quiet:, command_runner:, events:, bootstrap_command:)
221
269
  if bootstrap_command == %w[bundle install]
222
270
  return {
@@ -18,7 +18,7 @@ end
18
18
 
19
19
  if structuredmerge_remote
20
20
  # Merge recipes for released installs.
21
- gem "kettle-jem", ">= 7.0"
21
+ gem "kettle-jem", {KJ|KETTLE_JEM_DEPENDENCY_ARGUMENTS}
22
22
  end
23
23
 
24
24
  if kettle_remote
@@ -5,7 +5,7 @@ module Kettle
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = "7.1.21"
8
+ VERSION = "7.1.22"
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/jem.rb CHANGED
@@ -52,6 +52,19 @@ module Kettle
52
52
  GEMSPEC_DEPENDENCY_MINIMUM_REQUIREMENTS = {
53
53
  "rspec-stubbed_env" => ">= 1.0.6"
54
54
  }.freeze
55
+ # Template dependency transitions are location-independent. Prepare finds
56
+ # declarations in the destination and reconciles their complete requirement
57
+ # set in place before Bundler is allowed to evaluate that destination.
58
+ TEMPLATE_MANAGED_DEPENDENCIES = [
59
+ {name: "nomono", requirements: ["~> 1.1", ">= 1.1.5"], bootstrap: true},
60
+ {name: "kettle-dev", requirements: ["~> 3.0", ">= 3.0.32"], bootstrap: true},
61
+ {
62
+ name: "kettle-changelog",
63
+ requirements: ["~> 1.0", ">= 1.0.7"],
64
+ bootstrap: true,
65
+ ruby_requirement: ">= 4.0.0"
66
+ }
67
+ ].freeze
55
68
  DEFAULT_GEMSPEC_METADATA_VALUES = {
56
69
  "allowed_push_host" => "TODO: Set to your gem server 'https://example.com'"
57
70
  }.freeze
@@ -5428,7 +5441,9 @@ module Kettle
5428
5441
  end
5429
5442
 
5430
5443
  def ensure_monorepo_root_gemfile_dependencies(content)
5431
- updated = ensure_trailing_newline(content.to_s.empty? ? %(source "https://gem.coop"\n) : content.to_s).dup
5444
+ updated = reconcile_template_managed_dependencies(
5445
+ ensure_trailing_newline(content.to_s.empty? ? %(source "https://gem.coop"\n) : content.to_s).dup
5446
+ )
5432
5447
  monorepo_root_gemfile_dependency_lines.each do |line|
5433
5448
  next if gemfile_declares_gem?(updated, line.fetch(:name))
5434
5449
 
@@ -5445,7 +5460,7 @@ module Kettle
5445
5460
  {name: "kettle-dev", source: %(gem "kettle-dev", "~> 3.0", ">= 3.0.32"\n)},
5446
5461
  {name: "kettle-drift", source: %(gem "kettle-drift", "~> 1.0", ">= 1.0.13"\n)},
5447
5462
  {name: "kettle-family", source: %(gem "kettle-family", "~> 1.2", ">= 1.2.95"\n)},
5448
- {name: "kettle-jem", source: %(gem "kettle-jem", "~> 7.1", ">= 7.1.20"\n)},
5463
+ {name: PACKAGE_NAME, source: kettle_jem_dependency_source},
5449
5464
  {name: "kettle-test", source: %(gem "kettle-test", "~> 2.0", ">= 2.0.21"\n)},
5450
5465
  {name: "rake", source: %(gem "rake", "~> 13.0"\n)},
5451
5466
  {name: "rspec", source: %(gem "rspec", "~> 3.0"\n)},
@@ -5469,6 +5484,17 @@ module Kettle
5469
5484
  end
5470
5485
  end
5471
5486
 
5487
+ def kettle_jem_dependency_requirements(version_module: Version)
5488
+ require_relative "jem/version_gem" unless version_module.respond_to?(:major)
5489
+
5490
+ ["~> #{version_module.major}.#{version_module.minor}", ">= #{version_module::VERSION}"]
5491
+ end
5492
+
5493
+ def kettle_jem_dependency_source(version_module: Version)
5494
+ requirements = kettle_jem_dependency_requirements(version_module: version_module)
5495
+ %(gem #{PACKAGE_NAME.inspect}, #{requirements.map(&:inspect).join(", ")}\n)
5496
+ end
5497
+
5472
5498
  def gemspec_path_declares_gem?(path, gem_name)
5473
5499
  normalized_path = path.to_s.delete_suffix("/")
5474
5500
  normalized_path == "gems/#{gem_name}"
@@ -9889,6 +9915,76 @@ module Kettle
9889
9915
  end
9890
9916
  end
9891
9917
 
9918
+ def template_managed_dependency(name)
9919
+ TEMPLATE_MANAGED_DEPENDENCIES.find { |dependency| dependency.fetch(:name) == name.to_s }
9920
+ end
9921
+
9922
+ def template_managed_dependency_requirements(name, version_module: Version)
9923
+ return kettle_jem_dependency_requirements(version_module: version_module) if name.to_s == PACKAGE_NAME
9924
+
9925
+ template_managed_dependency(name)&.fetch(:requirements)
9926
+ end
9927
+
9928
+ def template_managed_dependency_names(bootstrap: nil, ruby_version: RUBY_VERSION)
9929
+ TEMPLATE_MANAGED_DEPENDENCIES.filter_map do |dependency|
9930
+ next if !bootstrap.nil? && dependency.fetch(:bootstrap) != bootstrap
9931
+ next unless template_managed_dependency_active?(dependency, ruby_version)
9932
+
9933
+ dependency.fetch(:name)
9934
+ end
9935
+ end
9936
+
9937
+ def template_bootstrap_dependency_names(ruby_version: RUBY_VERSION)
9938
+ ([PACKAGE_NAME] + template_managed_dependency_names(bootstrap: true, ruby_version: ruby_version)).uniq
9939
+ end
9940
+
9941
+ def template_managed_dependency_active?(dependency, ruby_version = RUBY_VERSION)
9942
+ requirement = dependency[:ruby_requirement]
9943
+ return true unless requirement
9944
+
9945
+ Gem::Requirement.new(requirement).satisfied_by?(Gem::Version.new(ruby_version))
9946
+ end
9947
+
9948
+ def reconcile_template_managed_dependencies(source)
9949
+ replacements = ruby_call_records(source, nil).filter_map do |call|
9950
+ next unless template_managed_dependency_call?(call)
9951
+
9952
+ name = ruby_string_argument(call)
9953
+ requirements = template_managed_dependency_requirements(name)
9954
+ next unless requirements
9955
+
9956
+ requirement_nodes = Array(call.arguments&.arguments).drop(1).reject do |argument|
9957
+ argument.is_a?(::Prism::KeywordHashNode)
9958
+ end
9959
+ if requirement_nodes.empty?
9960
+ raise Error, "Managed template dependency #{name} must use static version requirements"
9961
+ end
9962
+ requirement_nodes.each do |argument|
9963
+ value = ruby_static_string_value(argument)
9964
+ next if value && Gem::Requirement::OPS.keys.any? { |operator| value.start_with?(operator) }
9965
+
9966
+ raise Error, "Managed template dependency #{name} must use static version requirements"
9967
+ end
9968
+
9969
+ expected = requirements
9970
+ current = requirement_nodes.map { |argument| ruby_static_string_value(argument) }
9971
+ next if current == expected
9972
+
9973
+ {
9974
+ start_offset: requirement_nodes.first.location.start_offset,
9975
+ end_offset: requirement_nodes.last.location.end_offset,
9976
+ replacement: expected.map { |requirement| JSON.generate(requirement) }.join(", ")
9977
+ }
9978
+ end
9979
+ replace_source_offsets(source, replacements)
9980
+ end
9981
+
9982
+ def template_managed_dependency_call?(call)
9983
+ return call.receiver.nil? && call.name == :gem if call.name == :gem
9984
+
9985
+ %i[add_dependency add_runtime_dependency add_development_dependency].include?(call.name)
9986
+ end
9987
+
9892
9988
  def ruby_keyword_string_argument(call, key)
9893
9989
  keyword_hash = Array(call&.arguments&.arguments).find { |argument| argument.is_a?(::Prism::KeywordHashNode) }
9894
9990
  assoc = keyword_hash&.elements&.find do |element|
@@ -14341,7 +14437,7 @@ module Kettle
14341
14437
  root: ["src", "my", "kettle-dev"]
14342
14438
  )
14343
14439
  elsif Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("4.0.0")
14344
- gem "kettle-changelog", "~> 1.0", ">= 1.0.7"
14440
+ gem "kettle-changelog", #{template_managed_dependency("kettle-changelog").fetch(:requirements).map(&:inspect).join(", ")}
14345
14441
  end
14346
14442
  end
14347
14443
  RUBY
@@ -15035,6 +15131,7 @@ module Kettle
15035
15131
  compact_hash(
15036
15132
  freeze_token: config.dig("defaults", "freeze_token").to_s.empty? ? "kettle-jem" : config.dig("defaults", "freeze_token").to_s,
15037
15133
  kettle_jem_version: VERSION,
15134
+ kettle_jem_dependency_arguments: kettle_jem_dependency_requirements.map(&:inspect).join(", "),
15038
15135
  template_run_date: run_timestamp.strftime("%Y-%m-%d"),
15039
15136
  template_run_year: run_timestamp.year.to_s,
15040
15137
  kettle_dev_local_gems: kettle_dev_local_gems(config),
@@ -15358,6 +15455,7 @@ module Kettle
15358
15455
  {
15359
15456
  "KJ|FREEZE_TOKEN" => project_runtime[:freeze_token].to_s,
15360
15457
  "KJ|KETTLE_JEM_VERSION" => project_runtime[:kettle_jem_version].to_s,
15458
+ "KJ|KETTLE_JEM_DEPENDENCY_ARGUMENTS" => project_runtime[:kettle_jem_dependency_arguments].to_s,
15361
15459
  "KJ|TEMPLATE_RUN_DATE" => project_runtime[:template_run_date].to_s,
15362
15460
  "KJ|TEMPLATE_RUN_YEAR" => project_runtime[:template_run_year].to_s,
15363
15461
  "KJ|KETTLE_DEV_LOCAL_GEMS" => project_runtime[:kettle_dev_local_gems].to_s,
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-jem
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.21
4
+ version: 7.1.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -976,10 +976,10 @@ licenses:
976
976
  - PolyForm-Small-Business-1.0.0
977
977
  metadata:
978
978
  homepage_uri: https://structuredmerge.org
979
- source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.21
980
- changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.21/CHANGELOG.md
979
+ source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.22
980
+ changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.22/CHANGELOG.md
981
981
  bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
982
- documentation_uri: https://www.rubydoc.info/gems/kettle-jem/7.1.21
982
+ documentation_uri: https://www.rubydoc.info/gems/kettle-jem/7.1.22
983
983
  funding_uri: https://github.com/sponsors/pboling
984
984
  wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
985
985
  news_uri: https://www.railsbling.com/tags/kettle-jem
metadata.gz.sig CHANGED
Binary file