kettle-family 1.3.2 → 1.3.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: 7b4a15c15f854710fb75ab2c866cf5ebb2b6dd24f1418a1e694446139a347421
4
- data.tar.gz: 70bd34977e27daf2f6e5f8ef465b02c938438eb1346d3574fd7080f2f54d3661
3
+ metadata.gz: 99e6506531c29efe6259ee3f592eb50d654eeef9f7074a1179f4f61960d7efdd
4
+ data.tar.gz: d3424e4481d1ced32dbb89228de517e3a0adf30a099883879c16fbb9f6feec8e
5
5
  SHA512:
6
- metadata.gz: 271e6a288806993a5cf584f9ba8293854c9b7ecb32dbec18b8b6cf293856d8ca7f92bb6d320f8fd97de6ba82622573dee01112974416c95ee2d87e11312dc463
7
- data.tar.gz: 3fd884813e4bb730575704ceda96b874cacac2b83a80d597f86e1212ba266cb575526808abe1eb5061404cb5446c64946fa0575d10fae1993dd96868c3479639
6
+ metadata.gz: dfe9f450d1c65671d3599bfe3625952dc7106cfbfe896960bc236d0df04c033dd73116a94b49b5dba82502022ff503339e7a1457ff6c507888df0736489b5dd7
7
+ data.tar.gz: 4fc7889e44bd08e673899b0f3ea040ecec5129e4b90ef2ef166f11c51de0e4a05c2066036a8a600bf9dc9e35fd495f95ac60f603799709ec08e0e74522f22ef8
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,34 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.3.4] - 2026-10-02
34
+
35
+ - TAG: [v1.3.4][1.3.4t]
36
+ - COVERAGE: 93.26% -- 6571/7046 lines in 37 files
37
+ - BRANCH COVERAGE: 77.70% -- 2666/3431 branches in 37 files
38
+ - 33.33% documented
39
+
40
+ ### Added
41
+
42
+ - kettle-family installs the latest stable Bundler and updates family lockfiles with `update-bundler`.
43
+
44
+ ### Fixed
45
+
46
+ - Lock each repository's worktree registry across process setup, parallel branch probes, and cleanup to prevent intermittent Git worktree failures.
47
+
48
+ - Family reports now include results from the update-bundler command.
49
+
50
+ ## [1.3.3] - 2026-09-28
51
+
52
+ - TAG: [v1.3.3][1.3.3t]
53
+ - COVERAGE: 93.25% -- 6538/7011 lines in 37 files
54
+ - BRANCH COVERAGE: 77.76% -- 2660/3421 branches in 37 files
55
+ - 33.57% documented
56
+
57
+ ### Fixed
58
+
59
+ - Compare filesystem paths by canonical identity throughout family workflows.
60
+
33
61
  ## [1.3.2] - 2026-09-28
34
62
 
35
63
  - TAG: [v1.3.2][1.3.2t]
@@ -2819,7 +2847,11 @@ Please file a bug if you notice a violation of semantic versioning.
2819
2847
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
2820
2848
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
2821
2849
 
2822
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.2...HEAD
2850
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.4...HEAD
2851
+ [1.3.4]: https://github.com/kettle-dev/kettle-family/compare/v1.3.3...v1.3.4
2852
+ [1.3.4t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.4
2853
+ [1.3.3]: https://github.com/kettle-dev/kettle-family/compare/v1.3.2...v1.3.3
2854
+ [1.3.3t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.3
2823
2855
  [1.3.2]: https://github.com/kettle-dev/kettle-family/compare/v1.3.1...v1.3.2
2824
2856
  [1.3.2t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.2
2825
2857
  [1.3.1]: https://github.com/kettle-dev/kettle-family/compare/v1.3.0...v1.3.1
data/README.md CHANGED
@@ -510,6 +510,7 @@ The command names below are available from `kettle-family --help`:
510
510
  | `gha-sha-pins` | Inspect or update GitHub Actions SHA pins. |
511
511
  | `bup` | Run `bundle update --all`, or update a named dependency, at the family root and per member. |
512
512
  | `bupb` | Run `bundle update --bundler` at the family root and per member. |
513
+ | `update-bundler` | Install the latest stable Bundler from RubyGems, then update family lockfiles. |
513
514
  | `bex` | Run an arbitrary `bundle exec` command per member. |
514
515
  | `install` | Build and install selected local family gems. |
515
516
  | `bump` | Check, plan, or execute family version alignment. |
@@ -614,6 +615,11 @@ root-first order with `bundle update --bundler`. A root that is itself a member
614
615
  is updated only once. A gem name may be passed to `bup` to update only that
615
616
  dependency, for example `kettle-family bup --execute kettle-test`.
616
617
 
618
+ `update-bundler` installs the latest stable Bundler from `https://rubygems.org`
619
+ without enabling prereleases, then runs the same root-first lockfile update as
620
+ `bupb`. Family mise tasks select an installed stable Bundler explicitly to
621
+ start the command, so an installed prerelease cannot intercept startup.
622
+
617
623
  Executed member updates commit successful lockfile changes by default. The
618
624
  family-root lockfile is also validated and committed for monorepos, where the
619
625
  root and members share one Git repository. A sibling-repository family root is
@@ -1186,7 +1192,7 @@ Thanks for RTFM. ☺️
1186
1192
  [📌gitmoji]: https://gitmoji.dev
1187
1193
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1188
1194
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1189
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-7.010-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1195
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-7.046-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1190
1196
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
1191
1197
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1192
1198
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -55,7 +55,7 @@ module Kettle
55
55
  return false if left.to_s.empty? || right.to_s.empty?
56
56
  return false unless File.file?(left) && File.file?(right)
57
57
 
58
- File.realpath(left) == File.realpath(right)
58
+ Paths.same?(left, right)
59
59
  end
60
60
 
61
61
  def member_local_release_config_from_branch(member)
@@ -4,14 +4,15 @@ require "command_kit"
4
4
  require "command_kit/commands"
5
5
  require "fileutils"
6
6
  require "optparse"
7
+ require "rubygems/dependency_installer"
7
8
 
8
9
  module Kettle
9
10
  module Family
10
11
  class CLI < CommandKit::Command
11
12
  include CommandKit::Commands
12
13
 
13
- COMMANDS = %w[version mise-trust discover plan report metadata check clean-unreleased reconcile-releases reset test lint docs template gha-sha-pins bup bupb bex install bump bump-version add-changelog release push pull sync up branch-lanes release-state state].freeze
14
- WORKFLOW_COMMANDS = %w[check reset test lint docs template gha-sha-pins bup bupb bex release push pull sync up].freeze
14
+ COMMANDS = %w[version mise-trust discover plan report metadata check clean-unreleased reconcile-releases reset test lint docs template gha-sha-pins bup bupb update-bundler bex install bump bump-version add-changelog release push pull sync up branch-lanes release-state state].freeze
15
+ WORKFLOW_COMMANDS = %w[check reset test lint docs template gha-sha-pins bup bupb update-bundler bex release push pull sync up].freeze
15
16
 
16
17
  command_name "kettle-family"
17
18
  usage "[options] COMMAND [ARGS...]"
@@ -434,6 +435,31 @@ module Kettle
434
435
  description "Plan or execute bundle update --bundler."
435
436
  end
436
437
 
438
+ class UpdateBundler < WorkflowCommand
439
+ command_name "update-bundler"
440
+ usage "[options]"
441
+ description "Install the latest stable Bundler, then update family lockfiles."
442
+
443
+ def run(*args)
444
+ unexpected_arguments!(args)
445
+ install_latest_stable_bundler if truthy_option?(:execute)
446
+ run_family("update-bundler")
447
+ end
448
+
449
+ private
450
+
451
+ def install_latest_stable_bundler
452
+ original_sources = Gem.sources.to_a
453
+ Gem.sources = ["https://rubygems.org"]
454
+ Gem::DependencyInstaller.new(domain: :remote, prerelease: false, document: []).install("bundler")
455
+ Gem::Specification.reset
456
+ rescue => error
457
+ raise Error, "could not install latest stable Bundler from RubyGems: #{error.message}"
458
+ ensure
459
+ Gem.sources = original_sources if original_sources
460
+ end
461
+ end
462
+
437
463
  class Bex < WorkflowCommand
438
464
  command_name "bex"
439
465
  usage "[options] -- COMMAND [ARGS...]"
@@ -626,6 +652,7 @@ module Kettle
626
652
  command GhaShaPins
627
653
  command Bup
628
654
  command Bupb
655
+ command UpdateBundler
629
656
  command Bex
630
657
  command Install
631
658
  command Bump
@@ -23,6 +23,7 @@ module Kettle
23
23
  ].freeze
24
24
  TRANSFER_CHANGELOG_TOTAL_UNSET = Object.new.freeze
25
25
  CACHE_MISS = Object.new.freeze
26
+ WORKTREE_ADMIN_MUTEX = Mutex.new
26
27
 
27
28
  def initialize(members:, config: nil, jobs: nil)
28
29
  @members = members
@@ -57,21 +58,30 @@ module Kettle
57
58
  def branch_results(event_handler: nil)
58
59
  root = git_root
59
60
  selected_names = members.map(&:name)
60
- parallel_map(release_target_branches) do |branch|
61
- branch_results_for(branch: branch, root: root, selected_names: selected_names, event_handler: event_handler)
62
- end.flatten
61
+ with_branch_worktrees(root: root, branches: release_target_branches) do |entries|
62
+ parallel_map(entries) do |entry|
63
+ if entry[:error]
64
+ error_result(branch: entry[:branch], error: entry[:error])
65
+ else
66
+ branch_results_for_worktree(
67
+ branch: entry[:branch],
68
+ worktree_root: entry[:worktree_root],
69
+ selected_names: selected_names,
70
+ event_handler: event_handler
71
+ )
72
+ end
73
+ end.flatten
74
+ end
63
75
  end
64
76
 
65
- def branch_results_for(branch:, root:, selected_names:, event_handler:)
66
- with_branch_worktree(root: root, branch: branch) do |worktree_root|
67
- branch_members = discover_branch_members(worktree_root: worktree_root, selected_names: selected_names)
68
- if shared_changelog?
69
- results = parallel_map(branch_members) { |member| check_shared_changelog_member_or_local(member, branch: branch, event_handler: event_handler) }
70
- next normalize_shared_version_bump(results)
71
- end
72
-
73
- parallel_map(branch_members) { |member| check_member(member, branch: branch, event_handler: event_handler) }
77
+ def branch_results_for_worktree(branch:, worktree_root:, selected_names:, event_handler:)
78
+ branch_members = discover_branch_members(worktree_root: worktree_root, selected_names: selected_names)
79
+ if shared_changelog?
80
+ results = parallel_map(branch_members) { |member| check_shared_changelog_member_or_local(member, branch: branch, event_handler: event_handler) }
81
+ return normalize_shared_version_bump(results)
74
82
  end
83
+
84
+ parallel_map(branch_members) { |member| check_member(member, branch: branch, event_handler: event_handler) }
75
85
  rescue Error => error
76
86
  [error_result(branch: branch, error: error)]
77
87
  end
@@ -826,25 +836,52 @@ module Kettle
826
836
  File.realpath(stdout.strip)
827
837
  end
828
838
 
829
- def with_branch_worktree(root:, branch:)
830
- base = File.join(root, "tmp", "kettle-family-release-state")
831
- FileUtils.mkdir_p(base)
832
- worktree_root = File.join(base, "worktree-#{Process.pid}-#{SecureRandom.hex(8)}")
833
- add_branch_worktree(root: root, branch: branch, worktree_root: worktree_root)
834
- yield worktree_root
835
- ensure
836
- remove_branch_worktree(root: root, worktree_root: worktree_root)
839
+ def with_branch_worktrees(root:, branches:)
840
+ with_worktree_registry_lock(root: root) do
841
+ base = File.join(root, "tmp", "kettle-family-release-state")
842
+ FileUtils.mkdir_p(base)
843
+ entries = branches.map do |branch|
844
+ {branch: branch, worktree_root: File.join(base, "worktree-#{Process.pid}-#{SecureRandom.hex(8)}")}
845
+ end
846
+ entries.each do |entry|
847
+ add_branch_worktree(root: root, branch: entry[:branch], worktree_root: entry[:worktree_root])
848
+ rescue Error => error
849
+ entry[:error] = error
850
+ end
851
+ yield entries
852
+ ensure
853
+ entries&.reverse_each do |entry|
854
+ remove_branch_worktree(root: root, worktree_root: entry[:worktree_root])
855
+ end
856
+ end
857
+ end
858
+
859
+ def with_worktree_registry_lock(root:)
860
+ stdout, stderr, status = Open3.capture3("git", "rev-parse", "--git-common-dir", chdir: root)
861
+ raise Error, "could not determine git common directory for #{root}: #{stderr}" unless status.success?
862
+
863
+ lock_path = File.join(File.expand_path(stdout.strip, root), "kettle-family-worktree.lock")
864
+ File.open(lock_path, File::RDWR | File::CREAT, 0o600) do |lock_file|
865
+ lock_file.flock(File::LOCK_EX)
866
+ yield
867
+ ensure
868
+ lock_file&.flock(File::LOCK_UN)
869
+ end
837
870
  end
838
871
 
839
872
  def add_branch_worktree(root:, branch:, worktree_root:)
840
- _stdout, stderr, status = Open3.capture3("git", "worktree", "add", "--detach", worktree_root, branch, chdir: root)
841
- raise Error, "could not add worktree for #{branch}: #{stderr}" unless status.success?
873
+ WORKTREE_ADMIN_MUTEX.synchronize do
874
+ _stdout, stderr, status = Open3.capture3("git", "worktree", "add", "--detach", worktree_root, branch, chdir: root)
875
+ raise Error, "could not add worktree for #{branch}: #{stderr}" unless status.success?
876
+ end
842
877
  end
843
878
 
844
879
  def remove_branch_worktree(root:, worktree_root:)
845
880
  return unless worktree_root && Dir.exist?(worktree_root)
846
881
 
847
- Open3.capture3("git", "worktree", "remove", "--force", worktree_root, chdir: root)
882
+ WORKTREE_ADMIN_MUTEX.synchronize do
883
+ Open3.capture3("git", "worktree", "remove", "--force", worktree_root, chdir: root)
884
+ end
848
885
  end
849
886
 
850
887
  def discover_branch_members(worktree_root:, selected_names:)
@@ -29,6 +29,7 @@ module Kettle
29
29
  template
30
30
  test
31
31
  up
32
+ update-bundler
32
33
  ].freeze
33
34
 
34
35
  attr_reader :family_name, :family_mode, :order_mode, :members, :selected_members, :config_path, :command, :results, :branch_lanes, :release_target_branches, :member_release_target_branches, :release_mode, :release_resume_arguments, :warnings, :event_log_dirs
@@ -5,7 +5,7 @@ module Kettle
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = "1.3.2"
8
+ VERSION = "1.3.4"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -574,7 +574,7 @@ module Kettle
574
574
 
575
575
  runner = CommandRunner.new(execute: execute, accept: accept)
576
576
  results = []
577
- update_family_root_bundle(runner: runner, memo: results) if %w[bup bupb].include?(command)
577
+ update_family_root_bundle(runner: runner, memo: results) if %w[bup bupb update-bundler].include?(command)
578
578
  return results unless results.all?(&:ok?)
579
579
 
580
580
  if command == "gha-sha-pins" && execute
@@ -607,10 +607,10 @@ module Kettle
607
607
 
608
608
  normalize_lockfiles(member: member, runner: runner, memo: memo, phase: "normalize_lockfiles") if command == "template"
609
609
  commit_gha_sha_pins(member: member, runner: runner, memo: memo) if command == "gha-sha-pins"
610
- if command == "bupb"
610
+ if %w[bupb update-bundler].include?(command)
611
611
  bupb_appraisal_results(member: member, runner: runner, memo: memo)
612
612
  end
613
- if %w[bup bupb].include?(command) && memo.last&.ok? && validate_bundle_update_lockfile(member: member, memo: memo)
613
+ if %w[bup bupb update-bundler].include?(command) && memo.last&.ok? && validate_bundle_update_lockfile(member: member, memo: memo)
614
614
  commit_bundle_update(member: member, runner: runner, memo: memo)
615
615
  end
616
616
  commit_bex_changes(member: member, runner: runner, memo: memo) if command == "bex"
@@ -644,7 +644,7 @@ module Kettle
644
644
  return false unless File.file?(File.join(config.root, "Gemfile"))
645
645
 
646
646
  family_members.none? do |member|
647
- File.expand_path(member.root) == File.expand_path(config.root)
647
+ Paths.same?(member.root, config.root)
648
648
  end
649
649
  end
650
650
 
@@ -1752,9 +1752,11 @@ module Kettle
1752
1752
  end
1753
1753
 
1754
1754
  def tag_template_worktree_results(results, entries)
1755
- branches_by_root = entries.to_h { |entry| [File.expand_path(entry.fetch(:member).root), entry.fetch(:branch)] }
1756
1755
  results.each do |result|
1757
- result.branch = branches_by_root[File.expand_path(result.workdir)] if result.workdir
1756
+ next unless result.workdir
1757
+
1758
+ entry = entries.find { |candidate| Paths.same?(candidate.fetch(:member).root, result.workdir) }
1759
+ result.branch = entry.fetch(:branch) if entry
1758
1760
  end
1759
1761
  end
1760
1762
 
@@ -4081,7 +4083,7 @@ module Kettle
4081
4083
  return gha_sha_pins_command(command_text: command_text)
4082
4084
  end
4083
4085
  return bup_command if command == "bup"
4084
- return bupb_command if command == "bupb"
4086
+ return bupb_command if %w[bupb update-bundler].include?(command)
4085
4087
  return bex_command if command == "bex"
4086
4088
 
4087
4089
  command_for(command)
@@ -4106,11 +4108,11 @@ module Kettle
4106
4108
 
4107
4109
  def stable_bundler_version
4108
4110
  active_version = Gem.loaded_specs["bundler"]&.version
4109
- return active_version if active_version && !active_version.prerelease?
4110
-
4111
4111
  Gem::Specification
4112
4112
  .find_all_by_name("bundler")
4113
4113
  .map(&:version)
4114
+ .push(active_version)
4115
+ .compact
4114
4116
  .reject(&:prerelease?)
4115
4117
  .max
4116
4118
  end
@@ -4348,15 +4350,15 @@ module Kettle
4348
4350
  return false if config.path
4349
4351
  return false unless config.family_mode == "monorepo"
4350
4352
  return false unless members.one?
4351
- return false unless File.expand_path(config.members_root) == File.expand_path(config.root)
4352
- return false unless File.expand_path(members.first.root) == File.expand_path(config.root)
4353
+ return false unless Paths.same?(config.members_root, config.root)
4354
+ return false unless Paths.same?(members.first.root, config.root)
4353
4355
 
4354
4356
  family_env_name = config.family_local_path_env_name
4355
4357
  family_env_name && !env_overrides.key?(family_env_name)
4356
4358
  end
4357
4359
 
4358
4360
  def command_env
4359
- return bundle_update_env if %w[bup bupb].include?(command)
4361
+ return bundle_update_env if %w[bup bupb update-bundler].include?(command)
4360
4362
 
4361
4363
  workflow_env
4362
4364
  end
@@ -5531,7 +5533,7 @@ module Kettle
5531
5533
  end
5532
5534
  return false unless spec&.source.is_a?(Bundler::Source::Path)
5533
5535
 
5534
- File.expand_path(spec.source.path.to_s) == File.expand_path(File.join(template_local_kettle_jem_root, "kettle-jem"))
5536
+ Paths.same?(spec.source.path.to_s, File.join(template_local_kettle_jem_root, "kettle-jem"))
5535
5537
  rescue Bundler::LockfileError, Errno::ENOENT
5536
5538
  false
5537
5539
  end
@@ -5696,7 +5698,7 @@ module Kettle
5696
5698
  end
5697
5699
 
5698
5700
  def family_local_path_root_for(family_root)
5699
- return config.family_local_path_root if File.expand_path(family_root) == File.expand_path(config.root)
5701
+ return config.family_local_path_root if Paths.same?(family_root, config.root)
5700
5702
 
5701
5703
  relative = Pathname.new(config.family_local_path_root).relative_path_from(Pathname.new(config.root))
5702
5704
  File.expand_path(relative, family_root)
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: 1.3.2
4
+ version: 1.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -94,7 +94,7 @@ dependencies:
94
94
  version: '3.1'
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
- version: 3.1.4
97
+ version: 3.1.5
98
98
  type: :runtime
99
99
  prerelease: false
100
100
  version_requirements: !ruby/object:Gem::Requirement
@@ -104,7 +104,7 @@ dependencies:
104
104
  version: '3.1'
105
105
  - - ">="
106
106
  - !ruby/object:Gem::Version
107
- version: 3.1.4
107
+ version: 3.1.5
108
108
  - !ruby/object:Gem::Dependency
109
109
  name: kettle-gha-pins
110
110
  requirement: !ruby/object:Gem::Requirement
@@ -459,10 +459,10 @@ licenses:
459
459
  - AGPL-3.0-only
460
460
  metadata:
461
461
  homepage_uri: https://kettle-family.galtzo.com
462
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.2
463
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.2/CHANGELOG.md
462
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.4
463
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.4/CHANGELOG.md
464
464
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
465
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.2
465
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.4
466
466
  funding_uri: https://github.com/sponsors/pboling
467
467
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
468
468
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file