kettle-family 1.2.30 → 1.2.31

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: 945e285ec99d1d336abdf5a51c05135c99485cafeb1b47a65b81ab72818a6786
4
- data.tar.gz: 29bd40256a7b84edaf06dd9adc4f0d21db43a470b07e71b17dc3295e23f9fe4e
3
+ metadata.gz: 0e7218af91f4e5ff65ed580ef1eb24d8abc3e3c12e4f8f07eaa0061ed525bbe7
4
+ data.tar.gz: 462a5f7b86214cc3de01bc0a83dbcd67f9108176c9a0bf7ff4584edeb0034642
5
5
  SHA512:
6
- metadata.gz: 6a25b8669590d61a7a2ecd48fc64bf7d32d7d6090fcea810b13673534827f6609f700ecbeb0ef8b5dded509c4de135f385323a034a236bfd092a9d789c93d178
7
- data.tar.gz: '0508285a2bc3905833dffe7e0387909613f0ff669d9e600f640d1a052ab53f06fb67d6a7bc64a4bfbae1b8175f4bf5efc268f22e012fe6978802cbc01b4da55b'
6
+ metadata.gz: e98b590ac4ccbbb439943091520efd1fdf81605bbc966564aedb8578ce10c63aac85a6dabdc68a600e002239a3f6797c02b10130239ec303bdab2e041dad64a3
7
+ data.tar.gz: fdbf33733f9dd73fb52af5458b89b2b3c5d85ecd2c7ae954f7cf4b5b9d77fcb07e6a57fd25e5684fb3b7261ecdba7258b0e234d7247114007bdb4e77a1f13bb4
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,19 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.2.31] - 2026-08-05
34
+
35
+ - TAG: [v1.2.31][1.2.31t]
36
+ - COVERAGE: 94.10% -- 4976/5288 lines in 32 files
37
+ - BRANCH COVERAGE: 76.53% -- 1969/2573 branches in 32 files
38
+ - 29.61% documented
39
+
40
+ ### Fixed
41
+
42
+ - Route kettle-family GHA pin maintenance through kettle-gha-pins instead of the legacy kettle-gha-sha-pins executable.
43
+
44
+ - Run kettle-family release-state checks concurrently with --jobs while preserving ordered results.
45
+
33
46
  ## [1.2.30] - 2026-08-04
34
47
 
35
48
  - TAG: [v1.2.30][1.2.30t]
@@ -1787,7 +1800,9 @@ Please file a bug if you notice a violation of semantic versioning.
1787
1800
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
1788
1801
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
1789
1802
 
1790
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.30...HEAD
1803
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.31...HEAD
1804
+ [1.2.31]: https://github.com/kettle-dev/kettle-family/compare/v1.2.30...v1.2.31
1805
+ [1.2.31t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.31
1791
1806
  [1.2.30]: https://github.com/kettle-dev/kettle-family/compare/v1.2.29...v1.2.30
1792
1807
  [1.2.30t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.30
1793
1808
  [1.2.29]: https://github.com/kettle-dev/kettle-family/compare/v1.2.28...v1.2.29
data/README.md CHANGED
@@ -329,6 +329,7 @@ Audit changelog release state across the selected family members:
329
329
  ```console
330
330
  kettle-family release-state
331
331
  kettle-family state
332
+ kettle-family state --jobs 8
332
333
  ```
333
334
 
334
335
  `state` is an alias for `release-state`. The release-state report lists each
@@ -374,6 +375,10 @@ The supported release-state tokens are:
374
375
  When `--only` is omitted, `kettle-family bump` defaults to `--only bump` and
375
376
  `kettle-family release` defaults to `--only pending`.
376
377
 
378
+ State checks run concurrently by default, using up to four jobs or the number
379
+ of available CPU cores, whichever is lower. Use `--jobs N` to set an explicit
380
+ limit; results remain in family order.
381
+
377
382
  Most commands accept `--json` to print a machine-readable report instead of the
378
383
  text report, and `--report PATH` to write that JSON report while still printing
379
384
  the normal text report.
@@ -387,6 +392,10 @@ kettle-family gha-sha-pins --execute
387
392
  kettle-family gha-sha-pins --execute --upgrade minor
388
393
  ```
389
394
 
395
+ The family command name is retained for compatibility, but it delegates to
396
+ the current `kettle-gha-pins` executable for each selected member. The older
397
+ `kettle-gha-sha-pins` executable is a legacy direct entrypoint.
398
+
390
399
  Run release prep/build phases without publishing:
391
400
 
392
401
  ```console
@@ -828,7 +837,7 @@ Thanks for RTFM. ☺️
828
837
  [📌gitmoji]: https://gitmoji.dev
829
838
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
830
839
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
831
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-5.254-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
840
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-5.288-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
832
841
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
833
842
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
834
843
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -68,11 +68,17 @@ module Kettle
68
68
  end
69
69
  end
70
70
 
71
+ module ParallelOptions
72
+ def self.included(base)
73
+ base.option :jobs, value: {type: Integer, usage: "N"}, desc: "Parallel jobs for supported workflows"
74
+ end
75
+ end
76
+
71
77
  module WorkflowOptions
72
78
  def self.included(base)
73
79
  base.option :debug, desc: "Preserve debug environment for workflow commands"
74
80
  base.option :verbose, desc: "Pass verbose mode through to supported workflow commands"
75
- base.option :jobs, value: {type: Integer, usage: "N"}, desc: "Parallel jobs for supported executed workflows"
81
+ ParallelOptions.included(base)
76
82
  base.option :env, value: {type: String, usage: "KEY=VALUE"}, desc: "Override an environment variable for each member workflow command" do |value|
77
83
  parse_env_override(value, workflow_env)
78
84
  end
@@ -279,6 +285,8 @@ module Kettle
279
285
  end
280
286
 
281
287
  class ReleaseState < BaseCommand
288
+ include ParallelOptions
289
+
282
290
  command_name "release-state"
283
291
  usage "[options]"
284
292
  description "Report changelog release state for family members."
@@ -375,7 +383,7 @@ module Kettle
375
383
  class GhaShaPins < WorkflowCommand
376
384
  command_name "gha-sha-pins"
377
385
  usage "[options]"
378
- description "Plan or execute kettle-gha-sha-pins per member."
386
+ description "Plan or execute kettle-gha-pins per member."
379
387
 
380
388
  option :check, desc: "Check whether SHA pins would need edits"
381
389
  option :upgrade, value: {type: String, usage: "LEVEL"}, desc: "SHA pin upgrade strategy: major, minor, patch" do |value|
@@ -646,7 +654,7 @@ module Kettle
646
654
  Orderer.new(members: members, mode: config.order_mode, hints: config.order_hints).ordered
647
655
  end
648
656
  state_event_tape = release_state_event_tape(command: command, config: config, options: options)
649
- release_state_results = release_state_results_for_selection(config: config, members: ordered, only: effective_only)
657
+ release_state_results = release_state_results_for_selection(config: config, members: ordered, only: effective_only, jobs: options[:jobs])
650
658
  selected = Selection.new(members: ordered, release_state_results: release_state_results).apply(only: effective_only, exclude: options[:exclude], start_at: start_at.member)
651
659
  result_members = selected
652
660
  display_members = display_members_for(command: command, config: config, members: ordered, selected_members: selected)
@@ -698,10 +706,10 @@ module Kettle
698
706
  command_results_for_current_branch(command: command, config: config, members: members, options: options, start_at: start_at, state_event_handler: state_event_handler)
699
707
  end
700
708
 
701
- def release_state_results_for_selection(config:, members:, only:)
709
+ def release_state_results_for_selection(config:, members:, only:, jobs: nil)
702
710
  return nil unless only.to_s.split(",").map(&:strip).any? { |token| Selection.status_token?(token) }
703
711
 
704
- ReleaseStateCheck.new(config: config, members: members).results
712
+ ReleaseStateCheck.new(config: config, members: members, jobs: jobs).results
705
713
  end
706
714
 
707
715
  def default_only_filter(command:, only:)
@@ -743,7 +751,7 @@ module Kettle
743
751
  return clean_unreleased_results(config: config, members: members, options: options) if command == "clean-unreleased"
744
752
  return reconcile_release_results(config: config, members: members, options: options) if command == "reconcile-releases"
745
753
  return branch_lane_results(config: config, members: members) if command == "branch-lanes"
746
- return release_state_results(config: config, members: members, event_handler: state_event_handler) if command == "release-state"
754
+ return release_state_results(config: config, members: members, jobs: options[:jobs], event_handler: state_event_handler) if command == "release-state"
747
755
  return install_results(config: config, members: members, options: options) if command == "install"
748
756
  return [] unless WORKFLOW_COMMANDS.include?(command)
749
757
 
@@ -1089,8 +1097,8 @@ module Kettle
1089
1097
  LocalInstall.new(config: config, members: members, execute: options[:execute], jobs: options[:jobs]).results
1090
1098
  end
1091
1099
 
1092
- def release_state_results(config:, members:, event_handler: nil)
1093
- ReleaseStateCheck.new(config: config, members: members).results(event_handler: event_handler)
1100
+ def release_state_results(config:, members:, jobs: nil, event_handler: nil)
1101
+ ReleaseStateCheck.new(config: config, members: members, jobs: jobs).results(event_handler: event_handler)
1094
1102
  end
1095
1103
 
1096
1104
  def release_state_event_tape(command:, config:, options:)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "etc"
4
5
  require "fileutils"
5
6
  require "open3"
6
7
  require "rbconfig"
@@ -18,33 +19,34 @@ module Kettle
18
19
  ".kettle-jem.yml"
19
20
  ].freeze
20
21
  TRANSFER_CHANGELOG_TOTAL_UNSET = Object.new.freeze
22
+ CACHE_MISS = Object.new.freeze
21
23
 
22
- def initialize(members:, config: nil)
24
+ def initialize(members:, config: nil, jobs: nil)
23
25
  @members = members
24
26
  @config = config
27
+ @jobs = jobs
25
28
  @github_latest_release_by_repo = {}
26
29
  @transfer_changelog_status_by_root = {}
27
30
  @transfer_changelog_lag_by_key = {}
28
31
  @transfer_changelog_total_count = TRANSFER_CHANGELOG_TOTAL_UNSET
32
+ @cache_mutex = Mutex.new
33
+ @transfer_changelog_total_mutex = Mutex.new
29
34
  end
30
35
 
31
36
  def results(event_handler: nil)
32
37
  return branch_results(event_handler: event_handler) unless release_target_branches.empty?
33
- return members.map { |member| check_shared_changelog_member_or_local(member, event_handler: event_handler) } if shared_changelog?
38
+ return parallel_map(members) { |member| check_shared_changelog_member_or_local(member, event_handler: event_handler) } if shared_changelog?
34
39
 
35
- members.each_with_object([]) do |member, memo|
40
+ member_results = parallel_map(members) do |member|
36
41
  member_branch_results = member_local_branch_results(member, event_handler: event_handler)
37
- if member_branch_results
38
- memo.concat(member_branch_results)
39
- else
40
- memo << check_member(member, event_handler: event_handler)
41
- end
42
+ member_branch_results || check_member(member, event_handler: event_handler)
42
43
  end
44
+ member_results.flat_map { |result| Array(result) }
43
45
  end
44
46
 
45
47
  private
46
48
 
47
- attr_reader :members, :config
49
+ attr_reader :members, :config, :jobs
48
50
 
49
51
  def branch_results(event_handler: nil)
50
52
  root = git_root
@@ -53,11 +55,11 @@ module Kettle
53
55
  with_branch_worktree(root: root, branch: branch) do |worktree_root|
54
56
  branch_members = discover_branch_members(worktree_root: worktree_root, selected_names: selected_names)
55
57
  if shared_changelog?
56
- memo.concat(branch_members.map { |member| check_shared_changelog_member_or_local(member, branch: branch, event_handler: event_handler) })
58
+ memo.concat(parallel_map(branch_members) { |member| check_shared_changelog_member_or_local(member, branch: branch, event_handler: event_handler) })
57
59
  next
58
60
  end
59
61
 
60
- memo.concat(branch_members.map { |member| check_member(member, branch: branch, event_handler: event_handler) })
62
+ memo.concat(parallel_map(branch_members) { |member| check_member(member, branch: branch, event_handler: event_handler) })
61
63
  end
62
64
  rescue Error => error
63
65
  memo << error_result(branch: branch, error: error)
@@ -418,8 +420,8 @@ module Kettle
418
420
 
419
421
  def transfer_changelog_lag(last_entry_key)
420
422
  cache_key = last_entry_key.to_s
421
- @transfer_changelog_lag_by_key.fetch(cache_key) do
422
- @transfer_changelog_lag_by_key[cache_key] = in_process_transfer_changelog_lag(last_entry_key) || external_transfer_changelog_lag(cache_key)
423
+ cached_value(@transfer_changelog_lag_by_key, cache_key) do
424
+ in_process_transfer_changelog_lag(last_entry_key) || external_transfer_changelog_lag(cache_key)
423
425
  end
424
426
  rescue
425
427
  nil
@@ -427,18 +429,20 @@ module Kettle
427
429
 
428
430
  def transfer_changelog_status(root)
429
431
  cache_key = File.expand_path(root.to_s)
430
- @transfer_changelog_status_by_root.fetch(cache_key) do
431
- @transfer_changelog_status_by_root[cache_key] = in_process_transfer_changelog_status(cache_key) || external_transfer_changelog_status(cache_key)
432
+ cached_value(@transfer_changelog_status_by_root, cache_key) do
433
+ in_process_transfer_changelog_status(cache_key) || external_transfer_changelog_status(cache_key)
432
434
  end
433
435
  rescue
434
436
  nil
435
437
  end
436
438
 
437
439
  def transfer_changelog_total_count
438
- return @transfer_changelog_total_count unless @transfer_changelog_total_count.equal?(TRANSFER_CHANGELOG_TOTAL_UNSET)
440
+ @transfer_changelog_total_mutex.synchronize do
441
+ return @transfer_changelog_total_count unless @transfer_changelog_total_count.equal?(TRANSFER_CHANGELOG_TOTAL_UNSET)
439
442
 
440
- total_lag = transfer_changelog_lag(nil)
441
- @transfer_changelog_total_count = total_lag ? total_lag.fetch("missing_count", 0).to_i : nil
443
+ total_lag = transfer_changelog_lag(nil)
444
+ @transfer_changelog_total_count = total_lag ? total_lag.fetch("missing_count", 0).to_i : nil
445
+ end
442
446
  end
443
447
 
444
448
  def in_process_transfer_changelog_lag(last_entry_key)
@@ -509,15 +513,62 @@ module Kettle
509
513
  repo = github_repo_slug(root)
510
514
  return nil unless repo
511
515
 
512
- @github_latest_release_by_repo.fetch(repo) do
516
+ cached_value(@github_latest_release_by_repo, repo) do
513
517
  stdout, _stderr, status = Open3.capture3("gh", "release", "view", "--repo", repo, "--json", "tagName", "--jq", ".tagName")
514
- tag = status.success? ? normalize_github_release_tag(stdout) : nil
515
- @github_latest_release_by_repo[repo] = tag
518
+ status.success? ? normalize_github_release_tag(stdout) : nil
516
519
  end
517
520
  rescue SystemCallError
518
521
  nil
519
522
  end
520
523
 
524
+ def cached_value(cache, key)
525
+ cached = @cache_mutex.synchronize { cache.fetch(key, CACHE_MISS) }
526
+ return cached unless cached.equal?(CACHE_MISS)
527
+
528
+ value = yield
529
+ @cache_mutex.synchronize do
530
+ cache[key] = value unless cache.key?(key)
531
+ cache.fetch(key)
532
+ end
533
+ end
534
+
535
+ def state_jobs(items)
536
+ return 1 if items.length < 2
537
+
538
+ requested = jobs || [Etc.nprocessors, 4].min
539
+ requested.to_i.clamp(1, items.length)
540
+ end
541
+
542
+ def parallel_map(items, &block)
543
+ return [] if items.empty?
544
+
545
+ worker_count = state_jobs(items)
546
+ return items.map { |item| block.call(item) } if worker_count == 1
547
+
548
+ queue = Queue.new
549
+ items.each_with_index { |item, index| queue << [index, item] }
550
+ results = Array.new(items.length)
551
+ errors = []
552
+ errors_mutex = Mutex.new
553
+ workers = Array.new(worker_count) do
554
+ Thread.new do # rubocop:disable ThreadSafety/NewThread -- release-state checks intentionally run independently within the jobs limit.
555
+ loop do
556
+ index, item = queue.pop(true)
557
+ results[index] = block.call(item)
558
+ rescue ThreadError
559
+ break
560
+ rescue => error
561
+ errors_mutex.synchronize { errors << error }
562
+ break
563
+ end
564
+ end
565
+ end
566
+ workers.each(&:join)
567
+ raise errors.first unless errors.empty?
568
+
569
+ results
570
+ end
571
+
521
572
  def normalize_github_release_tag(value)
522
573
  tag = value.to_s.strip
523
574
  return nil if tag.empty?
@@ -33,7 +33,7 @@ module Kettle
33
33
  payload["sequence"] = @sequence_by_member.fetch(member)
34
34
  line = JSON.generate(payload)
35
35
  File.open(path_for(member), "a") { |file| file.puts(line) }
36
- @stream.puts(line) if @stream
36
+ @stream&.puts(line)
37
37
  @stream.flush if @stream&.respond_to?(:flush)
38
38
  end
39
39
  payload
@@ -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.2.30"
8
+ VERSION = "1.2.31"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -5,7 +5,7 @@ require "fileutils"
5
5
  require "json"
6
6
  require "etc"
7
7
  require "open3"
8
- require "pathname"
8
+ require "pathname" # rubocop:disable Lint/RedundantRequireStatement -- Workflow uses Pathname directly and must load it when required standalone.
9
9
  require "rbconfig"
10
10
  require "shellwords"
11
11
  require "yaml"
@@ -23,7 +23,7 @@ module Kettle
23
23
  "test" => "bundle exec kettle-test",
24
24
  "lint" => "bundle exec rake rubocop_gradual",
25
25
  "docs" => "bundle exec rake yard",
26
- "gha-sha-pins" => "bundle exec kettle-gha-sha-pins",
26
+ "gha-sha-pins" => "bundle exec kettle-gha-pins",
27
27
  "sync" => [
28
28
  "sh",
29
29
  "-lc",
@@ -2281,24 +2281,15 @@ module Kettle
2281
2281
  executable = local_kettle_jem_executable
2282
2282
  return command_text unless executable
2283
2283
 
2284
- if command_text.is_a?(Array)
2285
- argv = command_text.map(&:to_s)
2286
- index = argv.index("kettle-jem")
2287
- return command_text unless index
2288
-
2289
- prefix = argv[0...index]
2290
- prefix = prefix[0...-2] if prefix.last(2) == %w[bundle exec]
2291
- prefix + [RbConfig.ruby, executable] + argv[(index + 1)..]
2292
- else
2293
- argv = Shellwords.split(command_text.to_s)
2294
- index = argv.index("kettle-jem")
2295
- return command_text unless index
2296
- return command_text if argv.any? { |token| %w[&& || ; |].include?(token) }
2284
+ array_command = command_text.is_a?(Array)
2285
+ argv = array_command ? command_text.map(&:to_s) : Shellwords.split(command_text.to_s)
2286
+ index = argv.index("kettle-jem")
2287
+ return command_text unless index
2288
+ return command_text if !array_command && argv.any? { |token| %w[&& || ; |].include?(token) }
2297
2289
 
2298
- prefix = argv[0...index]
2299
- prefix = prefix[0...-2] if prefix.last(2) == %w[bundle exec]
2300
- prefix + [RbConfig.ruby, executable] + argv[(index + 1)..]
2301
- end
2290
+ prefix = argv[0...index]
2291
+ prefix = prefix[0...-2] if prefix.last(2) == %w[bundle exec]
2292
+ prefix + [RbConfig.ruby, executable] + argv[(index + 1)..]
2302
2293
  rescue ArgumentError
2303
2294
  command_text
2304
2295
  end
@@ -2965,10 +2956,8 @@ module Kettle
2965
2956
  env[key] = value if key.end_with?("_DEV", "_LOCAL") && local_path_env_requested?(key)
2966
2957
  end
2967
2958
  env.merge!(env_overrides)
2968
- env.merge!(
2969
- "K_JEM_TEMPLATING" => "false",
2970
- "BUNDLE_DISABLE_CHECKSUM_VALIDATION" => "true"
2971
- )
2959
+ env["K_JEM_TEMPLATING"] = "false"
2960
+ env["BUNDLE_DISABLE_CHECKSUM_VALIDATION"] = "true"
2972
2961
  env
2973
2962
  end
2974
2963
 
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.2.30
4
+ version: 1.2.31
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -360,10 +360,10 @@ licenses:
360
360
  - AGPL-3.0-only
361
361
  metadata:
362
362
  homepage_uri: https://kettle-family.galtzo.com
363
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.30
364
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.30/CHANGELOG.md
363
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.31
364
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.31/CHANGELOG.md
365
365
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
366
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.30
366
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.31
367
367
  funding_uri: https://github.com/sponsors/pboling
368
368
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
369
369
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file