kettle-family 0.1.31 → 0.1.32

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: 04edf8e4e21650602cc2162d54c6231a9c3ee61c9f3864cdd21ef7ac4ae127e7
4
- data.tar.gz: 51ccd928cf357a0a405c593226b239d41fad32ccf18f190ecf355168576de9d7
3
+ metadata.gz: ab7db3c6a2cfe0e5c9456e14e0d7b643ee341da154f591c5de6b81d09a423a65
4
+ data.tar.gz: 9b1b9ecbc828efccfc1d51327a9a4efe7b69b2726500cabdca5943c6ca742f4e
5
5
  SHA512:
6
- metadata.gz: 741ee2a5e4af039d3a44915ca6a93036ca265f59fa8f8a1d9cd011e888bf5e7acc7a637d4e99f12ec555915233f9096bddbca905f167fff28ff6e5ae5dc56a1f
7
- data.tar.gz: d3b4434516b929156187e73ee4b65afa4f153818ee958cbf0221924ad2c20adc9da28df6887ff57aabd265f68d07157e8381d1c554575fe842b432150fe9eb5d
6
+ metadata.gz: 4e95a38962f4f0f4dada8de8adf59e9c2910ef9c7984df519f474fda1167e86523a6bb54d87382da9dd7e231bbd44df08955fed36e916aeb28acf078354cfc28
7
+ data.tar.gz: 28c0f4e0592ef36165f5a4bfe62a82fb8c13c8ee2ff5a22ad28dddad84a4b5003a84d5272f84eeb936d920f2fc2a135d426e54ca8d0b48a84203b6e2cefc33d9
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,31 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [0.1.32] - 2026-07-01
34
+
35
+ - TAG: [v0.1.32][0.1.32t]
36
+ - COVERAGE: 95.46% -- 2227/2333 lines in 21 files
37
+ - BRANCH COVERAGE: 75.96% -- 714/940 branches in 21 files
38
+ - 29.82% documented
39
+
40
+ ### Added
41
+
42
+ - `kettle-family` now supports `--exclude` anywhere member selection is
43
+ available, selecting all members except the comma-separated exclusions.
44
+ - `kettle-family` now uses command-specific option parsing and help powered by
45
+ `command_kit`, keeping naked help focused on global options.
46
+
47
+ ### Fixed
48
+
49
+ - `kettle-family` reports a final summary for every command, including selected
50
+ release members left pending when parallel release execution stops after a
51
+ failure.
52
+
53
+ - `kettle-family release --execute` runs release members sequentially on
54
+ TruffleRuby to avoid a TruffleRuby 24.2 internal `ENV.replace` crash from
55
+ `Bundler.with_unbundled_env` inside parallel release threads
56
+ ([truffleruby/truffleruby#4352](https://github.com/truffleruby/truffleruby/issues/4352)).
57
+
33
58
  ## [0.1.31] - 2026-06-30
34
59
 
35
60
  - TAG: [v0.1.31][0.1.31t]
@@ -547,7 +572,9 @@ Please file a bug if you notice a violation of semantic versioning.
547
572
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
548
573
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
549
574
 
550
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v0.1.31...HEAD
575
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v0.1.32...HEAD
576
+ [0.1.32]: https://github.com/kettle-dev/kettle-family/compare/v0.1.31...v0.1.32
577
+ [0.1.32t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.1.32
551
578
  [0.1.31]: https://github.com/kettle-dev/kettle-family/compare/v0.1.30...v0.1.31
552
579
  [0.1.31t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.1.31
553
580
  [0.1.30]: https://github.com/kettle-dev/kettle-family/compare/v0.1.29...v0.1.30
data/README.md CHANGED
@@ -592,7 +592,7 @@ Thanks for RTFM. ☺️
592
592
  [📌gitmoji]: https://gitmoji.dev
593
593
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
594
594
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
595
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.071-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
595
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.333-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
596
596
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
597
597
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
598
598
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -1,192 +1,498 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "command_kit"
4
+ require "command_kit/commands"
3
5
  require "fileutils"
4
6
  require "optparse"
5
7
 
6
8
  module Kettle
7
9
  module Family
8
- class CLI
10
+ class CLI < CommandKit::Command
11
+ include CommandKit::Commands
12
+
9
13
  COMMANDS = %w[discover plan report metadata check test lint docs template gha-sha-pins bup bupb bex install bump-version add-changelog release push pull up branch-lanes release-state].freeze
10
14
  WORKFLOW_COMMANDS = %w[check test lint docs template gha-sha-pins bup bupb bex release push pull up].freeze
11
15
 
16
+ command_name "kettle-family"
17
+ usage "[options] COMMAND [ARGS...]"
18
+ description "Coordinate related Ruby gems as one family."
19
+
20
+ option :root, value: {type: String, usage: "PATH"}, desc: "Workspace or family root"
21
+ option :config, value: {type: String, usage: "PATH"}, desc: "Family config path"
22
+ option :json, desc: "Print JSON report to stdout"
23
+ option :report, value: {type: String, usage: "PATH"}, desc: "Write JSON report to PATH"
24
+
12
25
  def self.call(argv, out: $stdout, err: $stderr)
13
- new(argv, out: out, err: err).call
26
+ main(argv, stdout: out, stderr: err)
27
+ end
28
+
29
+ module SharedOptions
30
+ def self.included(base)
31
+ base.option :root, value: {type: String, usage: "PATH"}, desc: "Workspace or family root"
32
+ base.option :config, value: {type: String, usage: "PATH"}, desc: "Family config path"
33
+ base.option :json, desc: "Print JSON report to stdout"
34
+ base.option :report, value: {type: String, usage: "PATH"}, desc: "Write JSON report to PATH"
35
+ end
36
+ end
37
+
38
+ module SelectionOptions
39
+ def self.included(base)
40
+ base.option :only, value: {type: String, usage: "MEMBERS"}, desc: "Select comma-separated members"
41
+ base.option :exclude, value: {type: String, usage: "MEMBERS"}, desc: "Exclude comma-separated members"
42
+ base.option :start_at, long: "--start-at", value: {type: String, usage: "MEMBER[@BRANCH]"}, desc: "Select from member through the end of order"
43
+ end
44
+ end
45
+
46
+ module ExecutionOptions
47
+ def self.included(base)
48
+ base.option :execute, desc: "Execute external workflow commands"
49
+ base.option :dry_run, long: "--dry-run", desc: "Plan external workflow commands without running them" do
50
+ options[:execute] = false
51
+ end
52
+ end
53
+ end
54
+
55
+ module CommitOptions
56
+ def self.included(base)
57
+ base.option :commit, desc: "Allow workflow commands that change files to commit"
58
+ base.option :no_commit, long: "--no-commit", desc: "Skip automatic commits after mutating workflow commands" do
59
+ options[:commit] = false
60
+ end
61
+ base.option :allow_dirty, long: "--allow-dirty", desc: "Reserved for compatibility; member repos manage their own commit safety"
62
+ end
63
+ end
64
+
65
+ module WorkflowOptions
66
+ def self.included(base)
67
+ base.option :debug, desc: "Preserve debug environment for workflow commands"
68
+ base.option :jobs, value: {type: Integer, usage: "N"}, desc: "Parallel jobs for supported executed workflows"
69
+ base.option :env, value: {type: String, usage: "KEY=VALUE"}, desc: "Override an environment variable for each member workflow command" do |value|
70
+ parse_env_override(value, workflow_env)
71
+ end
72
+ end
73
+ end
74
+
75
+ module ReturningMain
76
+ def main(argv = [])
77
+ args = parse_options(argv)
78
+ return 1 unless valid_argument_count?(args)
79
+
80
+ run(*args)
81
+ rescue SystemExit => error
82
+ error.status
83
+ rescue Error, OptionParser::ParseError => error
84
+ stderr.puts("kettle-family: #{error.message}")
85
+ 1
86
+ end
87
+
88
+ private
89
+
90
+ def valid_argument_count?(args)
91
+ required_args = self.class.arguments.each_value.count(&:required?)
92
+ optional_args = self.class.arguments.each_value.count(&:optional?)
93
+ has_repeats_arg = self.class.arguments.each_value.any?(&:repeats?)
94
+ return true if args.length >= required_args && (has_repeats_arg || args.length <= (required_args + optional_args))
95
+
96
+ message = if args.length < required_args
97
+ "insufficient number of arguments"
98
+ else
99
+ "unexpected argument(s): #{args[(required_args + optional_args)..].join(" ")}"
100
+ end
101
+ stderr.puts("kettle-family: #{message}")
102
+ help_usage
103
+ false
104
+ end
105
+
106
+ def on_parse_error(error)
107
+ raise error
108
+ end
109
+ end
110
+
111
+ class BaseCommand < CommandKit::Command
112
+ prepend ReturningMain
113
+
114
+ include SharedOptions
115
+ include SelectionOptions
116
+
117
+ def initialize(**kwargs)
118
+ super
119
+ @workflow_env = {}
120
+ end
121
+
122
+ private
123
+
124
+ attr_reader :workflow_env
125
+
126
+ def family_options(overrides = {})
127
+ {
128
+ root: options[:root] || Dir.pwd,
129
+ config: options[:config],
130
+ only: options[:only],
131
+ exclude: options[:exclude],
132
+ start_at: options[:start_at],
133
+ json: truthy_option?(:json),
134
+ report: options[:report],
135
+ execute: truthy_option?(:execute),
136
+ debug: truthy_option?(:debug),
137
+ jobs: options[:jobs],
138
+ workflow_env: workflow_env,
139
+ changelog_section: nil,
140
+ changelog_entry: nil,
141
+ check: truthy_option?(:check),
142
+ from_version: nil,
143
+ gha_sha_pins_upgrade: "patch",
144
+ publish: false,
145
+ release_start_step: nil,
146
+ release_skip_steps: nil,
147
+ release_local_ci: false,
148
+ release_continue_ci_failures: false,
149
+ accept: true,
150
+ tag: false,
151
+ push: false,
152
+ commit: !options.key?(:commit) || options[:commit],
153
+ allow_dirty: truthy_option?(:allow_dirty),
154
+ target_version: nil,
155
+ bup_args: [],
156
+ bex_args: []
157
+ }.merge(overrides)
158
+ end
159
+
160
+ def run_family(command, overrides = {})
161
+ Kettle::Family::CLI.new(stdout: stdout, stderr: stderr).run_command(command, family_options(overrides))
162
+ end
163
+
164
+ def truthy_option?(name)
165
+ options.key?(name) && !!options[name]
166
+ end
167
+
168
+ def parse_env_override(value, env)
169
+ key, env_value = value.split("=", 2)
170
+ raise OptionParser::InvalidArgument, "--env requires KEY=VALUE" if key.to_s.empty? || env_value.nil?
171
+ raise OptionParser::InvalidArgument, "invalid environment variable name #{key.inspect}" unless key.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
172
+
173
+ env[key] = env_value
174
+ end
175
+
176
+ def parse_gha_sha_pins_upgrade(value)
177
+ normalized = value.to_s.downcase
178
+ return normalized if %w[major minor patch].include?(normalized)
179
+
180
+ raise OptionParser::InvalidArgument, "--upgrade must be one of: major, minor, patch"
181
+ end
182
+
183
+ def unexpected_arguments!(args)
184
+ raise OptionParser::InvalidArgument, "unexpected argument(s): #{args.join(" ")}" unless args.empty?
185
+ end
186
+ end
187
+
188
+ class Discover < BaseCommand
189
+ command_name "discover"
190
+ usage "[options]"
191
+ description "Discover family members and print selected order."
192
+
193
+ def run(*args)
194
+ unexpected_arguments!(args)
195
+ run_family("discover")
196
+ end
197
+ end
198
+
199
+ class Plan < Discover
200
+ command_name "plan"
201
+ description "Alias for discover while execution workflows are built."
202
+
203
+ def run(*args)
204
+ unexpected_arguments!(args)
205
+ run_family("plan")
206
+ end
207
+ end
208
+
209
+ class ReportCommand < Discover
210
+ command_name "report"
211
+ description "Print family discovery and configuration report."
212
+
213
+ def run(*args)
214
+ unexpected_arguments!(args)
215
+ run_family("report")
216
+ end
217
+ end
218
+
219
+ class Metadata < BaseCommand
220
+ command_name "metadata"
221
+ usage "[options]"
222
+ description "Print version, Ruby floor, license, and author metadata."
223
+
224
+ def run(*args)
225
+ unexpected_arguments!(args)
226
+ run_family("metadata")
227
+ end
228
+ end
229
+
230
+ class BranchLanes < BaseCommand
231
+ command_name "branch-lanes"
232
+ usage "[options]"
233
+ description "Audit configured branch lanes."
234
+
235
+ def run(*args)
236
+ unexpected_arguments!(args)
237
+ run_family("branch-lanes")
238
+ end
239
+ end
240
+
241
+ class ReleaseState < BaseCommand
242
+ command_name "release-state"
243
+ usage "[options]"
244
+ description "Report changelog release state for family members."
245
+
246
+ def run(*args)
247
+ unexpected_arguments!(args)
248
+ run_family("release-state")
249
+ end
250
+ end
251
+
252
+ class WorkflowCommand < BaseCommand
253
+ include ExecutionOptions
254
+ include WorkflowOptions
255
+ include CommitOptions
256
+
257
+ def run(*args)
258
+ unexpected_arguments!(args)
259
+ run_family(self.class.command_name)
260
+ end
261
+ end
262
+
263
+ class Check < WorkflowCommand
264
+ command_name "check"
265
+ usage "[options]"
266
+ description "Run internal read-only readiness checks."
267
+ end
268
+
269
+ class Test < WorkflowCommand
270
+ command_name "test"
271
+ usage "[options]"
272
+ description "Plan or execute configured test command per member."
273
+ end
274
+
275
+ class Lint < WorkflowCommand
276
+ command_name "lint"
277
+ usage "[options]"
278
+ description "Plan or execute configured lint command per member."
279
+ end
280
+
281
+ class Docs < WorkflowCommand
282
+ command_name "docs"
283
+ usage "[options]"
284
+ description "Plan or execute configured docs command per member."
285
+ end
286
+
287
+ class Template < WorkflowCommand
288
+ command_name "template"
289
+ usage "[options]"
290
+ description "Plan or execute kettle-jem templating per member."
291
+ end
292
+
293
+ class GhaShaPins < WorkflowCommand
294
+ command_name "gha-sha-pins"
295
+ usage "[options]"
296
+ description "Plan or execute kettle-gha-sha-pins per member."
297
+
298
+ option :check, desc: "Check whether SHA pins would need edits"
299
+ option :upgrade, value: {type: String, usage: "LEVEL"}, desc: "SHA pin upgrade strategy: major, minor, patch" do |value|
300
+ options[:upgrade] = parse_gha_sha_pins_upgrade(value)
301
+ end
302
+
303
+ def run(*args)
304
+ unexpected_arguments!(args)
305
+ run_family("gha-sha-pins", gha_sha_pins_upgrade: options[:upgrade] || "patch")
306
+ end
307
+ end
308
+
309
+ class Bup < WorkflowCommand
310
+ command_name "bup"
311
+ usage "[options] [GEM]"
312
+ description "Plan or execute bundle update --all, or bundle update GEM."
313
+ argument :gems, required: false, repeats: true, usage: "GEM", desc: "Gem name(s) to update"
314
+
315
+ def run(*bup_args)
316
+ run_family("bup", bup_args: bup_args)
317
+ end
318
+ end
319
+
320
+ class Bupb < WorkflowCommand
321
+ command_name "bupb"
322
+ usage "[options]"
323
+ description "Plan or execute bundle update --bundler."
324
+ end
325
+
326
+ class Bex < WorkflowCommand
327
+ command_name "bex"
328
+ usage "[options] -- COMMAND [ARGS...]"
329
+ description "Plan or execute bundle exec COMMAND per member."
330
+ argument :command, required: false, repeats: true, usage: "COMMAND [ARGS...]", desc: "Command and arguments to run through bundle exec"
331
+
332
+ def run(*bex_args)
333
+ raise Error, "bex requires COMMAND [ARGS]" if bex_args.empty?
334
+
335
+ run_family("bex", bex_args: bex_args)
336
+ end
337
+ end
338
+
339
+ class Install < BaseCommand
340
+ include ExecutionOptions
341
+
342
+ command_name "install"
343
+ usage "[options]"
344
+ description "Build and install selected local family gems."
345
+
346
+ option :jobs, value: {type: Integer, usage: "N"}, desc: "Parallel jobs for executed installs"
347
+
348
+ def run(*args)
349
+ unexpected_arguments!(args)
350
+ run_family("install")
351
+ end
352
+ end
353
+
354
+ class BumpVersion < BaseCommand
355
+ include ExecutionOptions
356
+ include CommitOptions
357
+
358
+ command_name "bump-version"
359
+ usage "[options] VERSION|major|minor|patch|pre"
360
+ description "Check, plan, or execute family version alignment."
361
+ argument :target_version, required: false, usage: "VERSION|major|minor|patch|pre", desc: "Version or bump target"
362
+
363
+ option :check, desc: "Check whether version bumps would need edits"
364
+ option :from, value: {type: String, usage: "VERSION"}, desc: "Require selected members to currently match VERSION"
365
+
366
+ def run(target_version = nil)
367
+ raise Error, "bump-version requires VERSION, major, minor, patch, or pre" unless target_version
368
+
369
+ run_family("bump-version", target_version: target_version, from_version: options[:from])
370
+ end
14
371
  end
15
372
 
16
- def initialize(argv, out:, err:)
17
- @argv = argv.dup
18
- @out = out
19
- @err = err
373
+ class AddChangelog < BaseCommand
374
+ include ExecutionOptions
375
+
376
+ command_name "add-changelog"
377
+ usage "[options]"
378
+ description "Add an entry to an existing Unreleased changelog section."
379
+
380
+ option :section, value: {type: String, usage: "NAME"}, desc: "Changelog section"
381
+ option :entry, value: {type: String, usage: "TEXT"}, desc: "Changelog entry"
382
+
383
+ def run(*args)
384
+ unexpected_arguments!(args)
385
+ run_family("add-changelog", changelog_section: options[:section], changelog_entry: options[:entry])
386
+ end
20
387
  end
21
388
 
22
- def call
23
- command = argv.shift || "help"
24
- return help if command == "help" || command == "--help" || command == "-h"
389
+ class Release < BaseCommand
390
+ include ExecutionOptions
391
+ include WorkflowOptions
392
+ include CommitOptions
25
393
 
26
- raise Error, "unknown command #{command.inspect}" unless COMMANDS.include?(command)
394
+ command_name "release"
395
+ usage "[options]"
396
+ description "Plan or execute release build/publish phases."
27
397
 
28
- target_version = argv.shift if command == "bump-version"
29
- raise Error, "bump-version requires VERSION, major, minor, patch, or pre" if command == "bump-version" && !target_version
30
- bup_args = parse_bup_args(command)
31
- bex_args = parse_bex_args_with_separator(command)
398
+ option :publish, desc: "Use publish release command instead of build command"
399
+ option :build_only, long: "--build-only", desc: "Use build release command" do
400
+ options[:publish] = false
401
+ end
402
+ option :start_step, long: "--start-step", value: {type: Integer, usage: "N"}, desc: "Pass start_step=N through to kettle-release commands"
403
+ option :skip_steps, long: "--skip-steps", value: {type: String, usage: "LIST"}, desc: "Pass skip_steps=LIST through to kettle-release commands"
404
+ option :local_ci, long: "--local-ci", desc: "Pass --local-ci through to kettle-release commands"
405
+ option :continue_ci_failures, long: "--continue-ci-failures", desc: "Set K_RELEASE_CI_CONTINUE=true for release commands"
406
+ option :accept, desc: "Answer yes to confirmation prompts in interactive commands"
407
+ option :no_accept, long: "--no-accept", desc: "Wait for user input at confirmation prompts" do
408
+ options[:accept] = false
409
+ end
410
+ option :tag, desc: "Add release tag phase"
411
+ option :push, desc: "Add release push phase"
412
+
413
+ def run(*args)
414
+ unexpected_arguments!(args)
415
+ run_family(
416
+ "release",
417
+ publish: truthy_option?(:publish),
418
+ release_start_step: options[:start_step],
419
+ release_skip_steps: options[:skip_steps],
420
+ release_local_ci: truthy_option?(:local_ci),
421
+ release_continue_ci_failures: truthy_option?(:continue_ci_failures),
422
+ accept: !options.key?(:accept) || options[:accept],
423
+ tag: truthy_option?(:tag),
424
+ push: truthy_option?(:push)
425
+ )
426
+ end
427
+ end
32
428
 
33
- options = parse_options(allow_remainder: command == "bex" && bex_args.empty?)
34
- bex_args = argv.shift(argv.length) if command == "bex" && bex_args.empty?
35
- raise Error, "bex requires COMMAND [ARGS]" if command == "bex" && bex_args.empty?
429
+ class Push < WorkflowCommand
430
+ command_name "push"
431
+ usage "[options]"
432
+ description "Plan or execute git push per member."
433
+ end
434
+
435
+ class Pull < WorkflowCommand
436
+ command_name "pull"
437
+ usage "[options]"
438
+ description "Plan or execute git pull --rebase per member."
439
+ end
440
+
441
+ class Up < WorkflowCommand
442
+ command_name "up"
443
+ usage "[options]"
444
+ description "Plan or execute git pull --rebase then git push per member."
445
+ end
446
+
447
+ command Discover
448
+ command Plan
449
+ command "report", ReportCommand
450
+ command Metadata
451
+ command Check
452
+ command Test
453
+ command Lint
454
+ command Docs
455
+ command Template
456
+ command GhaShaPins
457
+ command Bup
458
+ command Bupb
459
+ command Bex
460
+ command Install
461
+ command BumpVersion
462
+ command AddChangelog
463
+ command Release
464
+ command Push
465
+ command Pull
466
+ command Up
467
+ command BranchLanes
468
+ command ReleaseState
469
+
470
+ prepend ReturningMain
471
+
472
+ def run(command = nil, *argv)
473
+ return invoke(command, *argv) if command
474
+
475
+ help
476
+ 0
477
+ end
36
478
 
37
- options[:target_version] = target_version
38
- options[:bup_args] = bup_args
39
- options[:bex_args] = bex_args
40
- return help if options.delete(:help)
479
+ def on_unknown_command(name, _argv = [])
480
+ stderr.puts("kettle-family: unknown command #{name.inspect}")
481
+ 1
482
+ end
41
483
 
484
+ def run_command(command, options)
42
485
  report = build_report(command, options)
43
486
  write_report(report, options)
44
- out.puts(options[:json] ? report.to_json : report.to_text)
487
+ stdout.puts(options[:json] ? report.to_json : report.to_text)
45
488
  report.success? ? 0 : 1
46
489
  rescue Error, OptionParser::ParseError => error
47
- err.puts("kettle-family: #{error.message}")
490
+ stderr.puts("kettle-family: #{error.message}")
48
491
  1
49
492
  end
50
493
 
51
494
  private
52
495
 
53
- attr_reader :argv, :out, :err
54
-
55
- def help
56
- out.puts(<<~HELP)
57
- kettle-family: #{Kettle::Family::VERSION}
58
-
59
- Usage: kettle-family COMMAND [options]
60
- kettle-family bump-version VERSION|major|minor|patch|pre [options]
61
- kettle-family bup [GEM] [options]
62
- kettle-family bex [options] -- COMMAND [ARGS]
63
-
64
- Commands:
65
- discover Discover family members and print selected order
66
- plan Alias for discover while execution workflows are built
67
- report Print family discovery and configuration report
68
- metadata Print version, Ruby floor, license, and author metadata
69
- check Run internal read-only readiness checks
70
- test Plan or execute configured test command per member
71
- lint Plan or execute configured lint command per member
72
- docs Plan or execute configured docs command per member
73
- template Plan or execute kettle-jem templating per member
74
- gha-sha-pins Plan or execute kettle-gha-sha-pins per member
75
- bup Plan or execute bundle update --all, or bundle update GEM
76
- bupb Plan or execute bundle update --bundler
77
- bex Plan or execute bundle exec COMMAND per member
78
- install Build and install selected local family gems
79
- bump-version Check, plan, or execute family version alignment
80
- add-changelog Add an entry to an existing Unreleased changelog section
81
- release Plan or execute release build/publish phases
82
- push Plan or execute git push per member
83
- pull Plan or execute git pull --rebase per member
84
- up Plan or execute git pull --rebase then git push per member
85
- release-state Report changelog release state for family members
86
-
87
- Options:
88
- --root PATH Workspace or family root (default: current directory)
89
- --config PATH Family config path
90
- --only MEMBERS Select comma-separated members
91
- --start-at NAME Select from member through the end of order; use MEMBER@BRANCH for branch stacks
92
- --json Print JSON report to stdout
93
- --report PATH Write JSON report to PATH
94
- --execute Execute external workflow commands
95
- --dry-run Plan external workflow commands without running them (default)
96
- --debug Preserve debug environment for workflow commands
97
- --jobs N Parallel jobs for executed family templating, release, or install
98
- --env KEY=VALUE Override an environment variable for each member workflow command
99
- --section NAME Changelog section for add-changelog
100
- --entry TEXT Changelog entry for add-changelog
101
- --check Check whether bump-version or gha-sha-pins would need edits
102
- --from VERSION Require selected members to currently match VERSION
103
- --upgrade LEVEL GitHub Actions SHA pin upgrade strategy: major, minor, patch
104
- --publish Use publish release command instead of build command
105
- --build-only Use build release command (default)
106
- --start-step N Pass start_step=N through to kettle-release commands
107
- --skip-steps LIST Pass skip_steps=LIST through to kettle-release commands
108
- --local-ci Pass --local-ci through to kettle-release commands
109
- --continue-ci-failures
110
- Set K_RELEASE_CI_CONTINUE=true for release commands
111
- --accept Answer yes to confirmation prompts in interactive commands (default)
112
- --no-accept Wait for user input at confirmation prompts
113
- --tag Add release tag phase
114
- --push Add release push phase
115
- --commit Allow workflow commands that change files to commit (default)
116
- --no-commit Skip automatic commits after mutating workflow commands
117
- --allow-dirty Reserved for compatibility; member repos manage their own commit safety
118
- --help Print this help
119
- HELP
120
- 0
121
- end
122
-
123
- def parse_options(allow_remainder: false)
124
- options = {
125
- root: Dir.pwd,
126
- config: nil,
127
- only: nil,
128
- start_at: nil,
129
- json: false,
130
- report: nil,
131
- execute: false,
132
- debug: false,
133
- jobs: nil,
134
- workflow_env: {},
135
- changelog_section: nil,
136
- changelog_entry: nil,
137
- check: false,
138
- from_version: nil,
139
- gha_sha_pins_upgrade: "patch",
140
- publish: false,
141
- release_start_step: nil,
142
- release_skip_steps: nil,
143
- release_local_ci: false,
144
- release_continue_ci_failures: false,
145
- accept: true,
146
- tag: false,
147
- push: false,
148
- commit: true,
149
- allow_dirty: false
150
- }
151
- OptionParser.new do |parser|
152
- parser.on("--root PATH") { |value| options[:root] = value }
153
- parser.on("--config PATH") { |value| options[:config] = value }
154
- parser.on("--only MEMBER") { |value| options[:only] = value }
155
- parser.on("--start-at MEMBER[@BRANCH]") { |value| options[:start_at] = value }
156
- parser.on("--json") { options[:json] = true }
157
- parser.on("--report PATH") { |value| options[:report] = value }
158
- parser.on("--execute") { options[:execute] = true }
159
- parser.on("--dry-run") { options[:execute] = false }
160
- parser.on("--debug") { options[:debug] = true }
161
- parser.on("--jobs N", Integer) { |value| options[:jobs] = value }
162
- parser.on("--env KEY=VALUE") { |value| parse_env_override(value, options[:workflow_env]) }
163
- parser.on("--section NAME") { |value| options[:changelog_section] = value }
164
- parser.on("--entry TEXT") { |value| options[:changelog_entry] = value }
165
- parser.on("--check") { options[:check] = true }
166
- parser.on("--from VERSION") { |value| options[:from_version] = value }
167
- parser.on("--upgrade LEVEL") { |value| options[:gha_sha_pins_upgrade] = parse_gha_sha_pins_upgrade(value) }
168
- parser.on("--publish") { options[:publish] = true }
169
- parser.on("--build-only") { options[:publish] = false }
170
- parser.on("--start-step N", Integer) { |value| options[:release_start_step] = value }
171
- parser.on("--skip-steps LIST") { |value| options[:release_skip_steps] = value }
172
- parser.on("--local-ci") { options[:release_local_ci] = true }
173
- parser.on("--continue-ci-failures") { options[:release_continue_ci_failures] = true }
174
- parser.on("--accept") { options[:accept] = true }
175
- parser.on("--no-accept") { options[:accept] = false }
176
- parser.on("--tag") { options[:tag] = true }
177
- parser.on("--push") { options[:push] = true }
178
- parser.on("--commit") { options[:commit] = true }
179
- parser.on("--no-commit") { options[:commit] = false }
180
- parser.on("--allow-dirty") { options[:allow_dirty] = true }
181
- parser.on("--help") { options[:help] = true }
182
- end.parse!(argv)
183
- return options if allow_remainder
184
-
185
- raise OptionParser::InvalidArgument, "unexpected argument(s): #{argv.join(" ")}" unless argv.empty?
186
-
187
- options
188
- end
189
-
190
496
  def build_report(command, options)
191
497
  config = Config.load(root: options[:root], path: options[:config])
192
498
  start_at = parse_start_at(options[:start_at])
@@ -198,12 +504,8 @@ module Kettle
198
504
  else
199
505
  Orderer.new(members: members, mode: config.order_mode, hints: config.order_hints).ordered
200
506
  end
201
- selected = Selection.new(members: ordered).apply(only: options[:only], start_at: start_at.member)
202
- result_members = if command == "branch-lanes"
203
- ordered
204
- else
205
- selected
206
- end
507
+ selected = Selection.new(members: ordered).apply(only: options[:only], exclude: options[:exclude], start_at: start_at.member)
508
+ result_members = selected
207
509
  results = command_results(command: command, config: config, members: result_members, options: options, start_at: start_at)
208
510
  Report.new(
209
511
  family_name: config.family_name,
@@ -266,31 +568,12 @@ module Kettle
266
568
  ).results
267
569
  end
268
570
 
269
- def parse_bup_args(command)
270
- return [] unless command == "bup"
271
-
272
- args = []
273
- args << argv.shift while argv.first && !argv.first.start_with?("-")
274
- args
275
- end
276
-
277
- def parse_bex_args_with_separator(command)
278
- return [] unless command == "bex"
279
-
280
- separator_index = argv.index("--")
281
- return [] unless separator_index
282
-
283
- args = argv[(separator_index + 1)..] || []
284
- argv.slice!(separator_index..)
285
- args
286
- end
287
-
288
571
  def progress_io(command, options)
289
572
  return nil unless command == "template"
290
573
  return nil unless options[:execute]
291
574
  return nil if options[:json]
292
575
 
293
- out
576
+ stdout
294
577
  end
295
578
 
296
579
  def branch_target_command?(command, config)
@@ -380,21 +663,6 @@ module Kettle
380
663
  )
381
664
  end
382
665
 
383
- def parse_env_override(value, env)
384
- key, env_value = value.split("=", 2)
385
- raise OptionParser::InvalidArgument, "--env requires KEY=VALUE" if key.to_s.empty? || env_value.nil?
386
- raise OptionParser::InvalidArgument, "invalid environment variable name #{key.inspect}" unless key.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
387
-
388
- env[key] = env_value
389
- end
390
-
391
- def parse_gha_sha_pins_upgrade(value)
392
- normalized = value.to_s.downcase
393
- return normalized if %w[major minor patch].include?(normalized)
394
-
395
- raise OptionParser::InvalidArgument, "--upgrade must be one of: major, minor, patch"
396
- end
397
-
398
666
  def parse_start_at(value)
399
667
  return StartAt.new(nil, nil) unless value
400
668
 
@@ -5,6 +5,26 @@ require "json"
5
5
  module Kettle
6
6
  module Family
7
7
  class Report
8
+ MEMBER_RESULT_COMMANDS = %w[
9
+ add-changelog
10
+ bex
11
+ bump-version
12
+ bup
13
+ bupb
14
+ check
15
+ docs
16
+ gha-sha-pins
17
+ install
18
+ lint
19
+ pull
20
+ push
21
+ release
22
+ release-state
23
+ template
24
+ test
25
+ up
26
+ ].freeze
27
+
8
28
  attr_reader :family_name, :family_mode, :order_mode, :members, :selected_members, :config_path, :command, :results, :branch_lanes, :release_target_branches, :member_release_target_branches, :release_mode
9
29
 
10
30
  def initialize(family_name:, order_mode:, members:, selected_members:, config_path:, family_mode: nil, branch_lanes: {}, release_target_branches: [], member_release_target_branches: {}, release_mode: nil, command: nil, results: [])
@@ -36,6 +56,7 @@ module Kettle
36
56
  "release_mode" => release_mode,
37
57
  "command" => command,
38
58
  "results" => results.map(&:to_h),
59
+ "summary" => summary,
39
60
  "resume_hint" => resume_hint
40
61
  }
41
62
  end
@@ -61,11 +82,12 @@ module Kettle
61
82
  end
62
83
  append_release_waves(lines)
63
84
  append_results(lines)
85
+ append_summary(lines)
64
86
  lines.join("\n")
65
87
  end
66
88
 
67
89
  def success?
68
- results.all?(&:ok?)
90
+ results.all?(&:ok?) && summary_pending.empty?
69
91
  end
70
92
 
71
93
  private
@@ -88,6 +110,32 @@ module Kettle
88
110
  append_template_summary(lines) if command == "template"
89
111
  end
90
112
 
113
+ def append_summary(lines)
114
+ data = summary
115
+ lines << "summary:"
116
+ lines << " outcome: #{data.fetch("outcome")}"
117
+ lines << " selected: #{data.fetch("selected_count")}"
118
+ lines << " results: #{data.fetch("result_count")}"
119
+ lines << " succeeded: #{summary_list(data.fetch("succeeded"))}"
120
+ lines << " skipped: #{summary_list(data.fetch("skipped"))}"
121
+ lines << " failed: #{summary_list(data.fetch("failed").map { |entry| summary_entry(entry) })}"
122
+ lines << " pending: #{summary_list(data.fetch("pending").map { |entry| summary_entry(entry) })}"
123
+ lines << " resume: #{data.fetch("resume_hint")}" if data.fetch("resume_hint")
124
+ end
125
+
126
+ def summary
127
+ {
128
+ "outcome" => success? ? "success" : "failure",
129
+ "selected_count" => selected_members.length,
130
+ "result_count" => visible_results.length,
131
+ "succeeded" => summary_succeeded,
132
+ "skipped" => summary_skipped,
133
+ "failed" => summary_failed,
134
+ "pending" => summary_pending,
135
+ "resume_hint" => resume_hint
136
+ }
137
+ end
138
+
91
139
  def append_release_waves(lines)
92
140
  wave_results = results.select { |result| release_wave_result?(result) }
93
141
  return if wave_results.empty?
@@ -102,6 +150,10 @@ module Kettle
102
150
  result.phase == "release_wave"
103
151
  end
104
152
 
153
+ def visible_results
154
+ results.reject { |result| release_wave_result?(result) }
155
+ end
156
+
105
157
  def append_indented_output(lines, output)
106
158
  output.to_s.each_line(chomp: true) { |line| lines << " #{line}" }
107
159
  end
@@ -146,6 +198,85 @@ module Kettle
146
198
  "failed"
147
199
  end
148
200
 
201
+ def member_result_command?
202
+ MEMBER_RESULT_COMMANDS.include?(command)
203
+ end
204
+
205
+ def selected_names
206
+ selected_members.map(&:name)
207
+ end
208
+
209
+ def selected_member_results
210
+ return {} unless member_result_command?
211
+
212
+ visible_results.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |result, memo|
213
+ next unless selected_names.include?(result.member_name)
214
+
215
+ memo[result.member_name] << result
216
+ end
217
+ end
218
+
219
+ def summary_succeeded
220
+ selected_member_results.filter_map do |member_name, member_results|
221
+ next if member_results.empty?
222
+ next if member_results.any? { |result| !result.ok? }
223
+ next if member_results.all?(&:skipped)
224
+
225
+ member_name
226
+ end
227
+ end
228
+
229
+ def summary_skipped
230
+ selected_member_results.filter_map do |member_name, member_results|
231
+ next if member_results.empty?
232
+ next unless member_results.all?(&:skipped)
233
+
234
+ member_name
235
+ end
236
+ end
237
+
238
+ def summary_failed
239
+ visible_results.reject(&:ok?).map do |result|
240
+ {
241
+ "member" => result.member_name,
242
+ "phase" => result.phase,
243
+ "reason" => result.reason || "command failed"
244
+ }
245
+ end
246
+ end
247
+
248
+ def summary_pending
249
+ return [] unless member_result_command?
250
+ return [] if visible_results.empty?
251
+
252
+ ran = selected_member_results.keys
253
+ reason = pending_reason
254
+ (selected_names - ran).map do |member_name|
255
+ {
256
+ "member" => member_name,
257
+ "phase" => command,
258
+ "reason" => reason
259
+ }
260
+ end
261
+ end
262
+
263
+ def pending_reason
264
+ if visible_results.any? { |result| !result.ok? }
265
+ "not run after earlier failure"
266
+ else
267
+ "no command result recorded"
268
+ end
269
+ end
270
+
271
+ def summary_list(values)
272
+ values.empty? ? "none" : values.join(", ")
273
+ end
274
+
275
+ def summary_entry(entry)
276
+ reason = entry.fetch("reason")
277
+ "#{entry.fetch("member")} #{entry.fetch("phase")} (#{reason})"
278
+ end
279
+
149
280
  def append_metadata_results(lines)
150
281
  lines << "metadata:"
151
282
  rows = [["gem", "version", "ruby", "licenses", "authors"]]
@@ -7,9 +7,10 @@ module Kettle
7
7
  @members = members
8
8
  end
9
9
 
10
- def apply(only: nil, start_at: nil)
10
+ def apply(only: nil, exclude: nil, start_at: nil)
11
11
  selected = members
12
12
  selected = select_only(selected, only) if only
13
+ selected = select_exclude(selected, exclude) if exclude
13
14
  selected = select_start_at(selected, start_at) if start_at
14
15
  raise Error, "selection is empty" if selected.empty?
15
16
 
@@ -30,6 +31,16 @@ module Kettle
30
31
  selected.select { |candidate| names.include?(candidate.name) }
31
32
  end
32
33
 
34
+ def select_exclude(selected, exclude)
35
+ names = exclude.split(",").map(&:strip).reject(&:empty?)
36
+ raise Error, "--exclude requires at least one member" if names.empty?
37
+
38
+ unknown = names - members.map(&:name)
39
+ raise Error, "unknown member(s): #{unknown.join(", ")}" unless unknown.empty?
40
+
41
+ selected.reject { |candidate| names.include?(candidate.name) }
42
+ end
43
+
33
44
  def select_start_at(selected, start_at)
34
45
  index = selected.index { |candidate| candidate.name == start_at }
35
46
  raise Error, "unknown member #{start_at.inspect}" unless index
@@ -3,7 +3,7 @@
3
3
  module Kettle
4
4
  module Family
5
5
  module Version
6
- VERSION = "0.1.31"
6
+ VERSION = "0.1.32"
7
7
  end
8
8
  VERSION = Version::VERSION # Traditional Constant Location
9
9
  end
@@ -394,11 +394,18 @@ module Kettle
394
394
  end
395
395
 
396
396
  def release_jobs(release_members)
397
+ # TruffleRuby issue: https://github.com/truffleruby/truffleruby/issues/4352
398
+ return 1 if truffleruby?
399
+
397
400
  requested = jobs || config.release_jobs
398
401
  count = requested ? requested.to_i : [Etc.nprocessors, 4].min
399
402
  count.clamp(1, release_members.length)
400
403
  end
401
404
 
405
+ def truffleruby?
406
+ RUBY_ENGINE == "truffleruby"
407
+ end
408
+
402
409
  def release_waves(release_members)
403
410
  by_name = release_members.to_h { |member| [member.name, member] }
404
411
  pending = by_name.keys
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.31
4
+ version: 0.1.32
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -37,6 +37,34 @@ cert_chain:
37
37
  -----END CERTIFICATE-----
38
38
  date: 1980-01-02 00:00:00.000000000 Z
39
39
  dependencies:
40
+ - !ruby/object:Gem::Dependency
41
+ name: command_kit
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.6'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.6'
54
+ - !ruby/object:Gem::Dependency
55
+ name: command_kit-completion
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.1'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.1'
40
68
  - !ruby/object:Gem::Dependency
41
69
  name: tsort
42
70
  requirement: !ruby/object:Gem::Requirement
@@ -311,10 +339,10 @@ licenses:
311
339
  - AGPL-3.0-only
312
340
  metadata:
313
341
  homepage_uri: https://kettle-family.galtzo.com
314
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v0.1.31
315
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v0.1.31/CHANGELOG.md
342
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v0.1.32
343
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v0.1.32/CHANGELOG.md
316
344
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
317
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/0.1.31
345
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/0.1.32
318
346
  funding_uri: https://github.com/sponsors/pboling
319
347
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
320
348
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file