kettle-family 1.2.90 → 1.2.92

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: fd4de333d6aa3b2bc027ec582b2bcc0b890d61b20a0a57c70bcf25196b2f9c32
4
- data.tar.gz: 67754f998efad13f47f00f0e07222a17e9413c890b2cee1097ffbccd1114801d
3
+ metadata.gz: c1646ca3577c370624800077609c69c0decd4a50ad6924e82cfe3c050c1757df
4
+ data.tar.gz: 361e0ac1cd39a97b442d3296635055b5bd34c24ced7d3944cec4a0984df6babb
5
5
  SHA512:
6
- metadata.gz: 7238680a28fa74944856d3ebe514193ac070df02fa25b319e28090bbb18f8f6a0f989ccf0d73e84f485d5e7d446de692440304dd0e99cf79b9834f4ba1212143
7
- data.tar.gz: 75c99992e6539a9aa9e37f6ad5ea45afa0692bd9214f10df75f9c604f56af1799099e4cb1fa80b47bd9e843fa8a130103dffae010b14eb8e8f64936834447c35
6
+ metadata.gz: fa89492878000e8fa0b08b244b95a432ddb96e11130da0d4655087580b63260242b00f28a3f76af32c3a9dc8d9d2830f815f22f960f5c835b4b3af851196b510
7
+ data.tar.gz: 1a801e2a97fdb2726e8ac282f88686e87aff42b680b54e2798b09bd70aa4e6be3678e9bf45625ab057a3d2210e99cd59eeeba31fd2761880f27cabc2b53a1566
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,30 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.2.92] - 2026-09-07
34
+
35
+ - TAG: [v1.2.92][1.2.92t]
36
+ - COVERAGE: 93.52% -- 6144/6570 lines in 35 files
37
+ - BRANCH COVERAGE: 77.98% -- 2490/3193 branches in 35 files
38
+ - 32.41% documented
39
+
40
+ ### Fixed
41
+
42
+ - Preserve pre-existing monorepo member changes while templating in parallel worktrees, reject concurrent member edits without overwriting them, and count only fully materialized members as successful.
43
+
44
+ ## [1.2.91] - 2026-09-06
45
+
46
+ - TAG: [v1.2.91][1.2.91t]
47
+ - COVERAGE: 93.58% -- 6109/6528 lines in 35 files
48
+ - BRANCH COVERAGE: 78.06% -- 2483/3181 branches in 35 files
49
+ - 32.41% documented
50
+
51
+ ### Fixed
52
+
53
+ - Trust Mise configurations in disposable worktrees before member commands run.
54
+
55
+ - Remove retired appraisal declarations cleanly from files without a trailing newline during template bootstrap.
56
+
33
57
  ## [1.2.90] - 2026-09-06
34
58
 
35
59
  - TAG: [v1.2.90][1.2.90t]
@@ -2592,7 +2616,11 @@ Please file a bug if you notice a violation of semantic versioning.
2592
2616
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
2593
2617
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
2594
2618
 
2595
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.90...HEAD
2619
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.92...HEAD
2620
+ [1.2.92]: https://github.com/kettle-dev/kettle-family/compare/v1.2.91...v1.2.92
2621
+ [1.2.92t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.92
2622
+ [1.2.91]: https://github.com/kettle-dev/kettle-family/compare/v1.2.90...v1.2.91
2623
+ [1.2.91t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.91
2596
2624
  [1.2.90]: https://github.com/kettle-dev/kettle-family/compare/v1.2.89...v1.2.90
2597
2625
  [1.2.90t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.90
2598
2626
  [1.2.89]: https://github.com/kettle-dev/kettle-family/compare/v1.2.88...v1.2.89
data/README.md CHANGED
@@ -281,6 +281,22 @@ canonical development lockfile and CI checkout share that path graph. In both
281
281
  cases, release normalization rejects every path outside the configured allow
282
282
  list.
283
283
 
284
+ ### Template concurrency
285
+
286
+ Executed `kettle-jem` templating for monorepo members uses detached worktrees
287
+ when a dependency wave contains multiple ready members and the worker budget is
288
+ greater than one. Before a worker starts, Family copies that member's current
289
+ tracked and untracked state into its worktree. This preserves development
290
+ lockfile changes produced by local dependency updates instead of assuming that
291
+ the primary checkout matches `HEAD`.
292
+
293
+ After the worker succeeds, Family verifies that the same member in the primary
294
+ checkout has not changed since the snapshot. It then materializes the worker's
295
+ final member-scoped state and serializes the normal template commit. Changes
296
+ outside the member root are rejected, as are concurrent edits to that member;
297
+ in either case the primary member is left untouched. Template summaries count
298
+ a member only after worktree materialization and any required commit succeed.
299
+
284
300
  ### Test concurrency
285
301
 
286
302
  `kettle-family test --execute` runs independent sibling repositories in
@@ -1121,7 +1137,7 @@ Thanks for RTFM. ☺️
1121
1137
  [📌gitmoji]: https://gitmoji.dev
1122
1138
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1123
1139
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1124
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.494-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1140
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-6.570-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1125
1141
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
1126
1142
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1127
1143
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -66,7 +66,8 @@ module Kettle
66
66
  def obsolete_appraisal_edit(path, source, node)
67
67
  return unless legacy_appraisal_call?(node)
68
68
 
69
- line_start = source.rindex("\n", node.location.start_offset - 1).to_i + 1
69
+ preceding_newline = source.rindex("\n", node.location.start_offset - 1)
70
+ line_start = preceding_newline ? preceding_newline + 1 : 0
70
71
  line_end = source.index("\n", node.location.end_offset) || source.length
71
72
  line_end += 1 if line_end < source.length
72
73
  if final_standalone_call?(source, node, line_start, line_end)
@@ -230,8 +230,8 @@ module Kettle
230
230
  @otp_coordinator = otp_coordinator
231
231
  end
232
232
 
233
- def call(member:, phase:, command:, env: {}, interactive: false, stdout_line_handler: nil, log_path: nil, passthrough_output: true)
234
- argv = command_argv(member: member, command: command, env: env)
233
+ def call(member:, phase:, command:, env: {}, interactive: false, raw: false, stdout_line_handler: nil, log_path: nil, passthrough_output: true)
234
+ argv = command_argv(member: member, command: command, env: env, raw: raw)
235
235
  process_env = process_env(member: member, env: env)
236
236
  spawn_options = process_options
237
237
  return skipped_result(member: member, phase: phase, argv: argv) unless execute
@@ -598,9 +598,9 @@ module Kettle
598
598
  nil
599
599
  end
600
600
 
601
- def command_argv(member:, command:, env: {})
601
+ def command_argv(member:, command:, env: {}, raw: false)
602
602
  argv = normalize_command(command)
603
- return argv unless mise_configured?(member)
603
+ return argv if raw || !mise_configured?(member)
604
604
 
605
605
  command_env = nonsensitive_env(env)
606
606
  unset_env, set_env = command_env.partition { |_key, value| value.nil? }
@@ -332,11 +332,16 @@ module Kettle
332
332
  template_results = results.select { |result| result.phase == "template" }
333
333
  return if template_results.empty?
334
334
 
335
+ successful_names = summary_succeeded
336
+ template_results.select! do |result|
337
+ !result.ok? || successful_names.include?(result.member_name)
338
+ end
339
+
335
340
  changed_files = template_results.sum { |result| template_changed_file_count(result) }
336
341
  outcome_counts = template_results.map { |result| template_file_outcomes(result) }
337
342
  lines << "template summary:"
338
- lines << " #{template_results.count(&:ok?)}/#{selected_members.length} members ok"
339
- if outcome_counts.all?
343
+ lines << " #{successful_names.length}/#{selected_members.length} members ok"
344
+ if outcome_counts.any? && outcome_counts.all?
340
345
  lines << " #{outcome_counts.sum { |outcomes| outcomes.fetch(:checksum_hits) }} checksum hits"
341
346
  protected = outcome_counts.sum { |outcomes| outcomes.fetch(:checksum_protected) }
342
347
  lines << " #{protected} checksum-protected changes" if protected.positive?
@@ -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.90"
8
+ VERSION = "1.2.92"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "io/console"
4
4
  require "fileutils"
5
+ require "digest"
5
6
  require "json"
6
7
  require "etc"
7
8
  require "open3"
@@ -717,11 +718,14 @@ module Kettle
717
718
  results << result
718
719
  break unless result.ok?
719
720
 
720
- entries << {
721
+ entry = {
721
722
  member: relocate_member_to_worktree(member, worktree_root, source_root: config.root),
722
723
  original_member: member,
723
724
  worktree_root: worktree_root
724
725
  }
726
+ entries << entry
727
+ results.concat(worktree_mise_trust_results(entry, runner: runner, phase: "test_member_worktree_mise_trust"))
728
+ break unless results.last.ok?
725
729
  end
726
730
  [entries, results]
727
731
  end
@@ -1038,11 +1042,18 @@ module Kettle
1038
1042
  results << result
1039
1043
  break unless result.ok?
1040
1044
 
1041
- entries << {
1045
+ entry = {
1042
1046
  member: relocate_member_to_worktree(member, worktree_root, source_root: config.root),
1043
1047
  original_member: member,
1044
1048
  worktree_root: worktree_root
1045
1049
  }
1050
+ entries << entry
1051
+ prepare_monorepo_template_worktree(entry)
1052
+ results.concat(worktree_mise_trust_results(entry, runner: runner, phase: "template_member_worktree_mise_trust"))
1053
+ break unless results.last.ok?
1054
+ rescue => error
1055
+ results << template_worktree_failure_result(member, error, phase: "template_member_worktree_seed")
1056
+ break
1046
1057
  end
1047
1058
  [entries, results]
1048
1059
  end
@@ -1096,39 +1107,64 @@ module Kettle
1096
1107
  def materialize_monorepo_template_changes(entry)
1097
1108
  original_member = entry.fetch(:original_member)
1098
1109
  worktree_root = entry.fetch(:worktree_root)
1099
- relative_root = Pathname.new(original_member.root).relative_path_from(Pathname.new(config.root)).to_s
1110
+ relative_root = monorepo_template_relative_root(original_member)
1100
1111
  paths = monorepo_template_worktree_changed_paths(worktree_root)
1101
- return template_worktree_failure_result(original_member, paths) if paths.is_a?(String)
1112
+ return template_worktree_failure_result(original_member, paths, phase: "template_worktree_materialize") if paths.is_a?(String)
1102
1113
 
1103
1114
  invalid_paths = paths.reject { |path| path == relative_root || path.start_with?("#{relative_root}/") }
1104
1115
  unless invalid_paths.empty?
1105
1116
  return template_worktree_failure_result(
1106
1117
  original_member,
1107
- "isolated template worker modified shared path(s): #{invalid_paths.sort.join(", ")}; only #{relative_root}/ may be materialized"
1118
+ "isolated template worker modified shared path(s): #{invalid_paths.sort.join(", ")}; only #{relative_root}/ may be materialized",
1119
+ phase: "template_worktree_materialize"
1108
1120
  )
1109
1121
  end
1110
1122
 
1111
- tracked_paths = monorepo_template_worktree_tracked_paths(worktree_root)
1112
- return template_worktree_failure_result(original_member, tracked_paths) if tracked_paths.is_a?(String)
1113
-
1114
- runner = command_runner
1115
- control_member = monorepo_template_control_member.dup
1116
- control_member.name = original_member.name
1117
- command = if tracked_paths.empty?
1118
- ["true"]
1119
- else
1120
- source = Shellwords.escape(worktree_root)
1121
- scope = Shellwords.escape(relative_root)
1122
- ["sh", "-lc", "git -C #{source} diff --binary HEAD -- #{scope} | git apply --whitespace=nowarn"]
1123
+ initial_state = entry.fetch(:primary_member_state)
1124
+ current_state = monorepo_template_member_state(config.root, relative_root)
1125
+ changed_primary_paths = changed_monorepo_template_state_paths(initial_state, current_state)
1126
+ unless changed_primary_paths.empty?
1127
+ return template_worktree_failure_result(
1128
+ original_member,
1129
+ "primary member changed while isolated template worker ran: #{changed_primary_paths.join(", ")}",
1130
+ phase: "template_worktree_materialize"
1131
+ )
1123
1132
  end
1124
- result = runner.call(member: control_member, phase: "template_worktree_materialize", command: command)
1125
- return result unless result.ok?
1126
1133
 
1127
- copy_monorepo_template_untracked_paths(worktree_root, config.root, relative_root)
1128
- result.stdout = "#{paths.length} path(s) materialized from isolated worktree"
1129
- result
1134
+ materialized_paths = (entry.fetch(:primary_changed_paths) + paths).uniq.sort
1135
+ sync_monorepo_template_paths(worktree_root, config.root, materialized_paths)
1136
+ template_worktree_success_result(
1137
+ original_member,
1138
+ phase: "template_worktree_materialize",
1139
+ stdout: "#{materialized_paths.length} path(s) materialized from isolated worktree"
1140
+ )
1130
1141
  rescue => error
1131
- template_worktree_failure_result(original_member, error)
1142
+ template_worktree_failure_result(original_member, error, phase: "template_worktree_materialize")
1143
+ end
1144
+
1145
+ def prepare_monorepo_template_worktree(entry)
1146
+ original_member = entry.fetch(:original_member)
1147
+ relative_root = monorepo_template_relative_root(original_member)
1148
+ changed_paths = monorepo_template_worktree_changed_paths(config.root)
1149
+ raise Error, changed_paths if changed_paths.is_a?(String)
1150
+
1151
+ member_changed_paths = changed_paths.select do |path|
1152
+ path == relative_root || path.start_with?("#{relative_root}/")
1153
+ end
1154
+ sync_monorepo_template_paths(config.root, entry.fetch(:worktree_root), member_changed_paths)
1155
+ primary_state = monorepo_template_member_state(config.root, relative_root)
1156
+ worktree_state = monorepo_template_member_state(entry.fetch(:worktree_root), relative_root)
1157
+ unless primary_state == worktree_state
1158
+ changed = changed_monorepo_template_state_paths(primary_state, worktree_state)
1159
+ raise Error, "isolated template worker seed differs from primary member: #{changed.join(", ")}"
1160
+ end
1161
+
1162
+ entry[:primary_changed_paths] = member_changed_paths
1163
+ entry[:primary_member_state] = primary_state
1164
+ end
1165
+
1166
+ def monorepo_template_relative_root(member)
1167
+ Pathname.new(member.root).relative_path_from(Pathname.new(config.root)).to_s
1132
1168
  end
1133
1169
 
1134
1170
  def monorepo_template_worktree_changed_paths(worktree_root)
@@ -1149,6 +1185,28 @@ module Kettle
1149
1185
  git_worktree_paths(worktree_root, %w[ls-files --others --exclude-standard -z])
1150
1186
  end
1151
1187
 
1188
+ def monorepo_template_member_state(root, relative_root)
1189
+ paths = git_worktree_paths(root, ["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", relative_root])
1190
+ raise Error, paths if paths.is_a?(String)
1191
+
1192
+ paths.to_h { |path| [path, monorepo_template_path_signature(root, path)] }
1193
+ end
1194
+
1195
+ def monorepo_template_path_signature(root, path)
1196
+ full_path = File.join(root, path)
1197
+ stat = File.lstat(full_path)
1198
+ return ["symlink", File.readlink(full_path)] if stat.symlink?
1199
+ return ["file", stat.mode & 0o777, Digest::SHA256.file(full_path).hexdigest] if stat.file?
1200
+
1201
+ [stat.ftype, stat.mode & 0o777]
1202
+ rescue Errno::ENOENT
1203
+ ["missing"]
1204
+ end
1205
+
1206
+ def changed_monorepo_template_state_paths(before, after)
1207
+ (before.keys | after.keys).select { |path| before[path] != after[path] }.sort
1208
+ end
1209
+
1152
1210
  def git_worktree_paths(worktree_root, arguments)
1153
1211
  stdout, stderr, status = Open3.capture3("git", *arguments, chdir: worktree_root)
1154
1212
  return stdout.split("\0").reject(&:empty?) if status.success?
@@ -1156,14 +1214,19 @@ module Kettle
1156
1214
  "git #{arguments.join(" ")} failed in #{worktree_root}: #{stderr.strip}"
1157
1215
  end
1158
1216
 
1159
- def copy_monorepo_template_untracked_paths(worktree_root, primary_root, relative_root)
1160
- monorepo_template_worktree_untracked_paths(worktree_root).each do |path|
1161
- next unless path == relative_root || path.start_with?("#{relative_root}/")
1217
+ def sync_monorepo_template_paths(source_root, destination_root, paths)
1218
+ paths.each do |path|
1219
+ source = File.join(source_root, path)
1220
+ destination = File.join(destination_root, path)
1221
+ FileUtils.rm_rf(destination)
1222
+ next unless File.exist?(source) || File.symlink?(source)
1162
1223
 
1163
- source = File.join(worktree_root, path)
1164
- destination = File.join(primary_root, path)
1165
1224
  FileUtils.mkdir_p(File.dirname(destination))
1166
- FileUtils.cp_r(source, destination, preserve: true)
1225
+ if File.symlink?(source)
1226
+ File.symlink(File.readlink(source), destination)
1227
+ else
1228
+ FileUtils.cp_r(source, destination, preserve: true)
1229
+ end
1167
1230
  end
1168
1231
  end
1169
1232
 
@@ -1178,12 +1241,28 @@ module Kettle
1178
1241
  end
1179
1242
  end
1180
1243
 
1181
- def template_worktree_failure_result(member, error)
1244
+ def template_worktree_success_result(member, phase:, stdout:)
1245
+ CommandResult.new(
1246
+ member_name: member.name,
1247
+ phase: phase,
1248
+ command: ["internal", phase.tr("_", "-")],
1249
+ workdir: member.root,
1250
+ status: 0,
1251
+ success: true,
1252
+ stdout: stdout,
1253
+ stderr: "",
1254
+ elapsed_seconds: 0.0,
1255
+ skipped: false,
1256
+ reason: nil
1257
+ )
1258
+ end
1259
+
1260
+ def template_worktree_failure_result(member, error, phase: "template_member_worktree")
1182
1261
  message = error.is_a?(Exception) ? "#{error.class}: #{error.message}" : error.to_s
1183
1262
  CommandResult.new(
1184
1263
  member_name: member.name,
1185
- phase: "template_member_worktree",
1186
- command: ["internal", "template-member-worktree"],
1264
+ phase: phase,
1265
+ command: ["internal", phase.tr("_", "-")],
1187
1266
  workdir: member.root,
1188
1267
  status: 1,
1189
1268
  success: false,
@@ -1435,7 +1514,11 @@ module Kettle
1435
1514
  outcomes << add
1436
1515
  break unless add.ok?
1437
1516
 
1438
- entries << {branch: branch, member: relocate_member_to_worktree(member, worktree_root), worktree_root: worktree_root}
1517
+ entry = {branch: branch, member: relocate_member_to_worktree(member, worktree_root), worktree_root: worktree_root}
1518
+ entries << entry
1519
+ trust_results = worktree_mise_trust_results(entry.merge(original_member: member), runner: runner, phase: "template_worktree_mise_trust")
1520
+ outcomes.concat(trust_results)
1521
+ break unless trust_results.all?(&:ok?)
1439
1522
  end
1440
1523
 
1441
1524
  if outcomes.all?(&:ok?)
@@ -1491,6 +1574,45 @@ module Kettle
1491
1574
  end
1492
1575
  end
1493
1576
 
1577
+ # A git worktree has a distinct absolute path, and Mise deliberately
1578
+ # tracks trust by config path. Trust every active config between the
1579
+ # worktree root and the member command directory before invoking a
1580
+ # command there. This bootstrap must run raw: wrapping it in `mise exec`
1581
+ # would require the trust it is establishing.
1582
+ def worktree_mise_trust_results(entry, runner:, phase:)
1583
+ member = entry.fetch(:member)
1584
+ original_member = entry.fetch(:original_member, member)
1585
+ worktree_root = entry.fetch(:worktree_root)
1586
+ mise_config_roots_in_worktree(worktree_root, member.root).map do |root|
1587
+ result = runner.call(
1588
+ member: member,
1589
+ phase: phase,
1590
+ command: ["mise", "trust", "-C", root],
1591
+ raw: true
1592
+ )
1593
+ result.member_name = original_member.name
1594
+ result
1595
+ end
1596
+ end
1597
+
1598
+ def mise_config_roots_in_worktree(worktree_root, member_root)
1599
+ root_path = Pathname.new(worktree_root).realpath
1600
+ member_path = Pathname.new(member_root).realpath
1601
+ return [] unless member_path.to_s.start_with?("#{root_path}#{File::SEPARATOR}") || member_path == root_path
1602
+
1603
+ current = member_path
1604
+ roots = []
1605
+ loop do
1606
+ roots << current.to_s if %w[mise.toml .mise.toml].any? { |name| File.file?(current.join(name)) }
1607
+ break if current == root_path
1608
+
1609
+ current = current.parent
1610
+ end
1611
+ roots.reverse
1612
+ rescue Errno::ENOENT
1613
+ []
1614
+ end
1615
+
1494
1616
  def template_branch_worktree_entries_results(entries)
1495
1617
  wave_jobs = template_jobs(entries)
1496
1618
  queue = Queue.new
@@ -2038,7 +2160,10 @@ module Kettle
2038
2160
  results << result
2039
2161
  break unless result.ok?
2040
2162
 
2041
- entries << {member: relocate_member_to_worktree(member, root, source_root: config.root), original_member: member, worktree_root: root}
2163
+ entry = {member: relocate_member_to_worktree(member, root, source_root: config.root), original_member: member, worktree_root: root}
2164
+ entries << entry
2165
+ results.concat(worktree_mise_trust_results(entry, runner: runner, phase: "release_worktree_mise_trust"))
2166
+ break unless results.last.ok?
2042
2167
  end
2043
2168
  [entries, results]
2044
2169
  end
@@ -4296,13 +4421,25 @@ module Kettle
4296
4421
  def emit_template_progress_summary(results, progress:)
4297
4422
  return unless progress
4298
4423
 
4299
- template_results = results.select { |result| result.phase == "template" }
4424
+ successful_names = successful_template_member_names(results)
4425
+ template_results = results.select do |result|
4426
+ result.phase == "template" && (!result.ok? || successful_names.include?(result.member_name))
4427
+ end
4300
4428
  changed_files = template_results.sum { |result| template_changed_file_count(result) }
4301
4429
  outcome_counts = template_results.map { |result| template_file_outcomes(result) }
4302
4430
  progress.stop
4303
4431
  progress.summary(template_progress_summary_label(template_results, changed_files, outcome_counts))
4304
4432
  end
4305
4433
 
4434
+ def successful_template_member_names(results)
4435
+ results.group_by(&:member_name).filter_map do |member_name, member_results|
4436
+ next unless member_results.any? { |result| result.phase == "template" }
4437
+ next unless member_results.all?(&:ok?)
4438
+
4439
+ member_name
4440
+ end
4441
+ end
4442
+
4306
4443
  def emit_release_progress_summary(results, progress:)
4307
4444
  return unless progress
4308
4445
 
@@ -4763,7 +4900,7 @@ module Kettle
4763
4900
 
4764
4901
  def template_progress_summary_label(template_results, changed_files, outcome_counts)
4765
4902
  label = "template summary: #{template_results.count(&:ok?)}/#{members.length} members ok"
4766
- return "#{label}, #{template_summary_label(changed_files)}" unless outcome_counts.all?
4903
+ return "#{label}, #{template_summary_label(changed_files)}" if outcome_counts.empty? || !outcome_counts.all?
4767
4904
 
4768
4905
  totals = outcome_counts.each_with_object({checksum_hits: 0, checksum_protected: 0, unchanged: 0}) do |outcomes, memo|
4769
4906
  memo[:checksum_hits] += outcomes.fetch(:checksum_hits)
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.90
4
+ version: 1.2.92
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -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.90
447
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.90/CHANGELOG.md
446
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.92
447
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.92/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.90
449
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.92
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