kettle-family 1.2.79 → 1.2.80

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: de4d61b6ce08f508328ee4b060eb64c15e0d6fab02a2be03a09c0a373ee7f975
4
- data.tar.gz: e31c6e4f532a22f4fcb08606bc619986d83ef44f12302986ce087be914536cf4
3
+ metadata.gz: 6ccfe598bd7e6f842c4020a1763565a3a626ac61c6726f7a37652227494d6947
4
+ data.tar.gz: a49bf719e3d7bbf184baa6e9a2f3de8080a56af789dcc08b1b8df81ae074656c
5
5
  SHA512:
6
- metadata.gz: 84c2e9ac41a1d3da7cafb586952a9d2a3e218c774a1a71640fb9d1c334336b91d12a33b00bff6615911830e3bde06efb29bbe7fa9c9ae57291cdad324027b288
7
- data.tar.gz: bb2ccf29a236cb2e8e601a8f767dfb342347ca52a6d99446a03983011e1b5b7e2da628498abc34c47562c3f63de3940ec4aa30c15701ff10cc7e009dc51752db
6
+ metadata.gz: 4656fb34c6cffb5ebbd33c3052a62dec50a2c075e6abce72f2ff58d392d4918f1bf8cee49de268f09c2130ccad4bf2bde5c60537092e83de586e6d3ac42f6ed8
7
+ data.tar.gz: 0c681c8436b1246fcd0882c2dcd89a77255d6d77942ab29dfca35471f9a4f69f7b0efdfe09f2019fb55a6e2b0c775ac79c741b132c8124403aa026770c49d1e1
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,23 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.2.80] - 2026-09-01
34
+
35
+ - TAG: [v1.2.80][1.2.80t]
36
+ - COVERAGE: 93.26% -- 5662/6071 lines in 33 files
37
+ - BRANCH COVERAGE: 76.39% -- 2316/3032 branches in 33 files
38
+ - 30.69% documented
39
+
40
+ ### Added
41
+
42
+ - Template member target branches concurrently in isolated Git worktrees, while retaining the primary checkout for its current branch.
43
+
44
+ ### Fixed
45
+
46
+ - Keep non-targeted members in the dependency-aware parallel template scheduler when another family member has release target branches.
47
+
48
+ - Leave an unrelated primary checkout untouched while templating configured target branches in worktrees.
49
+
33
50
  ## [1.2.79] - 2026-09-01
34
51
 
35
52
  - TAG: [v1.2.79][1.2.79t]
@@ -2449,7 +2466,9 @@ Please file a bug if you notice a violation of semantic versioning.
2449
2466
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
2450
2467
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
2451
2468
 
2452
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.79...HEAD
2469
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.80...HEAD
2470
+ [1.2.80]: https://github.com/kettle-dev/kettle-family/compare/v1.2.79...v1.2.80
2471
+ [1.2.80t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.80
2453
2472
  [1.2.79]: https://github.com/kettle-dev/kettle-family/compare/v1.2.78...v1.2.79
2454
2473
  [1.2.79t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.79
2455
2474
  [1.2.78]: https://github.com/kettle-dev/kettle-family/compare/v1.2.77...v1.2.78
data/README.md CHANGED
@@ -1049,7 +1049,7 @@ Thanks for RTFM. ☺️
1049
1049
  [📌gitmoji]: https://gitmoji.dev
1050
1050
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1051
1051
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1052
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-5.974-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1052
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.071-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1053
1053
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
1054
1054
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1055
1055
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -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.2.79"
8
+ VERSION = "1.2.80"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -457,7 +457,7 @@ module Kettle
457
457
  end
458
458
 
459
459
  def template_sync_members_for(candidates)
460
- candidates.each_with_object([]) do |member, unique_members|
460
+ candidates.reject { |member| template_worktree_only_member?(member) }.each_with_object([]) do |member, unique_members|
461
461
  unique_members << member unless unique_members.any? { |candidate| git_root_for(candidate) == git_root_for(member) }
462
462
  end
463
463
  end
@@ -925,15 +925,181 @@ module Kettle
925
925
  def member_local_branch_target_results
926
926
  return release_member_local_branch_target_results if command == "release"
927
927
 
928
- members.each_with_object([]) do |member, memo|
928
+ target_members, current_branch_members = members.partition do |member|
929
+ member_local_release_config(member)
930
+ end
931
+ results = []
932
+ results = current_branch_results(current_branch_members) unless current_branch_members.empty?
933
+ return results unless results.all?(&:ok?)
934
+
935
+ target_members.each do |member|
929
936
  member_config = member_local_release_config(member)
930
- if member_config
931
- memo.concat(member_local_workflow(member: member, member_config: member_config).results)
937
+ if template_branch_worktrees?(member_config)
938
+ results.concat(template_branch_worktree_results(member: member, member_config: member_config))
932
939
  else
933
- memo.concat(current_branch_results([member]))
940
+ results.concat(member_local_workflow(member: member, member_config: member_config).results)
934
941
  end
935
- break memo unless memo.last&.ok?
942
+ break unless results.last&.ok?
936
943
  end
944
+ results
945
+ end
946
+
947
+ # Release target branches are independent checkouts. Templating them in
948
+ # linked worktrees avoids repeatedly switching the primary checkout and
949
+ # allows the branch stack to use the requested template worker budget.
950
+ def template_branch_worktree_results(member:, member_config:)
951
+ branches = BranchTargetConfig.branch_targets_for(command, member_config.release_target_branches)
952
+ return [] if branches.empty?
953
+
954
+ entries = []
955
+ outcomes = []
956
+ runner = command_runner
957
+ fetch = runner.call(member: member, phase: "template_branch_fetch", command: %w[git fetch --all --prune])
958
+ outcomes << fetch
959
+ if fetch.ok?
960
+ current_branch = template_current_branch(member)
961
+ if current_branch.is_a?(CommandResult)
962
+ outcomes << current_branch
963
+ else
964
+ branches.each do |branch|
965
+ if branch == current_branch
966
+ entries << {branch: branch, member: member, worktree_root: nil}
967
+ next
968
+ end
969
+
970
+ worktree_root = template_branch_worktree_path(member, branch)
971
+ add = runner.call(
972
+ member: member,
973
+ phase: "template_worktree_add",
974
+ command: ["git", "worktree", "add", worktree_root, branch]
975
+ )
976
+ add.branch = branch
977
+ outcomes << add
978
+ break unless add.ok?
979
+
980
+ entries << {branch: branch, member: relocate_member_to_worktree(member, worktree_root), worktree_root: worktree_root}
981
+ end
982
+
983
+ if outcomes.all?(&:ok?)
984
+ emit_template_event_line(member, ">", "branch worktrees #{entries.length} branches, #{template_jobs(entries)} jobs")
985
+ outcomes.concat(template_branch_worktree_entries_results(entries))
986
+ end
987
+ end
988
+ end
989
+ outcomes
990
+ ensure
991
+ outcomes&.concat(template_branch_worktree_cleanup_results(entries, runner: runner, control_member: member))
992
+ end
993
+
994
+ def template_branch_worktrees?(member_config)
995
+ command == "template" && execute && !member_config&.release_target_branches.to_a.empty?
996
+ end
997
+
998
+ def template_worktree_only_member?(member)
999
+ member_config = member_local_release_config(member)
1000
+ return false unless template_branch_worktrees?(member_config)
1001
+
1002
+ current_branch = template_current_branch(member)
1003
+ return false if current_branch.is_a?(CommandResult)
1004
+
1005
+ !member_config.release_target_branches.include?(current_branch)
1006
+ end
1007
+
1008
+ def template_current_branch(member)
1009
+ stdout, stderr, status = Open3.capture3("git", "branch", "--show-current", chdir: member.root)
1010
+ branch = stdout.strip
1011
+ return branch if status.success? && !branch.empty?
1012
+
1013
+ template_branch_failure_result(member, "could not determine current branch: #{stderr}")
1014
+ end
1015
+
1016
+ def template_branch_worktree_path(member, branch)
1017
+ safe_member = member.name.to_s.gsub(/[^A-Za-z0-9_.-]+/, "_")
1018
+ safe_branch = branch.to_s.gsub(/[^A-Za-z0-9_.-]+/, "_")
1019
+ File.join(config.root, "tmp", "kettle-family", "template-worktrees", safe_member, "#{Process.pid}-#{safe_branch}")
1020
+ end
1021
+
1022
+ def relocate_member_to_worktree(member, worktree_root)
1023
+ relocate = lambda do |path|
1024
+ next unless path
1025
+
1026
+ relative = Pathname.new(path).relative_path_from(Pathname.new(member.root))
1027
+ File.join(worktree_root, relative)
1028
+ end
1029
+ member.dup.tap do |worktree_member|
1030
+ worktree_member.root = worktree_root
1031
+ worktree_member.gemspec_path = relocate.call(member.gemspec_path)
1032
+ worktree_member.version_file = relocate.call(member.version_file)
1033
+ end
1034
+ end
1035
+
1036
+ def template_branch_worktree_entries_results(entries)
1037
+ queue = Queue.new
1038
+ entries.each_with_index { |entry, index| queue << [index, entry] }
1039
+ ordered_results = Array.new(entries.length)
1040
+ mutex = Mutex.new
1041
+ stop = false
1042
+ Array.new(template_jobs(entries)) do
1043
+ Thread.new do # rubocop:disable ThreadSafety/NewThread -- independent branch worktrees are safe to template concurrently.
1044
+ loop do
1045
+ break if mutex.synchronize { stop }
1046
+ index, entry = queue.pop(true)
1047
+ branch_results = template_branch_worktree_entry_results(entry)
1048
+ mutex.synchronize do
1049
+ ordered_results[index] = branch_results
1050
+ stop = true unless branch_results.all?(&:ok?)
1051
+ end
1052
+ rescue ThreadError
1053
+ break
1054
+ end
1055
+ end
1056
+ end.each(&:join)
1057
+ ordered_results.compact.flatten
1058
+ end
1059
+
1060
+ def template_branch_worktree_entry_results(entry)
1061
+ member = entry.fetch(:member)
1062
+ branch = entry.fetch(:branch)
1063
+ runner = command_runner
1064
+ results = []
1065
+ if git_upstream_for(member)
1066
+ results << runner.call(member: member, phase: "template_branch_sync", command: ["git", "rebase", "@{upstream}"])
1067
+ end
1068
+ results.concat(template_results_for_member(member)) if results.all?(&:ok?)
1069
+ tag_branch_results(results, branch)
1070
+ emit_template_event_line(member, results.all?(&:ok?) ? "." : "F", "branch #{branch}")
1071
+ results
1072
+ end
1073
+
1074
+ def template_branch_worktree_cleanup_results(entries, runner:, control_member:)
1075
+ entries.filter_map do |entry|
1076
+ worktree_root = entry[:worktree_root]
1077
+ next unless worktree_root && Dir.exist?(worktree_root)
1078
+
1079
+ result = runner.call(
1080
+ member: control_member,
1081
+ phase: "template_worktree_remove",
1082
+ command: ["git", "worktree", "remove", "--force", worktree_root]
1083
+ )
1084
+ result.branch = entry.fetch(:branch)
1085
+ result
1086
+ end
1087
+ end
1088
+
1089
+ def template_branch_failure_result(member, message)
1090
+ CommandResult.new(
1091
+ member_name: member.name,
1092
+ phase: "template_worktree",
1093
+ command: ["internal", "template-worktree"],
1094
+ workdir: member.root,
1095
+ status: 1,
1096
+ success: false,
1097
+ stdout: "",
1098
+ stderr: message,
1099
+ elapsed_seconds: 0.0,
1100
+ skipped: false,
1101
+ reason: "template branch worktree failed"
1102
+ )
937
1103
  end
938
1104
 
939
1105
  def branch_generated_lockfile_cleanup_required?
@@ -1150,7 +1316,9 @@ module Kettle
1150
1316
  end
1151
1317
 
1152
1318
  def branch_checkout_preflight_members
1153
- members_with_targets = members.select { |member| member_local_release_config(member) }
1319
+ members_with_targets = members.select do |member|
1320
+ member_local_release_config(member) && !template_worktree_only_member?(member)
1321
+ end
1154
1322
  members_with_targets = [family_member] if !config.release_target_branches.empty?
1155
1323
  members_with_targets
1156
1324
  end
@@ -3793,7 +3961,7 @@ module Kettle
3793
3961
  end
3794
3962
 
3795
3963
  def emit_template_event_line(member, mark, label)
3796
- return if label.to_s.empty?
3964
+ return if label.to_s.empty? || !progress_io
3797
3965
 
3798
3966
  synchronize_template_progress do
3799
3967
  progress_io.puts("[#{member.name}] #{mark} #{label}")
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.2.79
4
+ version: 1.2.80
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -441,10 +441,10 @@ licenses:
441
441
  - AGPL-3.0-only
442
442
  metadata:
443
443
  homepage_uri: https://kettle-family.galtzo.com
444
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.79
445
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.79/CHANGELOG.md
444
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.80
445
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.80/CHANGELOG.md
446
446
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
447
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.79
447
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.80
448
448
  funding_uri: https://github.com/sponsors/pboling
449
449
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
450
450
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file