kettle-family 1.2.91 → 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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +14 -1
- data/README.md +17 -1
- data/lib/kettle/family/report.rb +7 -2
- data/lib/kettle/family/version.rb +1 -1
- data/lib/kettle/family/workflow.rb +117 -32
- data.tar.gz.sig +0 -0
- metadata +4 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c1646ca3577c370624800077609c69c0decd4a50ad6924e82cfe3c050c1757df
|
|
4
|
+
data.tar.gz: 361e0ac1cd39a97b442d3296635055b5bd34c24ced7d3944cec4a0984df6babb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa89492878000e8fa0b08b244b95a432ddb96e11130da0d4655087580b63260242b00f28a3f76af32c3a9dc8d9d2830f815f22f960f5c835b4b3af851196b510
|
|
7
|
+
data.tar.gz: 1a801e2a97fdb2726e8ac282f88686e87aff42b680b54e2798b09bd70aa4e6be3678e9bf45625ab057a3d2210e99cd59eeeba31fd2761880f27cabc2b53a1566
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,17 @@ 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
|
+
|
|
33
44
|
## [1.2.91] - 2026-09-06
|
|
34
45
|
|
|
35
46
|
- TAG: [v1.2.91][1.2.91t]
|
|
@@ -2605,7 +2616,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
2605
2616
|
- Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
|
|
2606
2617
|
- Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
|
|
2607
2618
|
|
|
2608
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.
|
|
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
|
|
2609
2622
|
[1.2.91]: https://github.com/kettle-dev/kettle-family/compare/v1.2.90...v1.2.91
|
|
2610
2623
|
[1.2.91t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.91
|
|
2611
2624
|
[1.2.90]: https://github.com/kettle-dev/kettle-family/compare/v1.2.89...v1.2.90
|
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.
|
|
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
|
data/lib/kettle/family/report.rb
CHANGED
|
@@ -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 << " #{
|
|
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.
|
|
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"
|
|
@@ -1047,8 +1048,12 @@ module Kettle
|
|
|
1047
1048
|
worktree_root: worktree_root
|
|
1048
1049
|
}
|
|
1049
1050
|
entries << entry
|
|
1051
|
+
prepare_monorepo_template_worktree(entry)
|
|
1050
1052
|
results.concat(worktree_mise_trust_results(entry, runner: runner, phase: "template_member_worktree_mise_trust"))
|
|
1051
1053
|
break unless results.last.ok?
|
|
1054
|
+
rescue => error
|
|
1055
|
+
results << template_worktree_failure_result(member, error, phase: "template_member_worktree_seed")
|
|
1056
|
+
break
|
|
1052
1057
|
end
|
|
1053
1058
|
[entries, results]
|
|
1054
1059
|
end
|
|
@@ -1102,39 +1107,64 @@ module Kettle
|
|
|
1102
1107
|
def materialize_monorepo_template_changes(entry)
|
|
1103
1108
|
original_member = entry.fetch(:original_member)
|
|
1104
1109
|
worktree_root = entry.fetch(:worktree_root)
|
|
1105
|
-
relative_root =
|
|
1110
|
+
relative_root = monorepo_template_relative_root(original_member)
|
|
1106
1111
|
paths = monorepo_template_worktree_changed_paths(worktree_root)
|
|
1107
|
-
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)
|
|
1108
1113
|
|
|
1109
1114
|
invalid_paths = paths.reject { |path| path == relative_root || path.start_with?("#{relative_root}/") }
|
|
1110
1115
|
unless invalid_paths.empty?
|
|
1111
1116
|
return template_worktree_failure_result(
|
|
1112
1117
|
original_member,
|
|
1113
|
-
"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"
|
|
1114
1120
|
)
|
|
1115
1121
|
end
|
|
1116
1122
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
source = Shellwords.escape(worktree_root)
|
|
1127
|
-
scope = Shellwords.escape(relative_root)
|
|
1128
|
-
["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
|
+
)
|
|
1129
1132
|
end
|
|
1130
|
-
result = runner.call(member: control_member, phase: "template_worktree_materialize", command: command)
|
|
1131
|
-
return result unless result.ok?
|
|
1132
1133
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
+
)
|
|
1136
1141
|
rescue => error
|
|
1137
|
-
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
|
|
1138
1168
|
end
|
|
1139
1169
|
|
|
1140
1170
|
def monorepo_template_worktree_changed_paths(worktree_root)
|
|
@@ -1155,6 +1185,28 @@ module Kettle
|
|
|
1155
1185
|
git_worktree_paths(worktree_root, %w[ls-files --others --exclude-standard -z])
|
|
1156
1186
|
end
|
|
1157
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
|
+
|
|
1158
1210
|
def git_worktree_paths(worktree_root, arguments)
|
|
1159
1211
|
stdout, stderr, status = Open3.capture3("git", *arguments, chdir: worktree_root)
|
|
1160
1212
|
return stdout.split("\0").reject(&:empty?) if status.success?
|
|
@@ -1162,14 +1214,19 @@ module Kettle
|
|
|
1162
1214
|
"git #{arguments.join(" ")} failed in #{worktree_root}: #{stderr.strip}"
|
|
1163
1215
|
end
|
|
1164
1216
|
|
|
1165
|
-
def
|
|
1166
|
-
|
|
1167
|
-
|
|
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)
|
|
1168
1223
|
|
|
1169
|
-
source = File.join(worktree_root, path)
|
|
1170
|
-
destination = File.join(primary_root, path)
|
|
1171
1224
|
FileUtils.mkdir_p(File.dirname(destination))
|
|
1172
|
-
|
|
1225
|
+
if File.symlink?(source)
|
|
1226
|
+
File.symlink(File.readlink(source), destination)
|
|
1227
|
+
else
|
|
1228
|
+
FileUtils.cp_r(source, destination, preserve: true)
|
|
1229
|
+
end
|
|
1173
1230
|
end
|
|
1174
1231
|
end
|
|
1175
1232
|
|
|
@@ -1184,12 +1241,28 @@ module Kettle
|
|
|
1184
1241
|
end
|
|
1185
1242
|
end
|
|
1186
1243
|
|
|
1187
|
-
def
|
|
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")
|
|
1188
1261
|
message = error.is_a?(Exception) ? "#{error.class}: #{error.message}" : error.to_s
|
|
1189
1262
|
CommandResult.new(
|
|
1190
1263
|
member_name: member.name,
|
|
1191
|
-
phase:
|
|
1192
|
-
command: ["internal", "
|
|
1264
|
+
phase: phase,
|
|
1265
|
+
command: ["internal", phase.tr("_", "-")],
|
|
1193
1266
|
workdir: member.root,
|
|
1194
1267
|
status: 1,
|
|
1195
1268
|
success: false,
|
|
@@ -4348,13 +4421,25 @@ module Kettle
|
|
|
4348
4421
|
def emit_template_progress_summary(results, progress:)
|
|
4349
4422
|
return unless progress
|
|
4350
4423
|
|
|
4351
|
-
|
|
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
|
|
4352
4428
|
changed_files = template_results.sum { |result| template_changed_file_count(result) }
|
|
4353
4429
|
outcome_counts = template_results.map { |result| template_file_outcomes(result) }
|
|
4354
4430
|
progress.stop
|
|
4355
4431
|
progress.summary(template_progress_summary_label(template_results, changed_files, outcome_counts))
|
|
4356
4432
|
end
|
|
4357
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
|
+
|
|
4358
4443
|
def emit_release_progress_summary(results, progress:)
|
|
4359
4444
|
return unless progress
|
|
4360
4445
|
|
|
@@ -4815,7 +4900,7 @@ module Kettle
|
|
|
4815
4900
|
|
|
4816
4901
|
def template_progress_summary_label(template_results, changed_files, outcome_counts)
|
|
4817
4902
|
label = "template summary: #{template_results.count(&:ok?)}/#{members.length} members ok"
|
|
4818
|
-
return "#{label}, #{template_summary_label(changed_files)}"
|
|
4903
|
+
return "#{label}, #{template_summary_label(changed_files)}" if outcome_counts.empty? || !outcome_counts.all?
|
|
4819
4904
|
|
|
4820
4905
|
totals = outcome_counts.each_with_object({checksum_hits: 0, checksum_protected: 0, unchanged: 0}) do |outcomes, memo|
|
|
4821
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.
|
|
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.
|
|
447
|
-
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.
|
|
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.
|
|
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
|