kettle-family 1.2.13 → 1.2.15

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: 14997d7ae5a4ba5f2e3ec46f9fe9925a15020a0549fc779d70c57171492edd57
4
- data.tar.gz: e9e84f2d92c1b8e2434ddc01a2d275e4b9077b06644b854ec817893a7e4157f1
3
+ metadata.gz: 9ccc9063ef81ee8d6b8bee27424903371b47863d9fb2903b4461c2e132b757d9
4
+ data.tar.gz: ec46a0708e899590fb95d67d1f8e9dbe002195b21cf9a635c2c8b4a726a693c9
5
5
  SHA512:
6
- metadata.gz: aac3fc439504f17be4685f87f7bd1d4c213f885941f5c7793f1bca2369e92c384c7ae3cc8919fbf8f1023e7d1d812abe29c76d0a7fbef0301a5a8125b42217f3
7
- data.tar.gz: 9d4ad335a463968aad4643d36b616f37b5d99b1431c659f2f3ae329017f304e5115dff3be3be72829ca99744a8cdf4b81619949146ce1df6f6ae7516226a12e6
6
+ metadata.gz: 883955e631cd8b19ee2219a98267465307dec049d35c7f209f61ce23465eeb05665a88954b0dcac39e70f980aa39a02bdc6f2539c4897ef6418412a7d5ad1054
7
+ data.tar.gz: 8c584b648f8fe2914ac5077b8b181f398b2fe1507eb30278927c13408f5b3b7e6c4a7b1beba417f1dcc1a0ae80a0e7a460d433bb1336615a6ccb3759e2abee34
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,75 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.2.15] - 2026-07-31
34
+
35
+ - TAG: [v1.2.15][1.2.15t]
36
+ - COVERAGE: 94.03% -- 4192/4458 lines in 28 files
37
+ - BRANCH COVERAGE: 76.46% -- 1634/2137 branches in 28 files
38
+ - 28.88% documented
39
+
40
+ ### Fixed
41
+
42
+ - Family-owned release OTP lookups now render as structured secret-provider
43
+ progress events, including a visible `👀 🔒` authorization prompt cue, instead
44
+ of printing loose MFA status lines through the release tape.
45
+
46
+ ## [1.2.14] - 2026-07-31
47
+
48
+ - TAG: [v1.2.14][1.2.14t]
49
+ - COVERAGE: 94.22% -- 4175/4431 lines in 28 files
50
+ - BRANCH COVERAGE: 76.69% -- 1622/2115 branches in 28 files
51
+ - 28.88% documented
52
+
53
+ ### Added
54
+
55
+ - Release publish/build commands now write per-member transcript logs under
56
+ `tmp/kettle-family/release-*`, and failure summaries include the log path.
57
+
58
+ - Release progress now renders child `kettle-release` `remote_parity` events,
59
+ keeping remote fetch, skip, failure, and completion activity visible without
60
+ scraping raw command output.
61
+
62
+ - Release progress now renders child `kettle-release` `ci_monitor` events with
63
+ provider and workflow or pipeline context, so CI activity is visible as
64
+ structured progress.
65
+
66
+ - Release progress now renders child `kettle-release` `pre_release` events, so
67
+ pre-release checks are visible as structured progress while raw output stays
68
+ in the transcript log.
69
+
70
+ - Release progress now renders child `kettle-changelog` `changelog` events, so
71
+ release plan, coverage, and changelog update activity is visible as structured
72
+ progress.
73
+
74
+ ### Changed
75
+
76
+ - Family release now keeps raw child `kettle-release` output in the per-member
77
+ transcript log by default and renders structured NDJSON progress in the
78
+ terminal, with raw passthrough still available under verbose/debug output.
79
+ - Release summary footers now include the transcript log directory when release
80
+ logs were written, including successful runs where raw output stayed quiet.
81
+ - Release progress now renders child `command_step` summaries, so phases can
82
+ show context such as `release:bundle_lock:Gemfile` or
83
+ `release:yard:documentation`.
84
+ - Release progress now renders child `release_lockfile` and `release_probe`
85
+ events, so lockfile reset retries and published-gem availability probes show
86
+ as structured progress.
87
+ - Release progress now renders GitHub CI wait/start/tick events with completion
88
+ counts, so CI monitoring no longer appears stuck while waiting for workflows.
89
+ - Release preflight now renders through the shared progress event tape instead
90
+ of printing separate start and finish lines for each successful phase.
91
+
92
+ ### Fixed
93
+
94
+ - Release preflight no longer prints a nested 1Password authorization alert
95
+ between the phase start and finish lines; the preflight phase itself is the
96
+ user-facing authorization notice.
97
+
98
+ - Family release runs can now pass configured required release remotes through
99
+ to `kettle-release`, allowing mirrors to remain optional while primary remotes
100
+ still block release parity failures.
101
+
33
102
  ## [1.2.13] - 2026-07-31
34
103
 
35
104
  - TAG: [v1.2.13][1.2.13t]
@@ -1443,7 +1512,11 @@ Please file a bug if you notice a violation of semantic versioning.
1443
1512
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
1444
1513
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
1445
1514
 
1446
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.13...HEAD
1515
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.15...HEAD
1516
+ [1.2.15]: https://github.com/kettle-dev/kettle-family/compare/v1.2.14...v1.2.15
1517
+ [1.2.15t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.15
1518
+ [1.2.14]: https://github.com/kettle-dev/kettle-family/compare/v1.2.13...v1.2.14
1519
+ [1.2.14t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.14
1447
1520
  [1.2.13]: https://github.com/kettle-dev/kettle-family/compare/v1.2.12...v1.2.13
1448
1521
  [1.2.13t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.13
1449
1522
  [1.2.12]: https://github.com/kettle-dev/kettle-family/compare/v1.2.11...v1.2.12
data/README.md CHANGED
@@ -819,7 +819,7 @@ Thanks for RTFM. ☺️
819
819
  [📌gitmoji]: https://gitmoji.dev
820
820
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
821
821
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
822
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.297-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
822
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.458-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
823
823
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
824
824
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
825
825
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -151,6 +151,7 @@ module Kettle
151
151
  release_ci_workflows: nil,
152
152
  release_skip_bundle_audit: false,
153
153
  release_skip_remotes: nil,
154
+ release_required_remotes: nil,
154
155
  release_secrets_provider: nil,
155
156
  accept: true,
156
157
  tag: false,
@@ -464,6 +465,7 @@ module Kettle
464
465
  option :ci_workflows, long: "--ci-workflows", value: {type: String, usage: "LIST"}, desc: "Pass a comma-separated CI workflow monitor subset through to kettle-release commands"
465
466
  option :skip_bundle_audit, long: "--skip-bundle-audit", desc: "Skip bundle:audit/update during release rake checks"
466
467
  option :skip_remotes, long: "--skip-remotes", value: {type: String, usage: "LIST"}, desc: "Pass a comma-separated git remote skip list through to kettle-release commands"
468
+ option :required_remotes, long: "--required-remotes", value: {type: String, usage: "LIST"}, desc: "Pass a comma-separated required git remote list through to kettle-release commands"
467
469
  option :secrets_provider, long: "--secrets-provider", value: {type: String, usage: "NAME"}, desc: "Release secrets provider: interactive, 1password"
468
470
  option :no_auto_floors, long: "--no-auto-floors", desc: "Do not raise family dependency floors between member releases" do
469
471
  options[:no_auto_floors] = true
@@ -487,6 +489,7 @@ module Kettle
487
489
  release_ci_workflows: options[:ci_workflows],
488
490
  release_skip_bundle_audit: truthy_option?(:skip_bundle_audit),
489
491
  release_skip_remotes: options[:skip_remotes],
492
+ release_required_remotes: options[:required_remotes],
490
493
  release_secrets_provider: options[:secrets_provider],
491
494
  release_auto_dependency_floors: !truthy_option?(:no_auto_floors),
492
495
  accept: !options.key?(:accept) || options[:accept],
@@ -662,6 +665,7 @@ module Kettle
662
665
  ci_workflows: options[:release_ci_workflows],
663
666
  skip_bundle_audit: options[:release_skip_bundle_audit],
664
667
  skip_remotes: options[:release_skip_remotes],
668
+ required_remotes: options[:release_required_remotes],
665
669
  secrets_provider: release_secrets_provider(command: command, config: config, options: options),
666
670
  auto_dependency_floors: options[:release_auto_dependency_floors],
667
671
  gha_sha_pins_upgrade: options[:gha_sha_pins_upgrade],
@@ -15,7 +15,8 @@ module Kettle
15
15
  :skipped,
16
16
  :reason,
17
17
  :branch,
18
- :output_streamed
18
+ :output_streamed,
19
+ :log_path
19
20
  ) do
20
21
  def to_h
21
22
  {
@@ -31,7 +32,8 @@ module Kettle
31
32
  "elapsed_seconds" => elapsed_seconds,
32
33
  "skipped" => skipped,
33
34
  "reason" => reason,
34
- "output_streamed" => output_streamed?
35
+ "output_streamed" => output_streamed?,
36
+ "log_path" => log_path
35
37
  }
36
38
  end
37
39
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "open3"
4
4
  require "io/console"
5
+ require "fileutils"
5
6
 
6
7
  module Kettle
7
8
  module Family
@@ -11,10 +12,11 @@ module Kettle
11
12
  ].freeze
12
13
 
13
14
  class OtpCoordinator
14
- def initialize(input: $stdin, output: $stdout, queue_total: nil, secrets_provider: nil)
15
+ def initialize(input: $stdin, output: $stdout, queue_total: nil, secrets_provider: nil, event_handler: nil)
15
16
  @input = input
16
17
  @output = output
17
18
  @secrets_provider = secrets_provider
19
+ @event_handler = event_handler
18
20
  @mutex = Mutex.new
19
21
  @condition = ConditionVariable.new
20
22
  @prompting = false
@@ -40,7 +42,7 @@ module Kettle
40
42
  if @prompting
41
43
  generation = @generation
42
44
  @queued_count += 1
43
- render_queue_status_locked
45
+ render_queue_status_locked(member_name: member_name)
44
46
  return wait_for_response(generation)
45
47
  end
46
48
 
@@ -52,7 +54,7 @@ module Kettle
52
54
  start_prompt_locked(member_name: member_name)
53
55
  end
54
56
 
55
- response = read_response(chunk: chunk)
57
+ response = read_response(chunk: chunk, member_name: member_name)
56
58
  close_queue
57
59
  @mutex.synchronize do
58
60
  @response = response
@@ -66,7 +68,7 @@ module Kettle
66
68
 
67
69
  private
68
70
 
69
- attr_reader :secrets_provider
71
+ attr_reader :secrets_provider, :event_handler
70
72
 
71
73
  def wait_for_response(generation)
72
74
  @condition.wait(@mutex) while @prompting
@@ -82,22 +84,49 @@ module Kettle
82
84
  end
83
85
 
84
86
  def start_prompt_locked(member_name:)
85
- @output.puts
86
- @output.puts("[#{member_name}] RubyGems MFA requested.")
87
- render_queue_status_locked
87
+ unless emit_event(
88
+ member_name: member_name,
89
+ action: "mfa_requested",
90
+ label: "RubyGems MFA",
91
+ status: "requested",
92
+ mark: ">"
93
+ )
94
+ @output.puts
95
+ @output.puts("[#{member_name}] RubyGems MFA requested.")
96
+ end
97
+ render_queue_status_locked(member_name: member_name)
98
+ return if event_handler
99
+
88
100
  @output.puts("Queued prompts at entry will share this code; later prompts will ask again.")
89
101
  end
90
102
 
91
- def render_queue_status_locked
103
+ def render_queue_status_locked(member_name:)
92
104
  suffix = @queue_total ? " / #{@queue_total}" : ""
105
+ return if emit_event(
106
+ member_name: member_name,
107
+ action: "otp_queue",
108
+ label: "RubyGems MFA prompts",
109
+ status: "queued",
110
+ mark: ">",
111
+ queued: @queued_count,
112
+ total: @queue_total
113
+ )
114
+
93
115
  @output.puts("RubyGems MFA prompts queued: #{@queued_count}#{suffix}")
94
116
  @output.flush if @output.respond_to?(:flush)
95
117
  end
96
118
 
97
- def read_response(chunk:)
98
- provided = read_provider_response
119
+ def read_response(chunk:, member_name:)
120
+ provided = read_provider_response(member_name: member_name)
99
121
  unless provided.empty?
100
- @output.puts("RubyGems MFA code loaded from configured secrets provider.")
122
+ emit_event(
123
+ member_name: member_name,
124
+ action: "prompt_response",
125
+ label: "RubyGems MFA code",
126
+ source: secret_provider_source,
127
+ status: "ok",
128
+ mark: "."
129
+ ) || @output.puts("RubyGems MFA code loaded from configured secrets provider.")
101
130
  return provided
102
131
  end
103
132
 
@@ -116,17 +145,57 @@ module Kettle
116
145
  chunk.to_s.lines.last&.strip.to_s.empty? ? "Code:" : chunk.to_s.lines.last.strip
117
146
  end
118
147
 
119
- def read_provider_response
148
+ def read_provider_response(member_name:)
120
149
  return "" unless secrets_provider
121
150
 
122
151
  @output.print("\a") if @output.respond_to?(:print)
152
+ emit_event(
153
+ member_name: member_name,
154
+ action: "prompt_request",
155
+ label: "👀 🔒 watch for authorization prompt",
156
+ source: secret_provider_source,
157
+ status: "started",
158
+ mark: ">"
159
+ )
123
160
  secrets_provider.rubygems_otp.to_s
124
161
  rescue Error => error
125
162
  raise unless @input.respond_to?(:tty?) && @input.tty?
126
163
 
164
+ emit_event(
165
+ member_name: member_name,
166
+ action: "prompt_response",
167
+ label: "RubyGems MFA code",
168
+ source: secret_provider_source,
169
+ status: "failed",
170
+ mark: "F"
171
+ )
127
172
  @output.puts("#{error.message}; falling back to manual OTP entry.")
128
173
  ""
129
174
  end
175
+
176
+ def emit_event(member_name:, action:, label:, status:, mark:, **payload)
177
+ return false unless event_handler
178
+
179
+ event_handler.call(
180
+ member_name,
181
+ {
182
+ "event_version" => 1,
183
+ "type" => "secret_provider",
184
+ "action" => action,
185
+ "label" => label,
186
+ "status" => status,
187
+ "mark" => mark
188
+ }.merge(payload.transform_keys(&:to_s))
189
+ )
190
+ true
191
+ end
192
+
193
+ def secret_provider_source
194
+ return "" unless secrets_provider
195
+ return "1Password" if secrets_provider.is_a?(Secrets::OnePassword)
196
+
197
+ secrets_provider.class.name.to_s.split("::").last.to_s
198
+ end
130
199
  end
131
200
 
132
201
  FailureStatus = Struct.new(:exitstatus) do
@@ -143,26 +212,33 @@ module Kettle
143
212
  @otp_coordinator = otp_coordinator
144
213
  end
145
214
 
146
- def call(member:, phase:, command:, env: {}, interactive: false, stdout_line_handler: nil)
215
+ def call(member:, phase:, command:, env: {}, interactive: false, stdout_line_handler: nil, log_path: nil, passthrough_output: true)
147
216
  argv = command_argv(member: member, command: command, env: env)
148
217
  process_env = process_env(member: member, env: env)
149
218
  spawn_options = process_options
150
219
  return skipped_result(member: member, phase: phase, argv: argv) unless execute
151
220
 
152
221
  started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
153
- stdout, stderr, status = if interactive
154
- run_interactive(
155
- env: process_env,
156
- argv: argv,
157
- chdir: member.root,
158
- member_name: member.name,
159
- process_options: spawn_options,
160
- stdout_line_handler: stdout_line_handler
161
- )
162
- elsif stdout_line_handler
163
- run_streaming(env: process_env, argv: argv, chdir: member.root, process_options: spawn_options, stdout_line_handler: stdout_line_handler)
164
- else
165
- Open3.capture3(process_env, *argv, chdir: member.root, **spawn_options)
222
+ stdout, stderr, status = with_transcript(log_path, argv: argv, chdir: member.root) do |log_io|
223
+ if interactive
224
+ run_interactive(
225
+ env: process_env,
226
+ argv: argv,
227
+ chdir: member.root,
228
+ member_name: member.name,
229
+ process_options: spawn_options,
230
+ stdout_line_handler: stdout_line_handler,
231
+ log_io: log_io,
232
+ passthrough_output: passthrough_output
233
+ )
234
+ elsif stdout_line_handler
235
+ run_streaming(env: process_env, argv: argv, chdir: member.root, process_options: spawn_options, stdout_line_handler: stdout_line_handler, log_io: log_io)
236
+ else
237
+ result = Open3.capture3(process_env, *argv, chdir: member.root, **spawn_options)
238
+ transcript_write(log_io, result[0])
239
+ transcript_write(log_io, result[1])
240
+ result
241
+ end
166
242
  end
167
243
  stdout = normalize_output(stdout)
168
244
  stderr = normalize_output(stderr)
@@ -179,7 +255,8 @@ module Kettle
179
255
  elapsed_seconds: elapsed.round(3),
180
256
  skipped: false,
181
257
  reason: status.success? ? nil : "command failed",
182
- output_streamed: interactive
258
+ output_streamed: interactive,
259
+ log_path: log_path
183
260
  )
184
261
  end
185
262
 
@@ -187,7 +264,7 @@ module Kettle
187
264
 
188
265
  attr_reader :execute, :accept, :gem_signing_password, :otp_coordinator
189
266
 
190
- def run_streaming(env:, argv:, chdir:, process_options:, stdout_line_handler:)
267
+ def run_streaming(env:, argv:, chdir:, process_options:, stdout_line_handler:, log_io: nil)
191
268
  captured_stdout = +""
192
269
  captured_stderr = +""
193
270
  stdout_line_buffer = +""
@@ -197,12 +274,14 @@ module Kettle
197
274
  until readers.empty?
198
275
  ready = IO.select(readers)
199
276
  ready.first.each do |reader|
277
+ chunk = reader.readpartial(1024)
200
278
  if reader.equal?(output)
201
- chunk = reader.readpartial(1024)
202
279
  captured_stdout << chunk
280
+ transcript_write(log_io, chunk)
203
281
  stdout_line_buffer = stream_stdout_lines(stdout_line_buffer, chunk, stdout_line_handler)
204
282
  else
205
- captured_stderr << reader.readpartial(1024)
283
+ captured_stderr << chunk
284
+ transcript_write(log_io, chunk)
206
285
  end
207
286
  rescue EOFError
208
287
  readers.delete(reader)
@@ -222,7 +301,7 @@ module Kettle
222
301
  remainder
223
302
  end
224
303
 
225
- def run_interactive(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:)
304
+ def run_interactive(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:, log_io: nil, passthrough_output: true)
226
305
  if pty_available?
227
306
  return run_interactive_pty(
228
307
  env: env,
@@ -230,7 +309,9 @@ module Kettle
230
309
  chdir: chdir,
231
310
  member_name: member_name,
232
311
  process_options: process_options,
233
- stdout_line_handler: stdout_line_handler
312
+ stdout_line_handler: stdout_line_handler,
313
+ log_io: log_io,
314
+ passthrough_output: passthrough_output
234
315
  )
235
316
  end
236
317
 
@@ -240,11 +321,13 @@ module Kettle
240
321
  chdir: chdir,
241
322
  member_name: member_name,
242
323
  process_options: process_options,
243
- stdout_line_handler: stdout_line_handler
324
+ stdout_line_handler: stdout_line_handler,
325
+ log_io: log_io,
326
+ passthrough_output: passthrough_output
244
327
  )
245
328
  end
246
329
 
247
- def run_interactive_pty(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:)
330
+ def run_interactive_pty(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:, log_io: nil, passthrough_output: true)
248
331
  stdout = +""
249
332
  stderr = +""
250
333
  stdout_line_buffer = +""
@@ -259,10 +342,12 @@ module Kettle
259
342
  if reader.equal?(output)
260
343
  chunk = output.readpartial(1024)
261
344
  stdout << chunk
262
- stdout_line_buffer = print_interactive_stdout(stdout_line_buffer, chunk, stdout_line_handler)
345
+ transcript_write(log_io, chunk)
346
+ stdout_line_buffer = print_interactive_stdout(stdout_line_buffer, chunk, stdout_line_handler, passthrough_output: passthrough_output)
263
347
  handle_interactive_prompt(input, chunk, member_name: member_name)
264
348
  else
265
- input.write($stdin.readpartial(1024))
349
+ chunk = $stdin.readpartial(1024)
350
+ input.write(chunk)
266
351
  end
267
352
  end
268
353
  end
@@ -270,16 +355,17 @@ module Kettle
270
355
  # PTY raises EIO when the child process exits after closing the slave.
271
356
  rescue Error => error
272
357
  stderr << "#{error.message}\n"
358
+ transcript_write(log_io, "#{error.message}\n")
273
359
  terminate_process(pid)
274
360
  end
275
- flush_interactive_stdout(stdout_line_buffer, stdout_line_handler)
361
+ flush_interactive_stdout(stdout_line_buffer, stdout_line_handler, passthrough_output: passthrough_output)
276
362
  _, status = Process.wait2(pid)
277
363
  status = failure_status if !stderr.empty? && status.success?
278
364
  end
279
365
  [stdout, stderr, status]
280
366
  end
281
367
 
282
- def run_interactive_open3(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:)
368
+ def run_interactive_open3(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:, log_io: nil, passthrough_output: true)
283
369
  captured_stdout = +""
284
370
  captured_stderr = +""
285
371
  stdout_line_buffer = +""
@@ -303,19 +389,22 @@ module Kettle
303
389
  readers,
304
390
  member_name: member_name,
305
391
  stdout_line_buffer: stdout_line_buffer,
306
- stdout_line_handler: stdout_line_handler
392
+ stdout_line_handler: stdout_line_handler,
393
+ log_io: log_io,
394
+ passthrough_output: passthrough_output
307
395
  )
308
396
  end
309
397
  end
310
398
  end
311
399
  rescue Error => error
312
400
  captured_stderr << "#{error.message}\n"
401
+ transcript_write(log_io, "#{error.message}\n")
313
402
  terminate_process(wait_thread.pid)
314
403
  status = failure_status
315
404
  else
316
405
  status = wait_thread.value
317
406
  end
318
- flush_interactive_stdout(stdout_line_buffer, stdout_line_handler)
407
+ flush_interactive_stdout(stdout_line_buffer, stdout_line_handler, passthrough_output: passthrough_output)
319
408
  end
320
409
  [captured_stdout, captured_stderr, status]
321
410
  end
@@ -329,15 +418,19 @@ module Kettle
329
418
  readers,
330
419
  member_name:,
331
420
  stdout_line_buffer:,
332
- stdout_line_handler:
421
+ stdout_line_handler:,
422
+ log_io: nil,
423
+ passthrough_output: true
333
424
  )
334
425
  chunk = reader.readpartial(1024)
335
426
  if reader.equal?(output)
336
427
  captured_stdout << chunk
337
- stdout_line_buffer = print_interactive_stdout(stdout_line_buffer, chunk, stdout_line_handler)
428
+ transcript_write(log_io, chunk)
429
+ stdout_line_buffer = print_interactive_stdout(stdout_line_buffer, chunk, stdout_line_handler, passthrough_output: passthrough_output)
338
430
  else
339
431
  captured_stderr << chunk
340
- $stderr.print(chunk)
432
+ transcript_write(log_io, chunk)
433
+ $stderr.print(chunk) if passthrough_output
341
434
  end
342
435
  handle_interactive_prompt(input, chunk, member_name: member_name)
343
436
  stdout_line_buffer
@@ -346,36 +439,55 @@ module Kettle
346
439
  stdout_line_buffer
347
440
  end
348
441
 
349
- def print_interactive_stdout(buffer, chunk, stdout_line_handler)
350
- return print_interactive_chunk(chunk) unless stdout_line_handler
442
+ def with_transcript(log_path, argv:, chdir:)
443
+ return yield(nil) if log_path.to_s.empty?
444
+
445
+ FileUtils.mkdir_p(File.dirname(log_path))
446
+ File.open(log_path, "wb") do |log_io|
447
+ log_io.sync = true
448
+ log_io.puts("$ #{argv.join(" ")}")
449
+ log_io.puts("workdir: #{chdir}")
450
+ log_io.puts
451
+ yield(log_io)
452
+ end
453
+ end
454
+
455
+ def transcript_write(log_io, chunk)
456
+ return if log_io.nil? || chunk.to_s.empty?
457
+
458
+ log_io.write(chunk)
459
+ end
460
+
461
+ def print_interactive_stdout(buffer, chunk, stdout_line_handler, passthrough_output: true)
462
+ return print_interactive_chunk(chunk, passthrough_output: passthrough_output) unless stdout_line_handler
351
463
 
352
464
  pending = buffer + chunk
353
465
  lines = pending.lines
354
466
  remainder = pending.end_with?("\n") ? +"" : lines.pop.to_s
355
- lines.each { |line| print_interactive_line(line, stdout_line_handler) }
467
+ lines.each { |line| print_interactive_line(line, stdout_line_handler, passthrough_output: passthrough_output) }
356
468
  return +"" if remainder.empty?
357
469
  unless possible_event_line?(remainder)
358
- $stdout.print(remainder)
470
+ $stdout.print(remainder) if passthrough_output
359
471
  return +""
360
472
  end
361
473
 
362
474
  remainder
363
475
  end
364
476
 
365
- def flush_interactive_stdout(buffer, stdout_line_handler)
477
+ def flush_interactive_stdout(buffer, stdout_line_handler, passthrough_output: true)
366
478
  return if buffer.empty?
367
479
 
368
- print_interactive_line(buffer, stdout_line_handler)
480
+ print_interactive_line(buffer, stdout_line_handler, passthrough_output: passthrough_output)
369
481
  end
370
482
 
371
- def print_interactive_chunk(chunk)
372
- $stdout.print(chunk)
483
+ def print_interactive_chunk(chunk, passthrough_output: true)
484
+ $stdout.print(chunk) if passthrough_output
373
485
  +""
374
486
  end
375
487
 
376
- def print_interactive_line(line, stdout_line_handler)
488
+ def print_interactive_line(line, stdout_line_handler, passthrough_output: true)
377
489
  consumed = possible_event_line?(line) && stdout_line_handler.call(line.chomp)
378
- $stdout.print(line) unless consumed
490
+ $stdout.print(line) if !consumed && passthrough_output
379
491
  end
380
492
 
381
493
  def possible_event_line?(line)
@@ -249,6 +249,10 @@ module Kettle
249
249
  fetch_path("release", "publish_command") || command_for("release_publish") || "bundle exec kettle-release"
250
250
  end
251
251
 
252
+ def release_required_remotes
253
+ fetch_path("release", "required_remotes")
254
+ end
255
+
252
256
  def release_env
253
257
  stringify_env(fetch_path("release", "env") || {})
254
258
  end
@@ -134,6 +134,7 @@ module Kettle
134
134
  lines << "summary:"
135
135
  lines << " outcome: #{data.fetch("outcome")}"
136
136
  lines << " elapsed: #{format_elapsed(data.fetch("elapsed_seconds"))}"
137
+ lines << " logs: #{data.fetch("release_log_dirs").join(", ")}" unless data.fetch("release_log_dirs").empty?
137
138
  lines << " selected: #{data.fetch("selected_count")}"
138
139
  lines << " results: #{data.fetch("result_count")}"
139
140
  lines << " succeeded: #{summary_list(data.fetch("succeeded"))}"
@@ -164,10 +165,20 @@ module Kettle
164
165
  "skipped" => summary_skipped,
165
166
  "failed" => summary_failed,
166
167
  "pending" => summary_pending,
168
+ "release_log_dirs" => release_log_dirs,
167
169
  "resume_hint" => resume_hint
168
170
  }
169
171
  end
170
172
 
173
+ def release_log_dirs
174
+ return [] unless command == "release"
175
+
176
+ visible_results.filter_map do |result|
177
+ path = result.log_path.to_s if result.respond_to?(:log_path)
178
+ File.dirname(path) unless path.to_s.empty?
179
+ end.uniq
180
+ end
181
+
171
182
  def append_release_waves(lines)
172
183
  wave_results = results.select { |result| release_wave_result?(result) }
173
184
  return if wave_results.empty?
@@ -205,7 +216,10 @@ module Kettle
205
216
  end
206
217
 
207
218
  def append_result_stdout(lines, result)
208
- return if suppress_success_output?(result)
219
+ if suppress_success_output?(result)
220
+ append_result_log_path(lines, result) unless result.ok?
221
+ return
222
+ end
209
223
 
210
224
  if output_streamed?(result)
211
225
  append_streamed_output_summary(lines, result) unless result.ok?
@@ -216,6 +230,11 @@ module Kettle
216
230
  else
217
231
  append_indented_output(lines, result.stdout)
218
232
  end
233
+ append_result_log_path(lines, result) unless result.ok? || output_streamed?(result)
234
+ end
235
+
236
+ def append_result_log_path(lines, result)
237
+ lines << " log: #{result.log_path}" if result.respond_to?(:log_path) && !result.log_path.to_s.empty?
219
238
  end
220
239
 
221
240
  def suppress_success_output?(result)
@@ -239,6 +258,7 @@ module Kettle
239
258
  lines << " summary: #{summary}" if summary
240
259
  recommended_fix = streamed_recommended_fix(result)
241
260
  lines << " recommended fix: #{recommended_fix}" if recommended_fix
261
+ append_result_log_path(lines, result)
242
262
  lines << " output: omitted because it was already streamed"
243
263
  end
244
264
 
@@ -19,7 +19,7 @@ module Kettle
19
19
  end
20
20
 
21
21
  def authorize!
22
- gem_signing_passphrase
22
+ with_release_secret_alert_suppressed { gem_signing_passphrase }
23
23
  rescue => error
24
24
  raise Error, error.message
25
25
  end
@@ -31,10 +31,20 @@ module Kettle
31
31
  end
32
32
 
33
33
  def rubygems_otp
34
- super
34
+ with_release_secret_alert_suppressed { super }
35
35
  rescue => error
36
36
  raise Error, error.message
37
37
  end
38
+
39
+ private
40
+
41
+ def with_release_secret_alert_suppressed
42
+ previous = ENV.fetch("KETTLE_RELEASE_SECRET_ALERT", nil)
43
+ ENV["KETTLE_RELEASE_SECRET_ALERT"] = "false"
44
+ yield
45
+ ensure
46
+ previous.nil? ? ENV.delete("KETTLE_RELEASE_SECRET_ALERT") : ENV["KETTLE_RELEASE_SECRET_ALERT"] = previous
47
+ end
38
48
  end
39
49
  else
40
50
  class Provider
@@ -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.13"
8
+ VERSION = "1.2.15"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -16,6 +16,8 @@ require_relative "workflow_progress"
16
16
  module Kettle
17
17
  module Family
18
18
  class Workflow
19
+ PreflightProgressMember = Struct.new(:name)
20
+
19
21
  DEFAULT_COMMANDS = {
20
22
  "template" => "bundle exec kettle-jem install",
21
23
  "test" => "bundle exec kettle-test",
@@ -100,7 +102,7 @@ module Kettle
100
102
  REGISTRY_WAIT_ATTEMPTS = 15
101
103
  REGISTRY_WAIT_INTERVAL_SECONDS = 15
102
104
 
103
- def initialize(command:, config:, members:, execute: false, accept: true, commit: true, allow_dirty: false, publish: false, push: false, tag: false, start_step: nil, skip_steps: nil, local_ci: false, continue_ci_failures: false, ci_workflows: nil, skip_bundle_audit: false, skip_remotes: nil, auto_dependency_floors: nil, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, env_overrides: {}, debug: false, verbose: false, gem_signing_password: nil, secrets_provider: nil, jobs: nil, progress_io: nil, reset_target: nil, bup_args: [], bex_args: [], start_member: nil, start_branch: nil, **options)
105
+ def initialize(command:, config:, members:, execute: false, accept: true, commit: true, allow_dirty: false, publish: false, push: false, tag: false, start_step: nil, skip_steps: nil, local_ci: false, continue_ci_failures: false, ci_workflows: nil, skip_bundle_audit: false, skip_remotes: nil, required_remotes: nil, auto_dependency_floors: nil, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, env_overrides: {}, debug: false, verbose: false, gem_signing_password: nil, secrets_provider: nil, jobs: nil, progress_io: nil, reset_target: nil, bup_args: [], bex_args: [], start_member: nil, start_branch: nil, **options)
104
106
  @command = command
105
107
  @config = config
106
108
  @members = members
@@ -117,7 +119,8 @@ module Kettle
117
119
  @continue_ci_failures = continue_ci_failures
118
120
  @ci_workflows = validate_ci_workflows(ci_workflows)
119
121
  @skip_bundle_audit = skip_bundle_audit
120
- @skip_remotes = validate_skip_remotes(skip_remotes)
122
+ @skip_remotes = validate_remote_list(skip_remotes, "--skip-remotes")
123
+ @required_remotes = validate_remote_list(required_remotes.nil? ? config.release_required_remotes : required_remotes, "--required-remotes")
121
124
  @auto_dependency_floors = auto_dependency_floors.nil? ? config.release_auto_dependency_floors? : auto_dependency_floors
122
125
  @gha_sha_pins_upgrade = gha_sha_pins_upgrade
123
126
  @gha_sha_pins_check = gha_sha_pins_check
@@ -159,7 +162,7 @@ module Kettle
159
162
 
160
163
  private
161
164
 
162
- attr_reader :command, :config, :members, :execute, :accept, :commit, :allow_dirty, :publish, :push, :tag, :start_step, :skip_steps, :local_ci, :continue_ci_failures, :ci_workflows, :skip_bundle_audit, :skip_remotes, :auto_dependency_floors, :gha_sha_pins_upgrade, :gha_sha_pins_check, :env_overrides, :debug, :verbose, :jobs, :progress_io, :reset_target, :bup_args, :bex_args, :start_member, :start_branch
165
+ attr_reader :command, :config, :members, :execute, :accept, :commit, :allow_dirty, :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, :env_overrides, :debug, :verbose, :jobs, :progress_io, :reset_target, :bup_args, :bex_args, :start_member, :start_branch
163
166
 
164
167
  def current_branch_results(workflow_members)
165
168
  return check_results(workflow_members) if command == "check"
@@ -421,14 +424,23 @@ module Kettle
421
424
 
422
425
  def release_preflight_results
423
426
  phases = release_preflight_phases
424
- emit_release_preflight_start(phases)
427
+ progress = start_release_preflight_progress(phases)
428
+ progress&.start
429
+ preflight_member = release_preflight_progress_member
430
+ progress&.start_member(preflight_member, total: phases.length, status: phases.first.fetch(:label))
425
431
  phases.each_with_index do |phase, index|
426
- emit_release_preflight_phase(phase.fetch(:label), index: index, total: phases.length)
432
+ label = phase.fetch(:label)
433
+ progress&.update(preflight_member, status: label, mark: ">")
427
434
  result = send(phase.fetch(:method))
428
- emit_release_preflight_phase_finish(phase.fetch(:label), result)
435
+ progress&.advance(preflight_member, status: label, success: result.empty?, mark: result.empty? ? "." : "F")
436
+ unless result.empty?
437
+ progress&.finish_member(preflight_member, success: false, status: label)
438
+ progress&.stop
439
+ end
429
440
  return result unless result.empty?
430
441
  end
431
- emit_release_preflight_summary(phases)
442
+ progress&.finish_member(preflight_member, success: true, status: "ok")
443
+ progress&.stop
432
444
  []
433
445
  end
434
446
 
@@ -745,7 +757,9 @@ module Kettle
745
757
  command: release_command,
746
758
  env: release_env,
747
759
  interactive: release_command_interactive?,
748
- stdout_line_handler: release_event_line_handler(member, progress: progress)
760
+ stdout_line_handler: release_event_line_handler(member, progress: progress),
761
+ log_path: release_command_log_path(member, release_phase),
762
+ passthrough_output: release_command_passthrough_output?
749
763
  )
750
764
  emit_member_result_progress(member, memo.last, progress: progress)
751
765
  return memo unless memo.last.ok?
@@ -792,7 +806,29 @@ module Kettle
792
806
  def release_otp_coordinator
793
807
  return nil unless execute && release_command_interactive?
794
808
 
795
- @release_otp_coordinator ||= CommandRunner::OtpCoordinator.new(secrets_provider: @secrets_provider)
809
+ @release_otp_coordinator ||= CommandRunner::OtpCoordinator.new(
810
+ secrets_provider: @secrets_provider,
811
+ event_handler: method(:handle_release_otp_event)
812
+ )
813
+ end
814
+
815
+ def handle_release_otp_event(member_name, event)
816
+ progress = @release_progress
817
+ return unless progress
818
+
819
+ member = release_progress_member_for(member_name)
820
+ if progress_io
821
+ if verbose || debug
822
+ emit_release_event_progress(member, event)
823
+ elsif progress.tty?
824
+ emit_release_event_status(member, event, progress: progress)
825
+ end
826
+ end
827
+ end
828
+
829
+ def release_progress_member_for(member_name)
830
+ @release_progress_members_by_name ||= members.to_h { |member| [member.name, member] }
831
+ @release_progress_members_by_name.fetch(member_name.to_s) { PreflightProgressMember.new(member_name.to_s) }
796
832
  end
797
833
 
798
834
  def parallel_release_members?(release_members)
@@ -1378,6 +1414,10 @@ module Kettle
1378
1414
  publish || !!@gem_signing_password
1379
1415
  end
1380
1416
 
1417
+ def release_command_passthrough_output?
1418
+ verbose || debug
1419
+ end
1420
+
1381
1421
  def kettle_release_command?(command)
1382
1422
  case command
1383
1423
  when Array
@@ -1408,6 +1448,7 @@ module Kettle
1408
1448
  args << "--local-ci" if local_ci
1409
1449
  args << "--skip-bundle-audit" if skip_bundle_audit
1410
1450
  args << "--skip-remotes=#{skip_remotes}" if skip_remotes && !skip_remotes.to_s.empty?
1451
+ args << "--required-remotes=#{required_remotes}" if required_remotes && !required_remotes.to_s.empty?
1411
1452
  args << "--yes" if release_command_uses_kettle_release_yes? && !command_includes_arg?(command, "--yes")
1412
1453
  args << "--events" unless command_includes_arg?(command, "--events")
1413
1454
  return command if args.empty?
@@ -1436,12 +1477,14 @@ module Kettle
1436
1477
  workflows.join(",")
1437
1478
  end
1438
1479
 
1439
- def validate_skip_remotes(value)
1440
- return nil if value.nil? || value.to_s.empty?
1480
+ def validate_remote_list(value, option_name)
1481
+ return nil if value.nil?
1482
+
1483
+ remotes = Array(value).flat_map { |entry| entry.to_s.split(",") }.map(&:strip)
1484
+ return nil if remotes.all?(&:empty?)
1441
1485
 
1442
- remotes = value.to_s.split(",").map(&:strip)
1443
1486
  invalid = remotes.find { |remote| remote.empty? || !remote.match?(/\A[A-Za-z0-9_.-]+\z/) }
1444
- raise Error, "invalid --skip-remotes value #{value.inspect}" if invalid
1487
+ raise Error, "invalid #{option_name} value #{value.inspect}" if invalid
1445
1488
 
1446
1489
  remotes.join(",")
1447
1490
  end
@@ -1460,6 +1503,7 @@ module Kettle
1460
1503
  env["K_RELEASE_CI_WORKFLOWS"] = ci_workflows if ci_workflows && !ci_workflows.to_s.empty?
1461
1504
  env["KETTLE_DEV_SKIP_BUNDLE_AUDIT"] = "true" if skip_bundle_audit
1462
1505
  env["K_RELEASE_SKIP_REMOTES"] = skip_remotes if skip_remotes && !skip_remotes.to_s.empty?
1506
+ env["K_RELEASE_REQUIRED_REMOTES"] = required_remotes if required_remotes && !required_remotes.to_s.empty?
1463
1507
  env.merge!(kettle_release_secrets_env)
1464
1508
  env
1465
1509
  end
@@ -1903,6 +1947,24 @@ module Kettle
1903
1947
  progress
1904
1948
  end
1905
1949
 
1950
+ def start_release_preflight_progress(phases)
1951
+ return nil unless progress_io
1952
+
1953
+ phase_label = (phases.length == 1) ? "phase" : "phases"
1954
+ WorkflowProgress.new(
1955
+ io: progress_io,
1956
+ label: "release preflight",
1957
+ total: phases.length,
1958
+ jobs: 1,
1959
+ members: [release_preflight_progress_member],
1960
+ heading: "release preflight #{phases.length} #{phase_label}:"
1961
+ )
1962
+ end
1963
+
1964
+ def release_preflight_progress_member
1965
+ @release_preflight_progress_member ||= PreflightProgressMember.new("preflight")
1966
+ end
1967
+
1906
1968
  def template_phase_total(member = nil)
1907
1969
  total = 1
1908
1970
  total += 2 if config.normalize_lockfiles?
@@ -1927,6 +1989,20 @@ module Kettle
1927
1989
  end
1928
1990
  end
1929
1991
 
1992
+ def release_command_log_path(member, phase)
1993
+ return nil unless execute
1994
+
1995
+ File.join(release_log_dir, "#{safe_log_name(member.name)}-#{safe_log_name(phase)}.log")
1996
+ end
1997
+
1998
+ def release_log_dir
1999
+ @release_log_dir ||= File.join(config.root, "tmp", "kettle-family", "release-#{Time.now.strftime("%Y%m%d-%H%M%S")}-#{$$}")
2000
+ end
2001
+
2002
+ def safe_log_name(value)
2003
+ value.to_s.gsub(/[^A-Za-z0-9_.-]+/, "_")
2004
+ end
2005
+
1930
2006
  def template_initial_status(member = nil)
1931
2007
  return "prepare_lockfiles" if config.normalize_lockfiles?
1932
2008
  return "prepare_template_dependencies" if member.nil? || template_prepares_dependencies?(member)
@@ -1977,36 +2053,6 @@ module Kettle
1977
2053
  progress.summary("release summary: #{release_results.count(&:ok?)}/#{release_results.length} members ok")
1978
2054
  end
1979
2055
 
1980
- def emit_release_preflight_start(phases)
1981
- return unless progress_io
1982
-
1983
- phase_label = (phases.length == 1) ? "phase" : "phases"
1984
- progress_io.puts("release preflight #{phases.length} #{phase_label}:")
1985
- progress_io.flush
1986
- end
1987
-
1988
- def emit_release_preflight_phase(label, index:, total:)
1989
- return unless progress_io
1990
-
1991
- progress_io.puts("[release preflight] (#{index + 1}/#{total}) > #{label}")
1992
- progress_io.flush
1993
- end
1994
-
1995
- def emit_release_preflight_phase_finish(label, results)
1996
- return unless progress_io
1997
-
1998
- mark = results.empty? ? "." : "F"
1999
- progress_io.puts("[release preflight] #{mark} #{label}")
2000
- progress_io.flush
2001
- end
2002
-
2003
- def emit_release_preflight_summary(phases)
2004
- return unless progress_io
2005
-
2006
- progress_io.puts("release preflight summary: #{phases.length}/#{phases.length} phases ok")
2007
- progress_io.flush
2008
- end
2009
-
2010
2056
  def template_event_line_handler(member, progress: nil)
2011
2057
  lambda do |line|
2012
2058
  event = parse_template_event(line)
@@ -2087,10 +2133,21 @@ module Kettle
2087
2133
  when "run_start"
2088
2134
  emit_template_event_line(member, ">", "release")
2089
2135
  when "command_step"
2090
- label = [event["phase"], event["name"]].map(&:to_s).reject(&:empty?).join(":")
2091
- emit_template_event_line(member, template_event_mark(event), label)
2136
+ emit_template_event_line(member, template_event_mark(event), command_step_event_label(event))
2092
2137
  when "secret_provider"
2093
2138
  emit_template_event_line(member, release_event_status_mark(event), secret_provider_event_label(event))
2139
+ when "remote_parity"
2140
+ emit_template_event_line(member, release_event_status_mark(event), remote_parity_event_label(event))
2141
+ when "ci_monitor"
2142
+ emit_template_event_line(member, release_event_status_mark(event), ci_monitor_event_label(event))
2143
+ when "pre_release"
2144
+ emit_template_event_line(member, release_event_status_mark(event), pre_release_event_label(event))
2145
+ when "changelog"
2146
+ emit_template_event_line(member, release_event_status_mark(event), changelog_event_label(event))
2147
+ when "release_lockfile"
2148
+ emit_template_event_line(member, release_event_status_mark(event), release_lockfile_event_label(event))
2149
+ when "release_probe"
2150
+ emit_template_event_line(member, release_event_status_mark(event), release_probe_event_label(event))
2094
2151
  when "diagnostic"
2095
2152
  emit_template_event_line(member, "!", diagnostic_event_label(event))
2096
2153
  when "summary"
@@ -2103,9 +2160,21 @@ module Kettle
2103
2160
  when "run_start"
2104
2161
  "release"
2105
2162
  when "command_step"
2106
- [event["phase"], event["name"]].map(&:to_s).reject(&:empty?).join(":")
2163
+ command_step_event_label(event)
2107
2164
  when "secret_provider"
2108
2165
  secret_provider_event_label(event)
2166
+ when "remote_parity"
2167
+ remote_parity_event_label(event)
2168
+ when "ci_monitor"
2169
+ ci_monitor_event_label(event)
2170
+ when "pre_release"
2171
+ pre_release_event_label(event)
2172
+ when "changelog"
2173
+ changelog_event_label(event)
2174
+ when "release_lockfile"
2175
+ release_lockfile_event_label(event)
2176
+ when "release_probe"
2177
+ release_probe_event_label(event)
2109
2178
  when "diagnostic"
2110
2179
  diagnostic_event_label(event)
2111
2180
  when "summary"
@@ -2123,6 +2192,18 @@ module Kettle
2123
2192
  template_event_mark(event)
2124
2193
  when "secret_provider"
2125
2194
  template_event_mark(event)
2195
+ when "remote_parity"
2196
+ template_event_mark(event)
2197
+ when "ci_monitor"
2198
+ template_event_mark(event)
2199
+ when "pre_release"
2200
+ template_event_mark(event)
2201
+ when "changelog"
2202
+ template_event_mark(event)
2203
+ when "release_lockfile"
2204
+ template_event_mark(event)
2205
+ when "release_probe"
2206
+ template_event_mark(event)
2126
2207
  when "diagnostic"
2127
2208
  "!"
2128
2209
  when "summary"
@@ -2155,9 +2236,85 @@ module Kettle
2155
2236
  parts << purpose unless purpose.empty?
2156
2237
  parts << label unless label.empty?
2157
2238
  parts << source if purpose.empty? && label.empty? && !source.empty?
2239
+ parts << "#{event["queued"]}/#{event["total"]}" if event["queued"] && event["total"]
2240
+ parts.join(":")
2241
+ end
2242
+
2243
+ def command_step_event_label(event)
2244
+ parts = [event["phase"], event["name"]].map(&:to_s).reject(&:empty?)
2245
+ summary = event["summary"].to_s
2246
+ parts << summary unless summary.empty?
2158
2247
  parts.join(":")
2159
2248
  end
2160
2249
 
2250
+ def remote_parity_event_label(event)
2251
+ action = event["action"].to_s
2252
+ remote = event["remote"].to_s
2253
+ trunk = event["trunk"].to_s
2254
+ parts = ["remote"]
2255
+ parts << action unless action.empty?
2256
+ parts << remote unless remote.empty?
2257
+ parts << trunk if remote.empty? && !trunk.empty?
2258
+ parts.join(":")
2259
+ end
2260
+
2261
+ def ci_monitor_event_label(event)
2262
+ action = event["action"].to_s
2263
+ provider = event["provider"].to_s
2264
+ target = event["workflow"].to_s
2265
+ target = "pipeline" if target.empty? && provider == "gitlab"
2266
+ parts = ["ci"]
2267
+ parts << action unless action.empty?
2268
+ parts << provider unless provider.empty?
2269
+ parts << target unless target.empty?
2270
+ if %w[github_wait github_started github_tick].include?(action) && event["completed"] && event["total"]
2271
+ parts << "#{event["completed"]}/#{event["total"]}"
2272
+ end
2273
+ completed_workflow = event["completed_workflow"].to_s
2274
+ parts << completed_workflow unless completed_workflow.empty? || completed_workflow == target
2275
+ parts.join(":")
2276
+ end
2277
+
2278
+ def pre_release_event_label(event)
2279
+ action = event["action"].to_s
2280
+ check = event["check"].to_s
2281
+ parts = ["pre"]
2282
+ parts << action unless action.empty?
2283
+ parts << check unless check.empty?
2284
+ parts.join(":")
2285
+ end
2286
+
2287
+ def changelog_event_label(event)
2288
+ action = event["action"].to_s
2289
+ plan = event["plan"].to_s
2290
+ parts = ["changelog"]
2291
+ parts << action unless action.empty?
2292
+ parts << plan unless plan.empty? || action == "coverage"
2293
+ parts.join(":")
2294
+ end
2295
+
2296
+ def release_lockfile_event_label(event)
2297
+ action = event["action"].to_s
2298
+ stage = event["stage"].to_s
2299
+ stage = stage.tr(" ", "_")
2300
+ parts = ["lockfile"]
2301
+ parts << action unless action.empty?
2302
+ parts << stage unless stage.empty?
2303
+ parts << "#{event["attempt"]}/#{event["attempts"]}" if event["attempt"] && event["attempts"]
2304
+ parts.join(":")
2305
+ end
2306
+
2307
+ def release_probe_event_label(event)
2308
+ action = event["action"].to_s
2309
+ gem_name = event["gem"].to_s
2310
+ version = event["version"].to_s
2311
+ parts = ["probe"]
2312
+ parts << action unless action.empty?
2313
+ parts << [gem_name, version].reject(&:empty?).join("-")
2314
+ parts << "#{event["attempt"]}/#{event["attempts"]}" if event["attempt"] && event["attempts"]
2315
+ parts.reject(&:empty?).join(":")
2316
+ end
2317
+
2161
2318
  def diagnostic_event_label(event)
2162
2319
  message = event["message"]
2163
2320
  return message.to_s unless message.to_s.empty? || message.is_a?(Hash)
@@ -15,11 +15,12 @@ module Kettle
15
15
  DEFAULT_TERMINAL_WIDTH = 80
16
16
  ELLIPSIS = "..."
17
17
 
18
- def initialize(io:, label:, total:, jobs:, members: [], enabled: true, clock: nil)
18
+ def initialize(io:, label:, total:, jobs:, members: [], enabled: true, clock: nil, heading: nil)
19
19
  @io = io
20
20
  @label = label
21
21
  @total = total.to_i
22
22
  @jobs = jobs.to_i
23
+ @heading = heading
23
24
  @clock = clock || lambda { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
24
25
  @enabled = enabled && !!io
25
26
  @bars = {}
@@ -43,7 +44,7 @@ module Kettle
43
44
  synchronize do
44
45
  next if @started
45
46
 
46
- write_line("#{@label} #{@total} member#{plural(@total)} with #{@jobs} job#{plural(@jobs)}:")
47
+ write_line(@heading || "#{@label} #{@total} member#{plural(@total)} with #{@jobs} job#{plural(@jobs)}:")
47
48
  @started = true
48
49
  @line_order.each { |member_name| render_name(member_name, status: "") } if @tty
49
50
  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: 1.2.13
4
+ version: 1.2.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -74,7 +74,7 @@ dependencies:
74
74
  version: '2.3'
75
75
  - - ">="
76
76
  - !ruby/object:Gem::Version
77
- version: 2.5.12
77
+ version: 2.5.13
78
78
  type: :runtime
79
79
  prerelease: false
80
80
  version_requirements: !ruby/object:Gem::Requirement
@@ -84,7 +84,7 @@ dependencies:
84
84
  version: '2.3'
85
85
  - - ">="
86
86
  - !ruby/object:Gem::Version
87
- version: 2.5.12
87
+ version: 2.5.13
88
88
  - !ruby/object:Gem::Dependency
89
89
  name: kettle-test
90
90
  requirement: !ruby/object:Gem::Requirement
@@ -342,10 +342,10 @@ licenses:
342
342
  - AGPL-3.0-only
343
343
  metadata:
344
344
  homepage_uri: https://kettle-family.galtzo.com
345
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.13
346
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.13/CHANGELOG.md
345
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.15
346
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.15/CHANGELOG.md
347
347
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
348
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.13
348
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.15
349
349
  funding_uri: https://github.com/sponsors/pboling
350
350
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
351
351
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file