kettle-family 1.0.4 → 1.0.5

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: f81e03e49736eb1884f0272f7af5ee78b14239ace620cf5427da26cfbaf2df5c
4
- data.tar.gz: cc974713f574848b1740c413c1db51913bd99f8fc91fa2683b312f7cdbcfcf91
3
+ metadata.gz: fceb2fadcbbc93ac056bf4a56e6efc3b628a5891c094af2fdf626886ea211acb
4
+ data.tar.gz: a3b0a7d51a2fc8d3c6ef63209fff25912f8d144d223de52d2649c2144c58c5e2
5
5
  SHA512:
6
- metadata.gz: 7fb8fa2d38f8a84c537558ab48d90fd17f9933cd27e8017b60a9f6ecf89f54a928d0c21b058dcc0e41766d47372dffb4a2a125c18c0e7049d11ce31ddaac24cd
7
- data.tar.gz: 2e546a76ba606eb8078a477b696109a30a0acdfe23bda7267905fc926cb65dec5237f938f5d42ab8e8e75fb131a316d0303a3ec637284c7c41331e98a02bcdd2
6
+ metadata.gz: d0fbc13bfb0ed888c274d07cb7f59dacecb9aeeb0ef22fe6dcaf0e15632450d910000f6dc25dcacf1dc5bae5c603115073a0b48209c427a45508de40ecd2ba17
7
+ data.tar.gz: c684031130496ae7a414b4411fb62934bbdd6610f0f64cc27645b9074db07b03c7be05ab71aa033a1b5ecd19724e9601e6197c1c543b6bcaec2ee7b3ba93b84a
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,25 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.0.5] - 2026-07-23
34
+
35
+ - TAG: [v1.0.5][1.0.5t]
36
+ - COVERAGE: 94.41% -- 2772/2936 lines in 24 files
37
+ - BRANCH COVERAGE: 76.59% -- 978/1277 branches in 24 files
38
+ - 29.23% documented
39
+
40
+ ### Fixed
41
+
42
+ - Family releases now retry `bundle update` for dependent members after
43
+ wave-aware dependency floor bumps, and reject empty lockfile checksum entries
44
+ for the just-published gems, so a registry version endpoint becoming visible
45
+ before Bundler's dependency index no longer breaks the next publish.
46
+ - Failed interactive release results now summarize streamed command output
47
+ instead of replaying the full child-process transcript in the final report.
48
+ - `kettle-family state` now reports per-gem release-state rows for monorepos
49
+ that use a shared root changelog, while still preferring member-local
50
+ changelogs when present and retaining branch-stack release-state output.
51
+
33
52
  ## [1.0.4] - 2026-07-21
34
53
 
35
54
  - TAG: [v1.0.4][1.0.4t]
@@ -859,7 +878,9 @@ Please file a bug if you notice a violation of semantic versioning.
859
878
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
860
879
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
861
880
 
862
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.0.4...HEAD
881
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.0.5...HEAD
882
+ [1.0.5]: https://github.com/kettle-dev/kettle-family/compare/v1.0.4...v1.0.5
883
+ [1.0.5t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.0.5
863
884
  [1.0.4]: https://github.com/kettle-dev/kettle-family/compare/v1.0.3...v1.0.4
864
885
  [1.0.4t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.0.4
865
886
  [1.0.3]: https://github.com/kettle-dev/kettle-family/compare/v1.0.2...v1.0.3
data/README.md CHANGED
@@ -669,7 +669,7 @@ Thanks for RTFM. ☺️
669
669
  [📌gitmoji]: https://gitmoji.dev
670
670
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
671
671
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
672
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.849-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
672
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.936-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
673
673
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
674
674
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
675
675
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -14,7 +14,8 @@ module Kettle
14
14
  :elapsed_seconds,
15
15
  :skipped,
16
16
  :reason,
17
- :branch
17
+ :branch,
18
+ :output_streamed
18
19
  ) do
19
20
  def to_h
20
21
  {
@@ -29,7 +30,8 @@ module Kettle
29
30
  "stderr" => summarize(stderr),
30
31
  "elapsed_seconds" => elapsed_seconds,
31
32
  "skipped" => skipped,
32
- "reason" => reason
33
+ "reason" => reason,
34
+ "output_streamed" => output_streamed?
33
35
  }
34
36
  end
35
37
 
@@ -37,6 +39,10 @@ module Kettle
37
39
  success || skipped
38
40
  end
39
41
 
42
+ def output_streamed?
43
+ !!output_streamed
44
+ end
45
+
40
46
  private
41
47
 
42
48
  def summarize(output)
@@ -139,7 +139,8 @@ module Kettle
139
139
  stderr: stderr,
140
140
  elapsed_seconds: elapsed.round(3),
141
141
  skipped: false,
142
- reason: status.success? ? nil : "command failed"
142
+ reason: status.success? ? nil : "command failed",
143
+ output_streamed: interactive
143
144
  )
144
145
  end
145
146
 
@@ -17,7 +17,7 @@ module Kettle
17
17
 
18
18
  def results
19
19
  return branch_results unless release_target_branches.empty?
20
- return [check_family_changelog] if shared_changelog?
20
+ return members.map { |member| check_shared_changelog_member_or_local(member) } if shared_changelog?
21
21
 
22
22
  members.each_with_object([]) do |member, memo|
23
23
  member_branch_results = member_local_branch_results(member)
@@ -38,12 +38,12 @@ module Kettle
38
38
  selected_names = members.map(&:name)
39
39
  release_target_branches.each_with_object([]) do |branch, memo|
40
40
  with_branch_worktree(root: root, branch: branch) do |worktree_root|
41
+ branch_members = discover_branch_members(worktree_root: worktree_root, selected_names: selected_names)
41
42
  if shared_changelog?
42
- memo << check_family_changelog(branch: branch, worktree_root: worktree_root)
43
+ memo.concat(branch_members.map { |member| check_shared_changelog_member_or_local(member, branch: branch) })
43
44
  next
44
45
  end
45
46
 
46
- branch_members = discover_branch_members(worktree_root: worktree_root, selected_names: selected_names)
47
47
  memo.concat(branch_members.map { |member| check_member(member, branch: branch) })
48
48
  end
49
49
  rescue Error => error
@@ -54,7 +54,7 @@ module Kettle
54
54
  def check_member(member, branch: nil)
55
55
  started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
56
56
  command = release_state_command
57
- stdout, stderr, status = Open3.capture3(release_state_env, *command, chdir: release_state_workdir(member))
57
+ stdout, stderr, status = Open3.capture3(release_state_env(member), *command, chdir: release_state_workdir(member))
58
58
  elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
59
59
  success = status.success?
60
60
  state = success ? JSON.parse(stdout) : {}
@@ -69,26 +69,38 @@ module Kettle
69
69
  [RbConfig.ruby, "-S", "kettle-changelog", "--release-state", "--json"]
70
70
  end
71
71
 
72
- def check_family_changelog(branch: nil, worktree_root: nil)
73
- member = family_member(root: worktree_root ? branch_config_root(worktree_root) : config.root)
72
+ def check_shared_changelog_member_or_local(member, branch: nil)
73
+ return check_member(member, branch: branch) if member_local_changelog?(member)
74
+
75
+ check_shared_changelog_member(member, branch: branch)
76
+ end
77
+
78
+ def check_shared_changelog_member(member, branch: nil)
74
79
  started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
75
- state = family_changelog_state(member.root)
80
+ command = release_state_command
81
+ stdout, stderr, status = Open3.capture3(shared_changelog_env(member), *command, chdir: config.root)
76
82
  elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
83
+ success = status.success?
84
+ state = success ? JSON.parse(stdout) : {}
85
+ state = branch_filtered_state(member, state, branch) if success && branch
86
+ state = enrich_git_state(member.root, state) if success
77
87
  result(
78
88
  member: member,
79
- command: ["internal", "release-state", "root-changelog"],
80
- stdout: "",
81
- stderr: "",
82
- status: 0,
89
+ command: command,
90
+ stdout: stdout,
91
+ stderr: stderr,
92
+ status: status.exitstatus,
83
93
  elapsed: elapsed,
84
- success: true,
94
+ success: success,
85
95
  state: state,
86
96
  branch: branch
87
97
  )
98
+ rescue JSON::ParserError => error
99
+ result(member: member, command: command || release_state_command, stdout: stdout, stderr: stderr, status: 1, elapsed: elapsed || 0.0, success: false, state: {}, reason: "invalid release-state JSON: #{error.message}", branch: branch)
88
100
  rescue Error => error
89
101
  result(
90
102
  member: member,
91
- command: ["internal", "release-state", "root-changelog"],
103
+ command: command || release_state_command,
92
104
  stdout: "",
93
105
  stderr: error.message,
94
106
  status: 1,
@@ -100,28 +112,35 @@ module Kettle
100
112
  )
101
113
  end
102
114
 
103
- def family_member(root:)
104
- Member.new(
105
- name: config.family_name,
106
- root: root,
107
- gemspec_path: nil,
108
- version_file: nil,
109
- version: nil,
110
- dependencies: []
111
- )
112
- end
113
-
114
115
  def release_state_workdir(member)
115
116
  return member.root unless config
116
- return member.root if config.shared_changelog?
117
+ return config.root if config.shared_changelog? && !member_local_changelog?(member)
117
118
 
118
119
  config.changelog_workdir(member) || member.root
119
120
  end
120
121
 
121
- def release_state_env
122
+ def release_state_env(member = nil)
123
+ return member_changelog_env(member) if member && shared_changelog? && member_local_changelog?(member)
124
+
122
125
  config ? config.changelog_env : {}
123
126
  end
124
127
 
128
+ def shared_changelog_env(member)
129
+ env = release_state_env(member).merge("K_CHANGELOG_GEM_NAME" => member.name.to_s)
130
+ env["K_CHANGELOG_VERSION_FILE"] = member.version_file.to_s if member.version_file
131
+ env
132
+ end
133
+
134
+ def member_changelog_env(member)
135
+ env = {"K_CHANGELOG_GEM_NAME" => member.name.to_s}
136
+ env["K_CHANGELOG_VERSION_FILE"] = member.version_file.to_s if member.version_file
137
+ env
138
+ end
139
+
140
+ def member_local_changelog?(member)
141
+ File.file?(File.join(member.root, "CHANGELOG.md"))
142
+ end
143
+
125
144
  def branch_filtered_state(member, state, _branch)
126
145
  latest_released = branch_latest_released(member, state["latest_changelog_version"])
127
146
  return state unless latest_released
@@ -162,7 +162,9 @@ module Kettle
162
162
  def append_result_stdout(lines, result)
163
163
  return if suppress_success_output?(result)
164
164
 
165
- if template_event_stdout?(result)
165
+ if output_streamed?(result)
166
+ append_streamed_output_summary(lines, result) unless result.ok?
167
+ elsif template_event_stdout?(result)
166
168
  append_template_event_failure_summary(lines, result.stdout) unless result.ok?
167
169
  else
168
170
  append_indented_output(lines, result.stdout)
@@ -177,6 +179,33 @@ module Kettle
177
179
  command == "template" && result.phase == "template" && template_events(result.stdout).any?
178
180
  end
179
181
 
182
+ def output_streamed?(result)
183
+ result.respond_to?(:output_streamed?) && result.output_streamed?
184
+ end
185
+
186
+ def append_streamed_output_summary(lines, result)
187
+ summary = streamed_output_summary(result)
188
+ lines << " summary: #{summary}" if summary
189
+ recommended_fix = streamed_recommended_fix(result)
190
+ lines << " recommended fix: #{recommended_fix}" if recommended_fix
191
+ lines << " output: omitted because it was already streamed"
192
+ end
193
+
194
+ def streamed_output_summary(result)
195
+ streamed_output_lines(result).reverse.find do |line|
196
+ line.match?(/(?:failed|failure|error|exited|aborted)/i) && !line.match?(/\A(?:Files|CI|Actions)/)
197
+ end
198
+ end
199
+
200
+ def streamed_recommended_fix(result)
201
+ line = streamed_output_lines(result).reverse.find { |candidate| candidate.match?(/\ARecommended fix:\s+/) }
202
+ line&.sub(/\ARecommended fix:\s+/, "")
203
+ end
204
+
205
+ def streamed_output_lines(result)
206
+ [result.stdout, result.stderr].join("\n").lines.map(&:strip).reject(&:empty?)
207
+ end
208
+
180
209
  def append_template_event_failure_summary(lines, output)
181
210
  diagnostics = template_events(output).filter_map do |event|
182
211
  next unless event["type"] == "diagnostic"
@@ -3,7 +3,7 @@
3
3
  module Kettle
4
4
  module Family
5
5
  module Version
6
- VERSION = "1.0.4"
6
+ VERSION = "1.0.5"
7
7
  end
8
8
  VERSION = Version::VERSION # Traditional Constant Location
9
9
  end
@@ -542,6 +542,9 @@ module Kettle
542
542
  return if memo.any? && !memo.last.ok?
543
543
 
544
544
  append_registry_wait_result(released_members: released_members, dependent_members: affected_dependent_members, memo: memo)
545
+ return if memo.any? && !memo.last.ok?
546
+
547
+ append_dependency_floor_lockfile_results(released_members: released_members, dependent_members: affected_dependent_members, runner: runner, memo: memo)
545
548
  end
546
549
 
547
550
  def dependent_members_depending_on(released_members:, dependent_members:)
@@ -562,6 +565,85 @@ module Kettle
562
565
  memo << wait_for_registry_result(released_members: released_members, dependent_members: dependent_members)
563
566
  end
564
567
 
568
+ def append_dependency_floor_lockfile_results(released_members:, dependent_members:, runner:, memo:)
569
+ return unless execute && publish
570
+ return if dependent_members.empty?
571
+
572
+ dependent_members.each do |member|
573
+ memo << wait_for_dependency_floor_lockfiles_result(member: member, released_members: released_members, runner: runner)
574
+ break unless memo.last.ok?
575
+ end
576
+ end
577
+
578
+ def wait_for_dependency_floor_lockfiles_result(member:, released_members:, runner:)
579
+ result = nil
580
+ REGISTRY_WAIT_ATTEMPTS.times do |index|
581
+ result = runner.call(
582
+ member: member,
583
+ phase: "dependency_floor_lockfiles",
584
+ command: ["bundle", "update", *released_members.map(&:name)],
585
+ env: release_lockfile_env
586
+ )
587
+ validate_dependency_floor_lockfile_result(result: result, member: member, released_members: released_members) if result.ok?
588
+ annotate_dependency_floor_lockfile_result(result, index + 1)
589
+ break if result.ok?
590
+
591
+ sleep(REGISTRY_WAIT_INTERVAL_SECONDS) if index + 1 < REGISTRY_WAIT_ATTEMPTS
592
+ end
593
+ result
594
+ end
595
+
596
+ def validate_dependency_floor_lockfile_result(result:, member:, released_members:)
597
+ diagnostics = dependency_floor_lockfile_diagnostics(member: member, released_members: released_members)
598
+ return if diagnostics.empty?
599
+
600
+ result.status = 1
601
+ result.success = false
602
+ result.stderr = [result.stderr, *diagnostics].reject(&:empty?).join("\n")
603
+ end
604
+
605
+ def dependency_floor_lockfile_diagnostics(member:, released_members:)
606
+ lockfile = File.join(member.root, "Gemfile.lock")
607
+ return ["Gemfile.lock was not created by dependency floor lockfile refresh"] unless File.file?(lockfile)
608
+
609
+ lockfile_source = File.read(lockfile)
610
+ released_members.filter_map do |released_member|
611
+ checksum_line = checksum_line_for(lockfile_source: lockfile_source, member: released_member)
612
+ if checksum_line.nil?
613
+ "Gemfile.lock CHECKSUMS is missing #{released_member.name} #{released_member.version}"
614
+ elsif !checksum_line.include?("sha256=")
615
+ "Gemfile.lock CHECKSUMS has no sha256 for #{released_member.name} #{released_member.version}"
616
+ end
617
+ end
618
+ end
619
+
620
+ def checksum_line_for(lockfile_source:, member:)
621
+ in_checksums = false
622
+ lockfile_source.each_line do |line|
623
+ stripped = line.chomp
624
+ if stripped == "CHECKSUMS"
625
+ in_checksums = true
626
+ next
627
+ end
628
+ next unless in_checksums
629
+ break if !stripped.empty? && stripped == stripped.upcase && !stripped.start_with?(" ")
630
+
631
+ return stripped if stripped.start_with?(" #{member.name} (#{member.version})")
632
+ end
633
+ nil
634
+ end
635
+
636
+ def annotate_dependency_floor_lockfile_result(result, attempts)
637
+ return unless result
638
+
639
+ if result.ok?
640
+ message = "refreshed dependency floor lockfiles after #{attempts} attempt(s)"
641
+ result.stdout = [result.stdout, message].reject(&:empty?).join("\n")
642
+ else
643
+ result.reason = "dependency floor lockfile refresh failed after #{attempts} attempt(s)"
644
+ end
645
+ end
646
+
565
647
  def commit_dependency_floor_changes(dependent_members:, runner:, memo:)
566
648
  by_name = members.to_h { |member| [member.name, member] }
567
649
  dependent_members.each do |member_name|
data.tar.gz.sig CHANGED
@@ -1,2 +1,2 @@
1
- w����y�/�J��ө��Uv]��K���I��BL0��<���_����2��}ު~�ˇ���惴��toz]rKwg;{[� ݖ���?^غ�b}�����y�X�+�p1ݕ���k�|� uN��V[~Lk "�+��Zw����Y]�k�f��M�R���P&���iY>넄|���6��<���9�;����
2
- t�3R)�TT�S�S_�׻L���3$��p��M�����G8r ���i��'3n�?�54�� K/h��kb�xm����Gw�BiTVpsvf�|���l{��C���)��ɀ6{uV�lGL��I^��+�~����w.={�旐�Lq36��Z)�TM�d �'l�
1
+ in��xD�˯6�-kԇC!:A���G�K��%�� �!~�n>�25c�:�0<��s��?}���ĈrP0���Bz�%� �j��Ffs98��2����B�{�#�@ �<��\u J�Bd �`�'��q��vJ��&�B_k��}��͟#��Ǣ�
2
+ ����y���8
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.0.4
4
+ version: 1.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -74,7 +74,7 @@ dependencies:
74
74
  version: '2.3'
75
75
  - - ">="
76
76
  - !ruby/object:Gem::Version
77
- version: 2.3.4
77
+ version: 2.3.10
78
78
  type: :runtime
79
79
  prerelease: false
80
80
  version_requirements: !ruby/object:Gem::Requirement
@@ -84,7 +84,7 @@ dependencies:
84
84
  version: '2.3'
85
85
  - - ">="
86
86
  - !ruby/object:Gem::Version
87
- version: 2.3.4
87
+ version: 2.3.10
88
88
  - !ruby/object:Gem::Dependency
89
89
  name: kettle-test
90
90
  requirement: !ruby/object:Gem::Requirement
@@ -94,7 +94,7 @@ dependencies:
94
94
  version: '2.0'
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
- version: 2.0.10
97
+ version: 2.0.12
98
98
  type: :runtime
99
99
  prerelease: false
100
100
  version_requirements: !ruby/object:Gem::Requirement
@@ -104,7 +104,7 @@ dependencies:
104
104
  version: '2.0'
105
105
  - - ">="
106
106
  - !ruby/object:Gem::Version
107
- version: 2.0.10
107
+ version: 2.0.12
108
108
  - !ruby/object:Gem::Dependency
109
109
  name: tsort
110
110
  requirement: !ruby/object:Gem::Requirement
@@ -304,10 +304,10 @@ licenses:
304
304
  - AGPL-3.0-only
305
305
  metadata:
306
306
  homepage_uri: https://kettle-family.galtzo.com
307
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.0.4
308
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.0.4/CHANGELOG.md
307
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.0.5
308
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.0.5/CHANGELOG.md
309
309
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
310
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.0.4
310
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.0.5
311
311
  funding_uri: https://github.com/sponsors/pboling
312
312
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
313
313
  news_uri: https://www.railsbling.com/tags/kettle-family
@@ -337,7 +337,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
337
337
  - !ruby/object:Gem::Version
338
338
  version: '0'
339
339
  requirements: []
340
- rubygems_version: 4.0.16
340
+ rubygems_version: 4.0.17
341
341
  specification_version: 4
342
342
  summary: "\U0001F469‍\U0001F469‍\U0001F467‍\U0001F467 CLI helpers for managing related
343
343
  Ruby gems as a family."
metadata.gz.sig CHANGED
Binary file