kettle-family 1.2.94 → 1.2.96

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: 60682b3f2d2f1017624cc67b428ff954b71f03eca4fdb6034cbe229183cba970
4
- data.tar.gz: ea873857902fa44d2b2051e6449f21d84515139f005e1a9c148dfedf5062ce2f
3
+ metadata.gz: d2f776638603414ab6c27f457473dea3f6088bae63f58a45fcbc0f165fe0e026
4
+ data.tar.gz: b44e74efb8e840e929df24573b815df7b7a749f27e67f86f76561ab654b406ea
5
5
  SHA512:
6
- metadata.gz: a14c847688ce762dbf03be0229b1c5ac0da23d3892e2e803467b8dc6efe73df4ede2b43d64a6ffedbccaf38f11da162bfad6bbf574a44b29739dafe8b8d70cba
7
- data.tar.gz: 8ad7d29e783dace84ef807339b81907fe9e6e7c7f35ee60c4ac95b412a0972983c6eb7d9e8dad708801b270eda6ee0bb87d8759a22e3d4e29d6f254d4bfa6ff3
6
+ metadata.gz: 77f9c29784b453d8d270398c675ed52e18d8ba5f374fc8add80465bce88f59a679ab254080e3fe349685bff16134bb97541ef087d5c32116123c95e443b64685
7
+ data.tar.gz: 4e750df4e8f1f7f92ee8a653bac8ed02a95ee7ddfa5daa8f8eef92003b78e2c58c93b41498b0b93f8649626dcaf3a645f36fdedabf78928e35653131f14431c3
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.2.96] - 2026-09-08
34
+
35
+ - TAG: [v1.2.96][1.2.96t]
36
+ - COVERAGE: 93.49% -- 6235/6669 lines in 35 files
37
+ - BRANCH COVERAGE: 77.99% -- 2523/3235 branches in 35 files
38
+ - 32.75% documented
39
+
40
+ ### Fixed
41
+
42
+ - Render branch-worktree template events in distinct live progress rows.
43
+
44
+ - Run release-state branch targets concurrently and render distinct branch progress rows.
45
+
46
+ - Preserve configured release waves for ordinary sibling members when another member has release-target branches.
47
+
48
+ - Restore the version_gem runtime dependency and preserve worktree integration documentation during template updates.
49
+
50
+ ## [1.2.95] - 2026-09-07
51
+
52
+ - TAG: [v1.2.95][1.2.95t]
53
+ - COVERAGE: 93.49% -- 6199/6631 lines in 35 files
54
+ - BRANCH COVERAGE: 78.03% -- 2515/3223 branches in 35 files
55
+ - 32.58% documented
56
+
57
+ ### Fixed
58
+
59
+ - Run release-target branch worktrees through the full template bootstrap workflow before member preparation.
60
+
33
61
  ## [1.2.94] - 2026-09-07
34
62
 
35
63
  - TAG: [v1.2.94][1.2.94t]
@@ -2640,7 +2668,11 @@ Please file a bug if you notice a violation of semantic versioning.
2640
2668
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
2641
2669
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
2642
2670
 
2643
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.94...HEAD
2671
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.96...HEAD
2672
+ [1.2.96]: https://github.com/kettle-dev/kettle-family/compare/v1.2.95...v1.2.96
2673
+ [1.2.96t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.96
2674
+ [1.2.95]: https://github.com/kettle-dev/kettle-family/compare/v1.2.94...v1.2.95
2675
+ [1.2.95t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.95
2644
2676
  [1.2.94]: https://github.com/kettle-dev/kettle-family/compare/v1.2.93...v1.2.94
2645
2677
  [1.2.94t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.94
2646
2678
  [1.2.93]: https://github.com/kettle-dev/kettle-family/compare/v1.2.92...v1.2.93
data/README.md CHANGED
@@ -902,6 +902,23 @@ the template dependency graph. It must not invoke release-lockfile reset logic:
902
902
  release normalization intentionally disables local paths, while template
903
903
  recovery needs configured sibling paths to resolve unreleased family versions.
904
904
 
905
+ ### Branch Worktree Equivalence
906
+
907
+ A release-target branch worktree is a separate checkout of the same template
908
+ member, not a reduced template workflow. Before its member template body runs,
909
+ it executes the same debugger, Appraisal, and Nomono bootstrap boundary as an
910
+ ordinary member. It then executes the same lockfile preparation, dependency
911
+ preparation and recovery, `kettle-jem` invocation, lockfile normalization, and
912
+ template commit path with the same `template_local` environment.
913
+
914
+ Only checkout mechanics differ: Family creates and trusts the linked worktree,
915
+ rebases that target branch on its own upstream when one exists, attaches the
916
+ branch name to the results, and removes the worktree after completion. These
917
+ steps are necessary because each target has a distinct Git ref and an absolute
918
+ `mise` configuration path. They do not change the dependency graph or omit a
919
+ template phase. Regression specs assert that branch worktrees call the shared
920
+ bootstrap boundary and cannot start a member template body when it fails.
921
+
905
922
  ## 🔐 Security
906
923
 
907
924
  See [SECURITY.md][🔐security].
@@ -1154,7 +1171,7 @@ Thanks for RTFM. ☺️
1154
1171
  [📌gitmoji]: https://gitmoji.dev
1155
1172
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1156
1173
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1157
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.613-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1174
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.669-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1158
1175
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
1159
1176
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1160
1177
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -709,7 +709,7 @@ module Kettle
709
709
  display_selected_members = display_members_for(command: command, config: config, members: selected, selected_members: selected)
710
710
  print_execution_intent(command: command, config: config, members: display_selected_members, options: options, start_at: start_at)
711
711
  started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
712
- state_progress = release_state_progress(command: command, members: result_members, options: options)
712
+ state_progress = release_state_progress(command: command, config: config, members: result_members, options: options)
713
713
  state_progress&.start
714
714
  state_event_handler = release_state_event_handler(
715
715
  event_tape: state_event_tape,
@@ -901,21 +901,36 @@ module Kettle
901
901
  stdout
902
902
  end
903
903
 
904
- def release_state_progress(command:, members:, options:)
904
+ def release_state_progress(command:, config:, members:, options:)
905
905
  return nil unless command == "release-state"
906
906
  return nil if options[:events] || options[:json]
907
907
  return nil unless stdout.respond_to?(:tty?) && stdout.tty?
908
908
 
909
+ targets = release_state_progress_targets(config: config, members: members)
910
+
909
911
  WorkflowProgress.new(
910
912
  io: stdout,
911
913
  label: "release state",
912
- total: members.length,
913
- jobs: Concurrency.wave_jobs(requested: options[:jobs], item_count: members.length),
914
- members: members,
915
- heading: "release state #{members.length} member#{"s" unless members.length == 1}:"
914
+ total: targets.length,
915
+ jobs: Concurrency.wave_jobs(requested: options[:jobs], item_count: targets.length),
916
+ members: targets.map { |target| target.fetch(:progress_member) },
917
+ heading: "release state #{targets.length} target#{"s" unless targets.length == 1}:"
916
918
  )
917
919
  end
918
920
 
921
+ def release_state_progress_targets(config:, members:)
922
+ members.flat_map do |member|
923
+ member_config = member_release_config(member: member, config: config)
924
+ branches = member_config&.release_target_branches.to_a
925
+ next [{member: member, key: member.name, label: member.name, progress_member: member}] if branches.empty?
926
+
927
+ branches.map do |branch|
928
+ label = "#{member.name}@#{branch}"
929
+ {member: member, key: label, label: label, progress_member: member.dup.tap { |copy| copy.name = label }}
930
+ end
931
+ end
932
+ end
933
+
919
934
  def release_state_event_handler(event_tape:, progress:, members:)
920
935
  return nil unless event_tape || progress
921
936
 
@@ -927,18 +942,21 @@ module Kettle
927
942
  member = members_by_name[event["member"]]
928
943
  next unless member
929
944
 
945
+ branch = event["branch"].to_s
946
+ progress_key = branch.empty? ? member.name : "#{member.name}@#{branch}"
947
+
930
948
  action = event["action"].to_s
931
949
  status = event["status"].to_s
932
950
  case action
933
951
  when "member_start"
934
- progress.start_member(member, total: 5, status: "release_state")
952
+ progress.start_member(member, total: 5, status: "release_state", key: progress_key, label: progress_key)
935
953
  when "member_complete"
936
- progress.finish_member(member, success: status == "ok", status: status)
954
+ progress.finish_member(member, success: status == "ok", status: status, key: progress_key, label: progress_key)
937
955
  when "changelog_command", "computed_booleans", "git_state", "github_release", "transfer_changelog"
938
956
  if status == "running"
939
- progress.update(member, status: action)
957
+ progress.update(member, status: action, key: progress_key, label: progress_key)
940
958
  elsif status == "ok" || status == "failed"
941
- progress.advance(member, status: action, success: status == "ok")
959
+ progress.advance(member, status: action, success: status == "ok", key: progress_key, label: progress_key)
942
960
  end
943
961
  end
944
962
  end
@@ -5,6 +5,11 @@ require "open3"
5
5
  module Kettle
6
6
  module Family
7
7
  class Discovery
8
+ # Dir.chdir changes process-global state. Legacy gemspecs require this
9
+ # evaluation context, so serialize only that boundary; discovery callers
10
+ # may still resolve separate members and worktrees concurrently.
11
+ GEMSPEC_EVALUATION_MUTEX = Mutex.new
12
+
8
13
  def initialize(config:, release_dependency_member_names: nil)
9
14
  @config = config
10
15
  @release_dependency_member_names = release_dependency_member_names&.map(&:to_s)
@@ -174,9 +179,11 @@ module Kettle
174
179
  # evaluates gemspecs relative to the current process directory.
175
180
  # Gem::Specification.load caches by path, which is wrong for branch-stack
176
181
  # workflows that checkout different contents at the same path.
177
- # rubocop:disable ThreadSafety/DirChdir
178
- spec = Dir.chdir(File.dirname(path)) { eval_gemspec(path) }
179
- # rubocop:enable ThreadSafety/DirChdir
182
+ spec = GEMSPEC_EVALUATION_MUTEX.synchronize do
183
+ # rubocop:disable ThreadSafety/DirChdir
184
+ Dir.chdir(File.dirname(path)) { eval_gemspec(path) }
185
+ # rubocop:enable ThreadSafety/DirChdir
186
+ end
180
187
  raise Error, "could not load gemspec #{path}" unless spec
181
188
 
182
189
  spec
@@ -56,20 +56,23 @@ module Kettle
56
56
  def branch_results(event_handler: nil)
57
57
  root = git_root
58
58
  selected_names = members.map(&:name)
59
- release_target_branches.each_with_object([]) do |branch, memo|
60
- with_branch_worktree(root: root, branch: branch) do |worktree_root|
61
- branch_members = discover_branch_members(worktree_root: worktree_root, selected_names: selected_names)
62
- if shared_changelog?
63
- results = parallel_map(branch_members) { |member| check_shared_changelog_member_or_local(member, branch: branch, event_handler: event_handler) }
64
- memo.concat(normalize_shared_version_bump(results))
65
- next
66
- end
59
+ parallel_map(release_target_branches) do |branch|
60
+ branch_results_for(branch: branch, root: root, selected_names: selected_names, event_handler: event_handler)
61
+ end.flatten
62
+ end
67
63
 
68
- memo.concat(parallel_map(branch_members) { |member| check_member(member, branch: branch, event_handler: event_handler) })
64
+ def branch_results_for(branch:, root:, selected_names:, event_handler:)
65
+ with_branch_worktree(root: root, branch: branch) do |worktree_root|
66
+ branch_members = discover_branch_members(worktree_root: worktree_root, selected_names: selected_names)
67
+ if shared_changelog?
68
+ results = parallel_map(branch_members) { |member| check_shared_changelog_member_or_local(member, branch: branch, event_handler: event_handler) }
69
+ next normalize_shared_version_bump(results)
69
70
  end
70
- rescue Error => error
71
- memo << error_result(branch: branch, error: error)
71
+
72
+ parallel_map(branch_members) { |member| check_member(member, branch: branch, event_handler: event_handler) }
72
73
  end
74
+ rescue Error => error
75
+ [error_result(branch: branch, error: error)]
73
76
  end
74
77
 
75
78
  def check_member(member, branch: nil, event_handler: nil)
@@ -27,12 +27,12 @@ module Kettle
27
27
  "timestamp" => @wall_clock.call,
28
28
  "elapsed_seconds" => @clock.call - @started_at
29
29
  )
30
- member = payload.fetch("member", "family").to_s
30
+ target = target_key(payload)
31
31
  @mutex.synchronize do
32
- @sequence_by_member[member] += 1
33
- payload["sequence"] = @sequence_by_member.fetch(member)
32
+ @sequence_by_member[target] += 1
33
+ payload["sequence"] = @sequence_by_member.fetch(target)
34
34
  line = JSON.generate(payload)
35
- File.open(path_for(member), "a") { |file| file.puts(line) }
35
+ File.open(path_for(target), "a") { |file| file.puts(line) }
36
36
  @stream&.puts(line)
37
37
  @stream.flush if @stream&.respond_to?(:flush)
38
38
  end
@@ -43,8 +43,14 @@ module Kettle
43
43
 
44
44
  private
45
45
 
46
- def path_for(member)
47
- filename = member.gsub(/[^A-Za-z0-9_.-]+/, "_")
46
+ def target_key(payload)
47
+ member = payload.fetch("member", "family").to_s
48
+ branch = payload["branch"].to_s
49
+ branch.empty? ? member : "#{member}@#{branch}"
50
+ end
51
+
52
+ def path_for(target)
53
+ filename = target.gsub(/[^A-Za-z0-9_.-]+/, "_")
48
54
  File.join(@directory, "#{filename}.ndjson")
49
55
  end
50
56
  end
@@ -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.94"
8
+ VERSION = "1.2.96"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -932,14 +932,8 @@ module Kettle
932
932
  end
933
933
 
934
934
  def template_member_workflow_results(workflow_members)
935
- debugger_results = template_debugger_bootstrap_results(workflow_members)
936
- return debugger_results unless debugger_results.all?(&:ok?)
937
-
938
- appraisal_results = template_appraisal_bootstrap_results(workflow_members)
939
- return debugger_results + appraisal_results unless appraisal_results.all?(&:ok?)
940
-
941
- bootstrap_results = template_bootstrap_dependency_results(workflow_members)
942
- return debugger_results + appraisal_results + bootstrap_results unless bootstrap_results.all?(&:ok?)
935
+ bootstrap_results = template_member_bootstrap_results(workflow_members)
936
+ return bootstrap_results unless bootstrap_results.all?(&:ok?)
943
937
 
944
938
  template_progress = start_template_progress(workflow_members)
945
939
  results = template_dependency_waves(workflow_members).each_with_object([]) do |wave, memo|
@@ -948,7 +942,22 @@ module Kettle
948
942
  break memo unless wave_results.all?(&:ok?)
949
943
  end
950
944
  emit_template_progress_summary(results, progress: template_progress)
951
- debugger_results + appraisal_results + bootstrap_results + results
945
+ bootstrap_results + results
946
+ end
947
+
948
+ # This is the common preparation boundary for every template member,
949
+ # regardless of whether it runs in its primary checkout, a monorepo
950
+ # worktree, or a release-target branch worktree. Keep branch mechanics
951
+ # outside this method so they cannot silently omit a bootstrap phase.
952
+ def template_member_bootstrap_results(workflow_members)
953
+ debugger_results = template_debugger_bootstrap_results(workflow_members)
954
+ return debugger_results unless debugger_results.all?(&:ok?)
955
+
956
+ appraisal_results = template_appraisal_bootstrap_results(workflow_members)
957
+ return debugger_results + appraisal_results unless appraisal_results.all?(&:ok?)
958
+
959
+ bootstrap_results = template_bootstrap_dependency_results(workflow_members)
960
+ debugger_results + appraisal_results + bootstrap_results
952
961
  end
953
962
 
954
963
  def template_debugger_bootstrap_results(workflow_members)
@@ -1402,13 +1411,20 @@ module Kettle
1402
1411
  )
1403
1412
  end
1404
1413
 
1405
- def template_results_for_member(member, progress: nil, commit_changes: true, wave_jobs: 1)
1406
- progress&.start_member(member, total: template_phase_total(member), status: template_initial_status(member))
1414
+ def template_results_for_member(member, progress: nil, progress_key: nil, progress_label: nil, commit_changes: true, wave_jobs: 1)
1415
+ progress_key ||= member.name
1416
+ progress_label ||= member.name
1417
+ progress&.start_member(
1418
+ member,
1419
+ total: template_phase_total(member),
1420
+ status: template_initial_status(member),
1421
+ **template_progress_identity_options(member, progress_key, progress_label)
1422
+ )
1407
1423
  runner = CommandRunner.new(execute: execute, accept: accept)
1408
1424
  [].tap do |memo|
1409
1425
  if config.normalize_lockfiles?
1410
1426
  normalize_lockfiles(member: member, runner: runner, memo: memo, phase: "prepare_lockfiles", commit_changes: commit_changes)
1411
- emit_member_result_progress(member, memo.last, progress: progress)
1427
+ emit_member_result_progress(member, memo.last, progress: progress, progress_key: progress_key, progress_label: progress_label)
1412
1428
  return memo unless memo.last.ok?
1413
1429
  end
1414
1430
 
@@ -1419,7 +1435,7 @@ module Kettle
1419
1435
  commit_changes: commit_changes,
1420
1436
  wave_jobs: wave_jobs
1421
1437
  )
1422
- emit_member_result_progress(member, memo.last, progress: progress) if memo.last&.phase == "prepare_template_dependencies"
1438
+ emit_member_result_progress(member, memo.last, progress: progress, progress_key: progress_key, progress_label: progress_label) if memo.last&.phase == "prepare_template_dependencies"
1423
1439
  return memo if prepared == false
1424
1440
 
1425
1441
  memo << runner.call(
@@ -1427,22 +1443,23 @@ module Kettle
1427
1443
  phase: command,
1428
1444
  command: workflow_command(member),
1429
1445
  env: template_command_env(wave_jobs: wave_jobs),
1430
- stdout_line_handler: template_event_line_handler(member, progress: progress)
1446
+ stdout_line_handler: template_event_line_handler(member, progress: progress, progress_key: progress_key, progress_label: progress_label)
1431
1447
  )
1432
- emit_member_result_progress(member, memo.last, progress: progress)
1448
+ emit_member_result_progress(member, memo.last, progress: progress, progress_key: progress_key, progress_label: progress_label)
1433
1449
  return memo unless memo.last.ok?
1434
1450
 
1435
1451
  normalize_lockfiles(member: member, runner: runner, memo: memo, phase: "normalize_lockfiles", commit_changes: commit_changes)
1436
- emit_member_result_progress(member, memo.last, progress: progress)
1452
+ emit_member_result_progress(member, memo.last, progress: progress, progress_key: progress_key, progress_label: progress_label)
1437
1453
  commit_template_changes(member: member, runner: runner, memo: memo) if commit_changes
1438
- emit_member_result_progress(member, memo.last, progress: progress) if memo.last&.phase == "commit_template"
1454
+ emit_member_result_progress(member, memo.last, progress: progress, progress_key: progress_key, progress_label: progress_label) if memo.last&.phase == "commit_template"
1439
1455
  ensure
1440
1456
  template_result = memo.find { |result| result.phase == "template" } || memo.last
1441
1457
  if template_result
1442
1458
  progress&.finish_member(
1443
1459
  member,
1444
1460
  success: memo.all?(&:ok?),
1445
- status: template_member_finish_status(template_result)
1461
+ status: template_member_finish_status(template_result),
1462
+ **template_progress_identity_options(member, progress_key, progress_label)
1446
1463
  )
1447
1464
  end
1448
1465
  end
@@ -1559,7 +1576,13 @@ module Kettle
1559
1576
 
1560
1577
  if outcomes.all?(&:ok?)
1561
1578
  emit_template_event_line(member, ">", "branch worktrees #{entries.length} branches, #{template_jobs(entries)} jobs")
1562
- outcomes.concat(template_branch_worktree_entries_results(entries))
1579
+ progress = start_template_progress(
1580
+ entries.map { |entry| template_branch_progress_member(entry) },
1581
+ heading: "templating #{entries.length} branch worktree#{"s" unless entries.length == 1} with #{template_jobs(entries)} job#{"s" unless template_jobs(entries) == 1}:"
1582
+ )
1583
+ branch_results = template_branch_worktree_entries_results(entries, progress: progress)
1584
+ emit_template_progress_summary(branch_results, progress: progress)
1585
+ outcomes.concat(branch_results)
1563
1586
  end
1564
1587
  end
1565
1588
  end
@@ -1649,7 +1672,18 @@ module Kettle
1649
1672
  []
1650
1673
  end
1651
1674
 
1652
- def template_branch_worktree_entries_results(entries)
1675
+ def template_branch_worktree_entries_results(entries, progress: nil)
1676
+ results = template_branch_worktree_sync_results(entries)
1677
+ return results unless results.all?(&:ok?)
1678
+
1679
+ # A branch worktree is a different checkout, not a different template
1680
+ # profile. Run the exact same member bootstrap boundary used by normal
1681
+ # members before any branch begins its member template body.
1682
+ bootstrap_results = template_member_bootstrap_results(entries.map { |entry| entry.fetch(:member) })
1683
+ tag_template_worktree_results(bootstrap_results, entries)
1684
+ results.concat(bootstrap_results)
1685
+ return results unless bootstrap_results.all?(&:ok?)
1686
+
1653
1687
  wave_jobs = template_jobs(entries)
1654
1688
  queue = Queue.new
1655
1689
  entries.each_with_index { |entry, index| queue << [index, entry] }
@@ -1661,7 +1695,7 @@ module Kettle
1661
1695
  loop do
1662
1696
  break if mutex.synchronize { stop }
1663
1697
  index, entry = queue.pop(true)
1664
- branch_results = template_branch_worktree_entry_results(entry, wave_jobs: wave_jobs)
1698
+ branch_results = template_branch_worktree_entry_results(entry, progress: progress, wave_jobs: wave_jobs)
1665
1699
  mutex.synchronize do
1666
1700
  ordered_results[index] = branch_results
1667
1701
  stop = true unless branch_results.all?(&:ok?)
@@ -1671,23 +1705,59 @@ module Kettle
1671
1705
  end
1672
1706
  end
1673
1707
  end.each(&:join)
1674
- ordered_results.compact.flatten
1708
+ results + ordered_results.compact.flatten
1675
1709
  end
1676
1710
 
1677
- def template_branch_worktree_entry_results(entry, wave_jobs: 1)
1711
+ def template_branch_worktree_entry_results(entry, progress: nil, wave_jobs: 1)
1678
1712
  member = entry.fetch(:member)
1679
1713
  branch = entry.fetch(:branch)
1680
- runner = command_runner
1681
- results = []
1682
- if git_upstream_for(member)
1683
- results << runner.call(member: member, phase: "template_branch_sync", command: ["git", "rebase", "@{upstream}"])
1684
- end
1685
- results.concat(template_results_for_member(member, wave_jobs: wave_jobs)) if results.all?(&:ok?)
1714
+ # Bootstrap and upstream synchronization are deliberately performed
1715
+ # for the complete branch set before workers start. The member body is
1716
+ # the same method normal template waves invoke.
1717
+ results = template_results_for_member(
1718
+ member,
1719
+ progress: progress,
1720
+ progress_key: template_branch_progress_key(entry),
1721
+ progress_label: template_branch_progress_label(entry),
1722
+ wave_jobs: wave_jobs
1723
+ )
1686
1724
  tag_branch_results(results, branch)
1687
1725
  emit_template_event_line(member, results.all?(&:ok?) ? "." : "F", "branch #{branch}")
1688
1726
  results
1689
1727
  end
1690
1728
 
1729
+ def template_branch_progress_member(entry)
1730
+ entry.fetch(:member).dup.tap { |member| member.name = template_branch_progress_label(entry) }
1731
+ end
1732
+
1733
+ def template_branch_progress_key(entry)
1734
+ template_branch_progress_label(entry)
1735
+ end
1736
+
1737
+ def template_branch_progress_label(entry)
1738
+ "#{entry.fetch(:member).name}@#{entry.fetch(:branch)}"
1739
+ end
1740
+
1741
+ def template_branch_worktree_sync_results(entries)
1742
+ runner = command_runner
1743
+ entries.filter_map do |entry|
1744
+ member = entry.fetch(:member)
1745
+ upstream = git_upstream_for(member)
1746
+ next unless upstream
1747
+
1748
+ result = runner.call(member: member, phase: "template_branch_sync", command: ["git", "rebase", "@{upstream}"])
1749
+ result.branch = entry.fetch(:branch)
1750
+ result
1751
+ end
1752
+ end
1753
+
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
+ results.each do |result|
1757
+ result.branch = branches_by_root[File.expand_path(result.workdir)] if result.workdir
1758
+ end
1759
+ end
1760
+
1691
1761
  def template_branch_worktree_cleanup_results(entries, runner:, control_member:)
1692
1762
  entries.filter_map do |entry|
1693
1763
  worktree_root = entry[:worktree_root]
@@ -1964,18 +2034,21 @@ module Kettle
1964
2034
  end
1965
2035
 
1966
2036
  def release_member_local_branch_target_results
1967
- runner = command_runner
2037
+ target_members, current_branch_members = members.partition do |member|
2038
+ member_local_release_config(member)
2039
+ end
1968
2040
  results = []
1969
- append_family_changelog_result(runner: runner, memo: results) unless skip_changelog
2041
+
2042
+ # A member with release targets must not force unrelated sibling
2043
+ # repositories through one-at-a-time release workflows. Run those
2044
+ # members through the normal release scheduler first so configured
2045
+ # waves and their worker budget remain effective.
2046
+ results.concat(current_branch_results(current_branch_members)) unless current_branch_members.empty?
1970
2047
  return results unless results.all?(&:ok?)
1971
2048
 
1972
- members.each_with_object(results) do |member, memo|
2049
+ target_members.each_with_object(results) do |member, memo|
1973
2050
  member_config = member_local_release_config(member)
1974
- if member_config
1975
- memo.concat(member_local_workflow(member: member, member_config: member_config).results)
1976
- else
1977
- memo.concat(release_member_results([member], include_family_changelog: false))
1978
- end
2051
+ memo.concat(member_local_workflow(member: member, member_config: member_config).results)
1979
2052
  break memo unless memo.last&.ok?
1980
2053
  end
1981
2054
  end
@@ -4324,13 +4397,14 @@ module Kettle
4324
4397
  append_command_args(command_text, args)
4325
4398
  end
4326
4399
 
4327
- def start_template_progress(workflow_members)
4400
+ def start_template_progress(workflow_members, heading: nil)
4328
4401
  progress = WorkflowProgress.new(
4329
4402
  io: progress_io,
4330
4403
  label: "templating",
4331
4404
  total: workflow_members.length,
4332
4405
  jobs: template_jobs(workflow_members),
4333
- members: workflow_members
4406
+ members: workflow_members,
4407
+ heading: heading
4334
4408
  )
4335
4409
  progress.start
4336
4410
  progress
@@ -4434,10 +4508,16 @@ module Kettle
4434
4508
  kettle_jem_template_command?(command_text)
4435
4509
  end
4436
4510
 
4437
- def emit_member_result_progress(member, result, progress:)
4511
+ def emit_member_result_progress(member, result, progress:, progress_key: member.name, progress_label: member.name)
4438
4512
  return unless result
4439
4513
 
4440
- progress&.advance(member, status: result.phase, success: result.ok?, mark: command_result_progress_mark(result))
4514
+ progress&.advance(
4515
+ member,
4516
+ status: result.phase,
4517
+ success: result.ok?,
4518
+ mark: command_result_progress_mark(result),
4519
+ **template_progress_identity_options(member, progress_key, progress_label)
4520
+ )
4441
4521
  end
4442
4522
 
4443
4523
  def command_result_progress_mark(result)
@@ -4484,7 +4564,7 @@ module Kettle
4484
4564
  progress.summary("release summary: #{release_results.count(&:ok?)}/#{release_results.length} members ok")
4485
4565
  end
4486
4566
 
4487
- def template_event_line_handler(member, progress: nil)
4567
+ def template_event_line_handler(member, progress: nil, progress_key: member.name, progress_label: member.name)
4488
4568
  lambda do |line|
4489
4569
  event = parse_template_event(line)
4490
4570
  next false unless event
@@ -4493,7 +4573,7 @@ module Kettle
4493
4573
  if verbose || debug
4494
4574
  emit_template_event_progress(member, event)
4495
4575
  elsif progress&.tty?
4496
- emit_template_event_status(member, event, progress: progress)
4576
+ emit_template_event_status(member, event, progress: progress, progress_key: progress_key, progress_label: progress_label)
4497
4577
  end
4498
4578
  end
4499
4579
  true
@@ -4611,7 +4691,7 @@ module Kettle
4611
4691
  end
4612
4692
  end
4613
4693
 
4614
- def emit_template_event_status(member, event, progress:)
4694
+ def emit_template_event_status(member, event, progress:, progress_key: member.name, progress_label: member.name)
4615
4695
  status = case event["type"]
4616
4696
  when "phase_start", "phase_finish"
4617
4697
  event["phase"].to_s
@@ -4625,7 +4705,20 @@ module Kettle
4625
4705
  template_event_summary_label(event)
4626
4706
  end
4627
4707
  mark = template_event_status_mark(event)
4628
- progress&.update(member, status: status, mark: mark) if status && !status.empty?
4708
+ if status && !status.empty?
4709
+ progress&.update(
4710
+ member,
4711
+ status: status,
4712
+ mark: mark,
4713
+ **template_progress_identity_options(member, progress_key, progress_label)
4714
+ )
4715
+ end
4716
+ end
4717
+
4718
+ def template_progress_identity_options(member, progress_key, progress_label)
4719
+ return {} if progress_key == member.name && progress_label == member.name
4720
+
4721
+ {key: progress_key, label: progress_label}
4629
4722
  end
4630
4723
 
4631
4724
  def emit_release_event_progress(member, event)
@@ -27,6 +27,7 @@ module Kettle
27
27
  @started = false
28
28
  @stopped = false
29
29
  @member_totals = {}
30
+ @member_labels = @line_order.to_h { |member_name| [member_name, member_name] }
30
31
  @member_counts = Hash.new(0)
31
32
  @member_started_at = {}
32
33
  @member_finished_elapsed = {}
@@ -55,62 +56,66 @@ module Kettle
55
56
  end
56
57
  end
57
58
 
58
- def start_member(member, total:, status:)
59
+ def start_member(member, total:, status:, key: member.name, label: member.name)
59
60
  return unless @enabled
60
61
 
61
62
  synchronize do
62
- @member_totals[member.name] = total.to_i
63
- @member_counts[member.name] = 0
64
- @member_started_at[member.name] ||= monotonic_now
65
- @member_finished_elapsed.delete(member.name)
63
+ @member_labels[key] = label
64
+ @member_totals[key] = total.to_i
65
+ @member_counts[key] = 0
66
+ @member_started_at[key] ||= monotonic_now
67
+ @member_finished_elapsed.delete(key)
66
68
  if @tty
67
- render(member, status: status)
69
+ render(key, status: status)
68
70
  else
69
- write_line(non_tty_line(member, mark: ">", status: status))
71
+ write_line(non_tty_line(key, mark: ">", status: status))
70
72
  end
71
73
  end
72
74
  end
73
75
 
74
- def advance(member, status:, success: true, mark: nil)
76
+ def advance(member, status:, success: true, mark: nil, key: member.name, label: member.name)
75
77
  return unless @enabled
76
78
 
77
79
  synchronize do
80
+ @member_labels[key] = label
78
81
  event_mark = mark || (success ? "." : "F")
79
- increment_member_count(member)
82
+ increment_member_count(key)
80
83
  if @tty
81
- append_event(member, event_mark)
82
- render(member, status: status)
84
+ append_event(key, event_mark)
85
+ render(key, status: status)
83
86
  else
84
- write_line(non_tty_line(member, mark: event_mark, status: status))
87
+ write_line(non_tty_line(key, mark: event_mark, status: status))
85
88
  end
86
89
  end
87
90
  end
88
91
 
89
- def update(member, status:, mark: nil)
92
+ def update(member, status:, mark: nil, key: member.name, label: member.name)
90
93
  return unless @enabled
91
94
  return if status.to_s.empty?
92
95
 
93
96
  synchronize do
97
+ @member_labels[key] = label
94
98
  if @tty
95
- append_event(member, mark) if mark
96
- render(member, status: status)
99
+ append_event(key, mark) if mark
100
+ render(key, status: status)
97
101
  else
98
- write_line(non_tty_line(member, mark: mark || ">", status: status))
102
+ write_line(non_tty_line(key, mark: mark || ">", status: status))
99
103
  end
100
104
  end
101
105
  end
102
106
 
103
- def finish_member(member, success:, status:)
107
+ def finish_member(member, success:, status:, key: member.name, label: member.name)
104
108
  return unless @enabled
105
109
 
106
110
  synchronize do
107
- total = @member_totals[member.name].to_i
108
- @member_counts[member.name] = total if total.positive?
109
- @member_finished_elapsed[member.name] = elapsed_seconds(member)
111
+ @member_labels[key] = label
112
+ total = @member_totals[key].to_i
113
+ @member_counts[key] = total if total.positive?
114
+ @member_finished_elapsed[key] = elapsed_seconds_by_name(key)
110
115
  if @tty
111
- render(member, status: status)
116
+ render(key, status: status)
112
117
  else
113
- write_line(non_tty_line(member, mark: success ? "done" : "failed", status: status))
118
+ write_line(non_tty_line(key, mark: success ? "done" : "failed", status: status))
114
119
  end
115
120
  end
116
121
  end
@@ -168,15 +173,15 @@ module Kettle
168
173
 
169
174
  private
170
175
 
171
- def render(member, status:)
172
- render_name(member.name, status: status)
176
+ def render(member_key, status:)
177
+ render_name(member_key, status: status)
173
178
  render_tty_block if @tty
174
179
  end
175
180
 
176
- def append_event(member, mark)
181
+ def append_event(member_key, mark)
177
182
  return if mark.to_s.empty?
178
183
 
179
- @member_events[member.name] = (@member_events[member.name] + mark.to_s).chars.last(EVENT_WIDTH).join
184
+ @member_events[member_key] = (@member_events[member_key] + mark.to_s).chars.last(EVENT_WIDTH).join
180
185
  end
181
186
 
182
187
  def render_name(member_name, status:)
@@ -222,7 +227,7 @@ module Kettle
222
227
  def tty_line(member_name)
223
228
  Kernel.format(
224
229
  FORMAT,
225
- member: member_name,
230
+ member: @member_labels.fetch(member_name, member_name),
226
231
  progress: progress_text(member_name),
227
232
  duration: elapsed_text(member_name),
228
233
  events: @member_events[member_name].rjust(EVENT_WIDTH),
@@ -234,15 +239,15 @@ module Kettle
234
239
  "s" unless count == 1
235
240
  end
236
241
 
237
- def non_tty_line(member, mark:, status:)
238
- "[#{member.name}] #{progress_text(member.name)} #{elapsed_text(member.name)} #{mark} #{status}"
242
+ def non_tty_line(member_key, mark:, status:)
243
+ label = @member_labels.fetch(member_key, member_key)
244
+ "[#{label}] #{progress_text(member_key)} #{elapsed_text(member_key)} #{mark} #{status}"
239
245
  end
240
246
 
241
- def increment_member_count(member)
242
- member_name = member.name
243
- total = @member_totals[member_name].to_i
244
- @member_counts[member_name] += 1
245
- @member_counts[member_name] = total if total.positive? && @member_counts[member_name] > total
247
+ def increment_member_count(member_key)
248
+ total = @member_totals[member_key].to_i
249
+ @member_counts[member_key] += 1
250
+ @member_counts[member_key] = total if total.positive? && @member_counts[member_key] > total
246
251
  end
247
252
 
248
253
  def progress_text(member_name)
@@ -256,10 +261,6 @@ module Kettle
256
261
  format_elapsed(elapsed_seconds_by_name(member_name)).rjust(ELAPSED_WIDTH)
257
262
  end
258
263
 
259
- def elapsed_seconds(member)
260
- elapsed_seconds_by_name(member.name)
261
- end
262
-
263
264
  def elapsed_seconds_by_name(member_name)
264
265
  return @member_finished_elapsed[member_name] if @member_finished_elapsed.key?(member_name)
265
266
 
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.94
4
+ version: 1.2.96
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -89,22 +89,22 @@ dependencies:
89
89
  name: kettle-dev
90
90
  requirement: !ruby/object:Gem::Requirement
91
91
  requirements:
92
- - - ">="
92
+ - - "~>"
93
93
  - !ruby/object:Gem::Version
94
- version: 3.0.31
95
- - - "<"
94
+ version: '3.0'
95
+ - - ">="
96
96
  - !ruby/object:Gem::Version
97
- version: 4.0.0
97
+ version: 3.0.33
98
98
  type: :runtime
99
99
  prerelease: false
100
100
  version_requirements: !ruby/object:Gem::Requirement
101
101
  requirements:
102
- - - ">="
102
+ - - "~>"
103
103
  - !ruby/object:Gem::Version
104
- version: 3.0.31
105
- - - "<"
104
+ version: '3.0'
105
+ - - ">="
106
106
  - !ruby/object:Gem::Version
107
- version: 4.0.0
107
+ version: 3.0.33
108
108
  - !ruby/object:Gem::Dependency
109
109
  name: kettle-gha-pins
110
110
  requirement: !ruby/object:Gem::Requirement
@@ -131,20 +131,20 @@ dependencies:
131
131
  requirements:
132
132
  - - "~>"
133
133
  - !ruby/object:Gem::Version
134
- version: '7.0'
134
+ version: '7.1'
135
135
  - - ">="
136
136
  - !ruby/object:Gem::Version
137
- version: 7.0.0
137
+ version: 7.1.23
138
138
  type: :runtime
139
139
  prerelease: false
140
140
  version_requirements: !ruby/object:Gem::Requirement
141
141
  requirements:
142
142
  - - "~>"
143
143
  - !ruby/object:Gem::Version
144
- version: '7.0'
144
+ version: '7.1'
145
145
  - - ">="
146
146
  - !ruby/object:Gem::Version
147
- version: 7.0.0
147
+ version: 7.1.23
148
148
  - !ruby/object:Gem::Dependency
149
149
  name: kettle-test
150
150
  requirement: !ruby/object:Gem::Requirement
@@ -304,7 +304,7 @@ dependencies:
304
304
  version: '3.2'
305
305
  - - ">="
306
306
  - !ruby/object:Gem::Version
307
- version: 3.2.2
307
+ version: 3.2.3
308
308
  type: :development
309
309
  prerelease: false
310
310
  version_requirements: !ruby/object:Gem::Requirement
@@ -314,7 +314,7 @@ dependencies:
314
314
  version: '3.2'
315
315
  - - ">="
316
316
  - !ruby/object:Gem::Version
317
- version: 3.2.2
317
+ version: 3.2.3
318
318
  - !ruby/object:Gem::Dependency
319
319
  name: turbo_tests2
320
320
  requirement: !ruby/object:Gem::Requirement
@@ -324,7 +324,7 @@ dependencies:
324
324
  version: '3.2'
325
325
  - - ">="
326
326
  - !ruby/object:Gem::Version
327
- version: 3.2.6
327
+ version: 3.2.7
328
328
  type: :development
329
329
  prerelease: false
330
330
  version_requirements: !ruby/object:Gem::Requirement
@@ -334,7 +334,7 @@ dependencies:
334
334
  version: '3.2'
335
335
  - - ">="
336
336
  - !ruby/object:Gem::Version
337
- version: 3.2.6
337
+ version: 3.2.7
338
338
  - !ruby/object:Gem::Dependency
339
339
  name: ruby-progressbar
340
340
  requirement: !ruby/object:Gem::Requirement
@@ -443,10 +443,10 @@ licenses:
443
443
  - AGPL-3.0-only
444
444
  metadata:
445
445
  homepage_uri: https://kettle-family.galtzo.com
446
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.94
447
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.94/CHANGELOG.md
446
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.96
447
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.96/CHANGELOG.md
448
448
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
449
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.94
449
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.96
450
450
  funding_uri: https://github.com/sponsors/pboling
451
451
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
452
452
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file