kettle-family 1.0.6 → 1.1.0

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: 55c4dcb2f4f8b001f5e41fa816f63e0d1cc2996c28d864a1834ef264fe610cc6
4
- data.tar.gz: 4586ac1f9e25c405579cc62f17ddf5206a9ba81cd8a76f1d9f127e895783e8f7
3
+ metadata.gz: 2aaa5d7e3543091f4cc86ed298afe481609e4c57bd85a3b4d69e0e0ec587bc80
4
+ data.tar.gz: 36524c2f96f04ccd148048cbbaee7222e844f78e1fa5b5dd85dcd49c53975123
5
5
  SHA512:
6
- metadata.gz: 487b01bd066f0c03b5ce37fee27325c273353c03880dd29e90b93efc1c4bea0724e3dd46a9def8b0c8bdde5f9163b8226533bbdb7e4f9fc6acd67b69ab0c13d6
7
- data.tar.gz: 125aecb410e6ec7f2562455e18e38cc2672899378631e96b5f0d063f61bb902161b0b1736cb8e0552b8d645c067a0e8bd467cdcd735f42541a32eaef60ea1b80
6
+ metadata.gz: b9efc18d3efbcfa9e16aaa867a22a8b9e59fc337f9eb41f03f02829a88c6a4707eee5ff00c5c0b24931ea428d46841bf649af572a3a1fd84416b06aa46417d7d
7
+ data.tar.gz: fe0e3717d364977801a8272976d2e22ae41ae3b5fdf4d4066fe57b698a28ec45089b4b88fd0b1645d57402fd427a9b8bb457439c49352b8e39bb3aac62be660e
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,27 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.1.0] - 2026-07-23
34
+
35
+ - TAG: [v1.1.0][1.1.0t]
36
+ - COVERAGE: 94.37% -- 2934/3109 lines in 25 files
37
+ - BRANCH COVERAGE: 76.29% -- 1049/1375 branches in 25 files
38
+ - 28.68% documented
39
+
40
+ ### Changed
41
+
42
+ - `kettle-family state` now includes a `bump` boolean column, documents the
43
+ boolean release-state columns above the table, and accepts `--only bump` to
44
+ select members with unreleased changes whose `V.rb` differs from `V.rel`.
45
+ - `--only` release-state filters now accept the short table column names
46
+ `unrel`, `prep`, and `pend` in addition to `unreleased`, `prepared`, and
47
+ `pending`.
48
+ - The `kettle-family` executable now supports `-v` / `--version` and prints a
49
+ standard startup header on normal runs.
50
+ - Template and release workflows now use `tty-progressbar`-backed multi-line
51
+ member progress in normal TTY output, with kettle-jem NDJSON marks rendered
52
+ as a fixed-width sliding event tape instead of dots or raw event dumps.
53
+
33
54
  ## [1.0.6] - 2026-07-23
34
55
 
35
56
  - TAG: [v1.0.6][1.0.6t]
@@ -890,7 +911,9 @@ Please file a bug if you notice a violation of semantic versioning.
890
911
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
891
912
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
892
913
 
893
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.0.6...HEAD
914
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.1.0...HEAD
915
+ [1.1.0]: https://github.com/kettle-dev/kettle-family/compare/v1.0.6...v1.1.0
916
+ [1.1.0t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.1.0
894
917
  [1.0.6]: https://github.com/kettle-dev/kettle-family/compare/v1.0.5...v1.0.6
895
918
  [1.0.6t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.0.6
896
919
  [1.0.5]: https://github.com/kettle-dev/kettle-family/compare/v1.0.4...v1.0.5
data/README.md CHANGED
@@ -669,7 +669,7 @@ Thanks for RTFM. ☺️
669
669
  [📌gitmoji]: https://gitmoji.dev
670
670
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
671
671
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
672
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.945-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
672
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-3.109-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
673
673
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
674
674
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
675
675
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/exe/kettle-family CHANGED
@@ -1,6 +1,18 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ script_basename = File.basename(__FILE__)
5
+ repo_lib = File.expand_path("../lib", __dir__)
6
+ $LOAD_PATH.unshift(repo_lib) if File.directory?(repo_lib) && !$LOAD_PATH.include?(repo_lib)
7
+
8
+ require "kettle/family/version"
9
+ if ARGV.any? { |arg| arg == "-v" || arg == "--version" }
10
+ puts Kettle::Family::Version::VERSION
11
+ exit(0)
12
+ end
13
+
4
14
  require "kettle/family"
5
15
 
16
+ puts "== #{script_basename} v#{Kettle::Family::Version::VERSION} ==" unless ARGV.include?("--json")
17
+
6
18
  exit(Kettle::Family::CLI.call(ARGV))
@@ -37,7 +37,7 @@ module Kettle
37
37
 
38
38
  module SelectionOptions
39
39
  def self.included(base)
40
- base.option :only, value: {type: String, usage: "MEMBERS"}, desc: "Select comma-separated members, or release-state tokens: unreleased, prepared, pending"
40
+ base.option :only, value: {type: String, usage: "MEMBERS"}, desc: "Select comma-separated members, or release-state tokens: unreleased/unrel, prepared/prep, pending/pend, bump"
41
41
  base.option :exclude, value: {type: String, usage: "MEMBERS"}, desc: "Exclude comma-separated members"
42
42
  base.option :start_at, long: "--start-at", value: {type: String, usage: "MEMBER[@BRANCH]"}, desc: "Select from member through the end of order"
43
43
  end
@@ -59,6 +59,7 @@ module Kettle
59
59
  success = status.success?
60
60
  state = success ? JSON.parse(stdout) : {}
61
61
  state = branch_filtered_state(member, state, branch) if success && branch
62
+ state = state_with_computed_booleans(state) if success
62
63
  state = enrich_git_state(member.root, state) if success
63
64
  result(member: member, command: command, stdout: stdout, stderr: stderr, status: status.exitstatus, elapsed: elapsed, success: success, state: state, branch: branch)
64
65
  rescue JSON::ParserError => error
@@ -279,7 +280,7 @@ module Kettle
279
280
  unreleased_entries = unreleased_entries?(content)
280
281
  prepared_release_pending = !version.to_s.empty? && latest_changelog_version == version
281
282
  ahead = commits_ahead_of_release(root, latest_changelog_version)
282
- enrich_git_state(root, {
283
+ state_with_computed_booleans(enrich_git_state(root, {
283
284
  "gem_name" => config.family_name,
284
285
  "version" => version,
285
286
  "latest_released" => nil,
@@ -288,7 +289,17 @@ module Kettle
288
289
  "unreleased_entries" => unreleased_entries,
289
290
  "prepared_release_pending" => prepared_release_pending,
290
291
  "pending_release" => unreleased_entries || prepared_release_pending
291
- })
292
+ }))
293
+ end
294
+
295
+ def state_with_computed_booleans(state)
296
+ enriched = state.dup
297
+ enriched["bump_release_pending"] = bump_release_pending?(enriched)
298
+ enriched
299
+ end
300
+
301
+ def bump_release_pending?(state)
302
+ state["unreleased_entries"] == true && state["version"].to_s != state["latest_released"].to_s
292
303
  end
293
304
 
294
305
  def root_changelog_version(root)
@@ -367,6 +367,11 @@ module Kettle
367
367
 
368
368
  def append_release_state_results(lines)
369
369
  lines << "release state:"
370
+ lines << " boolean columns:"
371
+ lines << " unrel: unreleased changelog entries are present"
372
+ lines << " prep: V.ch.md matches V.rb and is ready to publish"
373
+ lines << " pend: unrel or prep"
374
+ lines << " bump: unrel is yes and V.rb differs from V.rel"
370
375
  rows = release_state_header
371
376
  results.each do |result|
372
377
  rows << release_state_row(result)
@@ -393,7 +398,8 @@ module Kettle
393
398
  release_state_ahead_behind(state),
394
399
  yes_no(state.fetch("unreleased_entries", nil)),
395
400
  yes_no(state.fetch("prepared_release_pending", nil)),
396
- yes_no(state.fetch("pending_release", nil))
401
+ yes_no(state.fetch("pending_release", nil)),
402
+ yes_no(state.fetch("bump_release_pending", nil))
397
403
  ]
398
404
  return row unless release_state_has_branches?
399
405
 
@@ -401,7 +407,7 @@ module Kettle
401
407
  end
402
408
 
403
409
  def release_state_header
404
- header = [["gem", "checkout", "V.rb", "V.rel", "V.ch.md", "🔼 / 🔽", "unrel", "prep", "pend"]]
410
+ header = [["gem", "checkout", "V.rb", "V.rel", "V.ch.md", "🔼 / 🔽", "unrel", "prep", "pend", "bump"]]
405
411
  return header unless release_state_has_branches?
406
412
 
407
413
  [["branch", *header.first]]
@@ -5,8 +5,12 @@ module Kettle
5
5
  class Selection
6
6
  STATUS_TOKEN_KEYS = {
7
7
  "unreleased" => "unreleased_entries",
8
+ "unrel" => "unreleased_entries",
8
9
  "prepared" => "prepared_release_pending",
9
- "pending" => "pending_release"
10
+ "prep" => "prepared_release_pending",
11
+ "pending" => "pending_release",
12
+ "pend" => "pending_release",
13
+ "bump" => "bump_release_pending"
10
14
  }.freeze
11
15
 
12
16
  def self.status_tokens
@@ -3,7 +3,7 @@
3
3
  module Kettle
4
4
  module Family
5
5
  module Version
6
- VERSION = "1.0.6"
6
+ VERSION = "1.1.0"
7
7
  end
8
8
  VERSION = Version::VERSION # Traditional Constant Location
9
9
  end
@@ -7,6 +7,8 @@ require "etc"
7
7
  require "open3"
8
8
  require "uri"
9
9
 
10
+ require_relative "workflow_progress"
11
+
10
12
  module Kettle
11
13
  module Family
12
14
  class Workflow
@@ -165,17 +167,16 @@ module Kettle
165
167
  ordered_results = Array.new(workflow_members.length)
166
168
  mutex = Mutex.new
167
169
  stop = false
168
- emit_template_progress_start(workflow_members)
170
+ template_progress = start_template_progress(workflow_members)
169
171
  Array.new(template_jobs(workflow_members)) do
170
172
  Thread.new do # rubocop:disable ThreadSafety/NewThread -- family templating intentionally runs independent members concurrently.
171
173
  loop do
172
174
  break if mutex.synchronize { stop }
173
175
  index, member = queue.pop(true)
174
- member_results = template_results_for_member(member)
176
+ member_results = template_results_for_member(member, progress: template_progress)
175
177
  mutex.synchronize do
176
178
  ordered_results[index] = member_results
177
179
  stop = true unless member_results.all?(&:ok?)
178
- emit_template_progress_mark(member_results)
179
180
  end
180
181
  rescue ThreadError
181
182
  break
@@ -183,19 +184,22 @@ module Kettle
183
184
  end
184
185
  end.each(&:join)
185
186
  flattened = ordered_results.compact.flatten
186
- emit_template_progress_summary(flattened)
187
+ emit_template_progress_summary(flattened, progress: template_progress)
187
188
  flattened
188
189
  end
189
190
 
190
- def template_results_for_member(member)
191
+ def template_results_for_member(member, progress: nil)
192
+ progress&.start_member(member, total: template_phase_total, status: template_initial_status)
191
193
  runner = CommandRunner.new(execute: execute, accept: accept)
192
194
  [].tap do |memo|
193
195
  if config.normalize_lockfiles?
194
196
  normalize_lockfiles(member: member, runner: runner, memo: memo, phase: "prepare_lockfiles")
197
+ emit_member_result_progress(member, memo.last, progress: progress)
195
198
  return memo unless memo.last.ok?
196
199
  end
197
200
 
198
201
  prepared = prepare_template_dependencies(member: member, runner: runner, memo: memo)
202
+ emit_member_result_progress(member, memo.last, progress: progress) if memo.last&.phase == "prepare_template_dependencies"
199
203
  return memo if prepared == false
200
204
 
201
205
  memo << runner.call(
@@ -203,11 +207,22 @@ module Kettle
203
207
  phase: command,
204
208
  command: workflow_command(member),
205
209
  env: workflow_env,
206
- stdout_line_handler: template_event_line_handler(member)
210
+ stdout_line_handler: template_event_line_handler(member, progress: progress)
207
211
  )
212
+ emit_member_result_progress(member, memo.last, progress: progress)
208
213
  return memo unless memo.last.ok?
209
214
 
210
215
  normalize_lockfiles(member: member, runner: runner, memo: memo, phase: "normalize_lockfiles")
216
+ emit_member_result_progress(member, memo.last, progress: progress)
217
+ ensure
218
+ template_result = memo.find { |result| result.phase == "template" } || memo.last
219
+ if template_result
220
+ progress&.finish_member(
221
+ member,
222
+ success: memo.all?(&:ok?),
223
+ status: template_member_finish_status(template_result)
224
+ )
225
+ end
211
226
  end
212
227
  end
213
228
 
@@ -366,13 +381,20 @@ module Kettle
366
381
  return results unless results.all?(&:ok?)
367
382
  return parallel_release_member_results(release_members, results) if parallel_release_members?(release_members)
368
383
 
369
- release_members.each_with_object(results) do |member, memo|
370
- memo.concat(release_results_for_member(member, runner: runner))
371
- break memo unless memo.last.ok?
384
+ release_progress = start_release_progress(release_members)
385
+ @release_progress = release_progress
386
+ begin
387
+ release_members.each_with_object(results) do |member, memo|
388
+ memo.concat(release_results_for_member(member, runner: runner))
389
+ break memo unless memo.last.ok?
372
390
 
373
- remaining_members = release_members.drop(release_members.index(member) + 1)
374
- append_dependency_floor_results(released_members: [member], dependent_members: remaining_members, runner: runner, memo: memo)
375
- break memo unless memo.last&.ok?
391
+ remaining_members = release_members.drop(release_members.index(member) + 1)
392
+ append_dependency_floor_results(released_members: [member], dependent_members: remaining_members, runner: runner, memo: memo)
393
+ break memo unless memo.last&.ok?
394
+ end
395
+ ensure
396
+ emit_release_progress_summary(results, progress: release_progress)
397
+ @release_progress = nil
376
398
  end
377
399
  end
378
400
 
@@ -380,16 +402,23 @@ module Kettle
380
402
  results = initial_results.dup
381
403
  waves = release_waves(release_members)
382
404
  completed_members = []
383
- waves.each_with_index do |wave, index|
384
- results << release_wave_result(wave, index: index, total: waves.length)
385
- wave_results = run_release_wave(wave)
386
- results.concat(wave_results.flatten)
387
- break unless wave_results.all? { |member_results| member_results.all?(&:ok?) }
388
-
389
- completed_members.concat(wave)
390
- remaining_members = release_members - completed_members
391
- append_dependency_floor_results(released_members: wave, dependent_members: remaining_members, runner: release_command_runner, memo: results)
392
- break unless results.last&.ok?
405
+ release_progress = start_release_progress(release_members)
406
+ @release_progress = release_progress
407
+ begin
408
+ waves.each_with_index do |wave, index|
409
+ results << release_wave_result(wave, index: index, total: waves.length)
410
+ wave_results = run_release_wave(wave)
411
+ results.concat(wave_results.flatten)
412
+ break unless wave_results.all? { |member_results| member_results.all?(&:ok?) }
413
+
414
+ completed_members.concat(wave)
415
+ remaining_members = release_members - completed_members
416
+ append_dependency_floor_results(released_members: wave, dependent_members: remaining_members, runner: release_command_runner, memo: results)
417
+ break unless results.last&.ok?
418
+ end
419
+ ensure
420
+ emit_release_progress_summary(results, progress: release_progress)
421
+ @release_progress = nil
393
422
  end
394
423
  results
395
424
  end
@@ -439,21 +468,27 @@ module Kettle
439
468
  end
440
469
 
441
470
  def release_results_for_member(member, runner:)
471
+ progress = @release_progress
472
+ progress&.start_member(member, total: release_phase_total, status: "check")
442
473
  [].tap do |memo|
443
474
  if skip_already_released?(member)
444
475
  memo << already_released_result(member)
476
+ emit_member_result_progress(member, memo.last, progress: progress)
445
477
  return memo
446
478
  end
447
479
 
448
480
  if config.release_normalize_lockfiles?
449
481
  normalize_release_lockfiles(member: member, runner: runner, memo: memo)
482
+ emit_member_result_progress(member, memo.last, progress: progress)
450
483
  return memo unless memo.last&.ok?
451
484
 
452
485
  commit_normalized_lockfiles(branch_members: [member], runner: runner, memo: memo, reason: "release")
486
+ emit_member_result_progress(member, memo.last, progress: progress)
453
487
  return memo unless memo.last&.ok?
454
488
  end
455
489
 
456
490
  append_release_internal_checks(member: member, memo: memo)
491
+ memo.last(2).each { |result| emit_member_result_progress(member, result, progress: progress) }
457
492
  return memo unless memo.last(2).all?(&:ok?)
458
493
 
459
494
  memo << runner.call(
@@ -463,9 +498,21 @@ module Kettle
463
498
  env: release_env,
464
499
  interactive: release_command_interactive?
465
500
  )
501
+ emit_member_result_progress(member, memo.last, progress: progress)
466
502
  return memo unless memo.last.ok?
467
503
 
504
+ before_git_phase_count = memo.length
468
505
  append_release_git_phases(member: member, runner: runner, memo: memo)
506
+ memo.drop(before_git_phase_count).each { |result| emit_member_result_progress(member, result, progress: progress) }
507
+ ensure
508
+ finished_result = memo.find { |result| result.phase == release_phase } || memo.last
509
+ if finished_result
510
+ progress&.finish_member(
511
+ member,
512
+ success: memo.all?(&:ok?),
513
+ status: release_member_finish_status(finished_result)
514
+ )
515
+ end
469
516
  end
470
517
  end
471
518
 
@@ -1096,30 +1143,103 @@ module Kettle
1096
1143
  append_command_args(command_text, args)
1097
1144
  end
1098
1145
 
1099
- def emit_template_progress_start(workflow_members)
1100
- return unless progress_io
1146
+ def start_template_progress(workflow_members)
1147
+ progress = WorkflowProgress.new(
1148
+ io: progress_io,
1149
+ label: "templating",
1150
+ total: workflow_members.length,
1151
+ jobs: template_jobs(workflow_members)
1152
+ )
1153
+ progress.start
1154
+ progress
1155
+ end
1156
+
1157
+ def start_release_progress(release_members)
1158
+ progress = WorkflowProgress.new(
1159
+ io: progress_io,
1160
+ label: release_progress_label,
1161
+ total: release_members.length,
1162
+ jobs: release_jobs(release_members)
1163
+ )
1164
+ progress.start
1165
+ progress
1166
+ end
1101
1167
 
1102
- progress_io.puts("templating #{workflow_members.length} member#{"s" unless workflow_members.length == 1} with #{template_jobs(workflow_members)} job#{"s" unless template_jobs(workflow_members) == 1}:")
1103
- progress_io.flush if progress_io.respond_to?(:flush)
1168
+ def template_phase_total
1169
+ config.normalize_lockfiles? ? 4 : 2
1104
1170
  end
1105
1171
 
1106
- def emit_template_progress_mark(member_results)
1107
- return unless progress_io
1172
+ def release_phase_total
1173
+ total = 3
1174
+ total += 2 if config.release_normalize_lockfiles?
1175
+ total += 1 if tag
1176
+ total += 1 if push
1177
+ total
1178
+ end
1108
1179
 
1109
- template_result = member_results.find { |result| result.phase == "template" } || member_results.last
1110
- progress_io.print(template_result.ok? ? "." : "F")
1111
- progress_io.flush if progress_io.respond_to?(:flush)
1180
+ def release_progress_label
1181
+ if release_phase == "release_publish"
1182
+ "publishing"
1183
+ else
1184
+ "releasing"
1185
+ end
1112
1186
  end
1113
1187
 
1114
- def template_event_line_handler(member)
1188
+ def template_initial_status
1189
+ config.normalize_lockfiles? ? "prepare_lockfiles" : "prepare_template_dependencies"
1190
+ end
1191
+
1192
+ def emit_member_result_progress(member, result, progress:)
1193
+ return unless result
1194
+
1195
+ progress&.advance(member, status: result.phase, success: result.ok?, mark: command_result_progress_mark(result))
1196
+ end
1197
+
1198
+ def command_result_progress_mark(result)
1199
+ return "S" if result.skipped
1200
+
1201
+ result.ok? ? "." : "F"
1202
+ end
1203
+
1204
+ def template_member_finish_status(result)
1205
+ changed_files = template_changed_file_count(result)
1206
+ "#{changed_files} file#{"s" unless changed_files == 1} changed"
1207
+ end
1208
+
1209
+ def release_member_finish_status(result)
1210
+ result.skipped ? "skipped #{result.phase}" : result.phase
1211
+ end
1212
+
1213
+ def emit_template_progress_summary(results, progress:)
1214
+ return unless progress
1215
+
1216
+ template_results = results.select { |result| result.phase == "template" }
1217
+ changed_files = template_results.sum { |result| template_changed_file_count(result) }
1218
+ progress.stop
1219
+ progress.summary("template summary: #{template_results.count(&:ok?)}/#{template_results.length} members ok, #{changed_files} file#{"s" unless changed_files == 1} changed")
1220
+ end
1221
+
1222
+ def emit_release_progress_summary(results, progress:)
1223
+ return unless progress
1224
+
1225
+ release_results = results.select { |result| result.phase == release_phase || result.phase == "release_skip" }
1226
+ progress.stop
1227
+ progress.summary("release summary: #{release_results.count(&:ok?)}/#{release_results.length} members ok")
1228
+ end
1229
+
1230
+ def template_event_line_handler(member, progress: nil)
1115
1231
  return nil unless progress_io
1116
- return nil unless verbose || debug
1232
+ return nil unless verbose || debug || progress&.tty?
1117
1233
 
1118
1234
  lambda do |line|
1119
1235
  event = parse_template_event(line)
1120
1236
  next unless event
1121
1237
 
1122
- emit_template_event_progress(member, event)
1238
+ if verbose || debug
1239
+ emit_template_event_progress(member, event)
1240
+ else
1241
+ emit_template_event_status(member, event, progress: progress)
1242
+ end
1123
1243
  end
1124
1244
  end
1125
1245
 
@@ -1151,6 +1271,39 @@ module Kettle
1151
1271
  end
1152
1272
  end
1153
1273
 
1274
+ def emit_template_event_status(member, event, progress:)
1275
+ status = case event["type"]
1276
+ when "phase_start", "phase_finish"
1277
+ event["phase"].to_s
1278
+ when "recipe"
1279
+ event["path"].to_s
1280
+ when "post_apply_step", "command_step"
1281
+ [event["phase"], event["name"]].map(&:to_s).reject(&:empty?).join(":")
1282
+ when "diagnostic"
1283
+ message = event["message"].to_s
1284
+ message.empty? ? event["kind"].to_s : message
1285
+ when "summary"
1286
+ "#{event["changed_count"].to_i} file#{"s" unless event["changed_count"].to_i == 1} changed"
1287
+ end
1288
+ mark = template_event_status_mark(event)
1289
+ progress&.update(member, status: status, mark: mark) if status && !status.empty?
1290
+ end
1291
+
1292
+ def template_event_status_mark(event)
1293
+ case event["type"]
1294
+ when "phase_start"
1295
+ ">"
1296
+ when "phase_finish"
1297
+ phase_finish_event_mark(event)
1298
+ when "recipe"
1299
+ template_event_mark(event, changed_mark: "*")
1300
+ when "post_apply_step", "command_step"
1301
+ template_event_mark(event)
1302
+ when "diagnostic"
1303
+ "!"
1304
+ end
1305
+ end
1306
+
1154
1307
  def emit_template_event_line(member, mark, label)
1155
1308
  return if label.to_s.empty?
1156
1309
 
@@ -1178,16 +1331,6 @@ module Kettle
1178
1331
  @template_progress_mutex.synchronize(&block)
1179
1332
  end
1180
1333
 
1181
- def emit_template_progress_summary(results)
1182
- return unless progress_io
1183
-
1184
- template_results = results.select { |result| result.phase == "template" }
1185
- changed_files = template_results.sum { |result| template_changed_file_count(result) }
1186
- progress_io.puts
1187
- progress_io.puts("template summary: #{template_results.count(&:ok?)}/#{template_results.length} members ok, #{changed_files} file#{"s" unless changed_files == 1} changed")
1188
- progress_io.flush if progress_io.respond_to?(:flush)
1189
- end
1190
-
1191
1334
  def template_changed_file_count(result)
1192
1335
  event_count = template_changed_file_count_from_events(result.stdout)
1193
1336
  return event_count if event_count
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-progressbar"
4
+
5
+ module Kettle
6
+ module Family
7
+ class WorkflowProgress
8
+ FORMAT = "%<member>-24s :events :status"
9
+ EVENT_WIDTH = 30
10
+
11
+ def initialize(io:, label:, total:, jobs:, enabled: true)
12
+ @io = io
13
+ @label = label
14
+ @total = total.to_i
15
+ @jobs = jobs.to_i
16
+ @enabled = enabled && !!io
17
+ @bars = {}
18
+ @member_totals = {}
19
+ @member_events = Hash.new("")
20
+ @mutex = Mutex.new
21
+ @tty = @enabled && io.respond_to?(:tty?) && io.tty?
22
+ @multibar = @tty ? TTY::ProgressBar::Multi.new(output: io) : nil
23
+ end
24
+
25
+ def start
26
+ return unless @enabled
27
+
28
+ write_line("#{@label} #{@total} member#{plural(@total)} with #{@jobs} job#{plural(@jobs)}:")
29
+ end
30
+
31
+ def start_member(member, total:, status:)
32
+ return unless @enabled
33
+
34
+ synchronize do
35
+ @member_totals[member.name] = total.to_i
36
+ if @tty
37
+ render(member, status: status)
38
+ else
39
+ write_line("[#{member.name}] > #{status}")
40
+ end
41
+ end
42
+ end
43
+
44
+ def advance(member, status:, success: true, mark: nil)
45
+ return unless @enabled
46
+
47
+ synchronize do
48
+ event_mark = mark || (success ? "." : "F")
49
+ if @tty
50
+ append_event(member, event_mark)
51
+ render(member, status: status)
52
+ else
53
+ write_line("[#{member.name}] #{event_mark} #{status}")
54
+ end
55
+ end
56
+ end
57
+
58
+ def update(member, status:, mark: nil)
59
+ return unless @enabled
60
+ return if status.to_s.empty?
61
+
62
+ synchronize do
63
+ if @tty
64
+ append_event(member, mark) if mark
65
+ render(member, status: status)
66
+ else
67
+ write_line("[#{member.name}] #{mark || ">"} #{status}")
68
+ end
69
+ end
70
+ end
71
+
72
+ def finish_member(member, success:, status:)
73
+ return unless @enabled
74
+
75
+ synchronize do
76
+ if @tty
77
+ render(member, status: status)
78
+ else
79
+ write_line("[#{member.name}] #{success ? "done" : "failed"} #{status}")
80
+ end
81
+ end
82
+ end
83
+
84
+ def stop
85
+ return unless @enabled
86
+
87
+ synchronize { @multibar&.stop }
88
+ end
89
+
90
+ def tty?
91
+ @tty
92
+ end
93
+
94
+ def summary(message)
95
+ return unless @enabled
96
+
97
+ synchronize do
98
+ @io.puts unless @tty
99
+ write_line(message)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def render(member, status:)
106
+ bar_for(member).advance(0, events: event_tape(member), status: status)
107
+ end
108
+
109
+ def append_event(member, mark)
110
+ return if mark.to_s.empty?
111
+
112
+ @member_events[member.name] = (@member_events[member.name] + mark.to_s).chars.last(EVENT_WIDTH).join
113
+ end
114
+
115
+ def event_tape(member)
116
+ @member_events[member.name].rjust(EVENT_WIDTH)
117
+ end
118
+
119
+ def bar_for(member)
120
+ @bars[member.name] ||= @multibar.register(Kernel.format(FORMAT, member: member.name), total: nil)
121
+ end
122
+
123
+ def plural(count)
124
+ "s" unless count == 1
125
+ end
126
+
127
+ def write_line(line)
128
+ @io.puts(line)
129
+ @io.flush if @io.respond_to?(:flush)
130
+ end
131
+
132
+ def synchronize(&block)
133
+ @mutex.synchronize(&block)
134
+ end
135
+ end
136
+ end
137
+ 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.0.6
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -119,6 +119,20 @@ dependencies:
119
119
  - - "~>"
120
120
  - !ruby/object:Gem::Version
121
121
  version: '0.2'
122
+ - !ruby/object:Gem::Dependency
123
+ name: tty-progressbar
124
+ requirement: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: '0.18'
129
+ type: :runtime
130
+ prerelease: false
131
+ version_requirements: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - "~>"
134
+ - !ruby/object:Gem::Version
135
+ version: '0.18'
122
136
  - !ruby/object:Gem::Dependency
123
137
  name: bundler-audit
124
138
  requirement: !ruby/object:Gem::Requirement
@@ -298,16 +312,17 @@ files:
298
312
  - lib/kettle/family/version_bump.rb
299
313
  - lib/kettle/family/version_gem.rb
300
314
  - lib/kettle/family/workflow.rb
315
+ - lib/kettle/family/workflow_progress.rb
301
316
  - sig/kettle/family.rbs
302
317
  homepage: https://github.com/kettle-dev/kettle-family
303
318
  licenses:
304
319
  - AGPL-3.0-only
305
320
  metadata:
306
321
  homepage_uri: https://kettle-family.galtzo.com
307
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.0.6
308
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.0.6/CHANGELOG.md
322
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.1.0
323
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.1.0/CHANGELOG.md
309
324
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
310
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.0.6
325
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.1.0
311
326
  funding_uri: https://github.com/sponsors/pboling
312
327
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
313
328
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file