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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +52 -1
- data/README.md +1 -1
- data/lib/kettle/family/cli.rb +1 -0
- data/lib/kettle/family/published_version_check.rb +165 -0
- data/lib/kettle/family/report.rb +39 -1
- data/lib/kettle/family/selection.rb +37 -0
- data/lib/kettle/family/unreleased_gem_cleanup.rb +211 -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 +29 -8
- 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: ec4f59e4397fe55738de3f0e3e482a2053d3c771716e9d94de40478f76ba80ae
|
|
4
|
+
data.tar.gz: 7ea569f07fa6cc8cc46cf5351cf93441ece85870c80ab51aed2458e0ce66346a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|
data/lib/kettle/family/cli.rb
CHANGED
|
@@ -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
|
data/lib/kettle/family/report.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,41 @@ 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 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
217
|
+
member_name: nil,
|
|
63
218
|
phase: PHASE,
|
|
64
219
|
command: command,
|
|
65
|
-
workdir:
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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.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.
|
|
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
|
|
@@ -324,7 +324,7 @@ dependencies:
|
|
|
324
324
|
version: '3.2'
|
|
325
325
|
- - ">="
|
|
326
326
|
- !ruby/object:Gem::Version
|
|
327
|
-
version: 3.2.
|
|
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.
|
|
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.
|
|
463
|
-
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.
|
|
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.
|
|
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
|