kettle-family 1.3.4 → 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 +63 -1
- data/README.md +1 -1
- data/lib/kettle/family/cli.rb +63 -6
- data/lib/kettle/family/published_version_check.rb +165 -0
- data/lib/kettle/family/release_state_check.rb +63 -2
- data/lib/kettle/family/report.rb +1 -0
- data/lib/kettle/family/selection.rb +27 -5
- data/lib/kettle/family/unreleased_gem_cleanup.rb +149 -14
- data/lib/kettle/family/version.rb +1 -1
- data/lib/kettle/family/workflow.rb +164 -31
- data/lib/kettle/family.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +27 -26
- 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,64 @@ 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
|
+
|
|
54
|
+
## [1.3.5] - 2026-10-05
|
|
55
|
+
|
|
56
|
+
- TAG: [v1.3.5][1.3.5t]
|
|
57
|
+
- COVERAGE: 93.16% -- 6636/7123 lines in 37 files
|
|
58
|
+
- BRANCH COVERAGE: 77.64% -- 2701/3479 branches in 37 files
|
|
59
|
+
- 33.64% documented
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- `Kettle::Family::ReleaseStateCheck` accepts a `branches:` scope, and `Kettle::Family::Selection.branches_matching` reports which of a member branch stack currently satisfies a release-state token. Both are the seams for per-branch release and bump decisions.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- A blocked release now reports phase `release_blocked` instead of `release_skip`, so a hard failure is never reported as a skipped member in the progress summary or report.
|
|
68
|
+
|
|
69
|
+
- [kc] kettle-jem/prepare: updated 12 project files:
|
|
70
|
+
- dependencies (12)
|
|
71
|
+
|
|
72
|
+
- [kc] kettle-jem/template: updated 6 project files:
|
|
73
|
+
- code and tests (1)
|
|
74
|
+
- dependencies (1)
|
|
75
|
+
- documentation (1)
|
|
76
|
+
- other (2)
|
|
77
|
+
- workflows (1)
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
- Already-released detection for branch-stack members is now scoped to the branch being released instead of the whole stack, so a clean published branch is no longer reported as having unreleased changes and blocked with a release_skip failure.
|
|
82
|
+
|
|
83
|
+
- A published version whose release tag is followed only by post-release maintenance commits (checksums, dependency-floor lockfile reconciliation) is now skipped as already released; only changes to the gemspec, version file, or changelog after the tag make the published version stale.
|
|
84
|
+
|
|
85
|
+
- Release-state status-token selection (`--only unreleased|prepared|pending|bump`) no longer collapses a branch-stack member to whichever branch happened to be evaluated last; a member is selected when any of its branches matches, and multiple tokens are still ANDed within a single branch.
|
|
86
|
+
|
|
87
|
+
- `kettle-family bump` now bumps each branch of a member branch stack independently: branches with no pending version bump are reported as skipped instead of being bumped, so an already-released branch is no longer given version bumps it never needed. An explicit `--only MEMBER` remains an unconditional request and is not filtered.
|
|
88
|
+
|
|
89
|
+
- Release-state worktrees orphaned by a killed run are reaped on the next run. Directories are named `worktree-PID-RANDOM`, so only those whose owning PID is no longer running are removed; live worktrees from concurrent runs are left alone.
|
|
90
|
+
|
|
33
91
|
## [1.3.4] - 2026-10-02
|
|
34
92
|
|
|
35
93
|
- TAG: [v1.3.4][1.3.4t]
|
|
@@ -2847,7 +2905,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
2847
2905
|
- Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
|
|
2848
2906
|
- Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
|
|
2849
2907
|
|
|
2850
|
-
[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
|
|
2911
|
+
[1.3.5]: https://github.com/kettle-dev/kettle-family/compare/v1.3.4...v1.3.5
|
|
2912
|
+
[1.3.5t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.5
|
|
2851
2913
|
[1.3.4]: https://github.com/kettle-dev/kettle-family/compare/v1.3.3...v1.3.4
|
|
2852
2914
|
[1.3.4t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.4
|
|
2853
2915
|
[1.3.3]: https://github.com/kettle-dev/kettle-family/compare/v1.3.2...v1.3.3
|
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
|
@@ -28,6 +28,16 @@ module Kettle
|
|
|
28
28
|
main(argv, stdout: out, stderr: err)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Defined as a singleton method above the `private` keyword: a `private`
|
|
32
|
+
# instance modifier does not apply to `def self.` definitions.
|
|
33
|
+
def self.default_only_filter(command:, only:)
|
|
34
|
+
return only unless only.to_s.empty?
|
|
35
|
+
return "bump" if %w[bump bump-version].include?(command)
|
|
36
|
+
return "pending" if command == "release"
|
|
37
|
+
|
|
38
|
+
only
|
|
39
|
+
end
|
|
40
|
+
|
|
31
41
|
module SharedOptions
|
|
32
42
|
def self.included(base)
|
|
33
43
|
base.option :root, value: {type: String, usage: "PATH"}, desc: "Workspace or family root"
|
|
@@ -188,7 +198,12 @@ module Kettle
|
|
|
188
198
|
end
|
|
189
199
|
|
|
190
200
|
def run_family(command, overrides = {})
|
|
191
|
-
|
|
201
|
+
effective = family_options(overrides)
|
|
202
|
+
# Thread the *effective* --only (including the command's default
|
|
203
|
+
# release-state token) downstream so per-branch logic sees the same
|
|
204
|
+
# selection the member selector used.
|
|
205
|
+
effective[:only] = Kettle::Family::CLI.default_only_filter(command: command, only: effective[:only])
|
|
206
|
+
Kettle::Family::CLI.new(stdout: stdout, stderr: stderr).run_command(command, effective)
|
|
192
207
|
end
|
|
193
208
|
|
|
194
209
|
def truthy_option?(name)
|
|
@@ -822,11 +837,7 @@ module Kettle
|
|
|
822
837
|
end
|
|
823
838
|
|
|
824
839
|
def default_only_filter(command:, only:)
|
|
825
|
-
|
|
826
|
-
return "bump" if %w[bump bump-version].include?(command)
|
|
827
|
-
return "pending" if command == "release"
|
|
828
|
-
|
|
829
|
-
only
|
|
840
|
+
self.class.default_only_filter(command: command, only: only)
|
|
830
841
|
end
|
|
831
842
|
|
|
832
843
|
def direct_member_only_names(only)
|
|
@@ -1122,6 +1133,17 @@ module Kettle
|
|
|
1122
1133
|
memo.last.branch = branch
|
|
1123
1134
|
break unless memo.last.ok?
|
|
1124
1135
|
|
|
1136
|
+
if (skipped = branch_bump_skip_result(
|
|
1137
|
+
member: member,
|
|
1138
|
+
branch: branch,
|
|
1139
|
+
command: command,
|
|
1140
|
+
options: options,
|
|
1141
|
+
release_state_results: release_state_results
|
|
1142
|
+
))
|
|
1143
|
+
memo << skipped
|
|
1144
|
+
next
|
|
1145
|
+
end
|
|
1146
|
+
|
|
1125
1147
|
branch_members = rediscovered_selected_members(config: member_config, selected_names: [member.name], command: command)
|
|
1126
1148
|
branch_members = [member] if branch_members.empty?
|
|
1127
1149
|
branch_results = command_results_for_current_branch(
|
|
@@ -1140,6 +1162,41 @@ module Kettle
|
|
|
1140
1162
|
end
|
|
1141
1163
|
end
|
|
1142
1164
|
|
|
1165
|
+
BUMP_COMMANDS = %w[bump bump-version].freeze
|
|
1166
|
+
|
|
1167
|
+
# Bumping a whole branch stack must be per-branch: a branch already
|
|
1168
|
+
# released with nothing pending has nothing to bump. `--only bump` is a
|
|
1169
|
+
# status-driven selection, so it is filtered by each branch's own
|
|
1170
|
+
# release-state. An explicit `--only MEMBER` is a direct request and is
|
|
1171
|
+
# honored as given.
|
|
1172
|
+
def branch_bump_skip_result(member:, branch:, command:, options:, release_state_results:)
|
|
1173
|
+
return unless BUMP_COMMANDS.include?(command)
|
|
1174
|
+
return unless status_token_only?(options[:only])
|
|
1175
|
+
|
|
1176
|
+
matching = Selection.branches_matching(release_state_results, member_name: member.name, tokens: %w[bump])
|
|
1177
|
+
return if matching.empty? || matching.include?(branch)
|
|
1178
|
+
|
|
1179
|
+
CommandResult.new(
|
|
1180
|
+
member_name: member.name,
|
|
1181
|
+
phase: command,
|
|
1182
|
+
command: ["internal", "branch-bump-skip", branch],
|
|
1183
|
+
workdir: member.root,
|
|
1184
|
+
status: 0,
|
|
1185
|
+
success: true,
|
|
1186
|
+
stdout: "#{member.name}@#{branch} has no unreleased entries at its released version; skipping bump on this branch",
|
|
1187
|
+
stderr: "",
|
|
1188
|
+
elapsed_seconds: 0.0,
|
|
1189
|
+
skipped: true,
|
|
1190
|
+
reason: "no version bump pending on #{branch}",
|
|
1191
|
+
branch: branch
|
|
1192
|
+
)
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
def status_token_only?(only)
|
|
1196
|
+
names = only.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
1197
|
+
!names.empty? && names.all? { |name| Selection.status_token?(name) }
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1143
1200
|
def rediscovered_selected_members(config:, selected_names:, command:)
|
|
1144
1201
|
discovered = Discovery.new(config: config).members
|
|
1145
1202
|
ordered = (command == "install") ? install_order(discovered, config) : Orderer.new(members: discovered, mode: config.order_mode, hints: config.order_hints).ordered
|
|
@@ -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
|
|
@@ -25,10 +25,16 @@ module Kettle
|
|
|
25
25
|
CACHE_MISS = Object.new.freeze
|
|
26
26
|
WORKTREE_ADMIN_MUTEX = Mutex.new
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
# +branches+ scopes the check to specific release target branches. A
|
|
29
|
+
# branch-stack member produces one result per branch; a per-branch
|
|
30
|
+
# decision (is this branch already published? does it need a bump?) must
|
|
31
|
+
# never consult the whole stack, or every branch inherits the state of
|
|
32
|
+
# the branch that happens to have unreleased entries.
|
|
33
|
+
def initialize(members:, config: nil, jobs: nil, branches: nil)
|
|
29
34
|
@members = members
|
|
30
35
|
@config = config
|
|
31
36
|
@jobs = jobs
|
|
37
|
+
@branches = branches&.map(&:to_s)&.reject(&:empty?)
|
|
32
38
|
@github_latest_release_by_repo = {}
|
|
33
39
|
@transfer_changelog_status_by_root = {}
|
|
34
40
|
@transfer_changelog_lag_by_key = {}
|
|
@@ -38,6 +44,7 @@ module Kettle
|
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
def results(event_handler: nil)
|
|
47
|
+
return scoped_checkout_results(event_handler: event_handler) if checked_out_scoped_results?
|
|
41
48
|
return branch_results(event_handler: event_handler) unless release_target_branches.empty?
|
|
42
49
|
if shared_changelog?
|
|
43
50
|
results = parallel_map(members) { |member| check_shared_changelog_member_or_local(member, event_handler: event_handler) }
|
|
@@ -53,7 +60,25 @@ module Kettle
|
|
|
53
60
|
|
|
54
61
|
private
|
|
55
62
|
|
|
56
|
-
attr_reader :members, :config, :jobs
|
|
63
|
+
attr_reader :members, :config, :jobs, :branches
|
|
64
|
+
|
|
65
|
+
# A single-branch scope whose checkout already sits on that branch needs
|
|
66
|
+
# no worktree: the primary checkout is the branch under test.
|
|
67
|
+
def checked_out_scoped_results?
|
|
68
|
+
return false unless branches&.one?
|
|
69
|
+
|
|
70
|
+
members.any? { |member| current_branch_for(member) == branches.first }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def scoped_checkout_results(event_handler: nil)
|
|
74
|
+
branch = branches.first
|
|
75
|
+
results = parallel_map(members) { |member| check_member(member, branch: branch, event_handler: event_handler) }
|
|
76
|
+
results.flatten
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def current_branch_for(member)
|
|
80
|
+
git_output(member.root, "branch", "--show-current")
|
|
81
|
+
end
|
|
57
82
|
|
|
58
83
|
def branch_results(event_handler: nil)
|
|
59
84
|
root = git_root
|
|
@@ -769,6 +794,7 @@ module Kettle
|
|
|
769
794
|
|
|
770
795
|
def release_target_branches
|
|
771
796
|
return [] unless config
|
|
797
|
+
return @branches if @branches
|
|
772
798
|
|
|
773
799
|
config.release_target_branches
|
|
774
800
|
end
|
|
@@ -840,6 +866,7 @@ module Kettle
|
|
|
840
866
|
with_worktree_registry_lock(root: root) do
|
|
841
867
|
base = File.join(root, "tmp", "kettle-family-release-state")
|
|
842
868
|
FileUtils.mkdir_p(base)
|
|
869
|
+
prune_stale_worktrees(root: root)
|
|
843
870
|
entries = branches.map do |branch|
|
|
844
871
|
{branch: branch, worktree_root: File.join(base, "worktree-#{Process.pid}-#{SecureRandom.hex(8)}")}
|
|
845
872
|
end
|
|
@@ -869,6 +896,40 @@ module Kettle
|
|
|
869
896
|
end
|
|
870
897
|
end
|
|
871
898
|
|
|
899
|
+
# A killed run (SIGKILL, terminal close) skips the ensure-block cleanup and
|
|
900
|
+
# leaves registered worktrees behind. Registered-but-ownerless worktrees
|
|
901
|
+
# are pinned refs that accumulate forever, so reap them on the way in.
|
|
902
|
+
def prune_stale_worktrees(root:)
|
|
903
|
+
base = File.join(root, "tmp", "kettle-family-release-state")
|
|
904
|
+
WORKTREE_ADMIN_MUTEX.synchronize do
|
|
905
|
+
Open3.capture3("git", "worktree", "prune", chdir: root)
|
|
906
|
+
orphaned_worktree_paths(base).each do |path|
|
|
907
|
+
Open3.capture3("git", "worktree", "remove", "--force", path, chdir: root)
|
|
908
|
+
FileUtils.rm_rf(path)
|
|
909
|
+
end
|
|
910
|
+
end
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
# Worktree directories are named worktree-PID-RANDOM. A PID that is no
|
|
914
|
+
# longer running cannot still own a live worktree.
|
|
915
|
+
def orphaned_worktree_paths(base)
|
|
916
|
+
Dir[File.join(base, "worktree-*")].select do |path|
|
|
917
|
+
pid = File.basename(path)[/worktree-(\d+)-/, 1]
|
|
918
|
+
next false if pid.nil? || pid == Process.pid.to_s
|
|
919
|
+
|
|
920
|
+
!process_alive?(pid)
|
|
921
|
+
end
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
def process_alive?(pid)
|
|
925
|
+
Process.kill(0, pid)
|
|
926
|
+
true
|
|
927
|
+
rescue Errno::ESRCH
|
|
928
|
+
false
|
|
929
|
+
rescue Errno::EPERM
|
|
930
|
+
true
|
|
931
|
+
end
|
|
932
|
+
|
|
872
933
|
def add_branch_worktree(root:, branch:, worktree_root:)
|
|
873
934
|
WORKTREE_ADMIN_MUTEX.synchronize do
|
|
874
935
|
_stdout, stderr, status = Open3.capture3("git", "worktree", "add", "--detach", worktree_root, branch, chdir: root)
|
data/lib/kettle/family/report.rb
CHANGED
|
@@ -21,6 +21,21 @@ module Kettle
|
|
|
21
21
|
STATUS_TOKEN_KEYS.key?(value.to_s)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# Defined as a singleton method above the `private` keyword: a `private`
|
|
25
|
+
# instance modifier does not apply to `def self.` definitions.
|
|
26
|
+
#
|
|
27
|
+
# Branches of one member that currently satisfy +tokens+. Used to decide
|
|
28
|
+
# per-branch bump work, so a bump never touches a branch that has nothing
|
|
29
|
+
# to release.
|
|
30
|
+
def self.branches_matching(release_state_results, member_name:, tokens:)
|
|
31
|
+
keys = Array(tokens).map(&:to_s).filter_map { |token| STATUS_TOKEN_KEYS[token] }
|
|
32
|
+
return [] if keys.empty?
|
|
33
|
+
|
|
34
|
+
Array(release_state_results).select do |result|
|
|
35
|
+
result.member_name == member_name && keys.all? { |key| result.state[key] == true }
|
|
36
|
+
end.map(&:branch).compact.uniq
|
|
37
|
+
end
|
|
38
|
+
|
|
24
39
|
def self.validate_release_state_only_filter!(only)
|
|
25
40
|
names = only.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
26
41
|
status_tokens = names.select { |name| status_token?(name) }
|
|
@@ -83,22 +98,29 @@ module Kettle
|
|
|
83
98
|
selected.drop(index)
|
|
84
99
|
end
|
|
85
100
|
|
|
101
|
+
# A branch-stack member yields one release-state result per branch, all
|
|
102
|
+
# sharing a member_name. Collapsing them by member_name alone would let
|
|
103
|
+
# whichever branch came last decide selection for every branch, so a
|
|
104
|
+
# member qualifies when *any* of its branches matches the token.
|
|
86
105
|
def select_release_state_status(selected, status_tokens)
|
|
87
106
|
results_by_member = release_state_results_by_member
|
|
88
|
-
failed = results_by_member.values.select { |result| !result.ok? }
|
|
89
|
-
raise Error, "release-state check failed for: #{failed.map(&:member_name).join(", ")}" unless failed.empty?
|
|
107
|
+
failed = results_by_member.values.flatten.select { |result| !result.ok? }
|
|
108
|
+
raise Error, "release-state check failed for: #{failed.map(&:member_name).uniq.join(", ")}" unless failed.empty?
|
|
90
109
|
|
|
91
110
|
selected.select do |candidate|
|
|
92
|
-
|
|
93
|
-
|
|
111
|
+
Array(results_by_member[candidate.name]).any? do |result|
|
|
112
|
+
status_tokens.all? { |token| truthy_state?(result.state[STATUS_TOKEN_KEYS.fetch(token)]) }
|
|
113
|
+
end
|
|
94
114
|
end
|
|
95
115
|
end
|
|
96
116
|
|
|
117
|
+
# Branch-stack members report one result per branch, all sharing a member
|
|
118
|
+
# name, so results are grouped per member instead of collapsed to one.
|
|
97
119
|
def release_state_results_by_member
|
|
98
120
|
raise Error, "--only release-state tokens require release-state results" unless release_state_results
|
|
99
121
|
|
|
100
122
|
release_state_results.each_with_object({}) do |result, memo|
|
|
101
|
-
memo[result.member_name]
|
|
123
|
+
(memo[result.member_name] ||= []) << result
|
|
102
124
|
end
|
|
103
125
|
end
|
|
104
126
|
|
|
@@ -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)
|
|
@@ -3978,28 +4030,38 @@ module Kettle
|
|
|
3978
4030
|
@release_progress&.update(member, status: "ci bundle #{relative} #{attempt}/#{REGISTRY_WAIT_ATTEMPTS}", mark: ">")
|
|
3979
4031
|
end
|
|
3980
4032
|
|
|
4033
|
+
# A published version only means "already released" when nothing
|
|
4034
|
+
# releasable landed after its tag. kettle-release commits checksums
|
|
4035
|
+
# (and the family commits dependency-floor lockfiles) *after* tagging,
|
|
4036
|
+
# so comparing HEAD to the tag directly never matches; compare the
|
|
4037
|
+
# release-relevant paths instead.
|
|
3981
4038
|
def already_released_result(member)
|
|
3982
4039
|
tag = release_tag_name(member.version)
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
4040
|
+
branch = current_member_branch(member)
|
|
4041
|
+
scope = branch ? "#{member.name}@#{branch}" : member.name
|
|
4042
|
+
if no_releasable_changes_since_tag?(member, tag)
|
|
4043
|
+
stdout = "#{scope} #{member.version} is already published#{branch_suffix(branch)} and nothing releasable " \
|
|
4044
|
+
"landed after #{tag}; skipping release"
|
|
3986
4045
|
reason = "already released"
|
|
3987
4046
|
skipped = true
|
|
3988
|
-
elsif unreleased_changes_pending?(member)
|
|
3989
|
-
stdout = "#{
|
|
3990
|
-
"
|
|
4047
|
+
elsif unreleased_changes_pending?(member, branch: branch)
|
|
4048
|
+
stdout = "#{scope} #{member.version} is already published#{branch_suffix(branch)}, but release-state reports " \
|
|
4049
|
+
"unreleased changes on this branch. Bump only this branch with " \
|
|
4050
|
+
"`kettle-family bump patch --execute --only #{member.name}` before releasing."
|
|
3991
4051
|
reason = "published version has unreleased changes"
|
|
3992
4052
|
skipped = false
|
|
3993
4053
|
else
|
|
3994
|
-
stdout = "#{
|
|
3995
|
-
"
|
|
4054
|
+
stdout = "#{scope} #{member.version} is already published#{branch_suffix(branch)}, and release-state reports " \
|
|
4055
|
+
"no unreleased changes on this branch; skipping release"
|
|
3996
4056
|
reason = "already released; no unreleased changes"
|
|
3997
4057
|
skipped = true
|
|
3998
4058
|
end
|
|
3999
4059
|
|
|
4000
4060
|
CommandResult.new(
|
|
4001
4061
|
member_name: member.name,
|
|
4002
|
-
phase:
|
|
4062
|
+
# A blocked release is a distinct terminal phase from a skip: it is a
|
|
4063
|
+
# hard failure and must never be reported as "skipped".
|
|
4064
|
+
phase: skipped ? "release_skip" : "release_blocked",
|
|
4003
4065
|
command: ["internal", "released-version-check", member.version],
|
|
4004
4066
|
workdir: member.root,
|
|
4005
4067
|
status: skipped ? 0 : 1,
|
|
@@ -4008,12 +4070,34 @@ module Kettle
|
|
|
4008
4070
|
stderr: "",
|
|
4009
4071
|
elapsed_seconds: 0.0,
|
|
4010
4072
|
skipped: skipped,
|
|
4011
|
-
reason: reason
|
|
4073
|
+
reason: reason,
|
|
4074
|
+
branch: branch
|
|
4012
4075
|
)
|
|
4013
4076
|
end
|
|
4014
4077
|
|
|
4015
|
-
def
|
|
4016
|
-
|
|
4078
|
+
def branch_suffix(branch)
|
|
4079
|
+
branch ? " on #{branch}" : ""
|
|
4080
|
+
end
|
|
4081
|
+
|
|
4082
|
+
def current_member_branch(member)
|
|
4083
|
+
return unless git_work_tree?(member.root)
|
|
4084
|
+
|
|
4085
|
+
stdout, _stderr, status = Open3.capture3("git", "branch", "--show-current", chdir: member.root)
|
|
4086
|
+
return unless status.success?
|
|
4087
|
+
|
|
4088
|
+
branch = stdout.strip
|
|
4089
|
+
branch.empty? ? nil : branch
|
|
4090
|
+
end
|
|
4091
|
+
|
|
4092
|
+
# Scope the check to the branch being released. Consulting the whole
|
|
4093
|
+
# branch stack here would report every already-published branch as
|
|
4094
|
+
# having unreleased changes whenever any other branch in the stack does.
|
|
4095
|
+
def unreleased_changes_pending?(member, branch: nil)
|
|
4096
|
+
results = ReleaseStateCheck.new(
|
|
4097
|
+
members: [member],
|
|
4098
|
+
config: config,
|
|
4099
|
+
branches: branch ? [branch] : nil
|
|
4100
|
+
).results
|
|
4017
4101
|
return true unless results.all?(&:ok?)
|
|
4018
4102
|
|
|
4019
4103
|
results.any? { |result| result.state.fetch("unreleased_entries", true) }
|
|
@@ -4023,12 +4107,42 @@ module Kettle
|
|
|
4023
4107
|
"v#{version}"
|
|
4024
4108
|
end
|
|
4025
4109
|
|
|
4026
|
-
|
|
4027
|
-
|
|
4110
|
+
# Post-release maintenance commits (checksums, lockfile reconciliation,
|
|
4111
|
+
# docs) land after the tag by design, so "HEAD != tag" cannot mean
|
|
4112
|
+
# "needs a release". Only a change to the gemspec, version file, or
|
|
4113
|
+
# changelog between the tag and HEAD makes the published version stale.
|
|
4114
|
+
def no_releasable_changes_since_tag?(member, tag)
|
|
4115
|
+
return false unless git_work_tree?(member.root)
|
|
4028
4116
|
|
|
4029
4117
|
tag_sha = git_rev_parse(member.root, "refs/tags/#{tag}^{}")
|
|
4030
|
-
|
|
4031
|
-
|
|
4118
|
+
return false if tag_sha.to_s.empty?
|
|
4119
|
+
return true if tag_sha == git_rev_parse(member.root, "HEAD")
|
|
4120
|
+
|
|
4121
|
+
releasable_changes_since_tag(member, tag).empty?
|
|
4122
|
+
end
|
|
4123
|
+
|
|
4124
|
+
def releasable_changes_since_tag(member, tag)
|
|
4125
|
+
paths = releasable_paths(member)
|
|
4126
|
+
return [] if paths.empty?
|
|
4127
|
+
|
|
4128
|
+
stdout, _stderr, status = Open3.capture3(
|
|
4129
|
+
"git", "diff", "--name-only", "#{tag}^{}", "HEAD", "--", *paths, chdir: member.root
|
|
4130
|
+
)
|
|
4131
|
+
# An unreadable diff is not evidence of "nothing to release".
|
|
4132
|
+
return [tag] unless status.success?
|
|
4133
|
+
|
|
4134
|
+
stdout.lines.map(&:strip).reject(&:empty?)
|
|
4135
|
+
end
|
|
4136
|
+
|
|
4137
|
+
def releasable_paths(member)
|
|
4138
|
+
paths = [member.gemspec_path, member.version_file, config.changelog_full_path(member)]
|
|
4139
|
+
paths.compact.filter_map do |path|
|
|
4140
|
+
next unless File.file?(path)
|
|
4141
|
+
|
|
4142
|
+
Pathname.new(path).relative_path_from(Pathname.new(member.root)).to_s
|
|
4143
|
+
rescue ArgumentError
|
|
4144
|
+
path.to_s
|
|
4145
|
+
end.uniq
|
|
4032
4146
|
end
|
|
4033
4147
|
|
|
4034
4148
|
def git_work_tree?(root)
|
|
@@ -4535,8 +4649,16 @@ module Kettle
|
|
|
4535
4649
|
total
|
|
4536
4650
|
end
|
|
4537
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.
|
|
4538
4660
|
def release_phase_total(member = nil)
|
|
4539
|
-
total =
|
|
4661
|
+
total = 4
|
|
4540
4662
|
if member
|
|
4541
4663
|
normalize = normalize_release_lockfiles?(member)
|
|
4542
4664
|
total += 2 if normalize
|
|
@@ -4634,7 +4756,9 @@ module Kettle
|
|
|
4634
4756
|
def emit_release_progress_summary(results, progress:)
|
|
4635
4757
|
return unless progress
|
|
4636
4758
|
|
|
4637
|
-
release_results = results.select
|
|
4759
|
+
release_results = results.select do |result|
|
|
4760
|
+
result.phase == release_phase || result.phase == "release_skip" || result.phase == "release_blocked"
|
|
4761
|
+
end
|
|
4638
4762
|
progress.stop
|
|
4639
4763
|
progress.summary("release summary: #{release_results.count(&:ok?)}/#{release_results.length} members ok")
|
|
4640
4764
|
end
|
|
@@ -5659,8 +5783,14 @@ module Kettle
|
|
|
5659
5783
|
end
|
|
5660
5784
|
|
|
5661
5785
|
def release_lockfile_readiness_would_fail?(member)
|
|
5662
|
-
|
|
5663
|
-
|
|
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?
|
|
5664
5794
|
end
|
|
5665
5795
|
|
|
5666
5796
|
def release_lockfile_has_local_path_remote?(member)
|
|
@@ -5911,11 +6041,14 @@ module Kettle
|
|
|
5911
6041
|
end
|
|
5912
6042
|
|
|
5913
6043
|
def bundle_update_lockfile_diagnostics(member)
|
|
5914
|
-
ReadinessCheck.call(
|
|
6044
|
+
readiness = ReadinessCheck.call(
|
|
5915
6045
|
member: member,
|
|
5916
6046
|
config: config,
|
|
5917
6047
|
allowed_local_path_roots: bundle_update_allowed_local_path_roots
|
|
5918
|
-
).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|
|
|
5919
6052
|
line.sub("release lockfile", "bundle update lockfile").chomp
|
|
5920
6053
|
end
|
|
5921
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
|
|
@@ -304,7 +304,7 @@ dependencies:
|
|
|
304
304
|
version: '3.2'
|
|
305
305
|
- - ">="
|
|
306
306
|
- !ruby/object:Gem::Version
|
|
307
|
-
version: 3.2.
|
|
307
|
+
version: 3.2.5
|
|
308
308
|
type: :development
|
|
309
309
|
prerelease: false
|
|
310
310
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -314,7 +314,7 @@ dependencies:
|
|
|
314
314
|
version: '3.2'
|
|
315
315
|
- - ">="
|
|
316
316
|
- !ruby/object:Gem::Version
|
|
317
|
-
version: 3.2.
|
|
317
|
+
version: 3.2.5
|
|
318
318
|
- !ruby/object:Gem::Dependency
|
|
319
319
|
name: turbo_tests2
|
|
320
320
|
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.13
|
|
328
328
|
type: :development
|
|
329
329
|
prerelease: false
|
|
330
330
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -334,21 +334,7 @@ dependencies:
|
|
|
334
334
|
version: '3.2'
|
|
335
335
|
- - ">="
|
|
336
336
|
- !ruby/object:Gem::Version
|
|
337
|
-
version: 3.2.
|
|
338
|
-
- !ruby/object:Gem::Dependency
|
|
339
|
-
name: webrick
|
|
340
|
-
requirement: !ruby/object:Gem::Requirement
|
|
341
|
-
requirements:
|
|
342
|
-
- - "~>"
|
|
343
|
-
- !ruby/object:Gem::Version
|
|
344
|
-
version: '1.9'
|
|
345
|
-
type: :development
|
|
346
|
-
prerelease: false
|
|
347
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
348
|
-
requirements:
|
|
349
|
-
- - "~>"
|
|
350
|
-
- !ruby/object:Gem::Version
|
|
351
|
-
version: '1.9'
|
|
337
|
+
version: 3.2.13
|
|
352
338
|
- !ruby/object:Gem::Dependency
|
|
353
339
|
name: ruby-progressbar
|
|
354
340
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -392,7 +378,7 @@ dependencies:
|
|
|
392
378
|
version: '2.0'
|
|
393
379
|
- - ">="
|
|
394
380
|
- !ruby/object:Gem::Version
|
|
395
|
-
version: 2.0.
|
|
381
|
+
version: 2.0.15
|
|
396
382
|
type: :development
|
|
397
383
|
prerelease: false
|
|
398
384
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -402,7 +388,21 @@ dependencies:
|
|
|
402
388
|
version: '2.0'
|
|
403
389
|
- - ">="
|
|
404
390
|
- !ruby/object:Gem::Version
|
|
405
|
-
version: 2.0.
|
|
391
|
+
version: 2.0.15
|
|
392
|
+
- !ruby/object:Gem::Dependency
|
|
393
|
+
name: webrick
|
|
394
|
+
requirement: !ruby/object:Gem::Requirement
|
|
395
|
+
requirements:
|
|
396
|
+
- - "~>"
|
|
397
|
+
- !ruby/object:Gem::Version
|
|
398
|
+
version: '1.9'
|
|
399
|
+
type: :development
|
|
400
|
+
prerelease: false
|
|
401
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
402
|
+
requirements:
|
|
403
|
+
- - "~>"
|
|
404
|
+
- !ruby/object:Gem::Version
|
|
405
|
+
version: '1.9'
|
|
406
406
|
description: "\U0001F469\U0001F469\U0001F467\U0001F467 Kettle::Family provides
|
|
407
407
|
scripts and conventions for coordinating related Ruby gems as one family."
|
|
408
408
|
email:
|
|
@@ -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
|