kettle-family 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4cb4c502493482718d093219b202a58d6b4eef4d99890bdd300a18646fd87448
4
- data.tar.gz: 396fa2013f8ebb7ae7a3f84f2c74a12d38af9443311f5da5629154d7cb730023
3
+ metadata.gz: 77de418bfa9adc07c6500af0e4231f28909e6480fb96b730e34ebb215add0f04
4
+ data.tar.gz: 3667086145e554a4d4d89e68a2c4b5b2b1b00f59c799040f69a918c236c725ec
5
5
  SHA512:
6
- metadata.gz: e81e3a9cfba607441cbd5ae8dce13002c92cd9a2744dce24a1928b03d607360bec111d1314fec7570a3365e1631b86ae62af6fe00498084eabc2d74d07bde916
7
- data.tar.gz: af9e4962ed8bcad15481eca3e38139ad610ab7ce470009ea6aa815e7c46c396efe382d1a520a24c3411d47d127b9311cc9f8137c6691fcaca226ddd046311d3b
6
+ metadata.gz: 30f2a3b90e0fc17022b97a384277dc004b78e4cee2a4a6d1bdf515425db3f2173a846ac00f7614031fda678ceacf6dcd7e7d75fbcb6bfc582ed29ea4debeddb7
7
+ data.tar.gz: c1569420377c2692180d7f04a8312de902efac20b84d390514e9599a6d7a5025761abe9a9e2b647ccb713f2dbdc8fa02ee142e52a9df0389f54c7393a0a726ef
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,32 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [0.2.4] - 2026-07-04
34
+
35
+ - TAG: [v0.2.4][0.2.4t]
36
+ - COVERAGE: 95.33% -- 2288/2400 lines in 22 files
37
+ - BRANCH COVERAGE: 76.42% -- 752/984 branches in 22 files
38
+ - 30.04% documented
39
+
40
+ ### Added
41
+
42
+ - Family releases now raise downstream family dependency floors after each
43
+ member release by default, with `--no-auto-floors` and
44
+ `release.auto_dependency_floors: false` opt-outs.
45
+
46
+ ## [0.2.3] - 2026-07-03
47
+
48
+ - TAG: [v0.2.3][0.2.3t]
49
+ - COVERAGE: 95.51% -- 2210/2314 lines in 21 files
50
+ - BRANCH COVERAGE: 76.41% -- 716/937 branches in 21 files
51
+ - 29.82% documented
52
+
53
+ ### Fixed
54
+
55
+ - Root-configured member branch target commands now rediscover the checked-out
56
+ member repository before version bumping, preventing old release lines from
57
+ being bumped to the current branch's version.
58
+
33
59
  ## [0.2.2] - 2026-07-02
34
60
 
35
61
  - TAG: [v0.2.2][0.2.2t]
@@ -617,7 +643,11 @@ Please file a bug if you notice a violation of semantic versioning.
617
643
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
618
644
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
619
645
 
620
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v0.2.2...HEAD
646
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v0.2.4...HEAD
647
+ [0.2.4]: https://github.com/kettle-dev/kettle-family/compare/v0.2.3...v0.2.4
648
+ [0.2.4t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.2.4
649
+ [0.2.3]: https://github.com/kettle-dev/kettle-family/compare/v0.2.2...v0.2.3
650
+ [0.2.3t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.2.3
621
651
  [0.2.2]: https://github.com/kettle-dev/kettle-family/compare/v0.2.1...v0.2.2
622
652
  [0.2.2t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.2.2
623
653
  [0.2.1]: https://github.com/kettle-dev/kettle-family/compare/v0.2.0...v0.2.1
data/README.md CHANGED
@@ -592,7 +592,7 @@ Thanks for RTFM. ☺️
592
592
  [📌gitmoji]: https://gitmoji.dev
593
593
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
594
594
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
595
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.308-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
595
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.400-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
596
596
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
597
597
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
598
598
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -28,6 +28,7 @@ module Kettle
28
28
  return if branches.nil? || branches.empty?
29
29
 
30
30
  data = config.data.merge(
31
+ "members" => config.data.fetch("members", {}).merge("roots" => ["."]),
31
32
  "release" => config.data.fetch("release", {}).merge("target_branches" => branches)
32
33
  )
33
34
  Config.new(root: member.root, path: config.path, data: data)
@@ -403,6 +403,9 @@ module Kettle
403
403
  option :skip_steps, long: "--skip-steps", value: {type: String, usage: "LIST"}, desc: "Pass skip_steps=LIST through to kettle-release commands"
404
404
  option :local_ci, long: "--local-ci", desc: "Pass --local-ci through to kettle-release commands"
405
405
  option :continue_ci_failures, long: "--continue-ci-failures", desc: "Set K_RELEASE_CI_CONTINUE=true for release commands"
406
+ option :no_auto_floors, long: "--no-auto-floors", desc: "Do not raise family dependency floors between member releases" do
407
+ options[:no_auto_floors] = true
408
+ end
406
409
  option :accept, desc: "Answer yes to confirmation prompts in interactive commands"
407
410
  option :no_accept, long: "--no-accept", desc: "Wait for user input at confirmation prompts" do
408
411
  options[:accept] = false
@@ -419,6 +422,7 @@ module Kettle
419
422
  release_skip_steps: options[:skip_steps],
420
423
  release_local_ci: truthy_option?(:local_ci),
421
424
  release_continue_ci_failures: truthy_option?(:continue_ci_failures),
425
+ release_auto_dependency_floors: !truthy_option?(:no_auto_floors),
422
426
  accept: !options.key?(:accept) || options[:accept],
423
427
  tag: truthy_option?(:tag),
424
428
  push: truthy_option?(:push)
@@ -555,6 +559,7 @@ module Kettle
555
559
  skip_steps: options[:release_skip_steps],
556
560
  local_ci: options[:release_local_ci],
557
561
  continue_ci_failures: options[:release_continue_ci_failures],
562
+ auto_dependency_floors: options[:release_auto_dependency_floors],
558
563
  gha_sha_pins_upgrade: options[:gha_sha_pins_upgrade],
559
564
  gha_sha_pins_check: options[:check],
560
565
  env_overrides: options[:workflow_env],
@@ -212,6 +212,10 @@ module Kettle
212
212
  fetch_path("release", "jobs")
213
213
  end
214
214
 
215
+ def release_auto_dependency_floors?
216
+ fetch_path("release", "auto_dependency_floors") != false
217
+ end
218
+
215
219
  def release_family_changelog?
216
220
  fetch_path("release", "family_changelog", "enabled") == true
217
221
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kettle/dev"
4
+
5
+ module Kettle
6
+ module Family
7
+ class DependencyFloor
8
+ DEPENDENCY_METHODS = %i[add_dependency add_runtime_dependency].freeze
9
+
10
+ def initialize(released_members:, dependent_members:, mode: :dry_run)
11
+ @released_members = released_members
12
+ @dependent_members = dependent_members
13
+ @mode = mode
14
+ @released_versions = released_members.to_h { |member| [member.name, Gem::Version.new(member.version.to_s)] }
15
+ end
16
+
17
+ def results
18
+ dependent_members.filter_map { |member| result_for(member) }
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :released_members, :dependent_members, :mode, :released_versions
24
+
25
+ def result_for(member)
26
+ edits = collect_edits(member)
27
+ return if edits.empty?
28
+
29
+ write_edits(edits) if mode == :execute
30
+ CommandResult.new(
31
+ member_name: member.name,
32
+ phase: "dependency_floor",
33
+ command: ["internal", "dependency-floor", released_versions.keys.join(",")],
34
+ workdir: member.root,
35
+ status: 0,
36
+ success: true,
37
+ stdout: edit_summary(member: member, edits: edits),
38
+ stderr: "",
39
+ elapsed_seconds: 0.0,
40
+ skipped: mode != :execute,
41
+ reason: (mode == :execute) ? nil : "dry-run; pass --execute to write"
42
+ )
43
+ end
44
+
45
+ def collect_edits(member)
46
+ return [] unless member.gemspec_path && File.file?(member.gemspec_path)
47
+
48
+ source = File.read(member.gemspec_path)
49
+ parse_result = with_dev_errors { Kettle::Dev::VersionBump.parse_source(source, member.gemspec_path) }
50
+ Kettle::Dev::VersionBump.each_node(parse_result.value).filter_map do |node|
51
+ dependency_edit_for(member.gemspec_path, source, node)
52
+ end
53
+ end
54
+
55
+ def dependency_edit_for(path, source, node)
56
+ return unless node.is_a?(Prism::CallNode) && DEPENDENCY_METHODS.include?(node.name)
57
+
58
+ args = node.arguments&.arguments || []
59
+ name_node = args.first
60
+ return unless name_node.is_a?(Prism::StringNode)
61
+
62
+ dependency_name = name_node.unescaped
63
+ released_version = released_versions[dependency_name]
64
+ return unless released_version
65
+
66
+ floor_node = requirement_nodes(args.drop(1)).find do |requirement_node|
67
+ requirement_node.unescaped.start_with?(">= ")
68
+ end
69
+ return unless floor_node
70
+
71
+ current_floor = floor_node.unescaped.delete_prefix(">= ").strip
72
+ return unless floor_lower_than?(current_floor, released_version)
73
+
74
+ replacement = Kettle::Dev::VersionBump.quote_like(floor_node.location.slice, ">= #{released_version}")
75
+ Kettle::Dev::VersionBump.file_edit(path, source, floor_node.location.start_offset, floor_node.location.end_offset, replacement)
76
+ end
77
+
78
+ def requirement_nodes(nodes)
79
+ nodes.flat_map do |node|
80
+ case node
81
+ when Prism::StringNode
82
+ [node]
83
+ when Prism::ArrayNode
84
+ requirement_nodes(node.elements)
85
+ else
86
+ []
87
+ end
88
+ end
89
+ end
90
+
91
+ def floor_lower_than?(current_floor, released_version)
92
+ Gem::Version.new(current_floor) < released_version
93
+ rescue ArgumentError
94
+ false
95
+ end
96
+
97
+ def write_edits(edits)
98
+ Kettle::Dev::VersionBump.write_edits(edits)
99
+ end
100
+
101
+ def edit_summary(member:, edits:)
102
+ verb = (mode == :execute) ? "updated" : "would update"
103
+ ["#{verb} #{edits.length} family dependency floor(s) for #{member.name}", *edits.map { |edit| "#{verb} #{edit.fetch(:path)}" }.uniq].join("\n")
104
+ end
105
+
106
+ def with_dev_errors
107
+ yield
108
+ rescue Kettle::Dev::Error => error
109
+ raise Error, error.message
110
+ end
111
+ end
112
+ end
113
+ end
@@ -96,8 +96,10 @@ module Kettle
96
96
  def load_gemspec(path)
97
97
  # Some legacy gemspecs use root-relative Kernel.load calls, and RubyGems
98
98
  # evaluates gemspecs relative to the current process directory.
99
+ # Gem::Specification.load caches by path, which is wrong for branch-stack
100
+ # workflows that checkout different contents at the same path.
99
101
  # rubocop:disable ThreadSafety/DirChdir
100
- spec = Dir.chdir(File.dirname(path)) { Gem::Specification.load(path) }
102
+ spec = Dir.chdir(File.dirname(path)) { eval_gemspec(path) }
101
103
  # rubocop:enable ThreadSafety/DirChdir
102
104
  raise Error, "could not load gemspec #{path}" unless spec
103
105
 
@@ -106,6 +108,16 @@ module Kettle
106
108
  raise Error, "could not load gemspec #{path}: #{error.message}"
107
109
  end
108
110
 
111
+ def eval_gemspec(path)
112
+ return unless File.file?(path)
113
+
114
+ code = Gem.open_file(path, "r:UTF-8:-", &:read)
115
+ spec = eval(code, binding, path) # rubocop:disable Security/Eval -- Mirrors RubyGems gemspec loading without its path cache.
116
+ return spec if spec.is_a?(Gem::Specification)
117
+
118
+ raise Error, "#{path} is not a Gem::Specification"
119
+ end
120
+
109
121
  def excluded_gemspec?(path)
110
122
  ignored_by_git?(path) || excluded_by_pattern?(path)
111
123
  end
@@ -3,7 +3,7 @@
3
3
  module Kettle
4
4
  module Family
5
5
  module Version
6
- VERSION = "0.2.2"
6
+ VERSION = "0.2.4"
7
7
  end
8
8
  VERSION = Version::VERSION # Traditional Constant Location
9
9
  end
@@ -44,7 +44,7 @@ module Kettle
44
44
  "BUNDLE_SUPPRESS_INSTALL_USING_MESSAGES" => "true"
45
45
  }.freeze
46
46
 
47
- def initialize(command:, config:, members:, execute: false, accept: true, commit: true, allow_dirty: false, publish: false, push: false, tag: false, start_step: nil, skip_steps: nil, local_ci: false, continue_ci_failures: false, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, env_overrides: {}, debug: false, gem_signing_password: nil, jobs: nil, progress_io: nil, bup_args: [], bex_args: [], start_member: nil, start_branch: nil)
47
+ def initialize(command:, config:, members:, execute: false, accept: true, commit: true, allow_dirty: false, publish: false, push: false, tag: false, start_step: nil, skip_steps: nil, local_ci: false, continue_ci_failures: false, auto_dependency_floors: nil, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, env_overrides: {}, debug: false, gem_signing_password: nil, jobs: nil, progress_io: nil, bup_args: [], bex_args: [], start_member: nil, start_branch: nil)
48
48
  @command = command
49
49
  @config = config
50
50
  @members = members
@@ -59,6 +59,7 @@ module Kettle
59
59
  @skip_steps = skip_steps
60
60
  @local_ci = local_ci
61
61
  @continue_ci_failures = continue_ci_failures
62
+ @auto_dependency_floors = auto_dependency_floors.nil? ? config.release_auto_dependency_floors? : auto_dependency_floors
62
63
  @gha_sha_pins_upgrade = gha_sha_pins_upgrade
63
64
  @gha_sha_pins_check = gha_sha_pins_check
64
65
  @env_overrides = env_overrides
@@ -85,7 +86,7 @@ module Kettle
85
86
 
86
87
  private
87
88
 
88
- attr_reader :command, :config, :members, :execute, :accept, :commit, :allow_dirty, :publish, :push, :tag, :start_step, :skip_steps, :local_ci, :continue_ci_failures, :gha_sha_pins_upgrade, :gha_sha_pins_check, :env_overrides, :debug, :jobs, :progress_io, :bup_args, :bex_args, :start_member, :start_branch
89
+ attr_reader :command, :config, :members, :execute, :accept, :commit, :allow_dirty, :publish, :push, :tag, :start_step, :skip_steps, :local_ci, :continue_ci_failures, :auto_dependency_floors, :gha_sha_pins_upgrade, :gha_sha_pins_check, :env_overrides, :debug, :jobs, :progress_io, :bup_args, :bex_args, :start_member, :start_branch
89
90
 
90
91
  def current_branch_results(workflow_members)
91
92
  return check_results(workflow_members) if command == "check"
@@ -286,6 +287,7 @@ module Kettle
286
287
  skip_steps: skip_steps,
287
288
  local_ci: local_ci,
288
289
  continue_ci_failures: continue_ci_failures,
290
+ auto_dependency_floors: auto_dependency_floors,
289
291
  gha_sha_pins_upgrade: gha_sha_pins_upgrade,
290
292
  gha_sha_pins_check: gha_sha_pins_check,
291
293
  env_overrides: env_overrides,
@@ -315,17 +317,27 @@ module Kettle
315
317
  release_members.each_with_object(results) do |member, memo|
316
318
  memo.concat(release_results_for_member(member, runner: runner))
317
319
  break memo unless memo.last.ok?
320
+
321
+ remaining_members = release_members.drop(release_members.index(member) + 1)
322
+ append_dependency_floor_results(released_members: [member], dependent_members: remaining_members, runner: runner, memo: memo)
323
+ break memo unless memo.last&.ok?
318
324
  end
319
325
  end
320
326
 
321
327
  def parallel_release_member_results(release_members, initial_results)
322
328
  results = initial_results.dup
323
329
  waves = release_waves(release_members)
330
+ completed_members = []
324
331
  waves.each_with_index do |wave, index|
325
332
  results << release_wave_result(wave, index: index, total: waves.length)
326
333
  wave_results = run_release_wave(wave)
327
334
  results.concat(wave_results.flatten)
328
335
  break unless wave_results.all? { |member_results| member_results.all?(&:ok?) }
336
+
337
+ completed_members.concat(wave)
338
+ remaining_members = release_members - completed_members
339
+ append_dependency_floor_results(released_members: wave, dependent_members: remaining_members, runner: release_command_runner, memo: results)
340
+ break unless results.last&.ok?
329
341
  end
330
342
  results
331
343
  end
@@ -477,6 +489,38 @@ module Kettle
477
489
  Array(member.dependencies).map(&:to_s).select { |dependency| by_name.key?(dependency) }
478
490
  end
479
491
 
492
+ def append_dependency_floor_results(released_members:, dependent_members:, runner:, memo:)
493
+ return unless auto_dependency_floors
494
+ return if dependent_members.empty?
495
+
496
+ floor_results = DependencyFloor.new(
497
+ released_members: released_members,
498
+ dependent_members: dependent_members,
499
+ mode: execute ? :execute : :dry_run
500
+ ).results
501
+ memo.concat(floor_results)
502
+ return unless floor_results.any? && floor_results.all?(&:ok?)
503
+
504
+ commit_dependency_floor_changes(dependent_members: floor_results.map(&:member_name), runner: runner, memo: memo) if execute && commit
505
+ end
506
+
507
+ def commit_dependency_floor_changes(dependent_members:, runner:, memo:)
508
+ by_name = members.to_h { |member| [member.name, member] }
509
+ dependent_members.each do |member_name|
510
+ member = by_name.fetch(member_name)
511
+ memo << runner.call(
512
+ member: member,
513
+ phase: "commit_dependency_floor",
514
+ command: [
515
+ "sh",
516
+ "-lc",
517
+ "if ! git diff --quiet -- '*.gemspec'; then git add -- '*.gemspec' && git commit -m '⬆️ Raise family dependency floors'; fi"
518
+ ]
519
+ )
520
+ break unless memo.last.ok?
521
+ end
522
+ end
523
+
480
524
  def distinct_git_roots?(release_members)
481
525
  roots = release_members.map { |member| git_root_for(member) }
482
526
  roots.uniq.length == roots.length
data/lib/kettle/family.rb CHANGED
@@ -11,6 +11,7 @@ require_relative "family/readiness_check"
11
11
  require_relative "family/changelog_check"
12
12
  require_relative "family/git_status"
13
13
  require_relative "family/version_bump"
14
+ require_relative "family/dependency_floor"
14
15
  require_relative "family/branch_lane_audit"
15
16
  require_relative "family/config"
16
17
  require_relative "family/branch_target_config"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-family
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -319,6 +319,7 @@ files:
319
319
  - lib/kettle/family/command_result.rb
320
320
  - lib/kettle/family/command_runner.rb
321
321
  - lib/kettle/family/config.rb
322
+ - lib/kettle/family/dependency_floor.rb
322
323
  - lib/kettle/family/discovery.rb
323
324
  - lib/kettle/family/git_status.rb
324
325
  - lib/kettle/family/local_install.rb
@@ -339,10 +340,10 @@ licenses:
339
340
  - AGPL-3.0-only
340
341
  metadata:
341
342
  homepage_uri: https://kettle-family.galtzo.com
342
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v0.2.2
343
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v0.2.2/CHANGELOG.md
343
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v0.2.4
344
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v0.2.4/CHANGELOG.md
344
345
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
345
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/0.2.2
346
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/0.2.4
346
347
  funding_uri: https://github.com/sponsors/pboling
347
348
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
348
349
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file