kettle-family 1.3.5 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +24 -1
- data/README.md +1 -1
- data/lib/kettle/family/published_version_check.rb +165 -0
- data/lib/kettle/family/unreleased_gem_cleanup.rb +149 -14
- data/lib/kettle/family/version.rb +1 -1
- data/lib/kettle/family/workflow.rb +83 -14
- data/lib/kettle/family.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +7 -6
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d912f3b7a33740ace19f7f0568177189b662819bec18bf94312de460f640fd9
|
|
4
|
+
data.tar.gz: d70a03c67ed0a96ce645d00ff769a920b500b3d1d3b231324a2d8170ea683cf1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca24fe1d3dff219117a561ecd8ff6d45edd6c1a4d4fb6db9172c874f8964676162747de8d3d45fa0caf1aa7707ec2733141eda5a993bbbcbc8d0fd81d5f8543c
|
|
7
|
+
data.tar.gz: b3f48464d35121fdee21d6d23158e81630ac373676a6581d876f2887e735cc60d5cb70c4a87359090b124a56707b544d13dd1b30f5745ecf93ef81775eb03bed
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,27 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [1.3.6] - 2026-10-07
|
|
34
|
+
|
|
35
|
+
- TAG: [v1.3.6][1.3.6t]
|
|
36
|
+
- COVERAGE: 93.25% -- 6743/7231 lines in 38 files
|
|
37
|
+
- BRANCH COVERAGE: 77.82% -- 2740/3521 branches in 38 files
|
|
38
|
+
- 34.02% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- [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.
|
|
43
|
+
|
|
44
|
+
- [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.
|
|
45
|
+
|
|
46
|
+
- [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.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- [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.
|
|
51
|
+
|
|
52
|
+
- [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`.
|
|
53
|
+
|
|
33
54
|
## [1.3.5] - 2026-10-05
|
|
34
55
|
|
|
35
56
|
- TAG: [v1.3.5][1.3.5t]
|
|
@@ -2884,7 +2905,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
2884
2905
|
- Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
|
|
2885
2906
|
- Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
|
|
2886
2907
|
|
|
2887
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.
|
|
2908
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.6...HEAD
|
|
2909
|
+
[1.3.6]: https://github.com/kettle-dev/kettle-family/compare/v1.3.5...v1.3.6
|
|
2910
|
+
[1.3.6t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.6
|
|
2888
2911
|
[1.3.5]: https://github.com/kettle-dev/kettle-family/compare/v1.3.4...v1.3.5
|
|
2889
2912
|
[1.3.5t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.5
|
|
2890
2913
|
[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.
|
|
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
|
|
@@ -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
|
|
@@ -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
|
-
|
|
18
|
-
|
|
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,40 @@ module Kettle
|
|
|
29
85
|
end
|
|
30
86
|
end
|
|
31
87
|
|
|
32
|
-
|
|
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:
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
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.
|
|
112
|
+
def unreleased_candidates(member:, release_state:)
|
|
113
|
+
return [] unless release_state&.ok?
|
|
114
|
+
|
|
115
|
+
latest_released = latest_released_version(release_state.state)
|
|
116
|
+
return [] unless latest_released
|
|
40
117
|
|
|
41
|
-
|
|
118
|
+
name = member.name
|
|
119
|
+
installed_versions(name).filter_map do |version|
|
|
120
|
+
[name, version] if version > latest_released
|
|
121
|
+
end
|
|
42
122
|
end
|
|
43
123
|
|
|
44
124
|
def latest_released_version(state)
|
|
@@ -51,21 +131,50 @@ module Kettle
|
|
|
51
131
|
end
|
|
52
132
|
|
|
53
133
|
def installed_versions(name)
|
|
54
|
-
Gem::Specification.find_all_by_name(name)
|
|
134
|
+
Gem::Specification.find_all_by_name(name)
|
|
135
|
+
.select { |spec| installed_spec?(spec) }
|
|
136
|
+
.map(&:version)
|
|
137
|
+
.uniq
|
|
138
|
+
.sort
|
|
55
139
|
end
|
|
56
140
|
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
return
|
|
141
|
+
def installed_spec?(spec)
|
|
142
|
+
path = spec.full_gem_path.to_s
|
|
143
|
+
return false if path.empty?
|
|
144
|
+
|
|
145
|
+
Gem.path.any? { |root| path.start_with?("#{root}#{File::SEPARATOR}") }
|
|
146
|
+
rescue
|
|
147
|
+
false
|
|
148
|
+
end
|
|
60
149
|
|
|
150
|
+
# Runs the batch once, then reports that one outcome against every member
|
|
151
|
+
# whose gems were in it. See the class comment for why per-member results
|
|
152
|
+
# are required even though only one command runs.
|
|
153
|
+
def batched_cleanup_results(contributors)
|
|
154
|
+
candidates = contributors.flat_map { |entry| entry.fetch(:candidates) }
|
|
155
|
+
# Versions are given as `name:version` arguments rather than `--version`,
|
|
156
|
+
# which RubyGems rejects alongside multiple gems. See the class comment
|
|
157
|
+
# for why `--all` is omitted.
|
|
158
|
+
command = ["gem", "uninstall", *candidates.map { |name, version| "#{name}:#{version}" }, "--executables"]
|
|
159
|
+
# gem uninstall does not depend on the working directory, so the first
|
|
160
|
+
# contributing member provides a real directory to execute in.
|
|
161
|
+
host = contributors.first.fetch(:member)
|
|
162
|
+
outcome = execute ? runner.call(member: host, phase: PHASE, command: command) : dry_run_outcome(command)
|
|
163
|
+
|
|
164
|
+
contributors.map do |entry|
|
|
165
|
+
member_result(entry.fetch(:member), outcome, entry.fetch(:candidates))
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def dry_run_outcome(command)
|
|
61
170
|
CommandResult.new(
|
|
62
|
-
member_name:
|
|
171
|
+
member_name: nil,
|
|
63
172
|
phase: PHASE,
|
|
64
173
|
command: command,
|
|
65
|
-
workdir:
|
|
174
|
+
workdir: nil,
|
|
66
175
|
status: nil,
|
|
67
176
|
success: true,
|
|
68
|
-
stdout:
|
|
177
|
+
stdout: nil,
|
|
69
178
|
stderr: "",
|
|
70
179
|
elapsed_seconds: 0.0,
|
|
71
180
|
skipped: true,
|
|
@@ -73,6 +182,32 @@ module Kettle
|
|
|
73
182
|
)
|
|
74
183
|
end
|
|
75
184
|
|
|
185
|
+
# One member's view of the shared batch outcome. The command and exit state
|
|
186
|
+
# are common to the whole batch, but each member reports only its own gems,
|
|
187
|
+
# so the per-member report stays readable and a failure names every gem
|
|
188
|
+
# that member contributed.
|
|
189
|
+
def member_result(member, outcome, own_candidates)
|
|
190
|
+
own = describe(own_candidates)
|
|
191
|
+
outcome_reason = outcome.reason
|
|
192
|
+
# Equivalent to `ok? ? reason : (own.empty? ? reason : batch message)`,
|
|
193
|
+
# with outcome.reason read once instead of twice.
|
|
194
|
+
reason = if outcome.ok? || own_candidates.empty?
|
|
195
|
+
outcome_reason
|
|
196
|
+
else
|
|
197
|
+
"batched gem uninstall including #{own} failed"
|
|
198
|
+
end
|
|
199
|
+
outcome.dup.tap do |copy|
|
|
200
|
+
copy.member_name = member.name
|
|
201
|
+
copy.workdir = member.root
|
|
202
|
+
copy.stdout = outcome.skipped ? "would uninstall #{own}" : outcome.stdout
|
|
203
|
+
copy.reason = reason
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def describe(candidates)
|
|
208
|
+
candidates.map { |name, version| "#{name} #{version}" }.join(", ")
|
|
209
|
+
end
|
|
210
|
+
|
|
76
211
|
def informational_result(member:, message:)
|
|
77
212
|
CommandResult.new(
|
|
78
213
|
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.
|
|
8
|
+
VERSION = "1.3.6"
|
|
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
|
-
|
|
2348
|
-
return memo unless
|
|
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
|
-
|
|
2531
|
-
return memo unless
|
|
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
|
-
|
|
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
|
-
|
|
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 <<
|
|
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 =
|
|
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
|
-
|
|
5727
|
-
|
|
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 /)
|
|
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.
|
|
4
|
+
version: 1.3.6
|
|
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.
|
|
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.
|
|
107
|
+
version: 3.1.8
|
|
108
108
|
- !ruby/object:Gem::Dependency
|
|
109
109
|
name: kettle-gha-pins
|
|
110
110
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -436,6 +436,7 @@ files:
|
|
|
436
436
|
- lib/kettle/family/nomono_bootstrap.rb
|
|
437
437
|
- lib/kettle/family/orderer.rb
|
|
438
438
|
- lib/kettle/family/paths.rb
|
|
439
|
+
- lib/kettle/family/published_version_check.rb
|
|
439
440
|
- lib/kettle/family/readiness_check.rb
|
|
440
441
|
- lib/kettle/family/release_graph.rb
|
|
441
442
|
- lib/kettle/family/release_reconciler.rb
|
|
@@ -459,10 +460,10 @@ licenses:
|
|
|
459
460
|
- AGPL-3.0-only
|
|
460
461
|
metadata:
|
|
461
462
|
homepage_uri: https://kettle-family.galtzo.com
|
|
462
|
-
source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.
|
|
463
|
-
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.
|
|
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
|
|
464
465
|
bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
|
|
465
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.
|
|
466
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.6
|
|
466
467
|
funding_uri: https://github.com/sponsors/pboling
|
|
467
468
|
wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
|
|
468
469
|
news_uri: https://www.railsbling.com/tags/kettle-family
|
metadata.gz.sig
CHANGED
|
Binary file
|