kettle-family 1.2.46 → 1.2.47

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: 927dd8ea2185b00cfff68f01eb09a93d0a1b7f48fcb2fc3c2a9c52b0c76898aa
4
- data.tar.gz: a2d7f81ebc0e07a395bfefbf72685c6dff5951e5440c7f4f6e53904a16dace28
3
+ metadata.gz: c628becd2f2b6b83516a9e8a4eaca11802e8080cb0bbbd38eede2cc609d5ef3f
4
+ data.tar.gz: bb7cb06c355256f93d0acceef553ac25b2ccc6a42c2890c3beb094bd2be4a795
5
5
  SHA512:
6
- metadata.gz: ff77a0078c229e82e5d04952006502e188f26df36621079af986af2807dd2117988f7dbd0e8d5159f67669198fd275860e757ac3f3f0b4b558a7697a619e6e03
7
- data.tar.gz: 75eca26e357b3570335ac16bd3668c01b3ab8482f6813431ad22e550b47bc5961432bcc1e88f7f83b57411c75fa55983e25ca1c77d0b1d4f8c754c52bf3f1b31
6
+ metadata.gz: 2e439b1c7f07dff944bf25a14c4827c4ca82645324cd84cce424ebb3cb0070080c647fb8571aa3f4666e1e05d91d4edfc634300eef70c5a990386428fc2bc20b
7
+ data.tar.gz: 419731e66a639aa39874168a2defa4a49b18c0cfa0decd07252c464dbddc85838c84169e668cc5897de61374ef6c831e2f163900167c1e06cd46fd247f2b8e7b
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,23 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.2.47] - 2026-08-10
34
+
35
+ - TAG: [v1.2.47][1.2.47t]
36
+ - COVERAGE: 93.58% -- 5161/5515 lines in 33 files
37
+ - BRANCH COVERAGE: 76.15% -- 2066/2713 branches in 33 files
38
+ - 29.86% documented
39
+
40
+ ### Fixed
41
+
42
+ - Share one authenticated release secret session with member kettle-release processes without repeating 1Password authorization prompts.
43
+
44
+ - Review GitHub Actions pins once during family releases and run member pre-release pin checks from the reviewed cache.
45
+
46
+ - Keep centralized GitHub Actions pin review payloads in release logs instead of replaying verbose JSON in terminal reports.
47
+
48
+ - Continue release preflight after successful GitHub Actions pin review.
49
+
33
50
  ## [1.2.46] - 2026-08-10
34
51
 
35
52
  - TAG: [v1.2.46][1.2.46t]
@@ -2003,7 +2020,9 @@ Please file a bug if you notice a violation of semantic versioning.
2003
2020
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
2004
2021
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
2005
2022
 
2006
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.46...HEAD
2023
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.47...HEAD
2024
+ [1.2.47]: https://github.com/kettle-dev/kettle-family/compare/v1.2.46...v1.2.47
2025
+ [1.2.47t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.47
2007
2026
  [1.2.46]: https://github.com/kettle-dev/kettle-family/compare/v1.2.45...v1.2.46
2008
2027
  [1.2.46t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.46
2009
2028
  [1.2.45]: https://github.com/kettle-dev/kettle-family/compare/v1.2.44...v1.2.45
data/README.md CHANGED
@@ -850,7 +850,7 @@ Thanks for RTFM. ☺️
850
850
  [📌gitmoji]: https://gitmoji.dev
851
851
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
852
852
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
853
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-5.429-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
853
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-5.515-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
854
854
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
855
855
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
856
856
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -220,6 +220,11 @@ module Kettle
220
220
  end
221
221
 
222
222
  def append_result_stdout(lines, result)
223
+ if machine_readable_result?(result)
224
+ append_result_log_path(lines, result) unless result.ok?
225
+ return
226
+ end
227
+
223
228
  if suppress_success_output?(result)
224
229
  append_result_log_path(lines, result) unless result.ok?
225
230
  return
@@ -245,6 +250,10 @@ module Kettle
245
250
  result.stdout.to_s.empty? || (command == "template" && result.ok?)
246
251
  end
247
252
 
253
+ def machine_readable_result?(result)
254
+ command == "release" && %w[gha_sha_pins_list gha_sha_pins_review].include?(result.phase.to_s)
255
+ end
256
+
248
257
  def template_event_stdout?(result)
249
258
  command == "template" && template_events(result.stdout).any?
250
259
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "securerandom"
6
+ require "socket"
7
+
8
+ module Kettle
9
+ module Family
10
+ module Secrets
11
+ # Serves release secret requests from member kettle-release processes.
12
+ # The family owns the configured provider and serializes all requests so
13
+ # one release session is shared across sequential and parallel waves.
14
+ class Broker
15
+ OPERATIONS = %w[gem_signing_passphrase rubygems_otp].freeze
16
+
17
+ attr_reader :path
18
+
19
+ def initialize(provider:, root:)
20
+ @provider = provider
21
+ directory = File.join(root, "tmp", "kettle-family")
22
+ FileUtils.mkdir_p(directory, mode: 0o700)
23
+ @path = File.join(directory, "secrets-#{SecureRandom.hex(8)}.sock")
24
+ @mutex = Mutex.new
25
+ @closed = false
26
+ end
27
+
28
+ def start
29
+ @server = UNIXServer.new(@path)
30
+ File.chmod(0o600, @path)
31
+ @thread = Thread.new { serve } # rubocop:disable ThreadSafety/NewThread -- Broker owns one listener for the family release session.
32
+ self
33
+ rescue
34
+ close
35
+ raise
36
+ end
37
+
38
+ def close
39
+ @closed = true
40
+ @server&.close
41
+ @thread&.join(1)
42
+ ensure
43
+ @thread&.kill if @thread&.alive?
44
+ FileUtils.rm_f(@path)
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :provider
50
+
51
+ def serve
52
+ loop do
53
+ break if @closed
54
+
55
+ client = @server.accept
56
+ Thread.new(client) { |connection| handle(connection) } # rubocop:disable ThreadSafety/NewThread -- Each socket request is isolated from the listener.
57
+ end
58
+ rescue IOError, Errno::EBADF
59
+ return if @closed
60
+
61
+ raise
62
+ end
63
+
64
+ def handle(connection)
65
+ request = JSON.parse(connection.gets.to_s)
66
+ operation = request.fetch("operation")
67
+ raise Error, "unsupported release secrets broker operation #{operation.inspect}" unless OPERATIONS.include?(operation)
68
+
69
+ value = @mutex.synchronize { provider.public_send(operation) }
70
+ connection.write(JSON.generate("ok" => true, "value" => value.to_s))
71
+ connection.write("\n")
72
+ rescue => error
73
+ connection.write(JSON.generate("ok" => false, "error" => error.message))
74
+ connection.write("\n")
75
+ ensure
76
+ connection&.close
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -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.46"
8
+ VERSION = "1.2.47"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -143,13 +143,7 @@ module Kettle
143
143
 
144
144
  def results
145
145
  if command == "release" && execute
146
- release_preflight = release_preflight_results
147
- return release_preflight unless release_preflight.empty?
148
-
149
- return branch_target_results unless config.release_target_branches.empty?
150
- return member_local_branch_target_results if member_local_branch_targets?
151
-
152
- return current_branch_results(members)
146
+ return with_release_secrets_broker { release_results }
153
147
  end
154
148
 
155
149
  return template_with_worktree_sync_results if command == "template" && execute
@@ -166,6 +160,30 @@ module Kettle
166
160
 
167
161
  private
168
162
 
163
+ def release_results
164
+ release_preflight = release_preflight_results
165
+ return release_preflight unless release_preflight.empty?
166
+
167
+ return branch_target_results unless config.release_target_branches.empty?
168
+ return member_local_branch_target_results if member_local_branch_targets?
169
+
170
+ current_branch_results(members)
171
+ end
172
+
173
+ def with_release_secrets_broker
174
+ return yield unless release_secrets_broker_required?
175
+
176
+ @release_secrets_broker = Secrets::Broker.new(provider: @secrets_provider, root: config.root).start
177
+ yield
178
+ ensure
179
+ @release_secrets_broker&.close
180
+ @release_secrets_broker = nil
181
+ end
182
+
183
+ def release_secrets_broker_required?
184
+ execute && release_command_delegates_secrets_to_kettle_release?
185
+ end
186
+
169
187
  attr_reader :command, :config, :members, :execute, :accept, :commit, :allow_dirty, :autostash, :publish, :push, :tag, :start_step, :skip_steps, :local_ci, :continue_ci_failures, :ci_workflows, :skip_bundle_audit, :skip_remotes, :required_remotes, :auto_dependency_floors, :gha_sha_pins_upgrade, :gha_sha_pins_check, :gha_sha_pins_ttl_days, :env_overrides, :debug, :verbose, :jobs, :progress_io, :reset_target, :bup_args, :bex_args, :start_member, :start_branch
170
188
 
171
189
  def template_with_worktree_sync_results
@@ -485,7 +503,7 @@ module Kettle
485
503
  results
486
504
  end
487
505
 
488
- def review_gha_sha_pins(workflow_members, runner:, memo:)
506
+ def review_gha_sha_pins(workflow_members, runner:, memo:, env: command_env)
489
507
  return true if workflow_members.empty?
490
508
 
491
509
  repositories = []
@@ -493,8 +511,9 @@ module Kettle
493
511
  result = runner.call(
494
512
  member: member,
495
513
  phase: "gha_sha_pins_list",
496
- command: gha_sha_pins_command(mode: :list),
497
- env: command_env
514
+ command: gha_sha_pins_command(mode: :list, command_text: command_for("gha-sha-pins")),
515
+ env: env,
516
+ log_path: gha_sha_pins_log_path(member, phase: "list")
498
517
  )
499
518
  memo << result
500
519
  return false unless result.ok?
@@ -505,7 +524,7 @@ module Kettle
505
524
  memo << CommandResult.new(
506
525
  member_name: member.name,
507
526
  phase: "gha_sha_pins_list",
508
- command: gha_sha_pins_command(mode: :list),
527
+ command: gha_sha_pins_command(mode: :list, command_text: command_for("gha-sha-pins")),
509
528
  workdir: member.root,
510
529
  status: 1,
511
530
  success: false,
@@ -513,7 +532,8 @@ module Kettle
513
532
  stderr: "invalid kettle-gha-pins list JSON: #{error.message}",
514
533
  elapsed_seconds: 0.0,
515
534
  skipped: false,
516
- reason: "invalid list output"
535
+ reason: "invalid list output",
536
+ log_path: result&.log_path
517
537
  )
518
538
  return false
519
539
  end
@@ -528,8 +548,9 @@ module Kettle
528
548
  review_result = runner.call(
529
549
  member: workflow_members.first,
530
550
  phase: "gha_sha_pins_review",
531
- command: gha_sha_pins_command(mode: :review, input: review_path),
532
- env: command_env
551
+ command: gha_sha_pins_command(mode: :review, input: review_path, command_text: command_for("gha-sha-pins")),
552
+ env: env,
553
+ log_path: gha_sha_pins_log_path(workflow_members.first, phase: "review")
533
554
  )
534
555
  memo << review_result
535
556
  review_result.ok?
@@ -537,6 +558,16 @@ module Kettle
537
558
  File.delete(review_path) if review_path && File.file?(review_path)
538
559
  end
539
560
 
561
+ def gha_sha_pins_log_path(member, phase:)
562
+ safe_name = member.name.to_s.gsub(/[^A-Za-z0-9_.-]+/, "_")
563
+ File.join(
564
+ config.root,
565
+ "tmp",
566
+ "kettle-family",
567
+ "gha-sha-pins-#{Process.pid}-#{object_id}-#{safe_name}-#{phase}.log"
568
+ )
569
+ end
570
+
540
571
  def template_member_workflow_results(workflow_members)
541
572
  debugger_results = template_debugger_bootstrap_results(workflow_members)
542
573
  return debugger_results unless debugger_results.all?(&:ok?)
@@ -867,12 +898,13 @@ module Kettle
867
898
  label = phase.fetch(:label)
868
899
  progress&.update(preflight_member, status: label, mark: ">")
869
900
  result = send(phase.fetch(:method))
870
- progress&.advance(preflight_member, status: label, success: result.empty?, mark: result.empty? ? "." : "F")
871
- unless result.empty?
901
+ failed = result.any? { |entry| !entry.ok? }
902
+ progress&.advance(preflight_member, status: label, success: !failed, mark: failed ? "F" : ".")
903
+ if failed
872
904
  progress&.finish_member(preflight_member, success: false, status: label)
873
905
  progress&.stop
906
+ return result
874
907
  end
875
- return result unless result.empty?
876
908
  end
877
909
  progress&.finish_member(preflight_member, success: true, status: "ok")
878
910
  progress&.stop
@@ -883,12 +915,27 @@ module Kettle
883
915
  dirty_phase = {label: "branch checkout readiness", method: :release_preflight_branch_checkout_dirty_results}
884
916
  signing_phase = {label: "gem signing password", method: :release_preflight_signing_password_results}
885
917
  secrets_phase = release_secrets_authorization_required? ? {label: "secrets provider authorization", method: :release_preflight_secrets_authorization_results} : nil
918
+ gha_sha_pins_phase = release_gha_sha_pins_review_required? ? {label: "GitHub Actions SHA pins", method: :release_preflight_gha_sha_pins_results} : nil
886
919
 
887
- if secrets_phase
888
- [secrets_phase, dirty_phase, signing_phase]
889
- else
890
- [dirty_phase, signing_phase]
891
- end
920
+ [secrets_phase, gha_sha_pins_phase, dirty_phase, signing_phase].compact
921
+ end
922
+
923
+ def release_preflight_gha_sha_pins_results
924
+ results = []
925
+ review_gha_sha_pins(members, runner: command_runner, memo: results, env: command_env)
926
+ results
927
+ end
928
+
929
+ def release_gha_sha_pins_review_required?
930
+ execute && kettle_release_command?(raw_release_command) && !release_gha_sha_pins_offline_requested?
931
+ end
932
+
933
+ def release_gha_sha_pins_offline_requested?
934
+ value = env_overrides.fetch(
935
+ "KETTLE_PRE_RELEASE_GHA_SHA_PINS_OFFLINE",
936
+ ENV.fetch("KETTLE_PRE_RELEASE_GHA_SHA_PINS_OFFLINE", "")
937
+ )
938
+ %w[true yes 1 on enabled].include?(value.to_s.strip.downcase)
892
939
  end
893
940
 
894
941
  def release_preflight_branch_checkout_dirty_results
@@ -1919,6 +1966,7 @@ module Kettle
1919
1966
  end
1920
1967
 
1921
1968
  def append_kettle_release_args(command)
1969
+ command = replace_kettle_release_provider(command) if @release_secrets_broker
1922
1970
  args = []
1923
1971
  args << "start_step=#{start_step}" if start_step
1924
1972
  args << "skip_steps=#{skip_steps}" if skip_steps && !skip_steps.to_s.empty?
@@ -1928,7 +1976,8 @@ module Kettle
1928
1976
  args << "--skip-remotes=#{skip_remotes}" if skip_remotes && !skip_remotes.to_s.empty?
1929
1977
  args << "--required-remotes=#{required_remotes}" if required_remotes && !required_remotes.to_s.empty?
1930
1978
  if release_command_delegates_secrets_to_kettle_release? && !command_includes_arg?(command, "--secrets-provider")
1931
- args << "--secrets-provider=1password"
1979
+ args << "--secrets-provider=family" if @release_secrets_broker
1980
+ args << "--secrets-provider=1password" unless @release_secrets_broker
1932
1981
  end
1933
1982
  args << "--yes" if release_command_uses_kettle_release_yes? && !command_includes_arg?(command, "--yes")
1934
1983
  args << "--events" unless command_includes_arg?(command, "--events")
@@ -1937,6 +1986,15 @@ module Kettle
1937
1986
  command.is_a?(Array) ? [*command, *args] : "#{command} #{args.join(" ")}"
1938
1987
  end
1939
1988
 
1989
+ def replace_kettle_release_provider(command)
1990
+ case command
1991
+ when Array
1992
+ command.map { |part| part.to_s.gsub(/--secrets-provider(?:=|\s+)\S+/, "--secrets-provider=family") }
1993
+ else
1994
+ command.to_s.gsub(/--secrets-provider(?:=|\s+)\S+/, "--secrets-provider=family")
1995
+ end
1996
+ end
1997
+
1940
1998
  def release_command_uses_kettle_release_yes?
1941
1999
  accept && kettle_release_command?(raw_release_command)
1942
2000
  end
@@ -1982,6 +2040,12 @@ module Kettle
1982
2040
  def release_env_for_member(member)
1983
2041
  env = release_env
1984
2042
  env.merge!(release_wave_local_path_env_for(member))
2043
+ # Family release performs one live pin review before member releases.
2044
+ # Child kettle-release must consume that reviewed cache instead of
2045
+ # repeating the GitHub API lookup for every member.
2046
+ if release_gha_sha_pins_review_required? || release_gha_sha_pins_offline_requested?
2047
+ env["KETTLE_PRE_RELEASE_GHA_SHA_PINS_OFFLINE"] = "true"
2048
+ end
1985
2049
  # Run the member's kettle-release from the local kettle-dev checkout
1986
2050
  # when the family workflow itself is using local release tooling. The
1987
2051
  # child release commands disable local dependency paths explicitly;
@@ -2151,10 +2215,11 @@ module Kettle
2151
2215
 
2152
2216
  secrets_config = config.release_secrets
2153
2217
  env = {
2154
- "KETTLE_RELEASE_SECRETS_PROVIDER" => "1password",
2218
+ "KETTLE_RELEASE_SECRETS_PROVIDER" => @release_secrets_broker ? "family" : "1password",
2155
2219
  "KETTLE_RELEASE_GEM_SIGNING_PASSPHRASE_SOURCE" => "cached",
2156
2220
  "KETTLE_RELEASE_GEM_SIGNING_PASSPHRASE" => @gem_signing_password.to_s
2157
2221
  }
2222
+ env["KETTLE_RELEASE_SECRETS_BROKER"] = @release_secrets_broker.path if @release_secrets_broker
2158
2223
  {
2159
2224
  "account" => "KETTLE_RELEASE_1PASSWORD_ACCOUNT",
2160
2225
  "cli" => "KETTLE_RELEASE_1PASSWORD_CLI",
@@ -2372,8 +2437,8 @@ module Kettle
2372
2437
  ["bundle", "exec", *Array(bex_args).map(&:to_s)]
2373
2438
  end
2374
2439
 
2375
- def gha_sha_pins_command(mode: nil, input: nil)
2376
- command_text = command_for(command)
2440
+ def gha_sha_pins_command(mode: nil, input: nil, command_text: nil)
2441
+ command_text ||= command_for(command)
2377
2442
  args = []
2378
2443
  if mode == :list
2379
2444
  return append_command_args(command_text, ["--list", "--json"])
data/lib/kettle/family.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "family/git_status"
10
10
  require_relative "family/branch_lane_audit"
11
11
  require_relative "family/config"
12
12
  require_relative "family/secrets"
13
+ require_relative "family/secrets_broker"
13
14
  require_relative "family/branch_target_config"
14
15
  require_relative "family/release_state_event_tape"
15
16
  require_relative "family/release_state_check"
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.46
4
+ version: 1.2.47
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -71,7 +71,7 @@ dependencies:
71
71
  requirements:
72
72
  - - ">="
73
73
  - !ruby/object:Gem::Version
74
- version: 2.5.26
74
+ version: 3.0.2
75
75
  - - "<"
76
76
  - !ruby/object:Gem::Version
77
77
  version: 4.0.0
@@ -81,7 +81,7 @@ dependencies:
81
81
  requirements:
82
82
  - - ">="
83
83
  - !ruby/object:Gem::Version
84
- version: 2.5.26
84
+ version: 3.0.2
85
85
  - - "<"
86
86
  - !ruby/object:Gem::Version
87
87
  version: 4.0.0
@@ -347,6 +347,7 @@ files:
347
347
  - lib/kettle/family/release_waves.rb
348
348
  - lib/kettle/family/report.rb
349
349
  - lib/kettle/family/secrets.rb
350
+ - lib/kettle/family/secrets_broker.rb
350
351
  - lib/kettle/family/selection.rb
351
352
  - lib/kettle/family/unreleased_gem_cleanup.rb
352
353
  - lib/kettle/family/version.rb
@@ -360,10 +361,10 @@ licenses:
360
361
  - AGPL-3.0-only
361
362
  metadata:
362
363
  homepage_uri: https://kettle-family.galtzo.com
363
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.46
364
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.46/CHANGELOG.md
364
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.47
365
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.47/CHANGELOG.md
365
366
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
366
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.46
367
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.47
367
368
  funding_uri: https://github.com/sponsors/pboling
368
369
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
369
370
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file