kettle-family 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5116144628e718706163387018ab7eaedc531e3792f1e170e7b64b38ecbfe959
4
- data.tar.gz: a30da1750923d4ca49729234405926594802a293e0cbe9f8b456397158196867
3
+ metadata.gz: 3a7c89b89e309502af9e996022c4c93d5b02b0118279464d197537e76bd2c65a
4
+ data.tar.gz: f80c4406bda1cd8e3a0d42c47da1e4fa645303b71f1cd68a5381053c041e0351
5
5
  SHA512:
6
- metadata.gz: 51c828bd1d1dacb0ad5eea8670d16666e47a225398ebc25fa6539aba4732ad05a0a596a3d85a25d0ed0a6f5b1776f361b4ee91af6c4621a635c762f6f512f907
7
- data.tar.gz: 975b89c1f3f31291f322119c6e7af7e277a18d42ced28f54380f6c01e8e0a31e53c57cee977628486207dd3581efe08de657f1401fe008f42a3913b282a541e5
6
+ metadata.gz: 5160695320d1ce2a37cbb6e4cfa8dd45026a6784d660a932678aa601b5c4b5a22903530109537cf9e9d58f6658f20d0e2ea8d36ead281f0258a1743da7c1e565
7
+ data.tar.gz: 324f032033f94e3d7891d9935efd8ca1583c081eac9e14d265c2dcb3d258ffdbb310e8fb0e89587ccb55db73d265ced7cd10e681954a778cbcd981668f375a19
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,39 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [0.1.2] - 2026-06-13
34
+
35
+ - TAG: [v0.1.2][0.1.2t]
36
+ - COVERAGE: 94.17% -- 840/892 lines in 19 files
37
+ - BRANCH COVERAGE: 78.06% -- 242/310 branches in 19 files
38
+ - 41.80% documented
39
+
40
+ ## [0.1.1] - 2026-06-13
41
+
42
+ - TAG: [v0.1.1][0.1.1t]
43
+ - COVERAGE: 94.16% -- 838/890 lines in 19 files
44
+ - BRANCH COVERAGE: 77.92% -- 240/308 branches in 19 files
45
+ - 41.80% documented
46
+
47
+ ### Added
48
+
49
+ - Added configurable member discovery excludes via `members.exclude` /
50
+ `members.ignore`.
51
+ - Added `kettle-family release-state` to report changelog release state across
52
+ family members using `kettle-changelog --release-state --json`.
53
+
54
+ ### Changed
55
+
56
+ - Retemplated generated project files with `kettle-dev` >= 2.2.7.
57
+
58
+ ### Fixed
59
+
60
+ - Member discovery now filters configured excludes and git-ignored paths before
61
+ loading gemspecs, avoiding duplicate fixture/tmp gemspecs in recursive family
62
+ roots.
63
+ - Member discovery now skips default `spec/` and `test/` fixture trees before
64
+ loading gemspecs, avoiding fixture load failures in family roots.
65
+
33
66
  ## [0.1.0] - 2026-06-10
34
67
 
35
68
  - TAG: [v0.1.0][0.1.0t]
@@ -58,6 +91,10 @@ Please file a bug if you notice a violation of semantic versioning.
58
91
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
59
92
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
60
93
 
61
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v0.1.0...HEAD
94
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v0.1.2...HEAD
95
+ [0.1.2]: https://github.com/kettle-dev/kettle-family/compare/v0.1.1...v0.1.2
96
+ [0.1.2t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.1.2
97
+ [0.1.1]: https://github.com/kettle-dev/kettle-family/compare/v0.1.0...v0.1.1
98
+ [0.1.1t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.1.1
62
99
  [0.1.0]: https://github.com/kettle-dev/kettle-family/compare/e4a9ca8ed52605b6375bbdd4f745b905a68b8b24...v0.1.0
63
100
  [0.1.0t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.1.0
data/README.md CHANGED
@@ -126,6 +126,22 @@ gem install kettle-family
126
126
  `kettle-family` reads `.kettle-family.yml` from the family root by default.
127
127
  Use `--config PATH` to load a different file.
128
128
 
129
+ Member discovery is recursive under the configured member roots. Discovery skips
130
+ gemspecs ignored by git and any configured `members.exclude` glob patterns before
131
+ loading gemspec metadata, so fixture or temporary gemspecs do not create duplicate
132
+ members. Exclude patterns are matched relative to the family root and to each
133
+ member root.
134
+
135
+ ```yaml
136
+ family:
137
+ members_root: gems
138
+
139
+ members:
140
+ exclude:
141
+ - "**/tmp/**"
142
+ - "**/vendor/**"
143
+ ```
144
+
129
145
  For a flat repository that releases from multiple long-lived branches, list the
130
146
  release branches under `release.target_branches`. The branch list is processed
131
147
  in order. Each branch must be clean enough for `git checkout`, and each branch
@@ -170,6 +186,16 @@ kettle-family discover
170
186
  kettle-family release
171
187
  ```
172
188
 
189
+ Audit changelog release state across the selected family members:
190
+
191
+ ```console
192
+ kettle-family release-state
193
+ ```
194
+
195
+ The release-state report lists each gem's current `version.rb`, latest published
196
+ release, latest versioned `CHANGELOG.md` section, and whether pending changelog
197
+ work exists in either `Unreleased` or an unpublished prepared release section.
198
+
173
199
  Run release prep/build phases without publishing:
174
200
 
175
201
  ```console
@@ -547,7 +573,7 @@ Thanks for RTFM. ☺️
547
573
  [📌gitmoji]: https://gitmoji.dev
548
574
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
549
575
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
550
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.787-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
576
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.892-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
551
577
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
552
578
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
553
579
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -6,7 +6,7 @@ require "optparse"
6
6
  module Kettle
7
7
  module Family
8
8
  class CLI
9
- COMMANDS = %w[discover plan report check test lint docs template bump-version release branch-lanes].freeze
9
+ COMMANDS = %w[discover plan report check test lint docs template bump-version release branch-lanes release-state].freeze
10
10
  WORKFLOW_COMMANDS = %w[check test lint docs template release].freeze
11
11
 
12
12
  def self.call(argv, out: $stdout, err: $stderr)
@@ -61,6 +61,7 @@ module Kettle
61
61
  bump-version Check, plan, or execute family version alignment
62
62
  release Plan or execute release build/publish phases
63
63
  branch-lanes Audit configured branch lane release mappings
64
+ release-state Report changelog release state for family members
64
65
 
65
66
  Options:
66
67
  --root PATH Workspace or family root (default: current directory)
@@ -138,7 +139,11 @@ module Kettle
138
139
  def build_report(command, options)
139
140
  config = Config.load(root: options[:root], path: options[:config])
140
141
  members = Discovery.new(config: config).members
141
- ordered = Orderer.new(members: members, mode: config.order_mode, hints: config.order_hints).ordered
142
+ ordered = if command == "release-state"
143
+ members.sort_by(&:name)
144
+ else
145
+ Orderer.new(members: members, mode: config.order_mode, hints: config.order_hints).ordered
146
+ end
142
147
  selected = Selection.new(members: ordered).apply(only: options[:only], start_at: options[:start_at])
143
148
  result_members = if command == "branch-lanes"
144
149
  ordered
@@ -163,6 +168,7 @@ module Kettle
163
168
  def command_results(command:, config:, members:, options:)
164
169
  return bump_version_results(members: members, options: options) if command == "bump-version"
165
170
  return branch_lane_results(config: config, members: members) if command == "branch-lanes"
171
+ return release_state_results(members: members) if command == "release-state"
166
172
  return [] unless WORKFLOW_COMMANDS.include?(command)
167
173
 
168
174
  Workflow.new(
@@ -201,6 +207,10 @@ module Kettle
201
207
  BranchLaneAudit.new(config: config, members: members).results
202
208
  end
203
209
 
210
+ def release_state_results(members:)
211
+ ReleaseStateCheck.new(members: members).results
212
+ end
213
+
204
214
  def write_report(report, options)
205
215
  return unless options[:report]
206
216
 
@@ -13,8 +13,7 @@ module Kettle
13
13
  :stderr,
14
14
  :elapsed_seconds,
15
15
  :skipped,
16
- :reason,
17
- keyword_init: true
16
+ :reason
18
17
  ) do
19
18
  def to_h
20
19
  {
@@ -6,6 +6,7 @@ module Kettle
6
6
  module Family
7
7
  class Config
8
8
  DEFAULT_PATHS = [".kettle-family.yml", ".structuredmerge/kettle-family.yml"].freeze
9
+ DEFAULT_MEMBER_EXCLUDES = ["**/vendor/**", "**/tmp/**", "**/spec/**", "**/test/**"].freeze
9
10
 
10
11
  attr_reader :data, :path, :root
11
12
 
@@ -62,6 +63,12 @@ module Kettle
62
63
  members.fetch("discover", true)
63
64
  end
64
65
 
66
+ def member_exclude_patterns
67
+ members = data.fetch("members", {})
68
+ patterns = members.fetch("exclude", nil) || members.fetch("ignore", nil) || []
69
+ DEFAULT_MEMBER_EXCLUDES + Array(patterns)
70
+ end
71
+
65
72
  def order_mode
66
73
  fetch_path("members", "order", "mode") || "dependency"
67
74
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "open3"
4
+
3
5
  module Kettle
4
6
  module Family
5
7
  class Discovery
@@ -30,7 +32,7 @@ module Kettle
30
32
  gemspecs = config.member_roots.flat_map do |root|
31
33
  Dir.glob(File.join(root, "**", "*.gemspec"))
32
34
  end
33
- gemspecs.reject! { |path| path.include?("/vendor/") }
35
+ gemspecs.reject! { |path| excluded_gemspec?(path) }
34
36
  gemspecs.map { |path| member_from_gemspec(path) }
35
37
  end
36
38
 
@@ -78,6 +80,42 @@ module Kettle
78
80
  rescue => error
79
81
  raise Error, "could not load gemspec #{path}: #{error.message}"
80
82
  end
83
+
84
+ def excluded_gemspec?(path)
85
+ ignored_by_git?(path) || excluded_by_pattern?(path)
86
+ end
87
+
88
+ def ignored_by_git?(path)
89
+ _stdout, _stderr, status = Open3.capture3("git", "check-ignore", "--quiet", "--", path, chdir: config.root)
90
+ status.success?
91
+ end
92
+
93
+ def excluded_by_pattern?(path)
94
+ config.member_exclude_patterns.any? do |pattern|
95
+ path_matches_pattern?(path, pattern)
96
+ end
97
+ end
98
+
99
+ def path_matches_pattern?(path, pattern)
100
+ relative_candidates(path).any? do |relative|
101
+ File.fnmatch?(pattern, relative, File::FNM_DOTMATCH | File::FNM_EXTGLOB)
102
+ end
103
+ end
104
+
105
+ def relative_candidates(path)
106
+ ([config.root] + config.member_roots).filter_map do |root|
107
+ relative_path(path, root)
108
+ end
109
+ end
110
+
111
+ def relative_path(path, root)
112
+ expanded_path = File.expand_path(path)
113
+ expanded_root = File.expand_path(root)
114
+ prefix = "#{expanded_root}/"
115
+ return unless expanded_path.start_with?(prefix)
116
+
117
+ expanded_path.delete_prefix(prefix)
118
+ end
81
119
  end
82
120
  end
83
121
  end
@@ -8,8 +8,7 @@ module Kettle
8
8
  :gemspec_path,
9
9
  :version_file,
10
10
  :version,
11
- :dependencies,
12
- keyword_init: true
11
+ :dependencies
13
12
  ) do
14
13
  def to_h
15
14
  {
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+
6
+ module Kettle
7
+ module Family
8
+ class ReleaseStateCheck
9
+ COMMAND = ["bundle", "exec", "kettle-changelog", "--release-state", "--json"].freeze
10
+
11
+ def initialize(members:)
12
+ @members = members
13
+ end
14
+
15
+ def results
16
+ members.map { |member| check_member(member) }
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :members
22
+
23
+ def check_member(member)
24
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
25
+ stdout, stderr, status = Open3.capture3(*COMMAND, chdir: member.root)
26
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
27
+ success = status.success?
28
+ state = success ? JSON.parse(stdout) : {}
29
+ result(member: member, stdout: stdout, stderr: stderr, status: status.exitstatus, elapsed: elapsed, success: success, state: state)
30
+ rescue JSON::ParserError => error
31
+ result(member: member, stdout: stdout, stderr: stderr, status: 1, elapsed: elapsed || 0.0, success: false, state: {}, reason: "invalid release-state JSON: #{error.message}")
32
+ end
33
+
34
+ def result(member:, stdout:, stderr:, status:, elapsed:, success:, state:, reason: nil)
35
+ ReleaseStateResult.new(
36
+ member_name: member.name,
37
+ command: COMMAND,
38
+ workdir: member.root,
39
+ status: status,
40
+ success: success,
41
+ stdout: stdout,
42
+ stderr: stderr,
43
+ elapsed_seconds: elapsed,
44
+ state: state,
45
+ reason: reason || (success ? nil : "release state check failed")
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kettle
4
+ module Family
5
+ class ReleaseStateResult
6
+ attr_reader :member_name, :phase, :command, :workdir, :status, :success, :stdout, :stderr, :elapsed_seconds, :skipped, :reason, :state
7
+
8
+ def initialize(member_name:, command:, workdir:, status:, success:, stdout:, stderr:, elapsed_seconds:, state:, reason: nil)
9
+ @member_name = member_name
10
+ @phase = "release_state"
11
+ @command = command
12
+ @workdir = workdir
13
+ @status = status
14
+ @success = success
15
+ @stdout = stdout
16
+ @stderr = stderr
17
+ @elapsed_seconds = elapsed_seconds
18
+ @skipped = false
19
+ @reason = reason
20
+ @state = state
21
+ end
22
+
23
+ def ok?
24
+ success
25
+ end
26
+
27
+ def to_h
28
+ {
29
+ "member" => member_name,
30
+ "phase" => phase,
31
+ "command" => command,
32
+ "workdir" => workdir,
33
+ "status" => status,
34
+ "success" => success,
35
+ "stdout" => stdout.to_s.lines.last(20).map(&:chomp).join("\n"),
36
+ "stderr" => stderr.to_s.lines.last(20).map(&:chomp).join("\n"),
37
+ "elapsed_seconds" => elapsed_seconds,
38
+ "skipped" => skipped,
39
+ "reason" => reason,
40
+ "release_state" => state
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -65,6 +65,7 @@ module Kettle
65
65
 
66
66
  def append_results(lines)
67
67
  return if results.empty?
68
+ return append_release_state_results(lines) if command == "release-state"
68
69
 
69
70
  lines << "results:"
70
71
  results.each do |result|
@@ -81,6 +82,55 @@ module Kettle
81
82
  "failed"
82
83
  end
83
84
 
85
+ def append_release_state_results(lines)
86
+ lines << "release state:"
87
+ rows = [["gem", "version.rb", "latest released", "latest changelog", "unreleased", "prepared", "pending"]]
88
+ results.each do |result|
89
+ rows << release_state_row(result)
90
+ end
91
+ lines.concat(format_table(rows).map { |line| " #{line}" })
92
+ failures = results.reject(&:ok?)
93
+ return if failures.empty?
94
+
95
+ lines << "release state errors:"
96
+ failures.each do |result|
97
+ lines << " failed #{result.member_name} #{result.reason || ""}".rstrip
98
+ lines << " #{result.stderr}" unless result.stderr.to_s.empty?
99
+ end
100
+ end
101
+
102
+ def release_state_row(result)
103
+ state = result.state || {}
104
+ [
105
+ state.fetch("gem_name", result.member_name).to_s,
106
+ state.fetch("version", "unknown").to_s,
107
+ state.fetch("latest_released", nil).to_s.empty? ? "unknown" : state.fetch("latest_released").to_s,
108
+ state.fetch("latest_changelog_version", nil).to_s.empty? ? "none" : state.fetch("latest_changelog_version").to_s,
109
+ yes_no(state.fetch("unreleased_entries", nil)),
110
+ yes_no(state.fetch("prepared_release_pending", nil)),
111
+ yes_no(state.fetch("pending_release", nil))
112
+ ]
113
+ end
114
+
115
+ def format_table(rows)
116
+ widths = rows.transpose.map { |column| column.map(&:length).max }
117
+ rows.flat_map.with_index do |row, index|
118
+ line = row.each_with_index.map { |value, i| value.ljust(widths.fetch(i)) }.join(" ").rstrip
119
+ index.zero? ? [line, widths.map { |width| "-" * width }.join(" ")] : [line]
120
+ end
121
+ end
122
+
123
+ def yes_no(value)
124
+ case value
125
+ when true
126
+ "yes"
127
+ when false
128
+ "no"
129
+ else
130
+ "unknown"
131
+ end
132
+ end
133
+
84
134
  def resume_hint
85
135
  failed = results.find { |result| !result.ok? }
86
136
  resume_hint_for(failed) if failed
@@ -3,7 +3,7 @@
3
3
  module Kettle
4
4
  module Family
5
5
  module Version
6
- VERSION = "0.1.0"
6
+ VERSION = "0.1.2"
7
7
  end
8
8
  VERSION = Version::VERSION # Traditional Constant Location
9
9
  end
data/lib/kettle/family.rb CHANGED
@@ -5,12 +5,14 @@ require "version_gem"
5
5
  require_relative "family/version"
6
6
  require_relative "family/member"
7
7
  require_relative "family/command_result"
8
+ require_relative "family/release_state_result"
8
9
  require_relative "family/command_runner"
9
10
  require_relative "family/readiness_check"
10
11
  require_relative "family/changelog_check"
11
12
  require_relative "family/git_status"
12
13
  require_relative "family/version_bump"
13
14
  require_relative "family/branch_lane_audit"
15
+ require_relative "family/release_state_check"
14
16
  require_relative "family/workflow"
15
17
  require_relative "family/config"
16
18
  require_relative "family/discovery"
@@ -1,6 +1,5 @@
1
1
  module Kettle
2
2
  module Family
3
- VERSION: String
4
3
  # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
4
  end
6
5
  end
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: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -80,7 +80,7 @@ dependencies:
80
80
  version: '2.2'
81
81
  - - ">="
82
82
  - !ruby/object:Gem::Version
83
- version: 2.2.3
83
+ version: 2.2.7
84
84
  type: :development
85
85
  prerelease: false
86
86
  version_requirements: !ruby/object:Gem::Requirement
@@ -90,7 +90,7 @@ dependencies:
90
90
  version: '2.2'
91
91
  - - ">="
92
92
  - !ruby/object:Gem::Version
93
- version: 2.2.3
93
+ version: 2.2.7
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: bundler-audit
96
96
  requirement: !ruby/object:Gem::Requirement
@@ -148,7 +148,7 @@ dependencies:
148
148
  version: '3.1'
149
149
  - - ">="
150
150
  - !ruby/object:Gem::Version
151
- version: 3.1.1
151
+ version: 3.1.2
152
152
  type: :development
153
153
  prerelease: false
154
154
  version_requirements: !ruby/object:Gem::Requirement
@@ -158,7 +158,7 @@ dependencies:
158
158
  version: '3.1'
159
159
  - - ">="
160
160
  - !ruby/object:Gem::Version
161
- version: 3.1.1
161
+ version: 3.1.2
162
162
  - !ruby/object:Gem::Dependency
163
163
  name: kettle-test
164
164
  requirement: !ruby/object:Gem::Requirement
@@ -294,6 +294,8 @@ files:
294
294
  - lib/kettle/family/member.rb
295
295
  - lib/kettle/family/orderer.rb
296
296
  - lib/kettle/family/readiness_check.rb
297
+ - lib/kettle/family/release_state_check.rb
298
+ - lib/kettle/family/release_state_result.rb
297
299
  - lib/kettle/family/report.rb
298
300
  - lib/kettle/family/selection.rb
299
301
  - lib/kettle/family/version.rb
@@ -306,10 +308,10 @@ licenses:
306
308
  - MIT
307
309
  metadata:
308
310
  homepage_uri: https://kettle-family.galtzo.com
309
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v0.1.0
310
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v0.1.0/CHANGELOG.md
311
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v0.1.2
312
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v0.1.2/CHANGELOG.md
311
313
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
312
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/0.1.0
314
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/0.1.2
313
315
  funding_uri: https://github.com/sponsors/pboling
314
316
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
315
317
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file