kettle-family 1.2.13 → 1.2.14

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: 417f2f02e871abec85184783775b1449c32b0caac52e7b18b4744840af2ac17d
4
+ data.tar.gz: c5013fc308ed12715d2184edf21ad6dea41dea2a5ae5df0bc521d6aba53b6c4a
5
5
  SHA512:
6
- metadata.gz: aac3fc439504f17be4685f87f7bd1d4c213f885941f5c7793f1bca2369e92c384c7ae3cc8919fbf8f1023e7d1d812abe29c76d0a7fbef0301a5a8125b42217f3
7
- data.tar.gz: 9d4ad335a463968aad4643d36b616f37b5d99b1431c659f2f3ae329017f304e5115dff3be3be72829ca99744a8cdf4b81619949146ce1df6f6ae7516226a12e6
6
+ metadata.gz: '091796994cdd4db8bb7e50d5cd8104e42a4680eaba541aa0897675ab835a3a51eed13ba63adbdd4fd1b8d099d426424d39f5b0a3b09e83235fba18f8dac63a65'
7
+ data.tar.gz: e744049cdd349d806bf4f14db21781e94a57b1e8a0b4fcb71e132637d6dedbc80e389b82031e9871f8ae82473836f9e0e1dce8e75aa1a3bf9c2bb1e79a61e68f
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,62 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.2.14] - 2026-07-31
34
+
35
+ - TAG: [v1.2.14][1.2.14t]
36
+ - COVERAGE: 94.22% -- 4175/4431 lines in 28 files
37
+ - BRANCH COVERAGE: 76.69% -- 1622/2115 branches in 28 files
38
+ - 28.88% documented
39
+
40
+ ### Added
41
+
42
+ - Release publish/build commands now write per-member transcript logs under
43
+ `tmp/kettle-family/release-*`, and failure summaries include the log path.
44
+
45
+ - Release progress now renders child `kettle-release` `remote_parity` events,
46
+ keeping remote fetch, skip, failure, and completion activity visible without
47
+ scraping raw command output.
48
+
49
+ - Release progress now renders child `kettle-release` `ci_monitor` events with
50
+ provider and workflow or pipeline context, so CI activity is visible as
51
+ structured progress.
52
+
53
+ - Release progress now renders child `kettle-release` `pre_release` events, so
54
+ pre-release checks are visible as structured progress while raw output stays
55
+ in the transcript log.
56
+
57
+ - Release progress now renders child `kettle-changelog` `changelog` events, so
58
+ release plan, coverage, and changelog update activity is visible as structured
59
+ progress.
60
+
61
+ ### Changed
62
+
63
+ - Family release now keeps raw child `kettle-release` output in the per-member
64
+ transcript log by default and renders structured NDJSON progress in the
65
+ terminal, with raw passthrough still available under verbose/debug output.
66
+ - Release summary footers now include the transcript log directory when release
67
+ logs were written, including successful runs where raw output stayed quiet.
68
+ - Release progress now renders child `command_step` summaries, so phases can
69
+ show context such as `release:bundle_lock:Gemfile` or
70
+ `release:yard:documentation`.
71
+ - Release progress now renders child `release_lockfile` and `release_probe`
72
+ events, so lockfile reset retries and published-gem availability probes show
73
+ as structured progress.
74
+ - Release progress now renders GitHub CI wait/start/tick events with completion
75
+ counts, so CI monitoring no longer appears stuck while waiting for workflows.
76
+ - Release preflight now renders through the shared progress event tape instead
77
+ of printing separate start and finish lines for each successful phase.
78
+
79
+ ### Fixed
80
+
81
+ - Release preflight no longer prints a nested 1Password authorization alert
82
+ between the phase start and finish lines; the preflight phase itself is the
83
+ user-facing authorization notice.
84
+
85
+ - Family release runs can now pass configured required release remotes through
86
+ to `kettle-release`, allowing mirrors to remain optional while primary remotes
87
+ still block release parity failures.
88
+
33
89
  ## [1.2.13] - 2026-07-31
34
90
 
35
91
  - TAG: [v1.2.13][1.2.13t]
@@ -1443,7 +1499,9 @@ Please file a bug if you notice a violation of semantic versioning.
1443
1499
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
1444
1500
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
1445
1501
 
1446
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.13...HEAD
1502
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.14...HEAD
1503
+ [1.2.14]: https://github.com/kettle-dev/kettle-family/compare/v1.2.13...v1.2.14
1504
+ [1.2.14t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.14
1447
1505
  [1.2.13]: https://github.com/kettle-dev/kettle-family/compare/v1.2.12...v1.2.13
1448
1506
  [1.2.13t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.13
1449
1507
  [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.431-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
@@ -143,26 +144,33 @@ module Kettle
143
144
  @otp_coordinator = otp_coordinator
144
145
  end
145
146
 
146
- def call(member:, phase:, command:, env: {}, interactive: false, stdout_line_handler: nil)
147
+ def call(member:, phase:, command:, env: {}, interactive: false, stdout_line_handler: nil, log_path: nil, passthrough_output: true)
147
148
  argv = command_argv(member: member, command: command, env: env)
148
149
  process_env = process_env(member: member, env: env)
149
150
  spawn_options = process_options
150
151
  return skipped_result(member: member, phase: phase, argv: argv) unless execute
151
152
 
152
153
  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)
154
+ stdout, stderr, status = with_transcript(log_path, argv: argv, chdir: member.root) do |log_io|
155
+ if interactive
156
+ run_interactive(
157
+ env: process_env,
158
+ argv: argv,
159
+ chdir: member.root,
160
+ member_name: member.name,
161
+ process_options: spawn_options,
162
+ stdout_line_handler: stdout_line_handler,
163
+ log_io: log_io,
164
+ passthrough_output: passthrough_output
165
+ )
166
+ elsif stdout_line_handler
167
+ run_streaming(env: process_env, argv: argv, chdir: member.root, process_options: spawn_options, stdout_line_handler: stdout_line_handler, log_io: log_io)
168
+ else
169
+ result = Open3.capture3(process_env, *argv, chdir: member.root, **spawn_options)
170
+ transcript_write(log_io, result[0])
171
+ transcript_write(log_io, result[1])
172
+ result
173
+ end
166
174
  end
167
175
  stdout = normalize_output(stdout)
168
176
  stderr = normalize_output(stderr)
@@ -179,7 +187,8 @@ module Kettle
179
187
  elapsed_seconds: elapsed.round(3),
180
188
  skipped: false,
181
189
  reason: status.success? ? nil : "command failed",
182
- output_streamed: interactive
190
+ output_streamed: interactive,
191
+ log_path: log_path
183
192
  )
184
193
  end
185
194
 
@@ -187,7 +196,7 @@ module Kettle
187
196
 
188
197
  attr_reader :execute, :accept, :gem_signing_password, :otp_coordinator
189
198
 
190
- def run_streaming(env:, argv:, chdir:, process_options:, stdout_line_handler:)
199
+ def run_streaming(env:, argv:, chdir:, process_options:, stdout_line_handler:, log_io: nil)
191
200
  captured_stdout = +""
192
201
  captured_stderr = +""
193
202
  stdout_line_buffer = +""
@@ -197,12 +206,14 @@ module Kettle
197
206
  until readers.empty?
198
207
  ready = IO.select(readers)
199
208
  ready.first.each do |reader|
209
+ chunk = reader.readpartial(1024)
200
210
  if reader.equal?(output)
201
- chunk = reader.readpartial(1024)
202
211
  captured_stdout << chunk
212
+ transcript_write(log_io, chunk)
203
213
  stdout_line_buffer = stream_stdout_lines(stdout_line_buffer, chunk, stdout_line_handler)
204
214
  else
205
- captured_stderr << reader.readpartial(1024)
215
+ captured_stderr << chunk
216
+ transcript_write(log_io, chunk)
206
217
  end
207
218
  rescue EOFError
208
219
  readers.delete(reader)
@@ -222,7 +233,7 @@ module Kettle
222
233
  remainder
223
234
  end
224
235
 
225
- def run_interactive(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:)
236
+ def run_interactive(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:, log_io: nil, passthrough_output: true)
226
237
  if pty_available?
227
238
  return run_interactive_pty(
228
239
  env: env,
@@ -230,7 +241,9 @@ module Kettle
230
241
  chdir: chdir,
231
242
  member_name: member_name,
232
243
  process_options: process_options,
233
- stdout_line_handler: stdout_line_handler
244
+ stdout_line_handler: stdout_line_handler,
245
+ log_io: log_io,
246
+ passthrough_output: passthrough_output
234
247
  )
235
248
  end
236
249
 
@@ -240,11 +253,13 @@ module Kettle
240
253
  chdir: chdir,
241
254
  member_name: member_name,
242
255
  process_options: process_options,
243
- stdout_line_handler: stdout_line_handler
256
+ stdout_line_handler: stdout_line_handler,
257
+ log_io: log_io,
258
+ passthrough_output: passthrough_output
244
259
  )
245
260
  end
246
261
 
247
- def run_interactive_pty(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:)
262
+ def run_interactive_pty(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:, log_io: nil, passthrough_output: true)
248
263
  stdout = +""
249
264
  stderr = +""
250
265
  stdout_line_buffer = +""
@@ -259,10 +274,12 @@ module Kettle
259
274
  if reader.equal?(output)
260
275
  chunk = output.readpartial(1024)
261
276
  stdout << chunk
262
- stdout_line_buffer = print_interactive_stdout(stdout_line_buffer, chunk, stdout_line_handler)
277
+ transcript_write(log_io, chunk)
278
+ stdout_line_buffer = print_interactive_stdout(stdout_line_buffer, chunk, stdout_line_handler, passthrough_output: passthrough_output)
263
279
  handle_interactive_prompt(input, chunk, member_name: member_name)
264
280
  else
265
- input.write($stdin.readpartial(1024))
281
+ chunk = $stdin.readpartial(1024)
282
+ input.write(chunk)
266
283
  end
267
284
  end
268
285
  end
@@ -270,16 +287,17 @@ module Kettle
270
287
  # PTY raises EIO when the child process exits after closing the slave.
271
288
  rescue Error => error
272
289
  stderr << "#{error.message}\n"
290
+ transcript_write(log_io, "#{error.message}\n")
273
291
  terminate_process(pid)
274
292
  end
275
- flush_interactive_stdout(stdout_line_buffer, stdout_line_handler)
293
+ flush_interactive_stdout(stdout_line_buffer, stdout_line_handler, passthrough_output: passthrough_output)
276
294
  _, status = Process.wait2(pid)
277
295
  status = failure_status if !stderr.empty? && status.success?
278
296
  end
279
297
  [stdout, stderr, status]
280
298
  end
281
299
 
282
- def run_interactive_open3(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:)
300
+ def run_interactive_open3(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:, log_io: nil, passthrough_output: true)
283
301
  captured_stdout = +""
284
302
  captured_stderr = +""
285
303
  stdout_line_buffer = +""
@@ -303,19 +321,22 @@ module Kettle
303
321
  readers,
304
322
  member_name: member_name,
305
323
  stdout_line_buffer: stdout_line_buffer,
306
- stdout_line_handler: stdout_line_handler
324
+ stdout_line_handler: stdout_line_handler,
325
+ log_io: log_io,
326
+ passthrough_output: passthrough_output
307
327
  )
308
328
  end
309
329
  end
310
330
  end
311
331
  rescue Error => error
312
332
  captured_stderr << "#{error.message}\n"
333
+ transcript_write(log_io, "#{error.message}\n")
313
334
  terminate_process(wait_thread.pid)
314
335
  status = failure_status
315
336
  else
316
337
  status = wait_thread.value
317
338
  end
318
- flush_interactive_stdout(stdout_line_buffer, stdout_line_handler)
339
+ flush_interactive_stdout(stdout_line_buffer, stdout_line_handler, passthrough_output: passthrough_output)
319
340
  end
320
341
  [captured_stdout, captured_stderr, status]
321
342
  end
@@ -329,15 +350,19 @@ module Kettle
329
350
  readers,
330
351
  member_name:,
331
352
  stdout_line_buffer:,
332
- stdout_line_handler:
353
+ stdout_line_handler:,
354
+ log_io: nil,
355
+ passthrough_output: true
333
356
  )
334
357
  chunk = reader.readpartial(1024)
335
358
  if reader.equal?(output)
336
359
  captured_stdout << chunk
337
- stdout_line_buffer = print_interactive_stdout(stdout_line_buffer, chunk, stdout_line_handler)
360
+ transcript_write(log_io, chunk)
361
+ stdout_line_buffer = print_interactive_stdout(stdout_line_buffer, chunk, stdout_line_handler, passthrough_output: passthrough_output)
338
362
  else
339
363
  captured_stderr << chunk
340
- $stderr.print(chunk)
364
+ transcript_write(log_io, chunk)
365
+ $stderr.print(chunk) if passthrough_output
341
366
  end
342
367
  handle_interactive_prompt(input, chunk, member_name: member_name)
343
368
  stdout_line_buffer
@@ -346,36 +371,55 @@ module Kettle
346
371
  stdout_line_buffer
347
372
  end
348
373
 
349
- def print_interactive_stdout(buffer, chunk, stdout_line_handler)
350
- return print_interactive_chunk(chunk) unless stdout_line_handler
374
+ def with_transcript(log_path, argv:, chdir:)
375
+ return yield(nil) if log_path.to_s.empty?
376
+
377
+ FileUtils.mkdir_p(File.dirname(log_path))
378
+ File.open(log_path, "wb") do |log_io|
379
+ log_io.sync = true
380
+ log_io.puts("$ #{argv.join(" ")}")
381
+ log_io.puts("workdir: #{chdir}")
382
+ log_io.puts
383
+ yield(log_io)
384
+ end
385
+ end
386
+
387
+ def transcript_write(log_io, chunk)
388
+ return if log_io.nil? || chunk.to_s.empty?
389
+
390
+ log_io.write(chunk)
391
+ end
392
+
393
+ def print_interactive_stdout(buffer, chunk, stdout_line_handler, passthrough_output: true)
394
+ return print_interactive_chunk(chunk, passthrough_output: passthrough_output) unless stdout_line_handler
351
395
 
352
396
  pending = buffer + chunk
353
397
  lines = pending.lines
354
398
  remainder = pending.end_with?("\n") ? +"" : lines.pop.to_s
355
- lines.each { |line| print_interactive_line(line, stdout_line_handler) }
399
+ lines.each { |line| print_interactive_line(line, stdout_line_handler, passthrough_output: passthrough_output) }
356
400
  return +"" if remainder.empty?
357
401
  unless possible_event_line?(remainder)
358
- $stdout.print(remainder)
402
+ $stdout.print(remainder) if passthrough_output
359
403
  return +""
360
404
  end
361
405
 
362
406
  remainder
363
407
  end
364
408
 
365
- def flush_interactive_stdout(buffer, stdout_line_handler)
409
+ def flush_interactive_stdout(buffer, stdout_line_handler, passthrough_output: true)
366
410
  return if buffer.empty?
367
411
 
368
- print_interactive_line(buffer, stdout_line_handler)
412
+ print_interactive_line(buffer, stdout_line_handler, passthrough_output: passthrough_output)
369
413
  end
370
414
 
371
- def print_interactive_chunk(chunk)
372
- $stdout.print(chunk)
415
+ def print_interactive_chunk(chunk, passthrough_output: true)
416
+ $stdout.print(chunk) if passthrough_output
373
417
  +""
374
418
  end
375
419
 
376
- def print_interactive_line(line, stdout_line_handler)
420
+ def print_interactive_line(line, stdout_line_handler, passthrough_output: true)
377
421
  consumed = possible_event_line?(line) && stdout_line_handler.call(line.chomp)
378
- $stdout.print(line) unless consumed
422
+ $stdout.print(line) if !consumed && passthrough_output
379
423
  end
380
424
 
381
425
  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
@@ -35,6 +35,16 @@ module Kettle
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.14"
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?
@@ -1378,6 +1392,10 @@ module Kettle
1378
1392
  publish || !!@gem_signing_password
1379
1393
  end
1380
1394
 
1395
+ def release_command_passthrough_output?
1396
+ verbose || debug
1397
+ end
1398
+
1381
1399
  def kettle_release_command?(command)
1382
1400
  case command
1383
1401
  when Array
@@ -1408,6 +1426,7 @@ module Kettle
1408
1426
  args << "--local-ci" if local_ci
1409
1427
  args << "--skip-bundle-audit" if skip_bundle_audit
1410
1428
  args << "--skip-remotes=#{skip_remotes}" if skip_remotes && !skip_remotes.to_s.empty?
1429
+ args << "--required-remotes=#{required_remotes}" if required_remotes && !required_remotes.to_s.empty?
1411
1430
  args << "--yes" if release_command_uses_kettle_release_yes? && !command_includes_arg?(command, "--yes")
1412
1431
  args << "--events" unless command_includes_arg?(command, "--events")
1413
1432
  return command if args.empty?
@@ -1436,12 +1455,14 @@ module Kettle
1436
1455
  workflows.join(",")
1437
1456
  end
1438
1457
 
1439
- def validate_skip_remotes(value)
1440
- return nil if value.nil? || value.to_s.empty?
1458
+ def validate_remote_list(value, option_name)
1459
+ return nil if value.nil?
1460
+
1461
+ remotes = Array(value).flat_map { |entry| entry.to_s.split(",") }.map(&:strip)
1462
+ return nil if remotes.all?(&:empty?)
1441
1463
 
1442
- remotes = value.to_s.split(",").map(&:strip)
1443
1464
  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
1465
+ raise Error, "invalid #{option_name} value #{value.inspect}" if invalid
1445
1466
 
1446
1467
  remotes.join(",")
1447
1468
  end
@@ -1460,6 +1481,7 @@ module Kettle
1460
1481
  env["K_RELEASE_CI_WORKFLOWS"] = ci_workflows if ci_workflows && !ci_workflows.to_s.empty?
1461
1482
  env["KETTLE_DEV_SKIP_BUNDLE_AUDIT"] = "true" if skip_bundle_audit
1462
1483
  env["K_RELEASE_SKIP_REMOTES"] = skip_remotes if skip_remotes && !skip_remotes.to_s.empty?
1484
+ env["K_RELEASE_REQUIRED_REMOTES"] = required_remotes if required_remotes && !required_remotes.to_s.empty?
1463
1485
  env.merge!(kettle_release_secrets_env)
1464
1486
  env
1465
1487
  end
@@ -1903,6 +1925,24 @@ module Kettle
1903
1925
  progress
1904
1926
  end
1905
1927
 
1928
+ def start_release_preflight_progress(phases)
1929
+ return nil unless progress_io
1930
+
1931
+ phase_label = (phases.length == 1) ? "phase" : "phases"
1932
+ WorkflowProgress.new(
1933
+ io: progress_io,
1934
+ label: "release preflight",
1935
+ total: phases.length,
1936
+ jobs: 1,
1937
+ members: [release_preflight_progress_member],
1938
+ heading: "release preflight #{phases.length} #{phase_label}:"
1939
+ )
1940
+ end
1941
+
1942
+ def release_preflight_progress_member
1943
+ @release_preflight_progress_member ||= PreflightProgressMember.new("preflight")
1944
+ end
1945
+
1906
1946
  def template_phase_total(member = nil)
1907
1947
  total = 1
1908
1948
  total += 2 if config.normalize_lockfiles?
@@ -1927,6 +1967,20 @@ module Kettle
1927
1967
  end
1928
1968
  end
1929
1969
 
1970
+ def release_command_log_path(member, phase)
1971
+ return nil unless execute
1972
+
1973
+ File.join(release_log_dir, "#{safe_log_name(member.name)}-#{safe_log_name(phase)}.log")
1974
+ end
1975
+
1976
+ def release_log_dir
1977
+ @release_log_dir ||= File.join(config.root, "tmp", "kettle-family", "release-#{Time.now.strftime("%Y%m%d-%H%M%S")}-#{$$}")
1978
+ end
1979
+
1980
+ def safe_log_name(value)
1981
+ value.to_s.gsub(/[^A-Za-z0-9_.-]+/, "_")
1982
+ end
1983
+
1930
1984
  def template_initial_status(member = nil)
1931
1985
  return "prepare_lockfiles" if config.normalize_lockfiles?
1932
1986
  return "prepare_template_dependencies" if member.nil? || template_prepares_dependencies?(member)
@@ -1977,36 +2031,6 @@ module Kettle
1977
2031
  progress.summary("release summary: #{release_results.count(&:ok?)}/#{release_results.length} members ok")
1978
2032
  end
1979
2033
 
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
2034
  def template_event_line_handler(member, progress: nil)
2011
2035
  lambda do |line|
2012
2036
  event = parse_template_event(line)
@@ -2087,10 +2111,21 @@ module Kettle
2087
2111
  when "run_start"
2088
2112
  emit_template_event_line(member, ">", "release")
2089
2113
  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)
2114
+ emit_template_event_line(member, template_event_mark(event), command_step_event_label(event))
2092
2115
  when "secret_provider"
2093
2116
  emit_template_event_line(member, release_event_status_mark(event), secret_provider_event_label(event))
2117
+ when "remote_parity"
2118
+ emit_template_event_line(member, release_event_status_mark(event), remote_parity_event_label(event))
2119
+ when "ci_monitor"
2120
+ emit_template_event_line(member, release_event_status_mark(event), ci_monitor_event_label(event))
2121
+ when "pre_release"
2122
+ emit_template_event_line(member, release_event_status_mark(event), pre_release_event_label(event))
2123
+ when "changelog"
2124
+ emit_template_event_line(member, release_event_status_mark(event), changelog_event_label(event))
2125
+ when "release_lockfile"
2126
+ emit_template_event_line(member, release_event_status_mark(event), release_lockfile_event_label(event))
2127
+ when "release_probe"
2128
+ emit_template_event_line(member, release_event_status_mark(event), release_probe_event_label(event))
2094
2129
  when "diagnostic"
2095
2130
  emit_template_event_line(member, "!", diagnostic_event_label(event))
2096
2131
  when "summary"
@@ -2103,9 +2138,21 @@ module Kettle
2103
2138
  when "run_start"
2104
2139
  "release"
2105
2140
  when "command_step"
2106
- [event["phase"], event["name"]].map(&:to_s).reject(&:empty?).join(":")
2141
+ command_step_event_label(event)
2107
2142
  when "secret_provider"
2108
2143
  secret_provider_event_label(event)
2144
+ when "remote_parity"
2145
+ remote_parity_event_label(event)
2146
+ when "ci_monitor"
2147
+ ci_monitor_event_label(event)
2148
+ when "pre_release"
2149
+ pre_release_event_label(event)
2150
+ when "changelog"
2151
+ changelog_event_label(event)
2152
+ when "release_lockfile"
2153
+ release_lockfile_event_label(event)
2154
+ when "release_probe"
2155
+ release_probe_event_label(event)
2109
2156
  when "diagnostic"
2110
2157
  diagnostic_event_label(event)
2111
2158
  when "summary"
@@ -2123,6 +2170,18 @@ module Kettle
2123
2170
  template_event_mark(event)
2124
2171
  when "secret_provider"
2125
2172
  template_event_mark(event)
2173
+ when "remote_parity"
2174
+ template_event_mark(event)
2175
+ when "ci_monitor"
2176
+ template_event_mark(event)
2177
+ when "pre_release"
2178
+ template_event_mark(event)
2179
+ when "changelog"
2180
+ template_event_mark(event)
2181
+ when "release_lockfile"
2182
+ template_event_mark(event)
2183
+ when "release_probe"
2184
+ template_event_mark(event)
2126
2185
  when "diagnostic"
2127
2186
  "!"
2128
2187
  when "summary"
@@ -2158,6 +2217,81 @@ module Kettle
2158
2217
  parts.join(":")
2159
2218
  end
2160
2219
 
2220
+ def command_step_event_label(event)
2221
+ parts = [event["phase"], event["name"]].map(&:to_s).reject(&:empty?)
2222
+ summary = event["summary"].to_s
2223
+ parts << summary unless summary.empty?
2224
+ parts.join(":")
2225
+ end
2226
+
2227
+ def remote_parity_event_label(event)
2228
+ action = event["action"].to_s
2229
+ remote = event["remote"].to_s
2230
+ trunk = event["trunk"].to_s
2231
+ parts = ["remote"]
2232
+ parts << action unless action.empty?
2233
+ parts << remote unless remote.empty?
2234
+ parts << trunk if remote.empty? && !trunk.empty?
2235
+ parts.join(":")
2236
+ end
2237
+
2238
+ def ci_monitor_event_label(event)
2239
+ action = event["action"].to_s
2240
+ provider = event["provider"].to_s
2241
+ target = event["workflow"].to_s
2242
+ target = "pipeline" if target.empty? && provider == "gitlab"
2243
+ parts = ["ci"]
2244
+ parts << action unless action.empty?
2245
+ parts << provider unless provider.empty?
2246
+ parts << target unless target.empty?
2247
+ if %w[github_wait github_started github_tick].include?(action) && event["completed"] && event["total"]
2248
+ parts << "#{event["completed"]}/#{event["total"]}"
2249
+ end
2250
+ completed_workflow = event["completed_workflow"].to_s
2251
+ parts << completed_workflow unless completed_workflow.empty? || completed_workflow == target
2252
+ parts.join(":")
2253
+ end
2254
+
2255
+ def pre_release_event_label(event)
2256
+ action = event["action"].to_s
2257
+ check = event["check"].to_s
2258
+ parts = ["pre"]
2259
+ parts << action unless action.empty?
2260
+ parts << check unless check.empty?
2261
+ parts.join(":")
2262
+ end
2263
+
2264
+ def changelog_event_label(event)
2265
+ action = event["action"].to_s
2266
+ plan = event["plan"].to_s
2267
+ parts = ["changelog"]
2268
+ parts << action unless action.empty?
2269
+ parts << plan unless plan.empty? || action == "coverage"
2270
+ parts.join(":")
2271
+ end
2272
+
2273
+ def release_lockfile_event_label(event)
2274
+ action = event["action"].to_s
2275
+ stage = event["stage"].to_s
2276
+ stage = stage.tr(" ", "_")
2277
+ parts = ["lockfile"]
2278
+ parts << action unless action.empty?
2279
+ parts << stage unless stage.empty?
2280
+ parts << "#{event["attempt"]}/#{event["attempts"]}" if event["attempt"] && event["attempts"]
2281
+ parts.join(":")
2282
+ end
2283
+
2284
+ def release_probe_event_label(event)
2285
+ action = event["action"].to_s
2286
+ gem_name = event["gem"].to_s
2287
+ version = event["version"].to_s
2288
+ parts = ["probe"]
2289
+ parts << action unless action.empty?
2290
+ parts << [gem_name, version].reject(&:empty?).join("-")
2291
+ parts << "#{event["attempt"]}/#{event["attempts"]}" if event["attempt"] && event["attempts"]
2292
+ parts.reject(&:empty?).join(":")
2293
+ end
2294
+
2161
2295
  def diagnostic_event_label(event)
2162
2296
  message = event["message"]
2163
2297
  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.14
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.14
346
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.14/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.14
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