kettle-family 1.3.5 → 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: 3e18a444e22f9d52b19ef8396ce75bc80b85bb579fb353889fecb8547ddc8bec
4
- data.tar.gz: b91bb8aa9018297be574e1eda7ddc26c6b8f314692cd4c4881c95fa4fe52062a
3
+ metadata.gz: ec4f59e4397fe55738de3f0e3e482a2053d3c771716e9d94de40478f76ba80ae
4
+ data.tar.gz: 7ea569f07fa6cc8cc46cf5351cf93441ece85870c80ab51aed2458e0ce66346a
5
5
  SHA512:
6
- metadata.gz: 50d83077bd30c1c673772be79765f02d263cf499b382355f8b647889255abb9608155e8a0434d6c98d549ddad5188186f50e56b9608537e47b1cb5f2c1cfec5a
7
- data.tar.gz: e81978953b9a9d94d2c518d770cd72e6b6efbcd5a3fd2c010da97f72cb9107d71624bd126cdf1799c1e17276f3546adaca3246089e151b6bccc7edc04db4cef2
6
+ metadata.gz: 9dac940565396b2209bd67e957af7f2e015318d70572c05cca684e8de26c43d75755a142727d994baa5fbd646ce674fa7ab6c84bdb913ca9c6685824fd3beca6
7
+ data.tar.gz: 53c75a31e0896365a67c7b0ec46a45830d1e71abe37843dc715ee7f086f39166fbf39c36620efc0760d00848a6ce11238da0ee81cf84cfa6ced5e192e13178e4
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,53 @@ 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
+
59
+ ## [1.3.6] - 2026-10-07
60
+
61
+ - TAG: [v1.3.6][1.3.6t]
62
+ - COVERAGE: 93.25% -- 6743/7231 lines in 38 files
63
+ - BRANCH COVERAGE: 77.82% -- 2740/3521 branches in 38 files
64
+ - 34.02% documented
65
+
66
+ ### Added
67
+
68
+ - [kc] published-version-check: `Kettle::Family::PublishedVersionCheck` detects lockfile pins that no registry serves — the signature of a locally built and installed gem leaking into a lockfile. Bundler resolves against the local gem dir, so an ordinary `bundle install` can pin such a version, and the entry it writes is a normal GEM-section line carrying a valid checksum taken from the installed spec, so nothing in the lockfile text distinguishes it and there is no offline marker either (installed specs look the same as fetched ones, and `Gem::Specification` has no `#remote`). Only asking the registry can tell. A pinned native gem is compared after splitting off its platform suffix, because a lockfile pins `nokogiri (1.19.4-x86_64-linux-gnu)` while the registry lists bare `1.19.4` once per platform; comparing the pinned string directly reported every native gem as unpublished, adding 10 false positives to one real lockfile. Prerelease versions are unaffected because they are dot-separated (`1.0.0.rc1`) and RubyGems version strings cannot contain a hyphen, so the first hyphen is always the version/platform boundary. Runs in `bup`/`bupb`/`update-bundler` before committing a lockfile update, and in release preflight, where a bad pin previously surfaced much later as an opaque bundler error during lockfile reset.
69
+
70
+ - [kc] published-version-memo: The published-version check fails open: when a registry cannot be consulted the version counts as unverifiable rather than unpublished, so a registry outage cannot turn every family member into a false failure. Registry answers are memoized per process keyed by gem, remote and version, so members sharing toolchain and sibling dependencies query the registry once for the whole run rather than once each. The memo is subordinate to the on-disk release marker that `kettle-release` writes after confirming a gem is available: a gem published during the current run is always re-queried rather than answered from a pre-publish memo entry, which would otherwise report the just-published version as unpublished and block a legitimate release.
71
+
72
+ - [kc] dependency-floor-published-version-gate: `kettle-family release` now gates the "Reconcile family dependencies" commit on the lockfile it is about to record. The preceding `bundle install` resolves against the local gem dir, so it can pin a locally built and installed version, and committing that produces a lockfile no isolated environment can resolve. The failure previously surfaced much later as an opaque bundler error during release lockfile reset ("Your bundle is locked to kettle-jem (7.1.29) ... but that version can no longer be found") rather than at the moment the bad pin was introduced; observed live, a release run's own reconcile commit flipped kettle-jem 7.1.28 to 7.1.29 in the tracked `Gemfile.lock` and then aborted at step 0. The existing floor diagnostics do not catch this, because they validate checksums only for released family members, while the pins that break resolution are typically toolchain dependencies that are not members at all and carry a valid checksum. Gems published during the same run are not blocked, since the check consults the on-disk release marker ahead of its process memo.
73
+
74
+ ### Fixed
75
+
76
+ - [kc] clean-unreleased-batch: `kettle-family clean-unreleased` now removes every unreleased installed version across the family in a single `gem uninstall` invocation instead of one invocation per gem. This is not merely fewer processes: RubyGems' `uninstall_specific` builds a `Gem::DependencyList` from every requested gem and removes them via `strongly_connected_components.flatten.reverse`, a topological sort of the whole set that removes dependents before their dependencies. A per-gem invocation sees only one gem and cannot sort, so it raises `Gem::DependencyRemovalException` ("Uninstallation aborted due to dependent gem(s)") whenever another installed gem still depends on it. Because family members are interdependent and the release order is dependency order, per-gem removal failed on the foundations: observed live, removing tree_haver first failed while ast-merge still required it, and four repeated passes still left two gems installed. One batched invocation removed both. Versions are passed as `name:version` arguments since RubyGems rejects `--version` alongside multiple gems, and `--all` is deliberately omitted because with gem arguments it is redundant while with none it means `uninstall_all`, which would remove every gem in the gem home.
77
+
78
+ - [kc] clean-unreleased-source-checkouts: `kettle-family clean-unreleased` no longer reports source checkouts as unreleased installed gems. Under `bundle exec`, bundler adds PATH and git sources to the load path, so `Gem::Specification.find_all_by_name` also returns specs whose `full_gem_path` points at a working copy rather than a gem home — for example tree_haver 7.1.10 resolving to `gems/tree_haver`. Those are not installed gems and `gem uninstall` cannot remove them, so the command reported work that could never succeed; after uninstalling all 31 genuinely installed unreleased gems in the structuredmerge family, it still listed all 31. Candidates are now filtered to specs under `Gem.path`.
79
+
33
80
  ## [1.3.5] - 2026-10-05
34
81
 
35
82
  - TAG: [v1.3.5][1.3.5t]
@@ -2884,7 +2931,11 @@ Please file a bug if you notice a violation of semantic versioning.
2884
2931
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
2885
2932
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
2886
2933
 
2887
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.5...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
2937
+ [1.3.6]: https://github.com/kettle-dev/kettle-family/compare/v1.3.5...v1.3.6
2938
+ [1.3.6t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.6
2888
2939
  [1.3.5]: https://github.com/kettle-dev/kettle-family/compare/v1.3.4...v1.3.5
2889
2940
  [1.3.5t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.5
2890
2941
  [1.3.4]: https://github.com/kettle-dev/kettle-family/compare/v1.3.3...v1.3.4
data/README.md CHANGED
@@ -1192,7 +1192,7 @@ Thanks for RTFM. ☺️
1192
1192
  [📌gitmoji]: https://gitmoji.dev
1193
1193
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1194
1194
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1195
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-7.123-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1195
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-7.231-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1196
1196
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
1197
1197
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1198
1198
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -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,
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kettle/dev/lockfile_reset"
4
+ require "kettle/dev/ruby_gems_versions"
5
+
6
+ module Kettle
7
+ module Family
8
+ # Detects versions pinned in a release lockfile that no registry actually
9
+ # serves — the signature of a locally built and installed gem leaking into
10
+ # a lockfile.
11
+ #
12
+ # Why this is needed at all: bundler resolves against the local gem dir, so
13
+ # an ordinary `bundle install` will happily pin a locally installed version.
14
+ # The resulting entry is a normal GEM-section line carrying a valid checksum
15
+ # taken from the installed spec, so it is indistinguishable from a published
16
+ # gem by inspection. There is also no offline marker for a local build —
17
+ # installed specs look the same as fetched ones, and Gem::Specification has
18
+ # no #remote — so the only reliable signal is asking the registry.
19
+ #
20
+ # Deliberately NOT applied to templating. Templating injects its own
21
+ # (often unreleased) version into a destination so it can run itself, and
22
+ # local templating from unreleased source is a routine workflow; gating that
23
+ # would break it. So this runs only where a lockfile is about to be
24
+ # committed or published, and templating's transient local pins get cleaned
25
+ # up later by a release-mode dependency update.
26
+ #
27
+ # Fails open: when the registry cannot be consulted the version counts as
28
+ # unverifiable rather than unpublished, so offline runs are never blocked by
29
+ # network conditions.
30
+ class PublishedVersionCheck
31
+ PHASE = "published_version_check"
32
+
33
+ # Process-local memo of registry answers, keyed by [gem name, remote].
34
+ #
35
+ # Two reasons this is safe and desirable:
36
+ #
37
+ # * Family members share almost all their toolchain and sibling
38
+ # dependencies, so one member's lookups serve every later member.
39
+ # Without this, a 30-member release re-queries the same gems 30
40
+ # times.
41
+ # * A single command invocation should see one consistent snapshot of
42
+ # the registry. Re-querying per member could otherwise report a gem
43
+ # as unpublished for early members and published for later ones if it
44
+ # lands mid-run.
45
+ #
46
+ # The memo is subordinate to the on-disk release marker, never above it.
47
+ # kettle-release writes ~/.local/state/kettle-dev/rubygems-cache-bust.json
48
+ # after publishing, so the filesystem is the authority on what this
49
+ # machine just released. A gem published *during* the current run — say
50
+ # kettle-dev 3.1.8, released before its dependents' floors are raised to
51
+ # it — would otherwise be answered from a pre-publish memo entry and
52
+ # falsely reported unpublished, blocking a legitimate release. So
53
+ # #published_versions always consults the marker first and skips the memo
54
+ # when it says that exact gem+version was just released. Verified: a
55
+ # pre-publish memo entry yields 0 live queries without the marker check
56
+ # and reports the new version missing.
57
+ #
58
+ # nil is memoized too, so an unreachable registry is asked once rather
59
+ # than once per pin. Lifetime is one process; nothing persists.
60
+ REGISTRY_MEMO = {}
61
+
62
+ def self.call(member:)
63
+ new(member: member).call
64
+ end
65
+
66
+ def initialize(member:)
67
+ @member = member
68
+ end
69
+
70
+ def call
71
+ result(diagnostics)
72
+ end
73
+
74
+ private
75
+
76
+ attr_reader :member
77
+
78
+ def diagnostics
79
+ lockfile = File.join(member.root, "Gemfile.lock")
80
+ return [] unless File.file?(lockfile)
81
+
82
+ registry_lockfile_specs(lockfile).filter_map { |spec| diagnostic_for(spec) }
83
+ end
84
+
85
+ def registry_lockfile_specs(lockfile)
86
+ Kettle::Dev::LockfileReset.registry_gem_specs_from_source(File.read(lockfile))
87
+ end
88
+
89
+ # A message for an unpublished pin, or nil when the version is published
90
+ # or the registry could not be consulted.
91
+ def diagnostic_for(spec)
92
+ return nil unless unpublished?(spec)
93
+
94
+ "release lockfile pins #{spec.fetch(:name)} #{spec.fetch(:version)}, " \
95
+ "which is not published on #{spec.fetch(:remote)} " \
96
+ "(locally installed but unreleased; run a release-mode dependency update to re-resolve)"
97
+ end
98
+
99
+ def unpublished?(spec)
100
+ remote = spec.fetch(:remote).to_s
101
+ return false if remote.empty?
102
+
103
+ pinned = version_without_platform(spec.fetch(:version))
104
+ versions = published_versions(spec.fetch(:name), remote, pinned)
105
+ return false if versions.nil?
106
+
107
+ !versions.include?(pinned)
108
+ end
109
+
110
+ # A lockfile pins a native gem as "1.19.4-x86_64-linux-gnu" while the
111
+ # registry lists its versions as bare "1.19.4", one entry per platform.
112
+ # Comparing the pinned string directly would report every native gem as
113
+ # unpublished, so split off the platform first.
114
+ #
115
+ # Splitting on the first "-" matches Bundler::LockfileParser. It is safe
116
+ # because RubyGems version strings cannot themselves contain a hyphen
117
+ # (prerelease segments are dot-separated, e.g. "1.0.0.rc1"), so the first
118
+ # hyphen is always the version/platform boundary.
119
+ def version_without_platform(pinned)
120
+ pinned.to_s.split("-", 2).first
121
+ end
122
+
123
+ # nil when the registry could not be consulted. Memoized per process,
124
+ # except for gems the on-disk release marker says were just published —
125
+ # see REGISTRY_MEMO for why bypassing the memo there is required.
126
+ def published_versions(name, remote, version)
127
+ key = [name, remote, version]
128
+ unless just_released?(name, version)
129
+ return REGISTRY_MEMO[key] if REGISTRY_MEMO.key?(key)
130
+ end
131
+
132
+ REGISTRY_MEMO[key] = Kettle::Dev::RubyGemsVersions.published_version_numbers(
133
+ name,
134
+ source: remote,
135
+ version: version
136
+ )
137
+ end
138
+
139
+ def just_released?(name, version)
140
+ return false if version.to_s.empty?
141
+
142
+ Kettle::Dev::RubyGemsVersions.recently_released?(name, version)
143
+ rescue
144
+ false
145
+ end
146
+
147
+ def result(found)
148
+ clean = found.empty?
149
+ CommandResult.new(
150
+ member_name: member.name,
151
+ phase: PHASE,
152
+ command: ["internal", PHASE],
153
+ workdir: member.root,
154
+ status: clean ? 0 : 1,
155
+ success: clean,
156
+ stdout: found.join("\n"),
157
+ stderr: "",
158
+ elapsed_seconds: 0.0,
159
+ skipped: false,
160
+ reason: clean ? nil : "lockfile pins versions no registry serves"
161
+ )
162
+ end
163
+ end
164
+ end
165
+ end
@@ -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
@@ -2,8 +2,49 @@
2
2
 
3
3
  module Kettle
4
4
  module Family
5
+ # Removes locally installed gem versions that no registry has published.
6
+ #
7
+ # Why this exists: bundler resolves against the local gem dir and picks the
8
+ # highest version satisfying a constraint, so a locally built unreleased
9
+ # version always wins over the published one. Floor constraints are `>=`, so
10
+ # there is no ceiling that stops it, and every `bundle install` or
11
+ # `bundle lock` re-pins the offender. Detection cannot fix resolution — the
12
+ # offenders have to be removed. Left in place they produce lockfiles that no
13
+ # isolated environment can resolve, which surfaces much later as an opaque
14
+ # bundler error ("locked to kettle-jem (7.1.29) ... can no longer be
15
+ # found").
16
+ #
17
+ # All candidates across the whole family are removed in ONE `gem uninstall`
18
+ # invocation rather than one invocation per gem. That is not merely fewer
19
+ # processes: RubyGems' `uninstall_specific` builds a Gem::DependencyList from
20
+ # every requested gem and removes them via
21
+ # `strongly_connected_components.flatten.reverse`, a topological sort of the
22
+ # whole set that removes dependents before their dependencies. A per-gem
23
+ # invocation sees only one gem and cannot sort, so it raises
24
+ # Gem::DependencyRemovalException ("Uninstallation aborted due to dependent
25
+ # gem(s)") whenever another installed gem still depends on it. Family
26
+ # members are heavily interdependent, so per-gem removal fails on the
27
+ # foundations: observed live, removing tree_haver first failed because
28
+ # ast-merge still required it, and four iterative passes still left two gems
29
+ # installed. One batched invocation removed both.
30
+ #
31
+ # The single run is still reported per member. Report keys results by member
32
+ # name for commands in MEMBER_RESULT_COMMANDS, and treats a member with no
33
+ # result as pending, which fails the whole summary. So attributing the batch
34
+ # to one synthetic family-level name left every member pending and reported
35
+ # `outcome: failure` with `failed: none`. Each contributing member instead
36
+ # receives the shared outcome of the one run.
37
+ #
38
+ # `--all` is deliberately not passed. With gem arguments present it is
39
+ # redundant (RubyGems routes to `uninstall_specific` either way), and with no
40
+ # arguments it means `uninstall_all`, which removes every gem in the gem
41
+ # home. Omitting it makes an empty argument list a plain usage error instead
42
+ # of a wipe. `--version` cannot be used either, because RubyGems rejects it
43
+ # alongside multiple gems; versions are given as `name:version` arguments,
44
+ # the form its own error message prescribes.
5
45
  class UnreleasedGemCleanup
6
46
  PHASE = "clean_unreleased"
47
+ UNKNOWN_RELEASED_MESSAGE = "latest released version is unknown; no cleanup attempted"
7
48
 
8
49
  def initialize(config:, members:, execute: false, runner: nil)
9
50
  @config = config
@@ -14,9 +55,24 @@ module Kettle
14
55
 
15
56
  def results
16
57
  release_states = release_state_by_member
17
- members.flat_map do |member|
18
- cleanup_results_for(member: member, release_state: release_states[member.name])
58
+ # Resolved once per member: candidates and the member's non-uninstall
59
+ # diagnostics both derive from the same release state and installed spec
60
+ # scan, so pairing them in one pass keeps installed specs enumerated a
61
+ # single time and avoids re-reading release_states per member.
62
+ planned = members.map do |member|
63
+ release_state = release_states[member.name]
64
+ candidates = unreleased_candidates(member: member, release_state: release_state)
65
+ {
66
+ member: member,
67
+ candidates: candidates,
68
+ diagnostics: member_diagnostics(member: member, release_state: release_state, candidates: candidates)
69
+ }
19
70
  end
71
+ diagnostics = planned.flat_map { |entry| entry.fetch(:diagnostics) }
72
+ contributors = planned.reject { |entry| entry.fetch(:candidates).empty? }
73
+ return diagnostics if contributors.empty?
74
+
75
+ diagnostics + batched_cleanup_results(contributors)
20
76
  end
21
77
 
22
78
  private
@@ -29,16 +85,41 @@ module Kettle
29
85
  end
30
86
  end
31
87
 
32
- def cleanup_results_for(member:, release_state:)
88
+ # Per-member outcomes that are not an uninstall: a release state that could
89
+ # not be read, a version that cannot be compared, or nothing to do. These
90
+ # stay per member so a failure names the member it came from. Members that
91
+ # do have candidates are reported by the batch result instead.
92
+ def member_diagnostics(member:, release_state:, candidates:)
33
93
  return [release_state_failure(member: member, release_state: release_state)] unless release_state&.ok?
34
94
 
35
95
  latest_released = latest_released_version(release_state.state)
36
- return [informational_result(member: member, message: "latest released version is unknown; no cleanup attempted")] unless latest_released
96
+ return [informational_result(member: member, message: UNKNOWN_RELEASED_MESSAGE)] unless latest_released
97
+ return [] unless candidates.empty?
98
+
99
+ [informational_result(member: member, message: "no unreleased installed versions found")]
100
+ end
37
101
 
38
- candidates = installed_versions(member.name).select { |version| version > latest_released }
39
- return [informational_result(member: member, message: "no unreleased installed versions found")] if candidates.empty?
102
+ # Installed versions newer than anything published, as [name, version] pairs.
103
+ #
104
+ # Only genuinely installed gems count. Under `bundle exec`, bundler adds
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.
113
+ def unreleased_candidates(member:, release_state:)
114
+ return [] unless release_state&.ok?
115
+
116
+ latest_released = latest_released_version(release_state.state)
117
+ return [] unless latest_released
40
118
 
41
- candidates.map { |version| cleanup_result(member: member, version: version) }
119
+ name = member.name
120
+ installed_versions(name).filter_map do |version|
121
+ [name, version] if version > latest_released
122
+ end
42
123
  end
43
124
 
44
125
  def latest_released_version(state)
@@ -50,22 +131,112 @@ module Kettle
50
131
  nil
51
132
  end
52
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.
53
147
  def installed_versions(name)
54
- Gem::Specification.find_all_by_name(name).map(&:version).uniq.sort
148
+ gem_home_specification_dirs.flat_map { |dir| installed_spec_versions_in(dir, name) }.uniq.sort
149
+ end
150
+
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
161
+
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
181
+ end
182
+
183
+ # Runs the batch once, then reports that one outcome against every member
184
+ # whose gems were in it. See the class comment for why per-member results
185
+ # are required even though only one command runs.
186
+ def batched_cleanup_results(contributors)
187
+ candidates = contributors.flat_map { |entry| entry.fetch(:candidates) }
188
+ # Versions are given as `name:version` arguments rather than `--version`,
189
+ # which RubyGems rejects alongside multiple gems. See the class comment
190
+ # for why `--all` is omitted.
191
+ command = ["gem", "uninstall", *candidates.map { |name, version| "#{name}:#{version}" }, "--executables"]
192
+ # gem uninstall does not depend on the working directory, so the first
193
+ # contributing member provides a real directory to execute in.
194
+ host = contributors.first.fetch(:member)
195
+ outcome = execute ? verified_outcome(candidates, host, command) : dry_run_outcome(command)
196
+
197
+ contributors.map do |entry|
198
+ member_result(entry.fetch(:member), outcome, entry.fetch(:candidates))
199
+ end
55
200
  end
56
201
 
57
- def cleanup_result(member:, version:)
58
- command = ["gem", "uninstall", member.name, "--version", version.to_s, "--executables", "--all"]
59
- return runner.call(member: member, phase: PHASE, command: command) if execute
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?
60
215
 
61
216
  CommandResult.new(
62
- member_name: member.name,
217
+ member_name: nil,
63
218
  phase: PHASE,
64
219
  command: command,
65
- workdir: member.root,
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
+
231
+ def dry_run_outcome(command)
232
+ CommandResult.new(
233
+ member_name: nil,
234
+ phase: PHASE,
235
+ command: command,
236
+ workdir: nil,
66
237
  status: nil,
67
238
  success: true,
68
- stdout: "would uninstall #{member.name} #{version}",
239
+ stdout: nil,
69
240
  stderr: "",
70
241
  elapsed_seconds: 0.0,
71
242
  skipped: true,
@@ -73,6 +244,32 @@ module Kettle
73
244
  )
74
245
  end
75
246
 
247
+ # One member's view of the shared batch outcome. The command and exit state
248
+ # are common to the whole batch, but each member reports only its own gems,
249
+ # so the per-member report stays readable and a failure names every gem
250
+ # that member contributed.
251
+ def member_result(member, outcome, own_candidates)
252
+ own = describe(own_candidates)
253
+ outcome_reason = outcome.reason
254
+ # Equivalent to `ok? ? reason : (own.empty? ? reason : batch message)`,
255
+ # with outcome.reason read once instead of twice.
256
+ reason = if outcome.ok? || own_candidates.empty?
257
+ outcome_reason
258
+ else
259
+ "batched gem uninstall including #{own} failed"
260
+ end
261
+ outcome.dup.tap do |copy|
262
+ copy.member_name = member.name
263
+ copy.workdir = member.root
264
+ copy.stdout = outcome.skipped ? "would uninstall #{own}" : outcome.stdout
265
+ copy.reason = reason
266
+ end
267
+ end
268
+
269
+ def describe(candidates)
270
+ candidates.map { |name, version| "#{name} #{version}" }.join(", ")
271
+ end
272
+
76
273
  def informational_result(member:, message:)
77
274
  CommandResult.new(
78
275
  member_name: member.name,
@@ -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.5"
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
@@ -2343,9 +2343,9 @@ module Kettle
2343
2343
  return memo
2344
2344
  end
2345
2345
 
2346
- append_release_internal_checks(member: member, memo: memo)
2347
- memo.last(2).each { |result| emit_member_result_progress(original_member, result, progress: progress) }
2348
- return memo unless memo.last(2).all?(&:ok?)
2346
+ internal_check_results = append_release_internal_checks(member: member, memo: memo)
2347
+ internal_check_results.each { |result| emit_member_result_progress(original_member, result, progress: progress) }
2348
+ return memo unless internal_check_results.all?(&:ok?)
2349
2349
 
2350
2350
  release_event_state = {}
2351
2351
  result = release_command_runner.call(
@@ -2526,9 +2526,9 @@ module Kettle
2526
2526
  end
2527
2527
  end
2528
2528
 
2529
- append_release_internal_checks(member: member, memo: memo)
2530
- memo.last(2).each { |result| emit_member_result_progress(member, result, progress: progress) }
2531
- return memo unless memo.last(2).all?(&:ok?)
2529
+ internal_check_results = append_release_internal_checks(member: member, memo: memo)
2530
+ internal_check_results.each { |result| emit_member_result_progress(member, result, progress: progress) }
2531
+ return memo unless internal_check_results.all?(&:ok?)
2532
2532
 
2533
2533
  release_event_state = {}
2534
2534
  release_result = runner.call(
@@ -2744,7 +2744,38 @@ module Kettle
2744
2744
  end
2745
2745
 
2746
2746
  reconciled_members = (floor_results.map(&:member_name) + lockfile_refresh_members.map { |member, _released_members| member.name }).uniq
2747
- commit_dependency_floor_changes(dependent_members: reconciled_members, runner: runner, memo: memo) if reconciled_members.any? && execute && commit
2747
+ return unless reconciled_members.any? && execute && commit
2748
+
2749
+ return unless validate_dependency_floor_published_versions(member_names: reconciled_members, memo: memo)
2750
+
2751
+ commit_dependency_floor_changes(dependent_members: reconciled_members, runner: runner, memo: memo)
2752
+ end
2753
+
2754
+ # Gates the "Reconcile family dependencies" commit on the lockfile it is
2755
+ # about to record.
2756
+ #
2757
+ # The preceding bundle install resolves against the local gem dir, so it
2758
+ # happily pins a locally built and installed version. Committing that
2759
+ # produces a lockfile no isolated environment can resolve, and the failure
2760
+ # surfaces much later as an opaque bundler error during release lockfile
2761
+ # reset ("locked to kettle-jem (7.1.29) ... can no longer be found")
2762
+ # rather than at the moment the bad pin was introduced. Observed live: a
2763
+ # release run's own reconcile commit flipped kettle-jem 7.1.28 -> 7.1.29
2764
+ # in the tracked Gemfile.lock and then aborted at step 0.
2765
+ #
2766
+ # Gems published during this very run are not a false positive, because
2767
+ # PublishedVersionCheck consults kettle-release's on-disk release marker
2768
+ # ahead of its process memo, and that marker is written only after the gem
2769
+ # is confirmed available on the registry.
2770
+ def validate_dependency_floor_published_versions(member_names:, memo:)
2771
+ by_name = members.to_h { |member| [member.name, member] }
2772
+ results = member_names.filter_map do |member_name|
2773
+ member = by_name[member_name]
2774
+ next unless member
2775
+
2776
+ PublishedVersionCheck.call(member: member).tap { |result| memo << result }
2777
+ end
2778
+ results.all?(&:ok?)
2748
2779
  end
2749
2780
 
2750
2781
  def dependent_members_depending_on(released_members:, dependent_members:)
@@ -3275,13 +3306,34 @@ module Kettle
3275
3306
  results.each { |result| result.branch = branch if result.respond_to?(:branch=) }
3276
3307
  end
3277
3308
 
3309
+ # Runs the internal pre-release checks for one member, appending each
3310
+ # result to +memo+. Returns the results appended by this call so callers
3311
+ # can inspect exactly them instead of relying on a positional
3312
+ # `memo.last(N)` window.
3313
+ #
3314
+ # Short-circuits on the first failure: a member with a broken lockfile
3315
+ # should not spend a release on its changelog, and the returned array
3316
+ # then holds only the failing result.
3278
3317
  def append_release_internal_checks(member:, memo:)
3279
- memo << ReadinessCheck.call(
3318
+ appended = []
3319
+ readiness = ReadinessCheck.call(
3280
3320
  member: member,
3281
3321
  config: config,
3282
3322
  allowed_local_path_roots: release_readiness_allowed_local_path_roots(member)
3283
3323
  )
3284
- memo << ChangelogCheck.call(member: member, config: config) if memo.last.ok?
3324
+ memo << readiness
3325
+ appended << readiness
3326
+ return appended unless readiness.ok?
3327
+
3328
+ published_versions = PublishedVersionCheck.call(member: member)
3329
+ memo << published_versions
3330
+ appended << published_versions
3331
+ return appended unless published_versions.ok?
3332
+
3333
+ changelog = ChangelogCheck.call(member: member, config: config)
3334
+ memo << changelog
3335
+ appended << changelog
3336
+ appended
3285
3337
  end
3286
3338
 
3287
3339
  def release_readiness_allowed_local_path_roots(member)
@@ -4597,8 +4649,16 @@ module Kettle
4597
4649
  total
4598
4650
  end
4599
4651
 
4652
+ # Progress denominator for one member's release. The base four are
4653
+ # check, published_version_check, release_changelog and release_build.
4654
+ #
4655
+ # This must stay in step with what append_release_internal_checks emits:
4656
+ # Progress#increment_member_count clamps the count to the total, so a
4657
+ # total that is too low silently saturates the progress display instead
4658
+ # of failing. Adding a release-internal phase without raising this base
4659
+ # would leave every member showing its final count early.
4600
4660
  def release_phase_total(member = nil)
4601
- total = 3
4661
+ total = 4
4602
4662
  if member
4603
4663
  normalize = normalize_release_lockfiles?(member)
4604
4664
  total += 2 if normalize
@@ -5723,8 +5783,14 @@ module Kettle
5723
5783
  end
5724
5784
 
5725
5785
  def release_lockfile_readiness_would_fail?(member)
5726
- result = ReadinessCheck.call(member: member, config: config, allowed_local_path_roots: release_allowed_local_path_roots)
5727
- result.stdout.lines.any? { |line| line.start_with?("release lockfile has local path remote at ") }
5786
+ readiness = ReadinessCheck.call(
5787
+ member: member,
5788
+ config: config,
5789
+ allowed_local_path_roots: release_allowed_local_path_roots
5790
+ )
5791
+ return true if readiness.stdout.lines.any? { |line| line.start_with?("release lockfile has local path remote at ") }
5792
+
5793
+ !PublishedVersionCheck.call(member: member).ok?
5728
5794
  end
5729
5795
 
5730
5796
  def release_lockfile_has_local_path_remote?(member)
@@ -5975,11 +6041,14 @@ module Kettle
5975
6041
  end
5976
6042
 
5977
6043
  def bundle_update_lockfile_diagnostics(member)
5978
- ReadinessCheck.call(
6044
+ readiness = ReadinessCheck.call(
5979
6045
  member: member,
5980
6046
  config: config,
5981
6047
  allowed_local_path_roots: bundle_update_allowed_local_path_roots
5982
- ).stdout.lines.grep(/^release lockfile has local path remote at /).map do |line|
6048
+ ).stdout.lines.grep(/^release lockfile has local path remote at /)
6049
+ unpublished = PublishedVersionCheck.call(member: member).stdout.lines
6050
+
6051
+ (readiness + unpublished).map do |line|
5983
6052
  line.sub("release lockfile", "bundle update lockfile").chomp
5984
6053
  end
5985
6054
  end
data/lib/kettle/family.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "family/command_result"
5
5
  require_relative "family/release_state_result"
6
6
  require_relative "family/command_runner"
7
7
  require_relative "family/readiness_check"
8
+ require_relative "family/published_version_check"
8
9
  require_relative "family/changelog_check"
9
10
  require_relative "family/git_status"
10
11
  require_relative "family/branch_lane_audit"
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.5
4
+ version: 1.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -94,7 +94,7 @@ dependencies:
94
94
  version: '3.1'
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
- version: 3.1.7
97
+ version: 3.1.8
98
98
  type: :runtime
99
99
  prerelease: false
100
100
  version_requirements: !ruby/object:Gem::Requirement
@@ -104,7 +104,7 @@ dependencies:
104
104
  version: '3.1'
105
105
  - - ">="
106
106
  - !ruby/object:Gem::Version
107
- version: 3.1.7
107
+ version: 3.1.8
108
108
  - !ruby/object:Gem::Dependency
109
109
  name: kettle-gha-pins
110
110
  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.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
@@ -436,6 +456,7 @@ files:
436
456
  - lib/kettle/family/nomono_bootstrap.rb
437
457
  - lib/kettle/family/orderer.rb
438
458
  - lib/kettle/family/paths.rb
459
+ - lib/kettle/family/published_version_check.rb
439
460
  - lib/kettle/family/readiness_check.rb
440
461
  - lib/kettle/family/release_graph.rb
441
462
  - lib/kettle/family/release_reconciler.rb
@@ -459,10 +480,10 @@ licenses:
459
480
  - AGPL-3.0-only
460
481
  metadata:
461
482
  homepage_uri: https://kettle-family.galtzo.com
462
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.5
463
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.5/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
464
485
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
465
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.5
486
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.7
466
487
  funding_uri: https://github.com/sponsors/pboling
467
488
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
468
489
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file