kettle-family 1.3.6 → 1.3.7

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: 8d912f3b7a33740ace19f7f0568177189b662819bec18bf94312de460f640fd9
4
- data.tar.gz: d70a03c67ed0a96ce645d00ff769a920b500b3d1d3b231324a2d8170ea683cf1
3
+ metadata.gz: ec4f59e4397fe55738de3f0e3e482a2053d3c771716e9d94de40478f76ba80ae
4
+ data.tar.gz: 7ea569f07fa6cc8cc46cf5351cf93441ece85870c80ab51aed2458e0ce66346a
5
5
  SHA512:
6
- metadata.gz: ca24fe1d3dff219117a561ecd8ff6d45edd6c1a4d4fb6db9172c874f8964676162747de8d3d45fa0caf1aa7707ec2733141eda5a993bbbcbc8d0fd81d5f8543c
7
- data.tar.gz: b3f48464d35121fdee21d6d23158e81630ac373676a6581d876f2887e735cc60d5cb70c4a87359090b124a56707b544d13dd1b30f5745ecf93ef81775eb03bed
6
+ metadata.gz: 9dac940565396b2209bd67e957af7f2e015318d70572c05cca684e8de26c43d75755a142727d994baa5fbd646ce674fa7ab6c84bdb913ca9c6685824fd3beca6
7
+ data.tar.gz: 53c75a31e0896365a67c7b0ec46a45830d1e71abe37843dc715ee7f086f39166fbf39c36620efc0760d00848a6ce11238da0ee81cf84cfa6ced5e192e13178e4
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,32 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.3.7] - 2026-10-09
34
+
35
+ - TAG: [v1.3.7][1.3.7t]
36
+ - COVERAGE: 93.30% -- 6778/7265 lines in 38 files
37
+ - BRANCH COVERAGE: 77.95% -- 2757/3537 branches in 38 files
38
+ - 34.47% documented
39
+
40
+ ### Changed
41
+
42
+ - Reject --start-at for template and test instead of silently dropping members. Those commands dispatch in dependency waves and a concurrent queue, so the set of members that still need work is not a suffix of the configured order and no positional value can select it. The flag now exits 1 with a message pointing at the equivalent --only selection. release is deliberately left accepting --start-at: it uses MEMBER@BRANCH to select a branch lane via release_target_branches, and its own resume hints already use --only rather than --start-at. Sequentially dispatched commands (lint, bup, check, docs, install, sync, push, pull, bump) keep positional --start-at, which is correct because they abort at the first failure so pending genuinely is a suffix.
43
+
44
+ - [kc] kettle-jem/prepare: updated 5 project files:
45
+ - dependencies (5)
46
+
47
+ - [kc] kettle-jem/template: updated 2 project files:
48
+ - code and tests (1)
49
+ - other (1)
50
+
51
+ ### Fixed
52
+
53
+ - Fix clean-unreleased missing locally installed unreleased versions. Under `bundle exec`, every RubyGems enumeration API (find_all_by_name, stubs_for, Specification.all) reports only the versions the active bundle resolves, not everything installed in the gem home. An unreleased version that was built and installed but is not pinned by the lockfile was therefore invisible, so cleanup reported "no unreleased installed versions found" with outcome success while leaving the offender installed -- blind in exactly the state it exists to correct, since re-resolving the lockfile to released versions is what hides the offender. Installed versions are now read by scanning the specification directories under each Gem.path root, which sees every installed version regardless of bundle activation and still excludes PATH/git source checkouts. Verified against the kettle-dev family: the fix now surfaces 10 unreleased members (kettle-rb 0.1.16, kettle-test 2.0.24, token-resolver 2.0.13, and others) that the previous implementation reported as none, while correctly leaving the two released members (kettle-dev 3.1.8, kettle-family 1.3.6) alone.
54
+
55
+ - Fix clean-unreleased --execute reporting success when gem uninstall removes nothing. RubyGems exits 0 and prints "Gem 'name' is not installed" when the target is absent from GEM_HOME, so an exit-status check could not distinguish a completed cleanup from one that silently did nothing -- the same silent-success failure mode as the enumeration bug. The batch outcome is now verified by re-enumerating installed versions afterwards, and any survivor turns the result into a failure that names it. Investigated and deliberately not implemented: `gem uninstall -i <root>` is not a workaround for a gem home other than GEM_HOME, because it fails with the same "not installed in GEM_HOME" error even when the path equals GEM_HOME. That case already fails loudly with a non-zero exit rather than being reported as success.
56
+
57
+ - Fix the resume hint for template and test silently dropping members that never ran. Those commands do not dispatch positionally: template groups members into dependency waves derived from gemspec dependencies, test uses a concurrent work queue, and in both a failure only sets a stop flag that prevents threads from popping further work -- members already in flight finish while later waves never run. The pending set is therefore interleaved through the configured order rather than being a suffix of it, so the emitted positional `--start-at <failed member>` dropped every pending member ordered ahead of the failure. Measured on the kettle-dev family: nomono failed in template wave 1 and left kettle-dev, kettle-changelog, kettle-family and kettle-soup-cover pending at ordered positions 6, 8, 9 and 11 while kettle-drift at 7 and kettle-wash at 10 had succeeded, so `--start-at nomono` resumed only nomono and token-resolver and reported outcome success having never templated the other four. Resume hints for these commands are now set-based, mirroring release: one `--only` hint per failure plus a combined `--only` hint naming every member that never ran. Sequentially dispatched commands keep positional `--start-at`, which remains correct because they abort at the first failure. A failed family-root phase still restarts the whole command, since it aborts before member dispatch. Note that template and test ignore the configured `release.waves`, which only affect release ordering.
58
+
33
59
  ## [1.3.6] - 2026-10-07
34
60
 
35
61
  - TAG: [v1.3.6][1.3.6t]
@@ -2905,7 +2931,9 @@ Please file a bug if you notice a violation of semantic versioning.
2905
2931
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
2906
2932
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
2907
2933
 
2908
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.6...HEAD
2934
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.7...HEAD
2935
+ [1.3.7]: https://github.com/kettle-dev/kettle-family/compare/v1.3.6...v1.3.7
2936
+ [1.3.7t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.7
2909
2937
  [1.3.6]: https://github.com/kettle-dev/kettle-family/compare/v1.3.5...v1.3.6
2910
2938
  [1.3.6t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.6
2911
2939
  [1.3.5]: https://github.com/kettle-dev/kettle-family/compare/v1.3.4...v1.3.5
@@ -715,6 +715,7 @@ module Kettle
715
715
  def build_report(command, options)
716
716
  config = Config.load(root: options[:root], path: options[:config])
717
717
  start_at = parse_start_at(options[:start_at])
718
+ Selection.validate_positional_start_at!(command, start_at.member)
718
719
  effective_only = default_only_filter(command: command, only: options[:only])
719
720
  discovery = Discovery.new(
720
721
  config: config,
@@ -711,11 +711,42 @@ module Kettle
711
711
  release_resume_hints
712
712
  end
713
713
 
714
+ # Commands whose member dispatch is not positional are defined once, on
715
+ # Selection, because that is also where --start-at is rejected for them.
716
+ # Report needs the same classification to emit a set-based resume hint
717
+ # instead of a positional one. See the documentation on
718
+ # Selection::NON_POSITIONAL_DISPATCH_COMMANDS for the measured evidence.
719
+ def non_positional_dispatch?
720
+ Selection.non_positional_dispatch?(command)
721
+ end
722
+
714
723
  def non_release_resume_hints
724
+ return non_positional_resume_hints if non_positional_dispatch?
725
+
715
726
  failed = results.find { |result| !result.ok? }
716
727
  failed ? [resume_hint_for(failed)] : []
717
728
  end
718
729
 
730
+ # Mirrors release_resume_hints: one hint per failure, plus a single set-based
731
+ # hint naming every member that never ran. --only selects by name rather
732
+ # than position, so it resumes exactly the unfinished members regardless of
733
+ # how dispatch ordered them, and the wave grouping is re-derived from the
734
+ # selected subset.
735
+ #
736
+ # A family-root failure takes precedence and suppresses the set-based hint:
737
+ # that phase aborts the entire run before member dispatch, so every member
738
+ # needs the whole command re-run rather than a subset resumed by name.
739
+ def non_positional_resume_hints
740
+ root_failure = results.find { |result| !result.ok? && family_root_phase?(result.phase) }
741
+ return [resume_hint_for(root_failure)] if root_failure
742
+
743
+ failed_hints = visible_results.reject(&:ok?).filter_map { |result| resume_hint_for(result) }.uniq
744
+ pending_names = summary_pending.map { |entry| entry.fetch("member") }
745
+ return failed_hints if pending_names.empty?
746
+
747
+ [*failed_hints, "kettle-family #{command} --only #{pending_names.join(",")}"]
748
+ end
749
+
719
750
  def release_resume_hints
720
751
  failed_hints = visible_results.reject(&:ok?).filter_map { |result| resume_hint_for(result) }.uniq
721
752
  pending_names = summary_pending.map { |entry| entry.fetch("member") }
@@ -726,11 +757,18 @@ module Kettle
726
757
 
727
758
  def resume_hint_for(result)
728
759
  return release_resume_hint(result) if command == "release"
729
- return "kettle-family #{command}" if result.phase.start_with?("family_root_") || result.phase == "commit_family_root_bundle_update"
760
+ return "kettle-family #{command}" if family_root_phase?(result.phase)
761
+ # A positional --start-at would silently drop pending members that are
762
+ # ordered ahead of this one, so wave-dispatched commands resume by name.
763
+ return "kettle-family #{command} --only #{result.member_name}" if non_positional_dispatch?
730
764
 
731
765
  "kettle-family #{command} --start-at #{result.member_name}"
732
766
  end
733
767
 
768
+ def family_root_phase?(phase)
769
+ phase.to_s.start_with?("family_root_") || phase == "commit_family_root_bundle_update"
770
+ end
771
+
734
772
  def release_resume_hint(result)
735
773
  return result.resume_command if result.respond_to?(:resume_command) && !result.resume_command.to_s.empty?
736
774
  return nil if result.phase == "aggregate_github_release"
@@ -13,6 +13,43 @@ module Kettle
13
13
  "bump" => "bump_release_pending"
14
14
  }.freeze
15
15
 
16
+ # Commands whose member dispatch is not positional, so `--start-at` cannot
17
+ # express "the members that still need work" for them.
18
+ #
19
+ # template groups members into dependency waves derived from gemspec
20
+ # dependencies and test uses a concurrent work queue. In both, a failure only
21
+ # sets a stop flag that prevents threads from popping further work: members
22
+ # already in flight finish while later waves never run. The pending set is
23
+ # therefore interleaved through the configured order rather than being a
24
+ # suffix of it, so no --start-at value selects exactly that set.
25
+ #
26
+ # Measured on the kettle-dev family: nomono failed in template wave 1 and
27
+ # left kettle-dev, kettle-changelog, kettle-family and kettle-soup-cover
28
+ # pending at ordered positions 6, 8, 9 and 11, while kettle-drift at 7 and
29
+ # kettle-wash at 10 had already succeeded. The pending positions were
30
+ # [5, 7, 8, 10] against a required suffix of [9, 10, 11, 12].
31
+ #
32
+ # These commands ignore the configured `release.waves`, which only feed
33
+ # display_members_for and so only affect release ordering.
34
+ NON_POSITIONAL_DISPATCH_COMMANDS = %w[template test].freeze
35
+
36
+ def self.non_positional_dispatch?(command)
37
+ NON_POSITIONAL_DISPATCH_COMMANDS.include?(command)
38
+ end
39
+
40
+ # Rejects `--start-at` for commands whose dispatch is not positional, rather
41
+ # than letting it silently drop members. Mirrors the class-method validation
42
+ # convention used by validate_release_state_only_filter! so callers in the CLI
43
+ # do not have to know which commands are affected.
44
+ def self.validate_positional_start_at!(command, start_at)
45
+ return if start_at.nil? || start_at.to_s.empty?
46
+ return unless non_positional_dispatch?(command)
47
+
48
+ raise Error, "--start-at is not supported for #{command}: members are dispatched in dependency " \
49
+ "waves, so a positional resume would silently skip members that never ran. " \
50
+ "Use --only #{start_at} (or the resume hint from the previous report) instead."
51
+ end
52
+
16
53
  def self.status_tokens
17
54
  STATUS_TOKEN_KEYS.keys
18
55
  end
@@ -102,13 +102,14 @@ module Kettle
102
102
  # Installed versions newer than anything published, as [name, version] pairs.
103
103
  #
104
104
  # Only genuinely installed gems count. Under `bundle exec`, bundler adds
105
- # PATH and git sources to the load path, so
106
- # Gem::Specification.find_all_by_name also returns specs whose
107
- # full_gem_path points at a source checkout rather than a gem home — e.g.
108
- # tree_haver 7.1.10 resolving to gems/tree_haver. Those are not installed
109
- # gems and `gem uninstall` cannot remove them, so treating them as
110
- # candidates would report work that can never succeed. Filtering on
111
- # Gem.path membership excludes them.
105
+ # PATH and git sources to the load path, so a source checkout can appear as
106
+ # a spec — e.g. tree_haver 7.1.10 resolving to gems/tree_haver rather than a
107
+ # gem home. That is not an installed gem and `gem uninstall` cannot remove
108
+ # it, so treating it as a candidate would report work that can never
109
+ # succeed, and once batched one such candidate would abort the whole
110
+ # removal. installed_versions therefore scans only specification
111
+ # directories under a Gem.path root, which excludes source checkouts by
112
+ # construction.
112
113
  def unreleased_candidates(member:, release_state:)
113
114
  return [] unless release_state&.ok?
114
115
 
@@ -130,21 +131,53 @@ module Kettle
130
131
  nil
131
132
  end
132
133
 
134
+ # Versions present in a gem home, oldest first.
135
+ #
136
+ # This scans the filesystem rather than asking RubyGems, because every
137
+ # RubyGems enumeration API is bundle-scoped: under `bundle exec`,
138
+ # find_all_by_name, stubs_for and Specification.all all report only the
139
+ # versions the active bundle resolves. An unreleased version installed in
140
+ # the gem home but not pinned by the lockfile is therefore invisible, which
141
+ # makes cleanup blind in exactly the state it exists to correct — once the
142
+ # lockfile re-resolves to released versions, the offenders it should remove
143
+ # stop being enumerated and the run reports "no unreleased installed
144
+ # versions found" with outcome success while leaving them installed.
145
+ # Observed live with kettle-rb: `gem list` showed 0.1.14, 0.1.15 and
146
+ # 0.1.16, while find_all_by_name under bundle exec returned only 0.1.15.
133
147
  def installed_versions(name)
134
- Gem::Specification.find_all_by_name(name)
135
- .select { |spec| installed_spec?(spec) }
136
- .map(&:version)
137
- .uniq
138
- .sort
148
+ gem_home_specification_dirs.flat_map { |dir| installed_spec_versions_in(dir, name) }.uniq.sort
139
149
  end
140
150
 
141
- def installed_spec?(spec)
142
- path = spec.full_gem_path.to_s
143
- return false if path.empty?
151
+ # Only specification directories under a Gem.path root are scanned. A PATH
152
+ # or git source checkout that bundler put on the load path is not in a gem
153
+ # home, so it stays excluded: `gem uninstall` cannot remove one, and one
154
+ # bogus candidate would abort the whole batch.
155
+ def gem_home_specification_dirs
156
+ Gem::Specification.dirs.select do |dir|
157
+ path = dir.to_s
158
+ !path.empty? && Gem.path.any? { |root| path.start_with?("#{root}#{File::SEPARATOR}") }
159
+ end
160
+ end
144
161
 
145
- Gem.path.any? { |root| path.start_with?("#{root}#{File::SEPARATOR}") }
146
- rescue
147
- false
162
+ # Specifications are stored as `name-VERSION.gemspec`, or
163
+ # `name-VERSION-PLATFORM.gemspec` for a native gem. The version is the part
164
+ # before the first hyphen, which matches Gem::Specification#version and so
165
+ # excludes the platform exactly as the previous implementation did. A
166
+ # filename whose version part does not parse is skipped rather than guessed
167
+ # at, so a similarly named gem (name-extra) cannot become a candidate.
168
+ def installed_spec_versions_in(dir, name)
169
+ prefix = "#{name}-"
170
+ Dir.glob(File.join(dir, "#{prefix}*.gemspec")).filter_map do |file|
171
+ version_text = File.basename(file, ".gemspec").delete_prefix(prefix).split("-", 2).first.to_s
172
+ version = parse_version(version_text)
173
+ version unless version.nil?
174
+ end
175
+ end
176
+
177
+ def parse_version(text)
178
+ Gem::Version.new(text)
179
+ rescue ArgumentError, TypeError
180
+ nil
148
181
  end
149
182
 
150
183
  # Runs the batch once, then reports that one outcome against every member
@@ -159,13 +192,42 @@ module Kettle
159
192
  # gem uninstall does not depend on the working directory, so the first
160
193
  # contributing member provides a real directory to execute in.
161
194
  host = contributors.first.fetch(:member)
162
- outcome = execute ? runner.call(member: host, phase: PHASE, command: command) : dry_run_outcome(command)
195
+ outcome = execute ? verified_outcome(candidates, host, command) : dry_run_outcome(command)
163
196
 
164
197
  contributors.map do |entry|
165
198
  member_result(entry.fetch(:member), outcome, entry.fetch(:candidates))
166
199
  end
167
200
  end
168
201
 
202
+ # `gem uninstall` exits 0 and prints "Gem 'name' is not installed" when it
203
+ # removes nothing, so the exit status alone cannot distinguish a completed
204
+ # cleanup from one that found the wrong gem home and silently did nothing.
205
+ # Reporting ok in that case is the same failure mode as the enumeration bug
206
+ # this class exists to prevent: a success verdict over work that never
207
+ # happened. The candidates are therefore re-enumerated after the batch, and
208
+ # any version still present turns the outcome into a failure that names it.
209
+ def verified_outcome(candidates, host, command)
210
+ outcome = runner.call(member: host, phase: PHASE, command: command)
211
+ return outcome unless outcome.success && !outcome.skipped
212
+
213
+ remaining = candidates.select { |name, version| installed_versions(name).include?(version) }
214
+ return outcome if remaining.empty?
215
+
216
+ CommandResult.new(
217
+ member_name: nil,
218
+ phase: PHASE,
219
+ command: command,
220
+ workdir: host.root,
221
+ status: outcome.status,
222
+ success: false,
223
+ stdout: outcome.stdout.to_s,
224
+ stderr: "#{outcome.stderr}still installed after gem uninstall: #{describe(remaining)}".strip,
225
+ elapsed_seconds: outcome.elapsed_seconds,
226
+ skipped: false,
227
+ reason: "gem uninstall reported success but #{describe(remaining)} remains installed"
228
+ )
229
+ end
230
+
169
231
  def dry_run_outcome(command)
170
232
  CommandResult.new(
171
233
  member_name: nil,
@@ -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.3.6"
8
+ VERSION = "1.3.7"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
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.3.6
4
+ version: 1.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -324,7 +324,7 @@ dependencies:
324
324
  version: '3.2'
325
325
  - - ">="
326
326
  - !ruby/object:Gem::Version
327
- version: 3.2.13
327
+ version: 3.2.14
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.13
337
+ version: 3.2.14
338
338
  - !ruby/object:Gem::Dependency
339
339
  name: ruby-progressbar
340
340
  requirement: !ruby/object:Gem::Requirement
@@ -389,6 +389,26 @@ dependencies:
389
389
  - - ">="
390
390
  - !ruby/object:Gem::Version
391
391
  version: 2.0.15
392
+ - !ruby/object:Gem::Dependency
393
+ name: gem_mine
394
+ requirement: !ruby/object:Gem::Requirement
395
+ requirements:
396
+ - - "~>"
397
+ - !ruby/object:Gem::Version
398
+ version: '0.1'
399
+ - - ">="
400
+ - !ruby/object:Gem::Version
401
+ version: 0.1.3
402
+ type: :development
403
+ prerelease: false
404
+ version_requirements: !ruby/object:Gem::Requirement
405
+ requirements:
406
+ - - "~>"
407
+ - !ruby/object:Gem::Version
408
+ version: '0.1'
409
+ - - ">="
410
+ - !ruby/object:Gem::Version
411
+ version: 0.1.3
392
412
  - !ruby/object:Gem::Dependency
393
413
  name: webrick
394
414
  requirement: !ruby/object:Gem::Requirement
@@ -460,10 +480,10 @@ licenses:
460
480
  - AGPL-3.0-only
461
481
  metadata:
462
482
  homepage_uri: https://kettle-family.galtzo.com
463
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.6
464
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.6/CHANGELOG.md
483
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.7
484
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.7/CHANGELOG.md
465
485
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
466
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.6
486
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.7
467
487
  funding_uri: https://github.com/sponsors/pboling
468
488
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
469
489
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file